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.
- package/CHANGELOG.md +73 -0
- package/dist/esm/assets/src/components/controls/Slider/{Slider.css.ts.vanilla-Cqm3fQ0S.css → Slider.css.ts.vanilla-CLf_RC0A.css} +50 -47
- package/dist/esm/assets/src/components/navigation/Menu/{Menu.css.ts.vanilla-B5iVe5S2.css → Menu.css.ts.vanilla-Bhh1L5yl.css} +14 -11
- package/dist/esm/assets/src/components/primitives/Tooltip/{Tooltip.css.ts.vanilla-CrEFoLBX.css → Tooltip.css.ts.vanilla-BqaiGrFQ.css} +12 -9
- package/dist/esm/assets/src/components/shell/AppShell/{AppShell.css.ts.vanilla-BYmtU0O_.css → AppShell.css.ts.vanilla-BLK2-DRL.css} +1 -0
- package/dist/esm/assets/src/components/shell/MenuBar/{MenuBar.css.ts.vanilla-TgPcl4yA.css → MenuBar.css.ts.vanilla-Dt3w4f-q.css} +18 -6
- package/dist/esm/components/controls/Slider/Slider.css.js +10 -11
- package/dist/esm/components/controls/Slider/Slider.css.js.map +1 -1
- package/dist/esm/components/controls/Slider/Slider.js +40 -7
- package/dist/esm/components/controls/Slider/Slider.js.map +1 -1
- package/dist/esm/components/feedback/Spinner/Spinner.js +12 -3
- package/dist/esm/components/feedback/Spinner/Spinner.js.map +1 -1
- package/dist/esm/components/navigation/ContextMenu/ContextMenu.js +2 -2
- package/dist/esm/components/navigation/ContextMenu/ContextMenu.js.map +1 -1
- package/dist/esm/components/navigation/Menu/Menu.css.js +11 -10
- package/dist/esm/components/navigation/Menu/Menu.css.js.map +1 -1
- package/dist/esm/components/navigation/Menu/Menu.js +2 -2
- package/dist/esm/components/navigation/Menu/Menu.js.map +1 -1
- package/dist/esm/components/primitives/IconButton/IconButton.js +4 -2
- package/dist/esm/components/primitives/IconButton/IconButton.js.map +1 -1
- package/dist/esm/components/primitives/Tooltip/Tooltip.css.js +6 -5
- package/dist/esm/components/primitives/Tooltip/Tooltip.css.js.map +1 -1
- package/dist/esm/components/primitives/Tooltip/Tooltip.js +12 -2
- package/dist/esm/components/primitives/Tooltip/Tooltip.js.map +1 -1
- package/dist/esm/components/primitives/viewport/Viewport.js +5 -2
- package/dist/esm/components/primitives/viewport/Viewport.js.map +1 -1
- package/dist/esm/components/shell/AppShell/AppShell.css.js +1 -1
- package/dist/esm/components/shell/MenuBar/MenuBar.css.js +7 -6
- package/dist/esm/components/shell/MenuBar/MenuBar.css.js.map +1 -1
- package/dist/esm/components/shell/MenuBar/MenuBar.js +52 -3
- package/dist/esm/components/shell/MenuBar/MenuBar.js.map +1 -1
- package/dist/esm/{styles.js → styles.css.js} +1 -1
- package/dist/esm/styles.css.js.map +1 -0
- package/dist/tokens/tokens.dark.css +1 -1
- package/dist/tokens/tokens.json +1 -1
- package/dist/tokens/tokens.light.css +1 -1
- package/dist/types/components/controls/Slider/Slider.d.ts +7 -0
- package/dist/types/components/feedback/Spinner/Spinner.d.ts +6 -1
- package/dist/types/components/feedback/Spinner/Spinner.types.d.ts +8 -0
- package/dist/types/components/primitives/IconButton/IconButton.d.ts +2 -1
- package/dist/types/components/primitives/viewport/Viewport.d.ts +5 -2
- package/dist/types/components/shell/MenuBar/MenuBar.d.ts +862 -0
- package/package.json +2 -2
- 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
|
|
3
|
+
import { useState, useRef, useMemo, useCallback, useEffect, useContext, useId, createContext } from 'react';
|
|
4
|
+
import { CheckIcon } from '../../Icons/CheckIcon.js';
|
|
5
|
+
import { CircleIcon } from '../../Icons/CircleIcon.js';
|
|
4
6
|
import { cx } from '../../../utils/cx.js';
|
|
5
|
-
import { menuBarRoot, separator, subTrigger, subDropdown, subContainer, shortcut, menuItem, trigger, dropdown, menuContainer } from './MenuBar.css.js';
|
|
7
|
+
import { menuBarRoot, separator, subTrigger, subDropdown, subContainer, indicatorSlot, shortcut, menuItem, trigger, dropdown, menuContainer } from './MenuBar.css.js';
|
|
6
8
|
|
|
7
9
|
// --- Context ---
|
|
8
10
|
const MenuBarContext = /*#__PURE__*/ createContext({
|
|
@@ -16,7 +18,7 @@ const MenuBarContext = /*#__PURE__*/ createContext({
|
|
|
16
18
|
});
|
|
17
19
|
const useMenuBar = () => useContext(MenuBarContext);
|
|
18
20
|
const SUBMENU_CLOSE_DELAY = 150;
|
|
19
|
-
const MENU_ITEM_SELECTOR = '[role="menuitem"]:not(:disabled)';
|
|
21
|
+
const MENU_ITEM_SELECTOR = '[role="menuitem"]:not(:disabled), [role="menuitemcheckbox"]:not(:disabled), [role="menuitemradio"]:not(:disabled)';
|
|
20
22
|
const ChevronRight = () => (jsx("span", { style: {
|
|
21
23
|
fontSize: '8px',
|
|
22
24
|
lineHeight: 1,
|
|
@@ -33,6 +35,50 @@ const MenuBarItem = ({ onClick, shortcut: shortcutText, disabled = false, icon,
|
|
|
33
35
|
return (jsxs("button", { role: "menuitem", onClick: handleClick, disabled: disabled, className: cx(menuItem, className), style: style, "data-testid": testId, ref: ref, type: "button", tabIndex: -1, ...rest, children: [icon && jsx("span", { children: icon }), jsx("span", { children: children }), shortcutText && jsx("span", { className: shortcut, children: shortcutText })] }));
|
|
34
36
|
};
|
|
35
37
|
MenuBarItem.displayName = 'MenuBar.Item';
|
|
38
|
+
// --- Checkable items (checkbox / radio) ---
|
|
39
|
+
const MenuBarRadioContext =
|
|
40
|
+
/*#__PURE__*/ createContext(null);
|
|
41
|
+
const MenuBarCheckboxItem = ({ checked, defaultChecked = false, onCheckedChange, shortcut: shortcutText, indicator = jsx(CheckIcon, { size: "sm" }), closeOnClick = false, disabled = false, children, className, style, testId, ref, ...rest }) => {
|
|
42
|
+
const { setOpenMenuId } = useMenuBar();
|
|
43
|
+
const [internalChecked, setInternalChecked] = useState(defaultChecked);
|
|
44
|
+
const isControlled = checked !== undefined;
|
|
45
|
+
const isChecked = isControlled ? checked : internalChecked;
|
|
46
|
+
const handleClick = useCallback(() => {
|
|
47
|
+
const next = !isChecked;
|
|
48
|
+
if (!isControlled)
|
|
49
|
+
setInternalChecked(next);
|
|
50
|
+
onCheckedChange?.(next);
|
|
51
|
+
if (closeOnClick)
|
|
52
|
+
setOpenMenuId(null);
|
|
53
|
+
}, [isChecked, isControlled, onCheckedChange, closeOnClick, setOpenMenuId]);
|
|
54
|
+
return (jsxs("button", { role: "menuitemcheckbox", "aria-checked": isChecked, onClick: handleClick, disabled: disabled, className: cx(menuItem, className), style: style, "data-testid": testId, ref: ref, type: "button", tabIndex: -1, ...rest, children: [jsx("span", { className: indicatorSlot, "aria-hidden": "true", children: isChecked ? indicator : null }), jsx("span", { children: children }), shortcutText && jsx("span", { className: shortcut, children: shortcutText })] }));
|
|
55
|
+
};
|
|
56
|
+
MenuBarCheckboxItem.displayName = 'MenuBar.CheckboxItem';
|
|
57
|
+
const MenuBarRadioGroup = ({ value, defaultValue, onValueChange, children, className, style, testId, ref, ...rest }) => {
|
|
58
|
+
const [internalValue, setInternalValue] = useState(defaultValue);
|
|
59
|
+
const isControlled = value !== undefined;
|
|
60
|
+
const selectedValue = isControlled ? value : internalValue;
|
|
61
|
+
const setValue = useCallback((next) => {
|
|
62
|
+
if (!isControlled)
|
|
63
|
+
setInternalValue(next);
|
|
64
|
+
onValueChange?.(next);
|
|
65
|
+
}, [isControlled, onValueChange]);
|
|
66
|
+
const contextValue = useMemo(() => ({ value: selectedValue, setValue }), [selectedValue, setValue]);
|
|
67
|
+
return (jsx(MenuBarRadioContext.Provider, { value: contextValue, children: jsx("div", { role: "group", className: className, style: style, "data-testid": testId, ref: ref, ...rest, children: children }) }));
|
|
68
|
+
};
|
|
69
|
+
MenuBarRadioGroup.displayName = 'MenuBar.RadioGroup';
|
|
70
|
+
const MenuBarRadioItem = ({ value, shortcut: shortcutText, indicator = jsx(CircleIcon, { size: "sm" }), closeOnClick = false, disabled = false, children, className, style, testId, ref, ...rest }) => {
|
|
71
|
+
const { setOpenMenuId } = useMenuBar();
|
|
72
|
+
const group = useContext(MenuBarRadioContext);
|
|
73
|
+
const isChecked = group?.value === value;
|
|
74
|
+
const handleClick = useCallback(() => {
|
|
75
|
+
group?.setValue(value);
|
|
76
|
+
if (closeOnClick)
|
|
77
|
+
setOpenMenuId(null);
|
|
78
|
+
}, [group, value, closeOnClick, setOpenMenuId]);
|
|
79
|
+
return (jsxs("button", { role: "menuitemradio", "aria-checked": isChecked, onClick: handleClick, disabled: disabled, className: cx(menuItem, className), style: style, "data-testid": testId, ref: ref, type: "button", tabIndex: -1, ...rest, children: [jsx("span", { className: indicatorSlot, "aria-hidden": "true", children: isChecked ? indicator : null }), jsx("span", { children: children }), shortcutText && jsx("span", { className: shortcut, children: shortcutText })] }));
|
|
80
|
+
};
|
|
81
|
+
MenuBarRadioItem.displayName = 'MenuBar.RadioItem';
|
|
36
82
|
const MenuBarSub = ({ label, children, disabled = false, className, style, testId, ref, ...rest }) => {
|
|
37
83
|
const { menuOffset } = useMenuBar();
|
|
38
84
|
const [open, setOpen] = useState(false);
|
|
@@ -214,6 +260,9 @@ MenuBarRoot.displayName = 'MenuBar';
|
|
|
214
260
|
const MenuBar = /*#__PURE__*/ Object.assign(MenuBarRoot, {
|
|
215
261
|
Menu: MenuBarMenu,
|
|
216
262
|
Item: MenuBarItem,
|
|
263
|
+
CheckboxItem: MenuBarCheckboxItem,
|
|
264
|
+
RadioGroup: MenuBarRadioGroup,
|
|
265
|
+
RadioItem: MenuBarRadioItem,
|
|
217
266
|
Sub: MenuBarSub,
|
|
218
267
|
Separator: MenuBarSeparator,
|
|
219
268
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MenuBar.js","sources":["src/components/shell/MenuBar/MenuBar.tsx"],"sourcesContent":["'use client';\n\nimport React, {\n createContext,\n useContext,\n useState,\n useCallback,\n useRef,\n useEffect,\n useId,\n KeyboardEvent,\n useMemo,\n} from 'react';\n\nimport { cx } from '@/utils/cx';\n\nimport {\n menuBarRoot,\n menuContainer,\n trigger,\n dropdown,\n menuItem,\n shortcut,\n separator,\n subTrigger,\n subDropdown,\n subContainer,\n} from './MenuBar.css';\n\nimport type {\n MenuBarProps,\n MenuBarMenuProps,\n MenuBarItemProps,\n MenuBarSubProps,\n MenuBarSeparatorProps,\n MenuBarContextValue,\n} from './MenuBar.types';\n\n// --- Context ---\n\nconst MenuBarContext = /*#__PURE__*/ createContext<MenuBarContextValue>({\n size: 'md',\n menuOffset: 2,\n openMenuId: null,\n setOpenMenuId: () => {},\n registerMenu: () => {},\n unregisterMenu: () => {},\n menuIds: [],\n});\n\nconst useMenuBar = () => useContext(MenuBarContext);\n\nconst SUBMENU_CLOSE_DELAY = 150;\n\nconst MENU_ITEM_SELECTOR = '[role=\"menuitem\"]:not(:disabled)';\n\nconst ChevronRight = () => (\n <span\n style={{\n fontSize: '8px',\n lineHeight: 1,\n }}\n >\n ▶\n </span>\n);\n\n// --- Sub-components ---\n\nconst MenuBarItem: React.FC<MenuBarItemProps> = ({\n onClick,\n shortcut: shortcutText,\n disabled = false,\n icon,\n children,\n\n className,\n style,\n testId,\n ref,\n ...rest\n}) => {\n const { setOpenMenuId } = useMenuBar();\n\n const handleClick = useCallback(() => {\n if (onClick) {\n onClick();\n setOpenMenuId(null);\n }\n }, [onClick, setOpenMenuId]);\n\n return (\n <button\n role=\"menuitem\"\n onClick={handleClick}\n disabled={disabled}\n className={cx(menuItem, className)}\n style={style}\n data-testid={testId}\n ref={ref}\n type=\"button\"\n tabIndex={-1}\n {...rest}\n >\n {icon && <span>{icon}</span>}\n <span>{children}</span>\n {shortcutText && <span className={shortcut}>{shortcutText}</span>}\n </button>\n );\n};\n\nMenuBarItem.displayName = 'MenuBar.Item';\n\nconst MenuBarSub: React.FC<MenuBarSubProps> = ({\n label,\n children,\n disabled = false,\n\n className,\n style,\n testId,\n ref,\n ...rest\n}) => {\n const { menuOffset } = useMenuBar();\n const [open, setOpen] = useState(false);\n const closeTimer = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n const handleEnter = useCallback(() => {\n if (closeTimer.current) {\n clearTimeout(closeTimer.current);\n closeTimer.current = null;\n }\n if (!disabled) setOpen(true);\n }, [disabled]);\n\n const handleLeave = useCallback(() => {\n closeTimer.current = setTimeout(() => {\n setOpen(false);\n }, SUBMENU_CLOSE_DELAY);\n }, []);\n\n useEffect(() => {\n return () => {\n if (closeTimer.current) clearTimeout(closeTimer.current);\n };\n }, []);\n\n return (\n <div\n ref={ref}\n className={cx(subContainer, className)}\n style={style}\n data-testid={testId}\n onMouseEnter={handleEnter}\n onMouseLeave={handleLeave}\n {...rest}\n >\n <button\n className={subTrigger}\n role=\"menuitem\"\n aria-haspopup=\"true\"\n aria-expanded={open}\n disabled={disabled}\n type=\"button\"\n tabIndex={-1}\n >\n <span>{label}</span>\n <ChevronRight />\n </button>\n {open && (\n <div\n className={subDropdown}\n role=\"menu\"\n style={{ left: `calc(100% + ${menuOffset}px)` }}\n >\n {children}\n </div>\n )}\n </div>\n );\n};\n\nMenuBarSub.displayName = 'MenuBar.Sub';\n\nconst MenuBarSeparator: React.FC<MenuBarSeparatorProps> = ({\n className,\n style,\n testId,\n ref,\n ...rest\n}) => {\n return (\n <div\n role=\"separator\"\n className={cx(separator, className)}\n style={style}\n data-testid={testId}\n ref={ref}\n {...rest}\n />\n );\n};\n\nMenuBarSeparator.displayName = 'MenuBar.Separator';\n\n// --- MenuBarMenu (top-level dropdown trigger) ---\n\nconst MenuBarMenu: React.FC<MenuBarMenuProps> = ({\n label,\n children,\n disabled = false,\n\n className,\n style,\n testId,\n ref,\n ...rest\n}) => {\n const menuId = useId();\n const {\n openMenuId,\n setOpenMenuId,\n registerMenu,\n unregisterMenu,\n menuOffset,\n } = useMenuBar();\n const triggerRef = useRef<HTMLButtonElement>(null);\n const dropdownRef = useRef<HTMLDivElement>(null);\n const isOpen = openMenuId === menuId;\n\n useEffect(() => {\n registerMenu(menuId);\n return () => {\n unregisterMenu(menuId);\n };\n }, [menuId, registerMenu, unregisterMenu]);\n\n const handleClick = useCallback(() => {\n if (disabled) return;\n setOpenMenuId(isOpen ? null : menuId);\n }, [disabled, isOpen, menuId, setOpenMenuId]);\n\n const handleMouseEnter = useCallback(() => {\n // Open on hover only when another menu is already open\n if (openMenuId !== null && openMenuId !== menuId && !disabled) {\n setOpenMenuId(menuId);\n }\n }, [openMenuId, menuId, disabled, setOpenMenuId]);\n\n const handleKeyDown = useCallback(\n (e: KeyboardEvent<HTMLButtonElement>) => {\n if (e.key === 'ArrowDown' || e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n setOpenMenuId(menuId);\n // Focus first item in dropdown after open\n setTimeout(() => {\n const firstItem = dropdownRef.current?.querySelector(\n MENU_ITEM_SELECTOR\n ) as HTMLElement | null;\n firstItem?.focus();\n }, 0);\n }\n if (e.key === 'Escape') {\n e.preventDefault();\n setOpenMenuId(null);\n triggerRef.current?.focus();\n }\n },\n [menuId, setOpenMenuId]\n );\n\n const handleDropdownKeyDown = useCallback(\n (e: KeyboardEvent<HTMLDivElement>) => {\n const items = Array.from(\n dropdownRef.current?.querySelectorAll<HTMLElement>(\n MENU_ITEM_SELECTOR\n ) ?? []\n );\n const currentIndex = items.indexOf(document.activeElement as HTMLElement);\n\n if (e.key === 'ArrowDown') {\n e.preventDefault();\n const next = currentIndex + 1 < items.length ? currentIndex + 1 : 0;\n items[next]?.focus();\n } else if (e.key === 'ArrowUp') {\n e.preventDefault();\n const prev =\n currentIndex - 1 >= 0 ? currentIndex - 1 : items.length - 1;\n items[prev]?.focus();\n } else if (e.key === 'Escape') {\n e.preventDefault();\n setOpenMenuId(null);\n triggerRef.current?.focus();\n }\n },\n [setOpenMenuId]\n );\n\n // Close dropdown when clicking outside\n useEffect(() => {\n if (!isOpen) return;\n\n const handleOutsideClick = (e: MouseEvent) => {\n const target = e.target as Node;\n if (\n !triggerRef.current?.contains(target) &&\n !dropdownRef.current?.contains(target)\n ) {\n setOpenMenuId(null);\n }\n };\n\n document.addEventListener('mousedown', handleOutsideClick);\n return () => {\n document.removeEventListener('mousedown', handleOutsideClick);\n };\n }, [isOpen, setOpenMenuId]);\n\n return (\n <div\n ref={ref}\n className={cx(menuContainer, className)}\n style={style}\n data-testid={testId}\n {...rest}\n >\n <button\n ref={triggerRef}\n className={trigger({ active: isOpen })}\n onClick={handleClick}\n onMouseEnter={handleMouseEnter}\n onKeyDown={handleKeyDown}\n disabled={disabled}\n type=\"button\"\n role=\"menuitem\"\n aria-haspopup=\"true\"\n aria-expanded={isOpen}\n tabIndex={-1}\n >\n {label}\n </button>\n {isOpen && (\n <div\n ref={dropdownRef}\n className={dropdown}\n role=\"menu\"\n aria-label={label}\n // Focusable programmatically so the open menu can receive focus and\n // handle arrow-key navigation across its items.\n tabIndex={-1}\n onKeyDown={handleDropdownKeyDown}\n style={{ top: `calc(100% + ${menuOffset}px)` }}\n >\n {children}\n </div>\n )}\n </div>\n );\n};\n\nMenuBarMenu.displayName = 'MenuBar.Menu';\n\n// --- Root Component ---\n\nconst MenuBarRoot: React.FC<MenuBarProps> = ({\n size = 'md',\n menuOffset = 2,\n children,\n\n className,\n style,\n testId,\n ref: externalRef,\n ...rest\n}) => {\n const [openMenuId, setOpenMenuId] = useState<string | null>(null);\n // Registered child menu ids, kept in state so the context value stays\n // reactive (previously a ref + forceUpdate, which read the ref during render).\n const [menuIds, setMenuIds] = useState<string[]>([]);\n const safeMenuOffset = Math.max(0, menuOffset);\n const barRef = useRef<HTMLDivElement>(null);\n const setBarRef = useMemo(\n () => (node: HTMLDivElement | null) => {\n barRef.current = node;\n if (typeof externalRef === 'function') {\n externalRef(node);\n } else if (externalRef && typeof externalRef === 'object') {\n externalRef.current = node;\n }\n },\n [externalRef]\n );\n\n const registerMenu = useCallback((id: string) => {\n setMenuIds(prev => (prev.includes(id) ? prev : [...prev, id]));\n }, []);\n\n const unregisterMenu = useCallback((id: string) => {\n setMenuIds(prev => prev.filter(m => m !== id));\n }, []);\n\n const handleBarKeyDown = useCallback(\n (e: KeyboardEvent<HTMLDivElement>) => {\n if (!barRef.current) return;\n\n const triggers = Array.from(\n barRef.current.querySelectorAll<HTMLElement>(\n ':scope > div > [role=\"menuitem\"]'\n )\n );\n const currentIndex = triggers.indexOf(\n document.activeElement as HTMLElement\n );\n\n if (e.key === 'ArrowRight') {\n e.preventDefault();\n const next = currentIndex + 1 < triggers.length ? currentIndex + 1 : 0;\n triggers[next]?.focus();\n // If menu is open, switch to next menu\n if (openMenuId !== null) {\n triggers[next]?.click();\n }\n } else if (e.key === 'ArrowLeft') {\n e.preventDefault();\n const prev =\n currentIndex - 1 >= 0 ? currentIndex - 1 : triggers.length - 1;\n triggers[prev]?.focus();\n if (openMenuId !== null) {\n triggers[prev]?.click();\n }\n }\n },\n [openMenuId]\n );\n\n const contextValue = useMemo(\n () => ({\n size,\n menuOffset: safeMenuOffset,\n openMenuId,\n setOpenMenuId,\n registerMenu,\n unregisterMenu,\n menuIds,\n }),\n [\n size,\n safeMenuOffset,\n openMenuId,\n setOpenMenuId,\n registerMenu,\n unregisterMenu,\n menuIds,\n ]\n );\n\n return (\n <MenuBarContext.Provider value={contextValue}>\n <div\n ref={setBarRef}\n className={cx(menuBarRoot({ size }), className)}\n style={style}\n data-testid={testId}\n role=\"menubar\"\n onKeyDown={handleBarKeyDown}\n tabIndex={0}\n {...rest}\n >\n {children}\n </div>\n </MenuBarContext.Provider>\n );\n};\n\nMenuBarRoot.displayName = 'MenuBar';\n\n// --- Compound Component ---\n\nexport const MenuBar = /*#__PURE__*/ Object.assign(MenuBarRoot, {\n Menu: MenuBarMenu,\n Item: MenuBarItem,\n Sub: MenuBarSub,\n Separator: MenuBarSeparator,\n});\n"],"names":[],"mappings":";;;;;;AAsCA;AAEA;AACE;AACA;AACA;AACA;AACA;AACA;AACA;AACD;AAED;AAEA;AAEA;AAEA;AAGM;AACA;AACD;AAML;AAEA;AAaE;AAEA;;AAEI;;;AAGJ;AAEA;AAkBF;AAEA;AAEA;AAWE;;AAEA;AAEA;AACE;AACE;AACA;;AAEF;;AACF;AAEA;AACE;;;;;AAMA;;AAC0B;AAC1B;;;AAoCJ;AAEA;AAEA;AAOE;AAUF;AAEA;AAEA;AAEA;AAWE;AACA;AAOA;AACA;AACA;;;AAIE;;AAEA;;AAGF;AACE;;;;AAIF;;;;;;AAOA;AAEI;;;;;;;;;AAWA;;;AAGE;;AAEJ;AAIF;AAEI;;AAOA;;AAEE;AACA;;AACK;;;AAIL;;AACK;;;AAGL;;AAEJ;;;AAMA;;AAEA;AACE;;;;;AAOF;AAEA;AACA;AACE;AACF;AACF;AAEA;;;;AAwCF;AAEA;AAEA;AAEA;;;;;;AAgBE;;AAGI;AACA;;;AAEO;AACL;;AAEJ;AAIF;;;AAIA;AACE;;AAGF;;;AAII;;AASA;;AAEE;AACA;;AAEA;AACE;;;AAEG;;;AAIL;AACA;AACE;;;AAGN;AAIF;;AAGI;;;;;;AAMD;;;;;;;;AASA;;AAmBL;AAEA;AAEA;AAEO;AACL;AACA;AACA;AACA;AACD;;"}
|
|
1
|
+
{"version":3,"file":"MenuBar.js","sources":["src/components/shell/MenuBar/MenuBar.tsx"],"sourcesContent":["'use client';\n\nimport React, {\n createContext,\n useContext,\n useState,\n useCallback,\n useRef,\n useEffect,\n useId,\n KeyboardEvent,\n useMemo,\n} from 'react';\n\nimport { CheckIcon } from '@/components/Icons/CheckIcon';\nimport { CircleIcon } from '@/components/Icons/CircleIcon';\nimport { cx } from '@/utils/cx';\n\nimport {\n menuBarRoot,\n menuContainer,\n trigger,\n dropdown,\n menuItem,\n shortcut,\n separator,\n subTrigger,\n subDropdown,\n subContainer,\n indicatorSlot,\n} from './MenuBar.css';\n\nimport type {\n MenuBarProps,\n MenuBarMenuProps,\n MenuBarItemProps,\n MenuBarSubProps,\n MenuBarSeparatorProps,\n MenuBarContextValue,\n MenuBarCheckboxItemProps,\n MenuBarRadioGroupProps,\n MenuBarRadioItemProps,\n MenuBarRadioGroupContextValue,\n} from './MenuBar.types';\n\n// --- Context ---\n\nconst MenuBarContext = /*#__PURE__*/ createContext<MenuBarContextValue>({\n size: 'md',\n menuOffset: 2,\n openMenuId: null,\n setOpenMenuId: () => {},\n registerMenu: () => {},\n unregisterMenu: () => {},\n menuIds: [],\n});\n\nconst useMenuBar = () => useContext(MenuBarContext);\n\nconst SUBMENU_CLOSE_DELAY = 150;\n\nconst MENU_ITEM_SELECTOR =\n '[role=\"menuitem\"]:not(:disabled), [role=\"menuitemcheckbox\"]:not(:disabled), [role=\"menuitemradio\"]:not(:disabled)';\n\nconst ChevronRight = () => (\n <span\n style={{\n fontSize: '8px',\n lineHeight: 1,\n }}\n >\n ▶\n </span>\n);\n\n// --- Sub-components ---\n\nconst MenuBarItem: React.FC<MenuBarItemProps> = ({\n onClick,\n shortcut: shortcutText,\n disabled = false,\n icon,\n children,\n\n className,\n style,\n testId,\n ref,\n ...rest\n}) => {\n const { setOpenMenuId } = useMenuBar();\n\n const handleClick = useCallback(() => {\n if (onClick) {\n onClick();\n setOpenMenuId(null);\n }\n }, [onClick, setOpenMenuId]);\n\n return (\n <button\n role=\"menuitem\"\n onClick={handleClick}\n disabled={disabled}\n className={cx(menuItem, className)}\n style={style}\n data-testid={testId}\n ref={ref}\n type=\"button\"\n tabIndex={-1}\n {...rest}\n >\n {icon && <span>{icon}</span>}\n <span>{children}</span>\n {shortcutText && <span className={shortcut}>{shortcutText}</span>}\n </button>\n );\n};\n\nMenuBarItem.displayName = 'MenuBar.Item';\n\n// --- Checkable items (checkbox / radio) ---\n\nconst MenuBarRadioContext =\n /*#__PURE__*/ createContext<MenuBarRadioGroupContextValue | null>(null);\n\nconst MenuBarCheckboxItem: React.FC<MenuBarCheckboxItemProps> = ({\n checked,\n defaultChecked = false,\n onCheckedChange,\n shortcut: shortcutText,\n indicator = <CheckIcon size=\"sm\" />,\n closeOnClick = false,\n disabled = false,\n children,\n\n className,\n style,\n testId,\n ref,\n ...rest\n}) => {\n const { setOpenMenuId } = useMenuBar();\n const [internalChecked, setInternalChecked] = useState(defaultChecked);\n const isControlled = checked !== undefined;\n const isChecked = isControlled ? checked : internalChecked;\n\n const handleClick = useCallback(() => {\n const next = !isChecked;\n if (!isControlled) setInternalChecked(next);\n onCheckedChange?.(next);\n if (closeOnClick) setOpenMenuId(null);\n }, [isChecked, isControlled, onCheckedChange, closeOnClick, setOpenMenuId]);\n\n return (\n <button\n role=\"menuitemcheckbox\"\n aria-checked={isChecked}\n onClick={handleClick}\n disabled={disabled}\n className={cx(menuItem, className)}\n style={style}\n data-testid={testId}\n ref={ref}\n type=\"button\"\n tabIndex={-1}\n {...rest}\n >\n <span className={indicatorSlot} aria-hidden=\"true\">\n {isChecked ? indicator : null}\n </span>\n <span>{children}</span>\n {shortcutText && <span className={shortcut}>{shortcutText}</span>}\n </button>\n );\n};\n\nMenuBarCheckboxItem.displayName = 'MenuBar.CheckboxItem';\n\nconst MenuBarRadioGroup: React.FC<MenuBarRadioGroupProps> = ({\n value,\n defaultValue,\n onValueChange,\n children,\n\n className,\n style,\n testId,\n ref,\n ...rest\n}) => {\n const [internalValue, setInternalValue] = useState<string | undefined>(\n defaultValue\n );\n const isControlled = value !== undefined;\n const selectedValue = isControlled ? value : internalValue;\n\n const setValue = useCallback(\n (next: string) => {\n if (!isControlled) setInternalValue(next);\n onValueChange?.(next);\n },\n [isControlled, onValueChange]\n );\n\n const contextValue = useMemo(\n () => ({ value: selectedValue, setValue }),\n [selectedValue, setValue]\n );\n\n return (\n <MenuBarRadioContext.Provider value={contextValue}>\n <div\n role=\"group\"\n className={className}\n style={style}\n data-testid={testId}\n ref={ref}\n {...rest}\n >\n {children}\n </div>\n </MenuBarRadioContext.Provider>\n );\n};\n\nMenuBarRadioGroup.displayName = 'MenuBar.RadioGroup';\n\nconst MenuBarRadioItem: React.FC<MenuBarRadioItemProps> = ({\n value,\n shortcut: shortcutText,\n indicator = <CircleIcon size=\"sm\" />,\n closeOnClick = false,\n disabled = false,\n children,\n\n className,\n style,\n testId,\n ref,\n ...rest\n}) => {\n const { setOpenMenuId } = useMenuBar();\n const group = useContext(MenuBarRadioContext);\n const isChecked = group?.value === value;\n\n const handleClick = useCallback(() => {\n group?.setValue(value);\n if (closeOnClick) setOpenMenuId(null);\n }, [group, value, closeOnClick, setOpenMenuId]);\n\n return (\n <button\n role=\"menuitemradio\"\n aria-checked={isChecked}\n onClick={handleClick}\n disabled={disabled}\n className={cx(menuItem, className)}\n style={style}\n data-testid={testId}\n ref={ref}\n type=\"button\"\n tabIndex={-1}\n {...rest}\n >\n <span className={indicatorSlot} aria-hidden=\"true\">\n {isChecked ? indicator : null}\n </span>\n <span>{children}</span>\n {shortcutText && <span className={shortcut}>{shortcutText}</span>}\n </button>\n );\n};\n\nMenuBarRadioItem.displayName = 'MenuBar.RadioItem';\n\nconst MenuBarSub: React.FC<MenuBarSubProps> = ({\n label,\n children,\n disabled = false,\n\n className,\n style,\n testId,\n ref,\n ...rest\n}) => {\n const { menuOffset } = useMenuBar();\n const [open, setOpen] = useState(false);\n const closeTimer = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n const handleEnter = useCallback(() => {\n if (closeTimer.current) {\n clearTimeout(closeTimer.current);\n closeTimer.current = null;\n }\n if (!disabled) setOpen(true);\n }, [disabled]);\n\n const handleLeave = useCallback(() => {\n closeTimer.current = setTimeout(() => {\n setOpen(false);\n }, SUBMENU_CLOSE_DELAY);\n }, []);\n\n useEffect(() => {\n return () => {\n if (closeTimer.current) clearTimeout(closeTimer.current);\n };\n }, []);\n\n return (\n <div\n ref={ref}\n className={cx(subContainer, className)}\n style={style}\n data-testid={testId}\n onMouseEnter={handleEnter}\n onMouseLeave={handleLeave}\n {...rest}\n >\n <button\n className={subTrigger}\n role=\"menuitem\"\n aria-haspopup=\"true\"\n aria-expanded={open}\n disabled={disabled}\n type=\"button\"\n tabIndex={-1}\n >\n <span>{label}</span>\n <ChevronRight />\n </button>\n {open && (\n <div\n className={subDropdown}\n role=\"menu\"\n style={{ left: `calc(100% + ${menuOffset}px)` }}\n >\n {children}\n </div>\n )}\n </div>\n );\n};\n\nMenuBarSub.displayName = 'MenuBar.Sub';\n\nconst MenuBarSeparator: React.FC<MenuBarSeparatorProps> = ({\n className,\n style,\n testId,\n ref,\n ...rest\n}) => {\n return (\n <div\n role=\"separator\"\n className={cx(separator, className)}\n style={style}\n data-testid={testId}\n ref={ref}\n {...rest}\n />\n );\n};\n\nMenuBarSeparator.displayName = 'MenuBar.Separator';\n\n// --- MenuBarMenu (top-level dropdown trigger) ---\n\nconst MenuBarMenu: React.FC<MenuBarMenuProps> = ({\n label,\n children,\n disabled = false,\n\n className,\n style,\n testId,\n ref,\n ...rest\n}) => {\n const menuId = useId();\n const {\n openMenuId,\n setOpenMenuId,\n registerMenu,\n unregisterMenu,\n menuOffset,\n } = useMenuBar();\n const triggerRef = useRef<HTMLButtonElement>(null);\n const dropdownRef = useRef<HTMLDivElement>(null);\n const isOpen = openMenuId === menuId;\n\n useEffect(() => {\n registerMenu(menuId);\n return () => {\n unregisterMenu(menuId);\n };\n }, [menuId, registerMenu, unregisterMenu]);\n\n const handleClick = useCallback(() => {\n if (disabled) return;\n setOpenMenuId(isOpen ? null : menuId);\n }, [disabled, isOpen, menuId, setOpenMenuId]);\n\n const handleMouseEnter = useCallback(() => {\n // Open on hover only when another menu is already open\n if (openMenuId !== null && openMenuId !== menuId && !disabled) {\n setOpenMenuId(menuId);\n }\n }, [openMenuId, menuId, disabled, setOpenMenuId]);\n\n const handleKeyDown = useCallback(\n (e: KeyboardEvent<HTMLButtonElement>) => {\n if (e.key === 'ArrowDown' || e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n setOpenMenuId(menuId);\n // Focus first item in dropdown after open\n setTimeout(() => {\n const firstItem = dropdownRef.current?.querySelector(\n MENU_ITEM_SELECTOR\n ) as HTMLElement | null;\n firstItem?.focus();\n }, 0);\n }\n if (e.key === 'Escape') {\n e.preventDefault();\n setOpenMenuId(null);\n triggerRef.current?.focus();\n }\n },\n [menuId, setOpenMenuId]\n );\n\n const handleDropdownKeyDown = useCallback(\n (e: KeyboardEvent<HTMLDivElement>) => {\n const items = Array.from(\n dropdownRef.current?.querySelectorAll<HTMLElement>(\n MENU_ITEM_SELECTOR\n ) ?? []\n );\n const currentIndex = items.indexOf(document.activeElement as HTMLElement);\n\n if (e.key === 'ArrowDown') {\n e.preventDefault();\n const next = currentIndex + 1 < items.length ? currentIndex + 1 : 0;\n items[next]?.focus();\n } else if (e.key === 'ArrowUp') {\n e.preventDefault();\n const prev =\n currentIndex - 1 >= 0 ? currentIndex - 1 : items.length - 1;\n items[prev]?.focus();\n } else if (e.key === 'Escape') {\n e.preventDefault();\n setOpenMenuId(null);\n triggerRef.current?.focus();\n }\n },\n [setOpenMenuId]\n );\n\n // Close dropdown when clicking outside\n useEffect(() => {\n if (!isOpen) return;\n\n const handleOutsideClick = (e: MouseEvent) => {\n const target = e.target as Node;\n if (\n !triggerRef.current?.contains(target) &&\n !dropdownRef.current?.contains(target)\n ) {\n setOpenMenuId(null);\n }\n };\n\n document.addEventListener('mousedown', handleOutsideClick);\n return () => {\n document.removeEventListener('mousedown', handleOutsideClick);\n };\n }, [isOpen, setOpenMenuId]);\n\n return (\n <div\n ref={ref}\n className={cx(menuContainer, className)}\n style={style}\n data-testid={testId}\n {...rest}\n >\n <button\n ref={triggerRef}\n className={trigger({ active: isOpen })}\n onClick={handleClick}\n onMouseEnter={handleMouseEnter}\n onKeyDown={handleKeyDown}\n disabled={disabled}\n type=\"button\"\n role=\"menuitem\"\n aria-haspopup=\"true\"\n aria-expanded={isOpen}\n tabIndex={-1}\n >\n {label}\n </button>\n {isOpen && (\n <div\n ref={dropdownRef}\n className={dropdown}\n role=\"menu\"\n aria-label={label}\n // Focusable programmatically so the open menu can receive focus and\n // handle arrow-key navigation across its items.\n tabIndex={-1}\n onKeyDown={handleDropdownKeyDown}\n style={{ top: `calc(100% + ${menuOffset}px)` }}\n >\n {children}\n </div>\n )}\n </div>\n );\n};\n\nMenuBarMenu.displayName = 'MenuBar.Menu';\n\n// --- Root Component ---\n\nconst MenuBarRoot: React.FC<MenuBarProps> = ({\n size = 'md',\n menuOffset = 2,\n children,\n\n className,\n style,\n testId,\n ref: externalRef,\n ...rest\n}) => {\n const [openMenuId, setOpenMenuId] = useState<string | null>(null);\n // Registered child menu ids, kept in state so the context value stays\n // reactive (previously a ref + forceUpdate, which read the ref during render).\n const [menuIds, setMenuIds] = useState<string[]>([]);\n const safeMenuOffset = Math.max(0, menuOffset);\n const barRef = useRef<HTMLDivElement>(null);\n const setBarRef = useMemo(\n () => (node: HTMLDivElement | null) => {\n barRef.current = node;\n if (typeof externalRef === 'function') {\n externalRef(node);\n } else if (externalRef && typeof externalRef === 'object') {\n externalRef.current = node;\n }\n },\n [externalRef]\n );\n\n const registerMenu = useCallback((id: string) => {\n setMenuIds(prev => (prev.includes(id) ? prev : [...prev, id]));\n }, []);\n\n const unregisterMenu = useCallback((id: string) => {\n setMenuIds(prev => prev.filter(m => m !== id));\n }, []);\n\n const handleBarKeyDown = useCallback(\n (e: KeyboardEvent<HTMLDivElement>) => {\n if (!barRef.current) return;\n\n const triggers = Array.from(\n barRef.current.querySelectorAll<HTMLElement>(\n ':scope > div > [role=\"menuitem\"]'\n )\n );\n const currentIndex = triggers.indexOf(\n document.activeElement as HTMLElement\n );\n\n if (e.key === 'ArrowRight') {\n e.preventDefault();\n const next = currentIndex + 1 < triggers.length ? currentIndex + 1 : 0;\n triggers[next]?.focus();\n // If menu is open, switch to next menu\n if (openMenuId !== null) {\n triggers[next]?.click();\n }\n } else if (e.key === 'ArrowLeft') {\n e.preventDefault();\n const prev =\n currentIndex - 1 >= 0 ? currentIndex - 1 : triggers.length - 1;\n triggers[prev]?.focus();\n if (openMenuId !== null) {\n triggers[prev]?.click();\n }\n }\n },\n [openMenuId]\n );\n\n const contextValue = useMemo(\n () => ({\n size,\n menuOffset: safeMenuOffset,\n openMenuId,\n setOpenMenuId,\n registerMenu,\n unregisterMenu,\n menuIds,\n }),\n [\n size,\n safeMenuOffset,\n openMenuId,\n setOpenMenuId,\n registerMenu,\n unregisterMenu,\n menuIds,\n ]\n );\n\n return (\n <MenuBarContext.Provider value={contextValue}>\n <div\n ref={setBarRef}\n className={cx(menuBarRoot({ size }), className)}\n style={style}\n data-testid={testId}\n role=\"menubar\"\n onKeyDown={handleBarKeyDown}\n tabIndex={0}\n {...rest}\n >\n {children}\n </div>\n </MenuBarContext.Provider>\n );\n};\n\nMenuBarRoot.displayName = 'MenuBar';\n\n// --- Compound Component ---\n\nexport const MenuBar = /*#__PURE__*/ Object.assign(MenuBarRoot, {\n Menu: MenuBarMenu,\n Item: MenuBarItem,\n CheckboxItem: MenuBarCheckboxItem,\n RadioGroup: MenuBarRadioGroup,\n RadioItem: MenuBarRadioItem,\n Sub: MenuBarSub,\n Separator: MenuBarSeparator,\n});\n"],"names":[],"mappings":";;;;;;;;AA6CA;AAEA;AACE;AACA;AACA;AACA;AACA;AACA;AACA;AACD;AAED;AAEA;AAEA;AAGA;AAGM;AACA;AACD;AAML;AAEA;AAaE;AAEA;;AAEI;;;AAGJ;AAEA;AAkBF;AAEA;AAEA;AAEA;AACE;AAEF;AAgBE;;AAEA;;AAGA;AACE;AACA;;AACA;AACA;;AACF;AAEA;AAqBF;AAEA;AAEA;;AAeE;;AAGA;AAEI;;AACA;AACF;;AASF;AAcF;AAEA;AAEA;AAcE;AACA;AACA;AAEA;AACE;AACA;;;AAGF;AAqBF;AAEA;AAEA;AAWE;;AAEA;AAEA;AACE;AACE;AACA;;AAEF;;AACF;AAEA;AACE;;;;;AAMA;;AAC0B;AAC1B;;;AAoCJ;AAEA;AAEA;AAOE;AAUF;AAEA;AAEA;AAEA;AAWE;AACA;AAOA;AACA;AACA;;;AAIE;;AAEA;;AAGF;AACE;;;;AAIF;;;;;;AAOA;AAEI;;;;;;;;;AAWA;;;AAGE;;AAEJ;AAIF;AAEI;;AAOA;;AAEE;AACA;;AACK;;;AAIL;;AACK;;;AAGL;;AAEJ;;;AAMA;;AAEA;AACE;;;;;AAOF;AAEA;AACA;AACE;AACF;AACF;AAEA;;;;AAwCF;AAEA;AAEA;AAEA;;;;;;AAgBE;;AAGI;AACA;;;AAEO;AACL;;AAEJ;AAIF;;;AAIA;AACE;;AAGF;;;AAII;;AASA;;AAEE;AACA;;AAEA;AACE;;;AAEG;;;AAIL;AACA;AACE;;;AAGN;AAIF;;AAGI;;;;;;AAMD;;;;;;;;AASA;;AAmBL;AAEA;AAEA;AAEO;AACL;AACA;AACA;AACA;AACA;AACA;AACA;AACD;;"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"styles.css.js","sources":[],"sourcesContent":[],"names":[],"mappings":";"}
|
package/dist/tokens/tokens.json
CHANGED
|
@@ -96,6 +96,13 @@ interface SliderBaseProps extends Omit<BaseComponent, 'onChange'> {
|
|
|
96
96
|
* @default true
|
|
97
97
|
*/
|
|
98
98
|
showTooltip?: boolean;
|
|
99
|
+
/**
|
|
100
|
+
* Placement of the value tooltip relative to the thumb. The tooltip is
|
|
101
|
+
* portaled to `document.body`, so it is never clipped by an ancestor's
|
|
102
|
+
* `overflow`.
|
|
103
|
+
* @default "top"
|
|
104
|
+
*/
|
|
105
|
+
tooltipPlacement?: 'top' | 'bottom';
|
|
99
106
|
/**
|
|
100
107
|
* Show tick marks along the track
|
|
101
108
|
* @default false
|
|
@@ -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
|
|
61
|
+
children?: React.ReactNode;
|
|
62
|
+
icon?: React.ReactNode;
|
|
62
63
|
size?: IconButtonSize | undefined;
|
|
63
64
|
variant?: IconButtonVariant | undefined;
|
|
64
65
|
radius?: IconButtonRadius | undefined;
|
|
@@ -2,8 +2,11 @@ import React from 'react';
|
|
|
2
2
|
import { ViewportProps } from './Viewport.types.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* Viewport — pan/zoom canvas + HTML container for editor-style
|
|
6
|
-
* such as node graphs, timelines, and 2D world editors.
|
|
5
|
+
* Viewport — **2D only.** A pan/zoom canvas + HTML container for editor-style
|
|
6
|
+
* surfaces such as node graphs, timelines, and 2D world editors. Despite the
|
|
7
|
+
* name it is *not* a 3D viewport: it owns a 2D pan/zoom transform, not a camera.
|
|
8
|
+
* For 3D, host your own WebGL/WebGPU canvas (e.g. inside an `AppShell.Dock`) and
|
|
9
|
+
* reach for `Viewport` only for 2D editor surfaces.
|
|
7
10
|
*
|
|
8
11
|
* Composes:
|
|
9
12
|
* - Multiple perf-isolated `<ViewportLayer>` canvases (one draw cycle each).
|