entangle-ui 0.11.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +81 -0
- package/dist/esm/assets/src/components/controls/Slider/{Slider.css.ts.vanilla-Cqm3fQ0S.css → Slider.css.ts.vanilla-CLf_RC0A.css} +50 -47
- package/dist/esm/assets/src/components/navigation/Menu/{Menu.css.ts.vanilla-B5iVe5S2.css → Menu.css.ts.vanilla-Bhh1L5yl.css} +14 -11
- package/dist/esm/assets/src/components/primitives/EditableText/EditableText.css.ts.vanilla-CrVGgD19.css +63 -0
- package/dist/esm/assets/src/components/primitives/Tooltip/{Tooltip.css.ts.vanilla-CrEFoLBX.css → Tooltip.css.ts.vanilla-BqaiGrFQ.css} +12 -9
- package/dist/esm/assets/src/components/shell/AppShell/{AppShell.css.ts.vanilla-BYmtU0O_.css → AppShell.css.ts.vanilla-BLK2-DRL.css} +1 -0
- package/dist/esm/assets/src/components/shell/MenuBar/{MenuBar.css.ts.vanilla-TgPcl4yA.css → MenuBar.css.ts.vanilla-Dt3w4f-q.css} +18 -6
- package/dist/esm/components/controls/Slider/Slider.css.js +10 -11
- package/dist/esm/components/controls/Slider/Slider.css.js.map +1 -1
- package/dist/esm/components/controls/Slider/Slider.js +40 -7
- package/dist/esm/components/controls/Slider/Slider.js.map +1 -1
- package/dist/esm/components/editor/ViewportGizmo/ViewportGizmo.js +3 -1
- package/dist/esm/components/editor/ViewportGizmo/ViewportGizmo.js.map +1 -1
- package/dist/esm/components/editor/ViewportGizmo/useGizmoInteraction.js +21 -27
- package/dist/esm/components/editor/ViewportGizmo/useGizmoInteraction.js.map +1 -1
- package/dist/esm/components/feedback/Spinner/Spinner.js +12 -3
- package/dist/esm/components/feedback/Spinner/Spinner.js.map +1 -1
- package/dist/esm/components/navigation/Breadcrumbs/BreadcrumbEllipsis.js +1 -0
- package/dist/esm/components/navigation/Breadcrumbs/BreadcrumbEllipsis.js.map +1 -1
- package/dist/esm/components/navigation/Breadcrumbs/BreadcrumbItem.js +1 -0
- package/dist/esm/components/navigation/Breadcrumbs/BreadcrumbItem.js.map +1 -1
- package/dist/esm/components/navigation/ContextMenu/ContextMenu.js +2 -2
- package/dist/esm/components/navigation/ContextMenu/ContextMenu.js.map +1 -1
- package/dist/esm/components/navigation/Menu/Menu.css.js +11 -10
- package/dist/esm/components/navigation/Menu/Menu.css.js.map +1 -1
- package/dist/esm/components/navigation/Menu/Menu.js +2 -2
- package/dist/esm/components/navigation/Menu/Menu.js.map +1 -1
- package/dist/esm/components/navigation/PathBar/PathBarSiblingMenu.js +1 -0
- package/dist/esm/components/navigation/PathBar/PathBarSiblingMenu.js.map +1 -1
- package/dist/esm/components/primitives/EditableText/EditableText.css.js +11 -0
- package/dist/esm/components/primitives/EditableText/EditableText.css.js.map +1 -0
- package/dist/esm/components/primitives/EditableText/EditableText.js +208 -0
- package/dist/esm/components/primitives/EditableText/EditableText.js.map +1 -0
- package/dist/esm/components/primitives/EditableText/editableTextLabels.js +16 -0
- package/dist/esm/components/primitives/EditableText/editableTextLabels.js.map +1 -0
- package/dist/esm/components/primitives/IconButton/IconButton.js +4 -2
- package/dist/esm/components/primitives/IconButton/IconButton.js.map +1 -1
- package/dist/esm/components/primitives/Tooltip/Tooltip.css.js +6 -5
- package/dist/esm/components/primitives/Tooltip/Tooltip.css.js.map +1 -1
- package/dist/esm/components/primitives/Tooltip/Tooltip.js +12 -2
- package/dist/esm/components/primitives/Tooltip/Tooltip.js.map +1 -1
- package/dist/esm/components/primitives/viewport/Viewport.js +5 -2
- package/dist/esm/components/primitives/viewport/Viewport.js.map +1 -1
- package/dist/esm/components/shell/AppShell/AppShell.css.js +1 -1
- package/dist/esm/components/shell/MenuBar/MenuBar.css.js +7 -6
- package/dist/esm/components/shell/MenuBar/MenuBar.css.js.map +1 -1
- package/dist/esm/components/shell/MenuBar/MenuBar.js +52 -3
- package/dist/esm/components/shell/MenuBar/MenuBar.js.map +1 -1
- package/dist/esm/index.js +2 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/{styles.js → styles.css.js} +1 -1
- package/dist/esm/styles.css.js.map +1 -0
- package/dist/tokens/tokens.dark.css +1 -1
- package/dist/tokens/tokens.json +1 -1
- package/dist/tokens/tokens.light.css +1 -1
- package/dist/types/components/controls/Slider/Slider.d.ts +7 -0
- package/dist/types/components/editor/ViewportGizmo/ViewportGizmo.types.d.ts +26 -3
- package/dist/types/components/feedback/Spinner/Spinner.d.ts +6 -1
- package/dist/types/components/feedback/Spinner/Spinner.types.d.ts +8 -0
- package/dist/types/components/primitives/EditableText/EditableText.d.ts +37 -0
- package/dist/types/components/primitives/EditableText/EditableText.types.d.ts +158 -0
- package/dist/types/components/primitives/EditableText/editableTextLabels.d.ts +11 -0
- package/dist/types/components/primitives/IconButton/IconButton.d.ts +2 -1
- package/dist/types/components/primitives/viewport/Viewport.d.ts +5 -2
- package/dist/types/components/shell/MenuBar/MenuBar.d.ts +862 -0
- package/dist/types/index.d.ts +3 -0
- package/package.json +2 -2
- package/dist/esm/styles.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Menu.js","sources":["src/components/navigation/Menu/Menu.tsx"],"sourcesContent":["'use client';\n\nimport { Menu as BaseMenu } from '@base-ui/react/menu';\nimport React from 'react';\n\nimport { CheckIcon } from '@/components/Icons/CheckIcon';\nimport { ChevronRightIcon } from '@/components/Icons/ChevronRightIcon';\nimport { CircleIcon } from '@/components/Icons/CircleIcon';\nimport { Button } from '@/components/primitives/Button';\nimport { useLatest } from '@/hooks/useLatest';\nimport { cx } from '@/utils/cx';\n\nimport {\n menuContentStyle,\n menuItemStyle,\n itemStartSlotStyle,\n itemLabelStyle,\n itemEndSlotStyle,\n shortcutStyle,\n groupLabelStyle,\n separatorStyle,\n} from './Menu.css';\nimport { MenuGapContext, DEFAULT_MENU_GAP } from './MenuGapContext';\n\nimport type {\n MenuRootProps,\n MenuTriggerProps,\n MenuContentProps,\n MenuItemProps,\n MenuGroupProps,\n MenuSeparatorProps,\n MenuRadioGroupProps,\n MenuRadioItemProps,\n MenuCheckboxItemProps,\n MenuSubProps,\n MenuSubTriggerProps,\n} from './Menu.types';\nimport type { MenuRootActions } from '@base-ui/react/menu';\n\n/**\n * Internal three-slot row used by every interactive menu entry.\n */\nconst ItemLayout = ({\n start,\n children,\n shortcut,\n endContent,\n}: {\n start?: React.ReactNode;\n children?: React.ReactNode;\n shortcut?: React.ReactNode;\n endContent?: React.ReactNode;\n}): React.ReactElement => (\n <>\n {start != null && <span className={itemStartSlotStyle}>{start}</span>}\n <span className={itemLabelStyle}>{children}</span>\n {(shortcut != null || endContent != null) && (\n <span className={itemEndSlotStyle}>\n {shortcut != null && <span className={shortcutStyle}>{shortcut}</span>}\n {endContent}\n </span>\n )}\n </>\n);\n\n/**\n * Merge `onClick` and its `onSelect` alias into one stable handler so a new\n * function identity isn't handed to Base UI on every render. Returns\n * `undefined` when neither is set, so no listener is attached.\n */\nconst useActivate = (\n onClick?: React.MouseEventHandler<HTMLElement>,\n onSelect?: React.MouseEventHandler<HTMLElement>\n): React.MouseEventHandler<HTMLElement> | undefined => {\n const handlersRef = useLatest({ onClick, onSelect });\n const handleActivate = React.useCallback<\n React.MouseEventHandler<HTMLElement>\n >(\n event => {\n handlersRef.current.onClick?.(event);\n handlersRef.current.onSelect?.(event);\n },\n [handlersRef]\n );\n return onClick || onSelect ? handleActivate : undefined;\n};\n\n/**\n * Composable menu for editor interfaces, built on `@base-ui/react` Menu\n * primitives. Compose the trigger, content and items as children — no config\n * object.\n *\n * @example\n * ```tsx\n * <Menu>\n * <Menu.Trigger>File</Menu.Trigger>\n * <Menu.Content>\n * <Menu.Item icon={<SaveIcon />} shortcut=\"⌘S\" onClick={save}>\n * Save\n * </Menu.Item>\n * <Menu.Separator />\n * <Menu.Sub>\n * <Menu.SubTrigger>Export</Menu.SubTrigger>\n * <Menu.SubContent>\n * <Menu.Item>PNG</Menu.Item>\n * <Menu.Item>SVG</Menu.Item>\n * </Menu.SubContent>\n * </Menu.Sub>\n * </Menu.Content>\n * </Menu>\n * ```\n */\nconst MenuRoot = ({\n children,\n open,\n defaultOpen,\n onOpenChange,\n modal,\n disabled,\n gap = DEFAULT_MENU_GAP,\n ref,\n}: MenuRootProps): React.ReactElement => {\n const actionsRef = React.useRef<MenuRootActions | null>(null);\n React.useImperativeHandle(\n ref,\n () => ({ close: () => actionsRef.current?.close() }),\n []\n );\n return (\n <MenuGapContext.Provider value={gap}>\n <BaseMenu.Root\n open={open}\n defaultOpen={defaultOpen}\n onOpenChange={onOpenChange}\n modal={modal}\n disabled={disabled}\n actionsRef={actionsRef}\n >\n {children}\n </BaseMenu.Root>\n </MenuGapContext.Provider>\n );\n};\nMenuRoot.displayName = 'Menu';\n\nconst MenuTrigger = ({\n children,\n render,\n disabled,\n openOnHover,\n className,\n testId,\n ...rest\n}: MenuTriggerProps): React.ReactElement => (\n <BaseMenu.Trigger\n render={render ?? <Button />}\n disabled={disabled}\n openOnHover={openOnHover}\n className={className}\n data-testid={testId}\n {...rest}\n >\n {children}\n </BaseMenu.Trigger>\n);\nMenuTrigger.displayName = 'Menu.Trigger';\n\nconst MenuContent = ({\n children,\n className,\n side,\n align = 'start',\n sideOffset,\n alignOffset,\n testId,\n ref,\n ...rest\n}: MenuContentProps): React.ReactElement => {\n const gap = React.useContext(MenuGapContext);\n return (\n <BaseMenu.Portal>\n <BaseMenu.Positioner\n side={side}\n align={align}\n sideOffset={sideOffset ?? gap}\n alignOffset={alignOffset}\n >\n <BaseMenu.Popup\n ref={ref}\n className={cx(menuContentStyle, className)}\n data-testid={testId}\n {...rest}\n >\n {children}\n </BaseMenu.Popup>\n </BaseMenu.Positioner>\n </BaseMenu.Portal>\n );\n};\nMenuContent.displayName = 'Menu.Content';\n\nconst MenuItem = React.memo(\n ({\n children,\n icon,\n shortcut,\n endContent,\n disabled,\n closeOnClick = true,\n onClick,\n onSelect,\n className,\n testId,\n ref,\n ...rest\n }: MenuItemProps): React.ReactElement => {\n const handleActivate = useActivate(onClick, onSelect);\n return (\n <BaseMenu.Item\n ref={ref}\n disabled={disabled}\n closeOnClick={closeOnClick}\n onClick={handleActivate}\n className={cx(menuItemStyle, className)}\n data-testid={testId}\n {...rest}\n >\n <ItemLayout start={icon} shortcut={shortcut} endContent={endContent}>\n {children}\n </ItemLayout>\n </BaseMenu.Item>\n );\n }\n);\nMenuItem.displayName = 'Menu.Item';\n\nconst MenuGroup = ({\n children,\n label,\n className,\n testId,\n ...rest\n}: MenuGroupProps): React.ReactElement => (\n <BaseMenu.Group className={className} data-testid={testId} {...rest}>\n {label != null && (\n <BaseMenu.GroupLabel className={groupLabelStyle}>\n {label}\n </BaseMenu.GroupLabel>\n )}\n {children}\n </BaseMenu.Group>\n);\nMenuGroup.displayName = 'Menu.Group';\n\nconst MenuSeparator = React.memo(\n ({ className, testId, ...rest }: MenuSeparatorProps): React.ReactElement => (\n <BaseMenu.Separator\n className={cx(separatorStyle, className)}\n data-testid={testId}\n {...rest}\n />\n )\n);\nMenuSeparator.displayName = 'Menu.Separator';\n\nconst MenuRadioGroup = ({\n children,\n value,\n defaultValue,\n onValueChange,\n className,\n testId,\n ...rest\n}: MenuRadioGroupProps): React.ReactElement => (\n <BaseMenu.RadioGroup\n value={value}\n defaultValue={defaultValue}\n onValueChange={onValueChange}\n className={className}\n data-testid={testId}\n {...rest}\n >\n {children}\n </BaseMenu.RadioGroup>\n);\nMenuRadioGroup.displayName = 'Menu.RadioGroup';\n\nconst MenuRadioItem = React.memo(\n ({\n children,\n value,\n indicator = <CircleIcon size=\"sm\" />,\n shortcut,\n endContent,\n disabled,\n closeOnClick = false,\n onClick,\n onSelect,\n className,\n testId,\n ref,\n ...rest\n }: MenuRadioItemProps): React.ReactElement => {\n const handleActivate = useActivate(onClick, onSelect);\n return (\n <BaseMenu.RadioItem\n ref={ref}\n value={value}\n disabled={disabled}\n closeOnClick={closeOnClick}\n onClick={handleActivate}\n className={cx(menuItemStyle, className)}\n data-testid={testId}\n {...rest}\n >\n <ItemLayout\n start={\n <BaseMenu.RadioItemIndicator>\n {indicator}\n </BaseMenu.RadioItemIndicator>\n }\n shortcut={shortcut}\n endContent={endContent}\n >\n {children}\n </ItemLayout>\n </BaseMenu.RadioItem>\n );\n }\n);\nMenuRadioItem.displayName = 'Menu.RadioItem';\n\nconst MenuCheckboxItem = React.memo(\n ({\n children,\n checked,\n defaultChecked,\n onCheckedChange,\n indicator = <CheckIcon size=\"sm\" />,\n shortcut,\n endContent,\n disabled,\n closeOnClick = false,\n onClick,\n onSelect,\n className,\n testId,\n ref,\n ...rest\n }: MenuCheckboxItemProps): React.ReactElement => {\n const handleActivate = useActivate(onClick, onSelect);\n return (\n <BaseMenu.CheckboxItem\n ref={ref}\n checked={checked}\n defaultChecked={defaultChecked}\n onCheckedChange={onCheckedChange}\n disabled={disabled}\n closeOnClick={closeOnClick}\n onClick={handleActivate}\n className={cx(menuItemStyle, className)}\n data-testid={testId}\n {...rest}\n >\n <ItemLayout\n start={\n <BaseMenu.CheckboxItemIndicator>\n {indicator}\n </BaseMenu.CheckboxItemIndicator>\n }\n shortcut={shortcut}\n endContent={endContent}\n >\n {children}\n </ItemLayout>\n </BaseMenu.CheckboxItem>\n );\n }\n);\nMenuCheckboxItem.displayName = 'Menu.CheckboxItem';\n\nconst MenuSub = ({\n children,\n defaultOpen,\n}: MenuSubProps): React.ReactElement => (\n <BaseMenu.SubmenuRoot defaultOpen={defaultOpen}>\n {children}\n </BaseMenu.SubmenuRoot>\n);\nMenuSub.displayName = 'Menu.Sub';\n\nconst MenuSubTrigger = React.memo(\n ({\n children,\n icon,\n disabled,\n onClick,\n className,\n testId,\n ref,\n ...rest\n }: MenuSubTriggerProps): React.ReactElement => (\n <BaseMenu.SubmenuTrigger\n ref={ref}\n disabled={disabled}\n onClick={onClick}\n className={cx(menuItemStyle, className)}\n data-testid={testId}\n {...rest}\n >\n <ItemLayout start={icon} endContent={<ChevronRightIcon size=\"sm\" />}>\n {children}\n </ItemLayout>\n </BaseMenu.SubmenuTrigger>\n )\n);\nMenuSubTrigger.displayName = 'Menu.SubTrigger';\n\n/**\n * Compound Menu API.\n *\n * `Content` and `SubContent` are the same popup surface. The gap from the\n * anchor is set once on the `Menu` root (`gap` prop) and inherited by both.\n */\nexport const Menu = /*#__PURE__*/ Object.assign(MenuRoot, {\n Trigger: MenuTrigger,\n Content: MenuContent,\n Item: MenuItem,\n Group: MenuGroup,\n Separator: MenuSeparator,\n RadioGroup: MenuRadioGroup,\n RadioItem: MenuRadioItem,\n CheckboxItem: MenuCheckboxItem,\n Sub: MenuSub,\n SubTrigger: MenuSubTrigger,\n SubContent: MenuContent,\n});\n"],"names":[],"mappings":";;;;;;;;;;;;;AAuCA;;AAEG;AACH;AAuBA;;;;AAIG;AACH;;;;;AAWI;;AAIJ;AAEA;;;;;;;;;;;;;;;;;;;;;;;;AAwBG;AACH;;;AAgBE;AAcF;AACA;AAEA;AAoBA;AAEA;;;AA+BA;AACA;AAEA;;;AA+BE;AAEF;AAEA;AAgBA;AAEA;AASA;AAEA;AAoBA;AAEA;;AAiBI;AAwBF;AAEF;AAEA;;AAmBI;AA0BF;AAEF;AAEA;AAQA;AAEA;AAyBA;AAEA;;;;;AAKG;AACI;AACL;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACD;;"}
|
|
1
|
+
{"version":3,"file":"Menu.js","sources":["src/components/navigation/Menu/Menu.tsx"],"sourcesContent":["'use client';\n\nimport { Menu as BaseMenu } from '@base-ui/react/menu';\nimport React from 'react';\n\nimport { CheckIcon } from '@/components/Icons/CheckIcon';\nimport { ChevronRightIcon } from '@/components/Icons/ChevronRightIcon';\nimport { CircleIcon } from '@/components/Icons/CircleIcon';\nimport { Button } from '@/components/primitives/Button';\nimport { useLatest } from '@/hooks/useLatest';\nimport { cx } from '@/utils/cx';\n\nimport {\n menuContentStyle,\n menuPositionerStyle,\n menuItemStyle,\n itemStartSlotStyle,\n itemLabelStyle,\n itemEndSlotStyle,\n shortcutStyle,\n groupLabelStyle,\n separatorStyle,\n} from './Menu.css';\nimport { MenuGapContext, DEFAULT_MENU_GAP } from './MenuGapContext';\n\nimport type {\n MenuRootProps,\n MenuTriggerProps,\n MenuContentProps,\n MenuItemProps,\n MenuGroupProps,\n MenuSeparatorProps,\n MenuRadioGroupProps,\n MenuRadioItemProps,\n MenuCheckboxItemProps,\n MenuSubProps,\n MenuSubTriggerProps,\n} from './Menu.types';\nimport type { MenuRootActions } from '@base-ui/react/menu';\n\n/**\n * Internal three-slot row used by every interactive menu entry.\n */\nconst ItemLayout = ({\n start,\n children,\n shortcut,\n endContent,\n}: {\n start?: React.ReactNode;\n children?: React.ReactNode;\n shortcut?: React.ReactNode;\n endContent?: React.ReactNode;\n}): React.ReactElement => (\n <>\n {start != null && <span className={itemStartSlotStyle}>{start}</span>}\n <span className={itemLabelStyle}>{children}</span>\n {(shortcut != null || endContent != null) && (\n <span className={itemEndSlotStyle}>\n {shortcut != null && <span className={shortcutStyle}>{shortcut}</span>}\n {endContent}\n </span>\n )}\n </>\n);\n\n/**\n * Merge `onClick` and its `onSelect` alias into one stable handler so a new\n * function identity isn't handed to Base UI on every render. Returns\n * `undefined` when neither is set, so no listener is attached.\n */\nconst useActivate = (\n onClick?: React.MouseEventHandler<HTMLElement>,\n onSelect?: React.MouseEventHandler<HTMLElement>\n): React.MouseEventHandler<HTMLElement> | undefined => {\n const handlersRef = useLatest({ onClick, onSelect });\n const handleActivate = React.useCallback<\n React.MouseEventHandler<HTMLElement>\n >(\n event => {\n handlersRef.current.onClick?.(event);\n handlersRef.current.onSelect?.(event);\n },\n [handlersRef]\n );\n return onClick || onSelect ? handleActivate : undefined;\n};\n\n/**\n * Composable menu for editor interfaces, built on `@base-ui/react` Menu\n * primitives. Compose the trigger, content and items as children — no config\n * object.\n *\n * @example\n * ```tsx\n * <Menu>\n * <Menu.Trigger>File</Menu.Trigger>\n * <Menu.Content>\n * <Menu.Item icon={<SaveIcon />} shortcut=\"⌘S\" onClick={save}>\n * Save\n * </Menu.Item>\n * <Menu.Separator />\n * <Menu.Sub>\n * <Menu.SubTrigger>Export</Menu.SubTrigger>\n * <Menu.SubContent>\n * <Menu.Item>PNG</Menu.Item>\n * <Menu.Item>SVG</Menu.Item>\n * </Menu.SubContent>\n * </Menu.Sub>\n * </Menu.Content>\n * </Menu>\n * ```\n */\nconst MenuRoot = ({\n children,\n open,\n defaultOpen,\n onOpenChange,\n modal,\n disabled,\n gap = DEFAULT_MENU_GAP,\n ref,\n}: MenuRootProps): React.ReactElement => {\n const actionsRef = React.useRef<MenuRootActions | null>(null);\n React.useImperativeHandle(\n ref,\n () => ({ close: () => actionsRef.current?.close() }),\n []\n );\n return (\n <MenuGapContext.Provider value={gap}>\n <BaseMenu.Root\n open={open}\n defaultOpen={defaultOpen}\n onOpenChange={onOpenChange}\n modal={modal}\n disabled={disabled}\n actionsRef={actionsRef}\n >\n {children}\n </BaseMenu.Root>\n </MenuGapContext.Provider>\n );\n};\nMenuRoot.displayName = 'Menu';\n\nconst MenuTrigger = ({\n children,\n render,\n disabled,\n openOnHover,\n className,\n testId,\n ...rest\n}: MenuTriggerProps): React.ReactElement => (\n <BaseMenu.Trigger\n render={render ?? <Button />}\n disabled={disabled}\n openOnHover={openOnHover}\n className={className}\n data-testid={testId}\n {...rest}\n >\n {children}\n </BaseMenu.Trigger>\n);\nMenuTrigger.displayName = 'Menu.Trigger';\n\nconst MenuContent = ({\n children,\n className,\n side,\n align = 'start',\n sideOffset,\n alignOffset,\n testId,\n ref,\n ...rest\n}: MenuContentProps): React.ReactElement => {\n const gap = React.useContext(MenuGapContext);\n return (\n <BaseMenu.Portal>\n <BaseMenu.Positioner\n side={side}\n align={align}\n sideOffset={sideOffset ?? gap}\n alignOffset={alignOffset}\n className={menuPositionerStyle}\n >\n <BaseMenu.Popup\n ref={ref}\n className={cx(menuContentStyle, className)}\n data-testid={testId}\n {...rest}\n >\n {children}\n </BaseMenu.Popup>\n </BaseMenu.Positioner>\n </BaseMenu.Portal>\n );\n};\nMenuContent.displayName = 'Menu.Content';\n\nconst MenuItem = React.memo(\n ({\n children,\n icon,\n shortcut,\n endContent,\n disabled,\n closeOnClick = true,\n onClick,\n onSelect,\n className,\n testId,\n ref,\n ...rest\n }: MenuItemProps): React.ReactElement => {\n const handleActivate = useActivate(onClick, onSelect);\n return (\n <BaseMenu.Item\n ref={ref}\n disabled={disabled}\n closeOnClick={closeOnClick}\n onClick={handleActivate}\n className={cx(menuItemStyle, className)}\n data-testid={testId}\n {...rest}\n >\n <ItemLayout start={icon} shortcut={shortcut} endContent={endContent}>\n {children}\n </ItemLayout>\n </BaseMenu.Item>\n );\n }\n);\nMenuItem.displayName = 'Menu.Item';\n\nconst MenuGroup = ({\n children,\n label,\n className,\n testId,\n ...rest\n}: MenuGroupProps): React.ReactElement => (\n <BaseMenu.Group className={className} data-testid={testId} {...rest}>\n {label != null && (\n <BaseMenu.GroupLabel className={groupLabelStyle}>\n {label}\n </BaseMenu.GroupLabel>\n )}\n {children}\n </BaseMenu.Group>\n);\nMenuGroup.displayName = 'Menu.Group';\n\nconst MenuSeparator = React.memo(\n ({ className, testId, ...rest }: MenuSeparatorProps): React.ReactElement => (\n <BaseMenu.Separator\n className={cx(separatorStyle, className)}\n data-testid={testId}\n {...rest}\n />\n )\n);\nMenuSeparator.displayName = 'Menu.Separator';\n\nconst MenuRadioGroup = ({\n children,\n value,\n defaultValue,\n onValueChange,\n className,\n testId,\n ...rest\n}: MenuRadioGroupProps): React.ReactElement => (\n <BaseMenu.RadioGroup\n value={value}\n defaultValue={defaultValue}\n onValueChange={onValueChange}\n className={className}\n data-testid={testId}\n {...rest}\n >\n {children}\n </BaseMenu.RadioGroup>\n);\nMenuRadioGroup.displayName = 'Menu.RadioGroup';\n\nconst MenuRadioItem = React.memo(\n ({\n children,\n value,\n indicator = <CircleIcon size=\"sm\" />,\n shortcut,\n endContent,\n disabled,\n closeOnClick = false,\n onClick,\n onSelect,\n className,\n testId,\n ref,\n ...rest\n }: MenuRadioItemProps): React.ReactElement => {\n const handleActivate = useActivate(onClick, onSelect);\n return (\n <BaseMenu.RadioItem\n ref={ref}\n value={value}\n disabled={disabled}\n closeOnClick={closeOnClick}\n onClick={handleActivate}\n className={cx(menuItemStyle, className)}\n data-testid={testId}\n {...rest}\n >\n <ItemLayout\n start={\n <BaseMenu.RadioItemIndicator>\n {indicator}\n </BaseMenu.RadioItemIndicator>\n }\n shortcut={shortcut}\n endContent={endContent}\n >\n {children}\n </ItemLayout>\n </BaseMenu.RadioItem>\n );\n }\n);\nMenuRadioItem.displayName = 'Menu.RadioItem';\n\nconst MenuCheckboxItem = React.memo(\n ({\n children,\n checked,\n defaultChecked,\n onCheckedChange,\n indicator = <CheckIcon size=\"sm\" />,\n shortcut,\n endContent,\n disabled,\n closeOnClick = false,\n onClick,\n onSelect,\n className,\n testId,\n ref,\n ...rest\n }: MenuCheckboxItemProps): React.ReactElement => {\n const handleActivate = useActivate(onClick, onSelect);\n return (\n <BaseMenu.CheckboxItem\n ref={ref}\n checked={checked}\n defaultChecked={defaultChecked}\n onCheckedChange={onCheckedChange}\n disabled={disabled}\n closeOnClick={closeOnClick}\n onClick={handleActivate}\n className={cx(menuItemStyle, className)}\n data-testid={testId}\n {...rest}\n >\n <ItemLayout\n start={\n <BaseMenu.CheckboxItemIndicator>\n {indicator}\n </BaseMenu.CheckboxItemIndicator>\n }\n shortcut={shortcut}\n endContent={endContent}\n >\n {children}\n </ItemLayout>\n </BaseMenu.CheckboxItem>\n );\n }\n);\nMenuCheckboxItem.displayName = 'Menu.CheckboxItem';\n\nconst MenuSub = ({\n children,\n defaultOpen,\n}: MenuSubProps): React.ReactElement => (\n <BaseMenu.SubmenuRoot defaultOpen={defaultOpen}>\n {children}\n </BaseMenu.SubmenuRoot>\n);\nMenuSub.displayName = 'Menu.Sub';\n\nconst MenuSubTrigger = React.memo(\n ({\n children,\n icon,\n disabled,\n onClick,\n className,\n testId,\n ref,\n ...rest\n }: MenuSubTriggerProps): React.ReactElement => (\n <BaseMenu.SubmenuTrigger\n ref={ref}\n disabled={disabled}\n onClick={onClick}\n className={cx(menuItemStyle, className)}\n data-testid={testId}\n {...rest}\n >\n <ItemLayout start={icon} endContent={<ChevronRightIcon size=\"sm\" />}>\n {children}\n </ItemLayout>\n </BaseMenu.SubmenuTrigger>\n )\n);\nMenuSubTrigger.displayName = 'Menu.SubTrigger';\n\n/**\n * Compound Menu API.\n *\n * `Content` and `SubContent` are the same popup surface. The gap from the\n * anchor is set once on the `Menu` root (`gap` prop) and inherited by both.\n */\nexport const Menu = /*#__PURE__*/ Object.assign(MenuRoot, {\n Trigger: MenuTrigger,\n Content: MenuContent,\n Item: MenuItem,\n Group: MenuGroup,\n Separator: MenuSeparator,\n RadioGroup: MenuRadioGroup,\n RadioItem: MenuRadioItem,\n CheckboxItem: MenuCheckboxItem,\n Sub: MenuSub,\n SubTrigger: MenuSubTrigger,\n SubContent: MenuContent,\n});\n"],"names":[],"mappings":";;;;;;;;;;;;;AAwCA;;AAEG;AACH;AAuBA;;;;AAIG;AACH;;;;;AAWI;;AAIJ;AAEA;;;;;;;;;;;;;;;;;;;;;;;;AAwBG;AACH;;;AAgBE;AAcF;AACA;AAEA;AAoBA;AAEA;;AAYE;AAoBF;AACA;AAEA;;;AA+BE;AAEF;AAEA;AAgBA;AAEA;AASA;AAEA;AAoBA;AAEA;;AAiBI;AAwBF;AAEF;AAEA;;AAmBI;AA0BF;AAEF;AAEA;AAQA;AAEA;AAyBA;AAEA;;;;;AAKG;AACI;AACL;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACD;;"}
|
|
@@ -102,6 +102,7 @@ import '../../primitives/Checkbox/Checkbox.js';
|
|
|
102
102
|
import '../../primitives/Checkbox/CheckboxGroup.js';
|
|
103
103
|
import '../../primitives/Code/Code.js';
|
|
104
104
|
import '../../primitives/Collapsible/Collapsible.js';
|
|
105
|
+
import '../../primitives/EditableText/EditableText.js';
|
|
105
106
|
import '../../primitives/HoverCard/HoverCard.js';
|
|
106
107
|
import '../../primitives/Icon/Icon.js';
|
|
107
108
|
import { IconButton } from '../../primitives/IconButton/IconButton.js';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PathBarSiblingMenu.js","sources":["src/components/navigation/PathBar/PathBarSiblingMenu.tsx"],"sourcesContent":["'use client';\n\nimport React from 'react';\n\nimport { ChevronDownIcon } from '@/components/Icons';\nimport { Menu } from '@/components/navigation/Menu';\nimport { IconButton } from '@/components/primitives';\n\nimport { pathBarSiblingTriggerStyle } from './PathBar.css';\nimport { resolveSiblings } from './pathUtils';\n\nimport type { PathBarSize, PathSegment } from './PathBar.types';\nimport type { ResolvedPathSegment } from './pathUtils';\n\ninterface PathBarSiblingMenuProps {\n /** The resolved segment this dropdown hangs off — the sibling anchor. */\n segment: ResolvedPathSegment;\n /** Index of `segment` in the trail; forwarded to `onSelect`. */\n index: number;\n /** Returns the raw sibling entries for the segment. */\n getSiblings: (\n segment: PathSegment,\n index: number\n ) => ReadonlyArray<string | PathSegment>;\n /** Called with the chosen sibling (already resolved to a concrete value). */\n onSelect: (index: number, sibling: ResolvedPathSegment) => void;\n /** Scale shared with the rest of the bar — sizes the caret button and icon. */\n size: PathBarSize;\n}\n\n/**\n * Trailing caret + dropdown for a single crumb's siblings (the VS Code path-bar\n * affordance). Rendered as a crumb's `endContent`, so it mounts only for crumbs\n * Breadcrumbs actually renders — `getSiblings` runs lazily, never for segments\n * hidden behind the overflow ellipsis. Renders nothing when the segment has no\n * siblings.\n */\nexport const PathBarSiblingMenu = ({\n segment,\n index,\n getSiblings,\n onSelect,\n size,\n}: PathBarSiblingMenuProps): React.ReactElement | null => {\n const siblings = resolveSiblings(segment, getSiblings(segment, index));\n if (siblings.length === 0) {\n return null;\n }\n\n return (\n <Menu>\n <Menu.Trigger\n render={\n <IconButton\n aria-label={`Browse ${segment.label} siblings`}\n size={size}\n radius=\"sm\"\n className={pathBarSiblingTriggerStyle}\n >\n <ChevronDownIcon size={size} decorative />\n </IconButton>\n }\n />\n <Menu.Content>\n {siblings.map((sibling, siblingIndex) => (\n <Menu.Item\n key={`${sibling.value}-${siblingIndex}`}\n icon={sibling.icon}\n onSelect={() => {\n onSelect(index, sibling);\n }}\n >\n {sibling.label}\n </Menu.Item>\n ))}\n </Menu.Content>\n </Menu>\n );\n};\n\nPathBarSiblingMenu.displayName = 'PathBarSiblingMenu';\n"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"PathBarSiblingMenu.js","sources":["src/components/navigation/PathBar/PathBarSiblingMenu.tsx"],"sourcesContent":["'use client';\n\nimport React from 'react';\n\nimport { ChevronDownIcon } from '@/components/Icons';\nimport { Menu } from '@/components/navigation/Menu';\nimport { IconButton } from '@/components/primitives';\n\nimport { pathBarSiblingTriggerStyle } from './PathBar.css';\nimport { resolveSiblings } from './pathUtils';\n\nimport type { PathBarSize, PathSegment } from './PathBar.types';\nimport type { ResolvedPathSegment } from './pathUtils';\n\ninterface PathBarSiblingMenuProps {\n /** The resolved segment this dropdown hangs off — the sibling anchor. */\n segment: ResolvedPathSegment;\n /** Index of `segment` in the trail; forwarded to `onSelect`. */\n index: number;\n /** Returns the raw sibling entries for the segment. */\n getSiblings: (\n segment: PathSegment,\n index: number\n ) => ReadonlyArray<string | PathSegment>;\n /** Called with the chosen sibling (already resolved to a concrete value). */\n onSelect: (index: number, sibling: ResolvedPathSegment) => void;\n /** Scale shared with the rest of the bar — sizes the caret button and icon. */\n size: PathBarSize;\n}\n\n/**\n * Trailing caret + dropdown for a single crumb's siblings (the VS Code path-bar\n * affordance). Rendered as a crumb's `endContent`, so it mounts only for crumbs\n * Breadcrumbs actually renders — `getSiblings` runs lazily, never for segments\n * hidden behind the overflow ellipsis. Renders nothing when the segment has no\n * siblings.\n */\nexport const PathBarSiblingMenu = ({\n segment,\n index,\n getSiblings,\n onSelect,\n size,\n}: PathBarSiblingMenuProps): React.ReactElement | null => {\n const siblings = resolveSiblings(segment, getSiblings(segment, index));\n if (siblings.length === 0) {\n return null;\n }\n\n return (\n <Menu>\n <Menu.Trigger\n render={\n <IconButton\n aria-label={`Browse ${segment.label} siblings`}\n size={size}\n radius=\"sm\"\n className={pathBarSiblingTriggerStyle}\n >\n <ChevronDownIcon size={size} decorative />\n </IconButton>\n }\n />\n <Menu.Content>\n {siblings.map((sibling, siblingIndex) => (\n <Menu.Item\n key={`${sibling.value}-${siblingIndex}`}\n icon={sibling.icon}\n onSelect={() => {\n onSelect(index, sibling);\n }}\n >\n {sibling.label}\n </Menu.Item>\n ))}\n </Menu.Content>\n </Menu>\n );\n};\n\nPathBarSiblingMenu.displayName = 'PathBarSiblingMenu';\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8BA;;;;;;AAMG;AACI;AAOL;AACA;AACE;;AAGF;AAoBY;AACF;AAQZ;AAEA;;"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import './../../../assets/src/components/primitives/EditableText/EditableText.css.ts.vanilla-CrVGgD19.css';
|
|
2
|
+
|
|
3
|
+
var displayDisabledStyle = 'EditableText_displayDisabledStyle__d2an012';
|
|
4
|
+
var displayReadOnlyStyle = 'EditableText_displayReadOnlyStyle__d2an011';
|
|
5
|
+
var displayStyle = 'EditableText_displayStyle__d2an010';
|
|
6
|
+
var editWrapStyle = 'EditableText_editWrapStyle__d2an013';
|
|
7
|
+
var inputStyle = 'EditableText_inputStyle__d2an016 EditableText_sharedCell__d2an014';
|
|
8
|
+
var sizerStyle = 'EditableText_sizerStyle__d2an015 EditableText_sharedCell__d2an014';
|
|
9
|
+
|
|
10
|
+
export { displayDisabledStyle, displayReadOnlyStyle, displayStyle, editWrapStyle, inputStyle, sizerStyle };
|
|
11
|
+
//# sourceMappingURL=EditableText.css.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"EditableText.css.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;"}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsxs, jsx } from 'react/jsx-runtime';
|
|
3
|
+
import { useState, useRef, useMemo, useCallback, useImperativeHandle } from 'react';
|
|
4
|
+
import { Text } from '../Text/Text.js';
|
|
5
|
+
import { textRecipe } from '../Text/Text.css.js';
|
|
6
|
+
import '../../../utils/devWarn.js';
|
|
7
|
+
import { useControlledState } from '../../../hooks/useControlledState/useControlledState.js';
|
|
8
|
+
import { useMergedRef } from '../../../hooks/useMergedRef/useMergedRef.js';
|
|
9
|
+
import { useLatest } from '../../../hooks/useLatest/useLatest.js';
|
|
10
|
+
import '../../../hooks/useBreakpoint/useBreakpoint.js';
|
|
11
|
+
import './../../../assets/src/theme/lightTheme.css.ts.vanilla-Ct6f9Iym.css';
|
|
12
|
+
import '@vanilla-extract/css';
|
|
13
|
+
import './../../../assets/src/theme/darkTheme.css.ts.vanilla-BHZ3ecRn.css';
|
|
14
|
+
import './../../../assets/src/theme/globalScrollbars.css.ts.vanilla-BAJwnUEJ.css';
|
|
15
|
+
import '../../../hooks/useNavigationHistory/useNavigationHistory.js';
|
|
16
|
+
import { cx } from '../../../utils/cx.js';
|
|
17
|
+
import { sizerStyle, inputStyle, editWrapStyle, displayDisabledStyle, displayReadOnlyStyle, displayStyle } from './EditableText.css.js';
|
|
18
|
+
import { resolveEditableTextLabels } from './editableTextLabels.js';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Text that looks like `<Text>` but turns into an inline editor when the user
|
|
22
|
+
* activates it — the editor-UI pattern for renaming layers, nodes, assets, and
|
|
23
|
+
* scene objects in place.
|
|
24
|
+
*
|
|
25
|
+
* The idle state renders a real `<Text>` (so it inherits every typography prop),
|
|
26
|
+
* and the editing state renders a chrome-less `<input>` that shares the exact
|
|
27
|
+
* same typography recipe, so the swap is visually seamless. The edit field
|
|
28
|
+
* auto-sizes to its content via a hidden sizer — no measurement effects.
|
|
29
|
+
*
|
|
30
|
+
* Editing commits on `Enter` (and on blur when `submitOnBlur`), cancels on
|
|
31
|
+
* `Escape`. `onChange` fires on commit only, with the new value.
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* ```tsx
|
|
35
|
+
* // Uncontrolled — click the text to rename in place
|
|
36
|
+
* <EditableText defaultValue="Untitled Layer" onChange={setName} />
|
|
37
|
+
*
|
|
38
|
+
* // Controlled, heading-styled, double-click to edit (rename convention)
|
|
39
|
+
* <EditableText
|
|
40
|
+
* variant="heading"
|
|
41
|
+
* value={name}
|
|
42
|
+
* onChange={setName}
|
|
43
|
+
* activationMode="double"
|
|
44
|
+
* placeholder="Name this node"
|
|
45
|
+
* />
|
|
46
|
+
* ```
|
|
47
|
+
*/
|
|
48
|
+
const EditableText = ({ value, defaultValue, onChange, placeholder, as = 'span', variant = 'body', size, weight, color = 'primary', lineHeight, align, truncate = false, mono = false, activationMode = 'single', disabled = false, readOnly = false, selectOnEdit = true, submitOnBlur = true, maxLength, labels: labelsProp, 'aria-label': ariaLabel, onEditStart, onEditEnd, onKeyDown, className, style, testId, ref, }) => {
|
|
49
|
+
const [currentValue, setValue] = useControlledState({
|
|
50
|
+
value,
|
|
51
|
+
defaultValue,
|
|
52
|
+
onChange,
|
|
53
|
+
fallback: '',
|
|
54
|
+
});
|
|
55
|
+
const [isEditing, setIsEditing] = useState(false);
|
|
56
|
+
const [draft, setDraft] = useState('');
|
|
57
|
+
const displayRef = useRef(null);
|
|
58
|
+
const inputRef = useRef(null);
|
|
59
|
+
// When an edit ends via keyboard we return focus to the display; a blur-driven
|
|
60
|
+
// end must NOT steal focus back (the user just clicked somewhere else).
|
|
61
|
+
const refocusDisplayRef = useRef(false);
|
|
62
|
+
// Guards the input's blur handler from firing a second end after Enter/Escape
|
|
63
|
+
// already tore the input down.
|
|
64
|
+
const endingRef = useRef(false);
|
|
65
|
+
const currentValueRef = useLatest(currentValue);
|
|
66
|
+
const draftRef = useLatest(draft);
|
|
67
|
+
const submitOnBlurRef = useLatest(submitOnBlur);
|
|
68
|
+
const selectOnEditRef = useLatest(selectOnEdit);
|
|
69
|
+
const onEditStartRef = useLatest(onEditStart);
|
|
70
|
+
const onEditEndRef = useLatest(onEditEnd);
|
|
71
|
+
const onKeyDownRef = useLatest(onKeyDown);
|
|
72
|
+
const labels = useMemo(() => resolveEditableTextLabels(labelsProp), [labelsProp]);
|
|
73
|
+
const startEditing = useCallback(() => {
|
|
74
|
+
if (disabled || readOnly)
|
|
75
|
+
return;
|
|
76
|
+
endingRef.current = false;
|
|
77
|
+
setDraft(currentValueRef.current);
|
|
78
|
+
setIsEditing(true);
|
|
79
|
+
onEditStartRef.current?.();
|
|
80
|
+
}, [disabled, readOnly, currentValueRef, onEditStartRef]);
|
|
81
|
+
const endEditing = useCallback((reason, refocus) => {
|
|
82
|
+
endingRef.current = true;
|
|
83
|
+
refocusDisplayRef.current = refocus;
|
|
84
|
+
setIsEditing(false);
|
|
85
|
+
onEditEndRef.current?.(reason);
|
|
86
|
+
}, [onEditEndRef]);
|
|
87
|
+
const commit = useCallback((refocus) => {
|
|
88
|
+
if (endingRef.current)
|
|
89
|
+
return;
|
|
90
|
+
const next = draftRef.current;
|
|
91
|
+
if (next !== currentValueRef.current) {
|
|
92
|
+
setValue(next);
|
|
93
|
+
}
|
|
94
|
+
endEditing('commit', refocus);
|
|
95
|
+
}, [draftRef, currentValueRef, setValue, endEditing]);
|
|
96
|
+
const cancel = useCallback((refocus) => {
|
|
97
|
+
if (endingRef.current)
|
|
98
|
+
return;
|
|
99
|
+
endEditing('cancel', refocus);
|
|
100
|
+
}, [endEditing]);
|
|
101
|
+
useImperativeHandle(ref, () => ({
|
|
102
|
+
edit: startEditing,
|
|
103
|
+
commit: () => {
|
|
104
|
+
commit(false);
|
|
105
|
+
},
|
|
106
|
+
cancel: () => {
|
|
107
|
+
cancel(false);
|
|
108
|
+
},
|
|
109
|
+
focus: () => {
|
|
110
|
+
(inputRef.current ?? displayRef.current)?.focus();
|
|
111
|
+
},
|
|
112
|
+
isEditing: () => isEditing,
|
|
113
|
+
getElement: () => inputRef.current ?? displayRef.current,
|
|
114
|
+
}), [startEditing, commit, cancel, isEditing]);
|
|
115
|
+
// Focus (and optionally select) the input the moment it mounts, without an
|
|
116
|
+
// effect. Stable identity, so it fires once per edit session, not per render.
|
|
117
|
+
const focusInput = useCallback((node) => {
|
|
118
|
+
if (node) {
|
|
119
|
+
node.focus();
|
|
120
|
+
if (selectOnEditRef.current)
|
|
121
|
+
node.select();
|
|
122
|
+
}
|
|
123
|
+
}, [selectOnEditRef]);
|
|
124
|
+
const mergedInputRef = useMergedRef(inputRef, focusInput);
|
|
125
|
+
// Return focus to the display element after a keyboard-driven edit end.
|
|
126
|
+
const refocusDisplay = useCallback((node) => {
|
|
127
|
+
if (node && refocusDisplayRef.current) {
|
|
128
|
+
node.focus();
|
|
129
|
+
refocusDisplayRef.current = false;
|
|
130
|
+
}
|
|
131
|
+
}, []);
|
|
132
|
+
const mergedDisplayRef = useMergedRef(displayRef, refocusDisplay);
|
|
133
|
+
const handleDisplayKeyDown = useCallback((event) => {
|
|
134
|
+
if (event.key === 'Enter' || event.key === 'F2') {
|
|
135
|
+
event.preventDefault();
|
|
136
|
+
startEditing();
|
|
137
|
+
}
|
|
138
|
+
}, [startEditing]);
|
|
139
|
+
const handleInputKeyDown = useCallback((event) => {
|
|
140
|
+
if (event.key === 'Enter') {
|
|
141
|
+
event.preventDefault();
|
|
142
|
+
commit(true);
|
|
143
|
+
}
|
|
144
|
+
else if (event.key === 'Escape') {
|
|
145
|
+
event.preventDefault();
|
|
146
|
+
cancel(true);
|
|
147
|
+
}
|
|
148
|
+
onKeyDownRef.current?.(event);
|
|
149
|
+
}, [commit, cancel, onKeyDownRef]);
|
|
150
|
+
const handleInputBlur = useCallback(() => {
|
|
151
|
+
if (submitOnBlurRef.current) {
|
|
152
|
+
commit(false);
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
cancel(false);
|
|
156
|
+
}
|
|
157
|
+
}, [commit, cancel, submitOnBlurRef]);
|
|
158
|
+
// Recipe args mirror Text's own gating so the input matches the display
|
|
159
|
+
// typography exactly.
|
|
160
|
+
const isInherit = variant === 'inherit';
|
|
161
|
+
const sizeVariant = size && !isInherit ? size : undefined;
|
|
162
|
+
const weightVariant = weight && !isInherit ? weight : undefined;
|
|
163
|
+
const lineHeightVariant = lineHeight && !isInherit ? lineHeight : undefined;
|
|
164
|
+
const useMono = mono || variant === 'code' ? true : undefined;
|
|
165
|
+
if (isEditing) {
|
|
166
|
+
const editTextClass = textRecipe({
|
|
167
|
+
variant,
|
|
168
|
+
color,
|
|
169
|
+
size: sizeVariant,
|
|
170
|
+
weight: weightVariant,
|
|
171
|
+
lineHeight: lineHeightVariant,
|
|
172
|
+
align,
|
|
173
|
+
mono: useMono,
|
|
174
|
+
});
|
|
175
|
+
return (
|
|
176
|
+
// The wrapper only groups the sizer + input; the input owns all
|
|
177
|
+
// interactivity, so the wrapper needs no role or key handler.
|
|
178
|
+
jsxs("span", { className: cx(editWrapStyle, className), style: style, "data-editing": "true", children: [jsx("span", { "aria-hidden": "true", className: cx(editTextClass, sizerStyle), children: `${draft.length > 0 ? draft : (placeholder ?? '')} ` }), jsx("input", { ref: mergedInputRef, type: "text", className: cx(editTextClass, inputStyle), value: draft, placeholder: placeholder, maxLength: maxLength, "aria-label": ariaLabel ?? labels.editLabel, onChange: event => {
|
|
179
|
+
setDraft(event.target.value);
|
|
180
|
+
}, onKeyDown: handleInputKeyDown, onBlur: handleInputBlur, onClick: event => {
|
|
181
|
+
// Don't let the click bubble to a parent row/select handler.
|
|
182
|
+
event.stopPropagation();
|
|
183
|
+
}, "data-testid": testId })] }));
|
|
184
|
+
}
|
|
185
|
+
const hasValue = currentValue.length > 0;
|
|
186
|
+
const showingPlaceholder = !hasValue && placeholder != null && placeholder.length > 0;
|
|
187
|
+
const displayContent = hasValue
|
|
188
|
+
? currentValue
|
|
189
|
+
: showingPlaceholder
|
|
190
|
+
? placeholder
|
|
191
|
+
: // Keep a clickable line box when there is nothing to show.
|
|
192
|
+
' ';
|
|
193
|
+
const displayColor = disabled
|
|
194
|
+
? 'disabled'
|
|
195
|
+
: showingPlaceholder
|
|
196
|
+
? 'muted'
|
|
197
|
+
: color;
|
|
198
|
+
const interactive = !disabled && !readOnly;
|
|
199
|
+
// When the value is empty there is no meaningful text content to name the
|
|
200
|
+
// control, so fall back to the placeholder or the edit label.
|
|
201
|
+
const resolvedAriaLabel = ariaLabel ?? (hasValue ? undefined : (placeholder ?? labels.editLabel));
|
|
202
|
+
return (jsx(Text, { as: as, variant: variant, size: size, weight: weight, color: displayColor, lineHeight: lineHeight, align: align, truncate: truncate, mono: mono, ref: mergedDisplayRef, className: cx(displayStyle, !interactive &&
|
|
203
|
+
(disabled ? displayDisabledStyle : displayReadOnlyStyle), className), style: style, role: interactive ? 'button' : undefined, tabIndex: interactive ? 0 : undefined, "aria-label": resolvedAriaLabel, "aria-disabled": disabled || undefined, onClick: interactive && activationMode === 'single' ? startEditing : undefined, onDoubleClick: interactive && activationMode === 'double' ? startEditing : undefined, onKeyDown: interactive ? handleDisplayKeyDown : undefined, testId: testId, children: displayContent }));
|
|
204
|
+
};
|
|
205
|
+
EditableText.displayName = 'EditableText';
|
|
206
|
+
|
|
207
|
+
export { EditableText };
|
|
208
|
+
//# sourceMappingURL=EditableText.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"EditableText.js","sources":["src/components/primitives/EditableText/EditableText.tsx"],"sourcesContent":["'use client';\n\nimport React, {\n useCallback,\n useImperativeHandle,\n useMemo,\n useRef,\n useState,\n} from 'react';\n\nimport { Text } from '@/components/primitives/Text';\nimport { textRecipe } from '@/components/primitives/Text/Text.css';\nimport { useControlledState, useLatest, useMergedRef } from '@/hooks';\nimport { cx } from '@/utils/cx';\n\nimport {\n displayStyle,\n displayReadOnlyStyle,\n displayDisabledStyle,\n editWrapStyle,\n sizerStyle,\n inputStyle,\n} from './EditableText.css';\nimport { resolveEditableTextLabels } from './editableTextLabels';\n\nimport type {\n EditableTextEndReason,\n EditableTextHandle,\n EditableTextProps,\n} from './EditableText.types';\n\n/**\n * Text that looks like `<Text>` but turns into an inline editor when the user\n * activates it — the editor-UI pattern for renaming layers, nodes, assets, and\n * scene objects in place.\n *\n * The idle state renders a real `<Text>` (so it inherits every typography prop),\n * and the editing state renders a chrome-less `<input>` that shares the exact\n * same typography recipe, so the swap is visually seamless. The edit field\n * auto-sizes to its content via a hidden sizer — no measurement effects.\n *\n * Editing commits on `Enter` (and on blur when `submitOnBlur`), cancels on\n * `Escape`. `onChange` fires on commit only, with the new value.\n *\n * @example\n * ```tsx\n * // Uncontrolled — click the text to rename in place\n * <EditableText defaultValue=\"Untitled Layer\" onChange={setName} />\n *\n * // Controlled, heading-styled, double-click to edit (rename convention)\n * <EditableText\n * variant=\"heading\"\n * value={name}\n * onChange={setName}\n * activationMode=\"double\"\n * placeholder=\"Name this node\"\n * />\n * ```\n */\nexport const EditableText = ({\n value,\n defaultValue,\n onChange,\n placeholder,\n as = 'span',\n variant = 'body',\n size,\n weight,\n color = 'primary',\n lineHeight,\n align,\n truncate = false,\n mono = false,\n activationMode = 'single',\n disabled = false,\n readOnly = false,\n selectOnEdit = true,\n submitOnBlur = true,\n maxLength,\n labels: labelsProp,\n 'aria-label': ariaLabel,\n onEditStart,\n onEditEnd,\n onKeyDown,\n className,\n style,\n testId,\n ref,\n}: EditableTextProps): React.ReactElement => {\n const [currentValue, setValue] = useControlledState({\n value,\n defaultValue,\n onChange,\n fallback: '',\n });\n\n const [isEditing, setIsEditing] = useState(false);\n const [draft, setDraft] = useState('');\n\n const displayRef = useRef<HTMLElement>(null);\n const inputRef = useRef<HTMLInputElement>(null);\n // When an edit ends via keyboard we return focus to the display; a blur-driven\n // end must NOT steal focus back (the user just clicked somewhere else).\n const refocusDisplayRef = useRef(false);\n // Guards the input's blur handler from firing a second end after Enter/Escape\n // already tore the input down.\n const endingRef = useRef(false);\n\n const currentValueRef = useLatest(currentValue);\n const draftRef = useLatest(draft);\n const submitOnBlurRef = useLatest(submitOnBlur);\n const selectOnEditRef = useLatest(selectOnEdit);\n const onEditStartRef = useLatest(onEditStart);\n const onEditEndRef = useLatest(onEditEnd);\n const onKeyDownRef = useLatest(onKeyDown);\n\n const labels = useMemo(\n () => resolveEditableTextLabels(labelsProp),\n [labelsProp]\n );\n\n const startEditing = useCallback(() => {\n if (disabled || readOnly) return;\n endingRef.current = false;\n setDraft(currentValueRef.current);\n setIsEditing(true);\n onEditStartRef.current?.();\n }, [disabled, readOnly, currentValueRef, onEditStartRef]);\n\n const endEditing = useCallback(\n (reason: EditableTextEndReason, refocus: boolean) => {\n endingRef.current = true;\n refocusDisplayRef.current = refocus;\n setIsEditing(false);\n onEditEndRef.current?.(reason);\n },\n [onEditEndRef]\n );\n\n const commit = useCallback(\n (refocus: boolean) => {\n if (endingRef.current) return;\n const next = draftRef.current;\n if (next !== currentValueRef.current) {\n setValue(next);\n }\n endEditing('commit', refocus);\n },\n [draftRef, currentValueRef, setValue, endEditing]\n );\n\n const cancel = useCallback(\n (refocus: boolean) => {\n if (endingRef.current) return;\n endEditing('cancel', refocus);\n },\n [endEditing]\n );\n\n useImperativeHandle(\n ref,\n (): EditableTextHandle => ({\n edit: startEditing,\n commit: () => {\n commit(false);\n },\n cancel: () => {\n cancel(false);\n },\n focus: () => {\n (inputRef.current ?? displayRef.current)?.focus();\n },\n isEditing: () => isEditing,\n getElement: () => inputRef.current ?? displayRef.current,\n }),\n [startEditing, commit, cancel, isEditing]\n );\n\n // Focus (and optionally select) the input the moment it mounts, without an\n // effect. Stable identity, so it fires once per edit session, not per render.\n const focusInput = useCallback(\n (node: HTMLInputElement | null) => {\n if (node) {\n node.focus();\n if (selectOnEditRef.current) node.select();\n }\n },\n [selectOnEditRef]\n );\n const mergedInputRef = useMergedRef(inputRef, focusInput);\n\n // Return focus to the display element after a keyboard-driven edit end.\n const refocusDisplay = useCallback((node: HTMLElement | null) => {\n if (node && refocusDisplayRef.current) {\n node.focus();\n refocusDisplayRef.current = false;\n }\n }, []);\n const mergedDisplayRef = useMergedRef(displayRef, refocusDisplay);\n\n const handleDisplayKeyDown = useCallback(\n (event: React.KeyboardEvent<HTMLElement>) => {\n if (event.key === 'Enter' || event.key === 'F2') {\n event.preventDefault();\n startEditing();\n }\n },\n [startEditing]\n );\n\n const handleInputKeyDown = useCallback(\n (event: React.KeyboardEvent<HTMLInputElement>) => {\n if (event.key === 'Enter') {\n event.preventDefault();\n commit(true);\n } else if (event.key === 'Escape') {\n event.preventDefault();\n cancel(true);\n }\n onKeyDownRef.current?.(event);\n },\n [commit, cancel, onKeyDownRef]\n );\n\n const handleInputBlur = useCallback(() => {\n if (submitOnBlurRef.current) {\n commit(false);\n } else {\n cancel(false);\n }\n }, [commit, cancel, submitOnBlurRef]);\n\n // Recipe args mirror Text's own gating so the input matches the display\n // typography exactly.\n const isInherit = variant === 'inherit';\n const sizeVariant = size && !isInherit ? size : undefined;\n const weightVariant = weight && !isInherit ? weight : undefined;\n const lineHeightVariant = lineHeight && !isInherit ? lineHeight : undefined;\n const useMono = mono || variant === 'code' ? true : undefined;\n\n if (isEditing) {\n const editTextClass = textRecipe({\n variant,\n color,\n size: sizeVariant,\n weight: weightVariant,\n lineHeight: lineHeightVariant,\n align,\n mono: useMono,\n });\n\n return (\n // The wrapper only groups the sizer + input; the input owns all\n // interactivity, so the wrapper needs no role or key handler.\n <span\n className={cx(editWrapStyle, className)}\n style={style}\n data-editing=\"true\"\n >\n <span aria-hidden=\"true\" className={cx(editTextClass, sizerStyle)}>\n {`${draft.length > 0 ? draft : (placeholder ?? '')} `}\n </span>\n <input\n ref={mergedInputRef}\n type=\"text\"\n className={cx(editTextClass, inputStyle)}\n value={draft}\n placeholder={placeholder}\n maxLength={maxLength}\n aria-label={ariaLabel ?? labels.editLabel}\n onChange={event => {\n setDraft(event.target.value);\n }}\n onKeyDown={handleInputKeyDown}\n onBlur={handleInputBlur}\n onClick={event => {\n // Don't let the click bubble to a parent row/select handler.\n event.stopPropagation();\n }}\n data-testid={testId}\n />\n </span>\n );\n }\n\n const hasValue = currentValue.length > 0;\n const showingPlaceholder =\n !hasValue && placeholder != null && placeholder.length > 0;\n const displayContent = hasValue\n ? currentValue\n : showingPlaceholder\n ? placeholder\n : // Keep a clickable line box when there is nothing to show.\n ' ';\n\n const displayColor = disabled\n ? 'disabled'\n : showingPlaceholder\n ? 'muted'\n : color;\n\n const interactive = !disabled && !readOnly;\n\n // When the value is empty there is no meaningful text content to name the\n // control, so fall back to the placeholder or the edit label.\n const resolvedAriaLabel =\n ariaLabel ?? (hasValue ? undefined : (placeholder ?? labels.editLabel));\n\n return (\n <Text\n as={as}\n variant={variant}\n size={size}\n weight={weight}\n color={displayColor}\n lineHeight={lineHeight}\n align={align}\n truncate={truncate}\n mono={mono}\n ref={mergedDisplayRef}\n className={cx(\n displayStyle,\n !interactive &&\n (disabled ? displayDisabledStyle : displayReadOnlyStyle),\n className\n )}\n style={style}\n role={interactive ? 'button' : undefined}\n tabIndex={interactive ? 0 : undefined}\n aria-label={resolvedAriaLabel}\n aria-disabled={disabled || undefined}\n onClick={\n interactive && activationMode === 'single' ? startEditing : undefined\n }\n onDoubleClick={\n interactive && activationMode === 'double' ? startEditing : undefined\n }\n onKeyDown={interactive ? handleDisplayKeyDown : undefined}\n testId={testId}\n >\n {displayContent}\n </Text>\n );\n};\n\nEditableText.displayName = 'EditableText';\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;AA+BA;;;;;;;;;;;;;;;;;;;;;;;;;;;AA2BG;AACI;AA8BL;;;;AAIE;AACD;;;AAKD;AACA;;;AAGA;;;AAGA;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AAEA;AAKA;;;AAEE;AACA;;AAEA;;;AAKE;AACA;;AAEA;AACF;AAIF;;;AAGI;AACA;;;AAGA;;AAKJ;;;AAGI;AACF;AAIF;AAGI;;;;;;;;;;AAUA;;;;;AAQJ;;;;;;AAME;;;AAMF;AACE;;AAEE;;;;AAKJ;AAEI;;AAEE;;AAEJ;AAIF;AAEI;;;;AAGO;;;;AAIP;;AAKJ;AACE;;;;;;;;;AASF;AACA;AACA;AACA;AACA;;;;;AAMI;AACA;AACA;;AAEA;AACD;;;;AAKC;AAiBM;AACF;;;AAMA;;AAOR;AACA;;AAGE;AACA;AACE;AACA;AACE;;AAGJ;AACA;AACE;;AAGJ;;;;;;AA0CF;AAEA;;"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* English defaults for every user-facing string EditableText renders. Override
|
|
3
|
+
* any subset by passing `labels` to `<EditableText>`; the rest fall back here.
|
|
4
|
+
*/
|
|
5
|
+
const DEFAULT_EDITABLE_TEXT_LABELS = {
|
|
6
|
+
editLabel: 'Edit text',
|
|
7
|
+
};
|
|
8
|
+
/** Merge a partial `labels` override onto the English defaults. */
|
|
9
|
+
function resolveEditableTextLabels(overrides) {
|
|
10
|
+
return overrides
|
|
11
|
+
? { ...DEFAULT_EDITABLE_TEXT_LABELS, ...overrides }
|
|
12
|
+
: DEFAULT_EDITABLE_TEXT_LABELS;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export { DEFAULT_EDITABLE_TEXT_LABELS, resolveEditableTextLabels };
|
|
16
|
+
//# sourceMappingURL=editableTextLabels.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"editableTextLabels.js","sources":["src/components/primitives/EditableText/editableTextLabels.ts"],"sourcesContent":["import type { EditableTextLabels } from './EditableText.types';\n\n/**\n * English defaults for every user-facing string EditableText renders. Override\n * any subset by passing `labels` to `<EditableText>`; the rest fall back here.\n */\nexport const DEFAULT_EDITABLE_TEXT_LABELS: EditableTextLabels = {\n editLabel: 'Edit text',\n};\n\n/** Merge a partial `labels` override onto the English defaults. */\nexport function resolveEditableTextLabels(\n overrides: Partial<EditableTextLabels> | undefined\n): EditableTextLabels {\n return overrides\n ? { ...DEFAULT_EDITABLE_TEXT_LABELS, ...overrides }\n : DEFAULT_EDITABLE_TEXT_LABELS;\n}\n"],"names":[],"mappings":"AAEA;;;AAGG;AACI,MAAM,4BAA4B,GAAuB;AAC9D,IAAA,SAAS,EAAE,WAAW;;AAGxB;AACM,SAAU,yBAAyB,CACvC,SAAkD,EAAA;AAElD,IAAA,OAAO;AACL,UAAE,EAAE,GAAG,4BAA4B,EAAE,GAAG,SAAS;UAC/C,4BAA4B;AAClC;;;;"}
|
|
@@ -48,13 +48,15 @@ import { loadingSpinnerRecipe, iconButtonRecipe } from './IconButton.css.js';
|
|
|
48
48
|
* </IconButton>
|
|
49
49
|
* ```
|
|
50
50
|
*/
|
|
51
|
-
const IconButton = /*#__PURE__*/ React.memo(({ children, className, size = 'md', variant = 'ghost', radius = 'md', disabled = false, loading = false, pressed = false, onClick, 'aria-label': ariaLabel, testId, style, ref, ...props }) => {
|
|
51
|
+
const IconButton = /*#__PURE__*/ React.memo(({ children, icon, className, size = 'md', variant = 'ghost', radius = 'md', disabled = false, loading = false, pressed = false, onClick, 'aria-label': ariaLabel, testId, style, ref, ...props }) => {
|
|
52
|
+
// `icon` mirrors Button's API; fall back to `children` for back-compat.
|
|
53
|
+
const glyph = icon ?? children;
|
|
52
54
|
return (jsx("button", { ref: ref, className: cx(iconButtonRecipe({
|
|
53
55
|
size,
|
|
54
56
|
variant,
|
|
55
57
|
radius,
|
|
56
58
|
pressed: pressed || undefined,
|
|
57
|
-
}), className), disabled: disabled || loading, onClick: onClick, "aria-label": ariaLabel, "aria-pressed": pressed, "data-testid": testId, style: style, ...props, children: loading ?
|
|
59
|
+
}), className), disabled: disabled || loading, onClick: onClick, "aria-label": ariaLabel, "aria-pressed": pressed, "data-testid": testId, style: style, ...props, children: loading ? jsx("div", { className: loadingSpinnerRecipe({ size }) }) : glyph }));
|
|
58
60
|
});
|
|
59
61
|
IconButton.displayName = 'IconButton';
|
|
60
62
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"IconButton.js","sources":["src/components/primitives/IconButton/IconButton.tsx"],"sourcesContent":["'use client';\n\n// src/primitives/IconButton/IconButton.tsx\nimport React from 'react';\n\nimport { cx } from '@/utils/cx';\n\nimport { iconButtonRecipe, loadingSpinnerRecipe } from './IconButton.css';\n\nimport type { Size, Variant } from '@/types/common';\nimport type { Prettify } from '@/types/utilities';\n\n/**\n * Standard size variants for IconButton using library sizing.\n */\nexport type IconButtonSize = Size;\n\n/**\n * Visual variants for IconButton - strict typing for controlled API.\n */\nexport type IconButtonVariant = Variant;\n\n/**\n * Border radius variants for IconButton shape control.\n */\nexport type IconButtonRadius = 'none' | 'sm' | 'md' | 'lg' | 'full';\n\nexport interface IconButtonBaseProps extends Omit<\n React.ButtonHTMLAttributes<HTMLButtonElement>,\n 'css' | 'children'\n> {\n /**\n * Icon component to display inside the button.\n * Should be an Icon component or similar icon element.\n * The button will automatically size itself based on the icon size.\n *\n * @example <SaveIcon />, <AddIcon />\n */\n children
|
|
1
|
+
{"version":3,"file":"IconButton.js","sources":["src/components/primitives/IconButton/IconButton.tsx"],"sourcesContent":["'use client';\n\n// src/primitives/IconButton/IconButton.tsx\nimport React from 'react';\n\nimport { cx } from '@/utils/cx';\n\nimport { iconButtonRecipe, loadingSpinnerRecipe } from './IconButton.css';\n\nimport type { Size, Variant } from '@/types/common';\nimport type { Prettify } from '@/types/utilities';\n\n/**\n * Standard size variants for IconButton using library sizing.\n */\nexport type IconButtonSize = Size;\n\n/**\n * Visual variants for IconButton - strict typing for controlled API.\n */\nexport type IconButtonVariant = Variant;\n\n/**\n * Border radius variants for IconButton shape control.\n */\nexport type IconButtonRadius = 'none' | 'sm' | 'md' | 'lg' | 'full';\n\nexport interface IconButtonBaseProps extends Omit<\n React.ButtonHTMLAttributes<HTMLButtonElement>,\n 'css' | 'children'\n> {\n /**\n * Icon component to display inside the button.\n * Should be an Icon component or similar icon element.\n * The button will automatically size itself based on the icon size.\n *\n * Equivalent to the `icon` prop — provide the glyph either way. When both are\n * set, `icon` wins. Kept optional so `<IconButton icon={…} />` matches\n * `Button`'s `icon` API.\n *\n * @example <SaveIcon />, <AddIcon />\n */\n children?: React.ReactNode;\n\n /**\n * Icon element to display inside the button, mirroring `Button`'s `icon`\n * prop so the two components share one API. Takes precedence over\n * `children` when both are provided.\n *\n * @example <SaveIcon />, <AddIcon />\n */\n icon?: React.ReactNode;\n\n /**\n * Button size using standard library sizing\n * Button will be square and sized appropriately for the icon\n * - `sm`: 20px square (for 12px icons)\n * - `md`: 24px square (for 16px icons)\n * - `lg`: 32px square (for 20px icons)\n * @default \"md\"\n */\n size?: IconButtonSize;\n\n /**\n * Button visual variant\n * - `default`: Transparent with border, fills on hover\n * - `ghost`: No border, subtle hover state\n * - `filled`: Solid background with accent color\n * @default \"ghost\"\n */\n variant?: IconButtonVariant;\n\n /**\n * Border radius for button shape control\n * - `none`: Sharp corners (0px)\n * - `sm`: Slightly rounded (2px)\n * - `md`: Moderately rounded (4px)\n * - `lg`: More rounded (6px)\n * - `full`: Fully circular\n * @default \"md\"\n */\n radius?: IconButtonRadius;\n\n /**\n * Whether the button is disabled\n * When true, button becomes non-interactive with reduced opacity\n * @default false\n */\n disabled?: boolean;\n\n /**\n * Whether the button is in a loading state\n * Shows spinner and disables interaction\n * @default false\n */\n loading?: boolean;\n\n /**\n * Whether the button appears pressed/active\n * Useful for toggle states and active tool indicators\n * @default false\n */\n pressed?: boolean;\n\n /**\n * Accessible label for screen readers\n * Required for proper accessibility\n * @example \"Save file\", \"Delete item\", \"Close dialog\"\n */\n 'aria-label': string;\n\n /**\n * Click event handler\n * Called when button is clicked (not when disabled/loading)\n */\n onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;\n\n /**\n * Test identifier for automated testing\n */\n testId?: string;\n\n ref?: React.Ref<HTMLButtonElement>;\n}\n\n/**\n * Props for the IconButton component with prettified type for better IntelliSense\n */\nexport type IconButtonProps = Prettify<IconButtonBaseProps>;\n\n/**\n * A specialized square button component for displaying icons in editor interfaces.\n *\n * Designed for toolbar actions, quick controls, and icon-based interactions.\n * Always square and sized appropriately for the contained icon.\n * Supports various border radius options from sharp corners to fully circular.\n *\n * @example\n * ```tsx\n * // Basic ghost button (default)\n * <IconButton aria-label=\"Save file\">\n * <SaveIcon />\n * </IconButton>\n *\n * // Circular filled button\n * <IconButton\n * variant=\"filled\"\n * radius=\"full\"\n * size=\"lg\"\n * aria-label=\"Add item\"\n * >\n * <AddIcon />\n * </IconButton>\n *\n * // Sharp corners for toolbar\n * <IconButton\n * variant=\"default\"\n * radius=\"none\"\n * aria-label=\"Settings\"\n * >\n * <SettingsIcon />\n * </IconButton>\n *\n * // Toggle state\n * <IconButton\n * pressed={isVisible}\n * radius=\"sm\"\n * aria-label=\"Toggle visibility\"\n * onClick={toggleVisibility}\n * >\n * <EyeIcon />\n * </IconButton>\n * ```\n */\nexport const IconButton = /*#__PURE__*/ React.memo<IconButtonProps>(\n ({\n children,\n icon,\n className,\n size = 'md',\n variant = 'ghost',\n radius = 'md',\n disabled = false,\n loading = false,\n pressed = false,\n onClick,\n 'aria-label': ariaLabel,\n testId,\n style,\n ref,\n ...props\n }) => {\n // `icon` mirrors Button's API; fall back to `children` for back-compat.\n const glyph = icon ?? children;\n return (\n <button\n ref={ref}\n className={cx(\n iconButtonRecipe({\n size,\n variant,\n radius,\n pressed: pressed || undefined,\n }),\n className\n )}\n disabled={disabled || loading}\n onClick={onClick}\n aria-label={ariaLabel}\n aria-pressed={pressed}\n data-testid={testId}\n style={style}\n {...props}\n >\n {loading ? <div className={loadingSpinnerRecipe({ size })} /> : glyph}\n </button>\n );\n }\n);\n\nIconButton.displayName = 'IconButton';\n"],"names":[],"mappings":";;;;;;AAkIA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA2CG;AACI;;AAmBH;;;;;;;AAwBF;AAGF;;"}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import './../../../assets/src/components/primitives/Tooltip/Tooltip.css.ts.vanilla-
|
|
1
|
+
import './../../../assets/src/components/primitives/Tooltip/Tooltip.css.ts.vanilla-BqaiGrFQ.css';
|
|
2
2
|
|
|
3
|
-
var arrowFillStyle = '
|
|
4
|
-
var tooltipArrowStyle = '
|
|
3
|
+
var arrowFillStyle = 'Tooltip_arrowFillStyle__u81kis3';
|
|
4
|
+
var tooltipArrowStyle = 'Tooltip_tooltipArrowStyle__u81kis4';
|
|
5
5
|
var tooltipContentStyle = 'Tooltip_tooltipContentStyle__u81kis0';
|
|
6
|
-
var
|
|
6
|
+
var tooltipPositionerStyle = 'Tooltip_tooltipPositionerStyle__u81kis1';
|
|
7
|
+
var tooltipTriggerStyle = 'Tooltip_tooltipTriggerStyle__u81kis2';
|
|
7
8
|
|
|
8
|
-
export { arrowFillStyle, tooltipArrowStyle, tooltipContentStyle, tooltipTriggerStyle };
|
|
9
|
+
export { arrowFillStyle, tooltipArrowStyle, tooltipContentStyle, tooltipPositionerStyle, tooltipTriggerStyle };
|
|
9
10
|
//# sourceMappingURL=Tooltip.css.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Tooltip.css.js","sources":[],"sourcesContent":[],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"Tooltip.css.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;"}
|
|
@@ -3,7 +3,7 @@ import { jsx, jsxs } from 'react/jsx-runtime';
|
|
|
3
3
|
import { Tooltip as Tooltip$1 } from '@base-ui/react/tooltip';
|
|
4
4
|
import { cx } from '../../../utils/cx.js';
|
|
5
5
|
import { StyledTooltipArrow, ArrowSvg } from './Arrow.js';
|
|
6
|
-
import { tooltipTriggerStyle, tooltipContentStyle } from './Tooltip.css.js';
|
|
6
|
+
import { tooltipPositionerStyle, tooltipTriggerStyle, tooltipContentStyle } from './Tooltip.css.js';
|
|
7
7
|
import { parsePlacement, parseCollisionStrategy } from './utils.js';
|
|
8
8
|
|
|
9
9
|
/**
|
|
@@ -135,6 +135,16 @@ const Tooltip = ({ children, title, placement = 'top', collision = 'smart', coll
|
|
|
135
135
|
finalPositionerProps.collisionBoundary = boundary;
|
|
136
136
|
}
|
|
137
137
|
}
|
|
138
|
+
// Merge our z-index positioner class with any caller-provided className,
|
|
139
|
+
// honoring Base UI's function-className form (power-user `positionerProps`).
|
|
140
|
+
const incomingPositionerClassName = finalPositionerProps.className;
|
|
141
|
+
let positionerClassName;
|
|
142
|
+
if (typeof incomingPositionerClassName === 'function') {
|
|
143
|
+
positionerClassName = state => cx(tooltipPositionerStyle, incomingPositionerClassName(state));
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
positionerClassName = cx(tooltipPositionerStyle, incomingPositionerClassName);
|
|
147
|
+
}
|
|
138
148
|
// Handle cursor tracking. Copy into a local object instead of mutating the
|
|
139
149
|
// caller-owned `rootProps` prop.
|
|
140
150
|
const finalRootProps = { ...rootProps };
|
|
@@ -155,7 +165,7 @@ const Tooltip = ({ children, title, placement = 'top', collision = 'smart', coll
|
|
|
155
165
|
transition: `opacity ${duration}ms ${easing}, transform ${duration}ms ${easing}`,
|
|
156
166
|
}
|
|
157
167
|
: { transition: 'none' };
|
|
158
|
-
return (jsx(Tooltip$1.Provider, { delay: delay, closeDelay: closeDelay, ...finalRootProps, children: jsxs(Tooltip$1.Root, { children: [jsx(Tooltip$1.Trigger, { render: props => (jsx("div", { ...props, className: cx(tooltipTriggerStyle, props.className) })), children: children }), jsx(Tooltip$1.Portal, { children: jsx(Tooltip$1.Positioner, { ...finalPositionerProps, children: jsx(Tooltip$1.Popup, { render: props => (jsxs("div", { ...props, className: cx(tooltipContentStyle, className, props.className), "data-testid": testId, id: id, style: {
|
|
168
|
+
return (jsx(Tooltip$1.Provider, { delay: delay, closeDelay: closeDelay, ...finalRootProps, children: jsxs(Tooltip$1.Root, { children: [jsx(Tooltip$1.Trigger, { render: props => (jsx("div", { ...props, className: cx(tooltipTriggerStyle, props.className) })), children: children }), jsx(Tooltip$1.Portal, { children: jsx(Tooltip$1.Positioner, { ...finalPositionerProps, className: positionerClassName, children: jsx(Tooltip$1.Popup, { render: props => (jsxs("div", { ...props, className: cx(tooltipContentStyle, className, props.className), "data-testid": testId, id: id, style: {
|
|
159
169
|
...animationStyle,
|
|
160
170
|
...style,
|
|
161
171
|
...props.style,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Tooltip.js","sources":["src/components/primitives/Tooltip/Tooltip.tsx"],"sourcesContent":["'use client';\n\n// src/components/primitives/Tooltip/Tooltip.tsx\nimport { Tooltip as BaseTooltip } from '@base-ui/react/tooltip';\nimport React from 'react';\n\nimport { cx } from '@/utils/cx';\n\nimport { ArrowSvg, StyledTooltipArrow } from './Arrow';\nimport { tooltipContentStyle, tooltipTriggerStyle } from './Tooltip.css';\nimport {\n CollisionAvoidance,\n TooltipAnimation,\n TooltipCollisionConfig,\n TooltipCollisionStrategy,\n TooltipPlacement,\n TooltipPositioner,\n BaseTooltipPositionerProps,\n BaseTooltipRootProps,\n} from './types';\nimport { parseCollisionStrategy, parsePlacement } from './utils';\n\nimport type { Prettify } from '@/types/utilities';\n\ninterface TooltipBaseProps extends Omit<\n React.HTMLAttributes<HTMLDivElement>,\n 'css' | 'title'\n> {\n /**\n * The trigger element that will show the tooltip on hover.\n * Must be a single React element or component.\n *\n * @example\n * <Button>Hover me</Button>\n * <IconButton><SaveIcon /></IconButton>\n */\n children: React.ReactElement;\n\n /**\n * The content to display in the tooltip.\n * Can be simple text or a complex React node for advanced customization.\n *\n * @example\n * \"Save file\"\n * <div><strong>Save</strong><br />Ctrl+S</div>\n */\n title: React.ReactNode;\n\n /**\n * Intuitive placement similar to MUI\n * @default \"top\"\n */\n placement?: TooltipPlacement;\n\n /**\n * Collision handling strategy\n * @default \"smart\"\n */\n collision?: TooltipCollisionStrategy;\n\n /**\n * Advanced collision configuration for power users\n * When provided, overrides the collision strategy\n */\n collisionConfig?: TooltipCollisionConfig;\n\n /**\n * Advanced positioning configuration\n */\n positioner?: TooltipPositioner;\n\n /**\n * Animation configuration\n */\n animation?: TooltipAnimation;\n\n /**\n * Delay in milliseconds before showing the tooltip\n * @default 600\n */\n delay?: number;\n\n /**\n * Delay in milliseconds before hiding the tooltip\n * @default 0\n */\n closeDelay?: number;\n\n /**\n * Whether to show the arrow pointing to the trigger\n * @default true\n */\n arrow?: boolean;\n\n /**\n * Whether the tooltip is disabled\n * When true, tooltip will not appear\n * @default false\n */\n disabled?: boolean;\n\n /**\n * Direct props passed to Base UI Root component\n * For power users who need full control\n */\n rootProps?: Partial<BaseTooltipRootProps>;\n\n /**\n * Direct props passed to Base UI Positioner component\n * For power users who need full control\n */\n positionerProps?: Partial<BaseTooltipPositionerProps>;\n\n /**\n * Test identifier for automated testing\n */\n testId?: string;\n\n ref?: React.Ref<HTMLDivElement>;\n}\n\n/**\n * Props for the Tooltip component with prettified type for better IntelliSense\n */\nexport type TooltipProps = Prettify<TooltipBaseProps>;\n\n/**\n * A tooltip component that displays contextual information on hover.\n *\n * Built on @base-ui/react for robust accessibility and positioning.\n * Provides an intuitive API similar to MUI with advanced positioning and collision handling.\n * Supports both simple text and complex React nodes for advanced tooltip content.\n *\n * @example\n * ```tsx\n * // Simple text tooltip\n * <Tooltip title=\"Save your work\">\n * <Button>Save</Button>\n * </Tooltip>\n *\n * // Intuitive positioning (MUI-style)\n * <Tooltip placement=\"bottom-start\" title=\"Menu options\">\n * <IconButton><MenuIcon /></IconButton>\n * </Tooltip>\n *\n * // Intelligent collision handling (new default)\n * <Tooltip collision=\"smart\" title=\"Intelligent positioning\">\n * <Button>Smart tooltip</Button>\n * </Tooltip>\n *\n * // Advanced collision configuration\n * <Tooltip\n * collisionConfig={{\n * side: 'flip',\n * align: 'shift',\n * fallbackAxisSide: 'start',\n * hideWhenDetached: true\n * }}\n * title=\"Custom collision handling\"\n * >\n * <Button>Advanced collision</Button>\n * </Tooltip>\n *\n * // Advanced positioning\n * <Tooltip\n * positioner={{\n * offset: 12,\n * padding: 10,\n * sticky: true,\n * boundary: '#container'\n * }}\n * title=\"Advanced positioning\"\n * >\n * <Button>Advanced</Button>\n * </Tooltip>\n *\n * // Custom animations\n * <Tooltip\n * animation={{\n * animated: true,\n * duration: 300,\n * easing: 'ease-in-out'\n * }}\n * title=\"Custom animation\"\n * >\n * <Button>Animated</Button>\n * </Tooltip>\n *\n * // Interactive tooltip\n * <Tooltip\n * hoverable={true}\n * closeDelay={300}\n * title={\n * <div>\n * <strong>Interactive Content</strong>\n * <br />\n * <button>Click me</button>\n * </div>\n * }\n * >\n * <Button>Hoverable tooltip</Button>\n * </Tooltip>\n *\n * // Full control with raw props\n * <Tooltip\n * arrow={false}\n * rootProps={{ trackCursorAxis: 'x' }}\n * positionerProps={{ arrowPadding: 20 }}\n * title=\"Full control\"\n * >\n * <Button>Power user</Button>\n * </Tooltip>\n * ```\n */\nexport const Tooltip: React.FC<TooltipProps> = ({\n children,\n title,\n placement = 'top',\n collision = 'smart',\n collisionConfig,\n positioner = {},\n animation = {},\n delay = 600,\n closeDelay = 0,\n arrow = true,\n disabled = false,\n rootProps = {},\n positionerProps = {},\n className,\n testId,\n id,\n style,\n ref,\n ...htmlProps\n}) => {\n // If disabled, return just the children without tooltip\n if (disabled) {\n return children;\n }\n\n // Parse placement into side and align\n const { side, align } = parsePlacement(placement);\n\n // Parse collision strategy or use custom config\n const collisionSettings = collisionConfig\n ? {\n collisionAvoidance: {\n side: collisionConfig.side ?? 'flip',\n align: collisionConfig.align ?? 'shift',\n fallbackAxisSide: collisionConfig.fallbackAxisSide ?? 'none',\n } as CollisionAvoidance,\n }\n : parseCollisionStrategy(collision);\n\n // Prepare positioner configuration\n const {\n offset = 8,\n padding = 8,\n sticky = false,\n boundary,\n trackCursor,\n } = positioner;\n\n // Prepare final positioner props\n const finalPositionerProps: Partial<BaseTooltipPositionerProps> = {\n side,\n align,\n sideOffset: offset,\n collisionPadding: padding,\n sticky,\n collisionAvoidance: collisionSettings.collisionAvoidance,\n ...positionerProps,\n };\n\n // Handle boundary if specified\n if (boundary) {\n if (typeof boundary === 'string') {\n const boundaryElement = document.querySelector(boundary);\n if (boundaryElement) {\n finalPositionerProps.collisionBoundary = boundaryElement;\n }\n } else {\n finalPositionerProps.collisionBoundary = boundary;\n }\n }\n\n // Handle cursor tracking. Copy into a local object instead of mutating the\n // caller-owned `rootProps` prop.\n const finalRootProps: Partial<BaseTooltipRootProps> = { ...rootProps };\n if (trackCursor) {\n finalRootProps.trackCursorAxis = trackCursor;\n }\n\n // Build animation transition style. Honors `prefers-reduced-motion: reduce`\n // by collapsing to `transition: none` regardless of caller-passed config —\n // user OS-level preference always wins over component config.\n const prefersReducedMotion =\n typeof window !== 'undefined' &&\n typeof window.matchMedia === 'function' &&\n window.matchMedia('(prefers-reduced-motion: reduce)').matches;\n const animated = animation.animated !== false && !prefersReducedMotion;\n const duration = animation.duration ?? 200;\n const easing = animation.easing ?? 'ease-out';\n\n const animationStyle: React.CSSProperties = animated\n ? {\n transition: `opacity ${duration}ms ${easing}, transform ${duration}ms ${easing}`,\n }\n : { transition: 'none' };\n\n return (\n <BaseTooltip.Provider\n delay={delay}\n closeDelay={closeDelay}\n {...finalRootProps}\n >\n <BaseTooltip.Root>\n <BaseTooltip.Trigger\n render={props => (\n <div\n {...props}\n className={cx(tooltipTriggerStyle, props.className)}\n />\n )}\n >\n {children}\n </BaseTooltip.Trigger>\n\n <BaseTooltip.Portal>\n <BaseTooltip.Positioner {...finalPositionerProps}>\n <BaseTooltip.Popup\n render={props => (\n <div\n {...props}\n className={cx(\n tooltipContentStyle,\n className,\n props.className\n )}\n data-testid={testId}\n id={id}\n style={{\n ...animationStyle,\n ...style,\n ...props.style,\n }}\n ref={ref}\n {...htmlProps}\n >\n {title}\n {arrow && (\n <StyledTooltipArrow>\n <ArrowSvg />\n </StyledTooltipArrow>\n )}\n </div>\n )}\n />\n </BaseTooltip.Positioner>\n </BaseTooltip.Portal>\n </BaseTooltip.Root>\n </BaseTooltip.Provider>\n );\n};\n\nTooltip.displayName = 'Tooltip';\n"],"names":[],"mappings":";;;;;;;;AA8HA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAuFG;AACI;;;AAuBH;;;;;;AAQA;AACI;AACE;AACA;AACA;AACqB;AACxB;AACH;;AAGF;;AASA;;;AAGE;AACA;;;AAGA;;;;AAKA;;;AAGI;;;;AAGF;;;;;AAMJ;;AAEE;;;;;AAMF;AAEE;AACA;;AAEF;AACA;;AAGE;;AAEG;AACH;;AAkCgB;AACA;;;AAoBpB;AAEA;;"}
|
|
1
|
+
{"version":3,"file":"Tooltip.js","sources":["src/components/primitives/Tooltip/Tooltip.tsx"],"sourcesContent":["'use client';\n\n// src/components/primitives/Tooltip/Tooltip.tsx\nimport { Tooltip as BaseTooltip } from '@base-ui/react/tooltip';\nimport React from 'react';\n\nimport { cx } from '@/utils/cx';\n\nimport { ArrowSvg, StyledTooltipArrow } from './Arrow';\nimport {\n tooltipContentStyle,\n tooltipTriggerStyle,\n tooltipPositionerStyle,\n} from './Tooltip.css';\nimport {\n CollisionAvoidance,\n TooltipAnimation,\n TooltipCollisionConfig,\n TooltipCollisionStrategy,\n TooltipPlacement,\n TooltipPositioner,\n BaseTooltipPositionerProps,\n BaseTooltipRootProps,\n} from './types';\nimport { parseCollisionStrategy, parsePlacement } from './utils';\n\nimport type { Prettify } from '@/types/utilities';\n\ninterface TooltipBaseProps extends Omit<\n React.HTMLAttributes<HTMLDivElement>,\n 'css' | 'title'\n> {\n /**\n * The trigger element that will show the tooltip on hover.\n * Must be a single React element or component.\n *\n * @example\n * <Button>Hover me</Button>\n * <IconButton><SaveIcon /></IconButton>\n */\n children: React.ReactElement;\n\n /**\n * The content to display in the tooltip.\n * Can be simple text or a complex React node for advanced customization.\n *\n * @example\n * \"Save file\"\n * <div><strong>Save</strong><br />Ctrl+S</div>\n */\n title: React.ReactNode;\n\n /**\n * Intuitive placement similar to MUI\n * @default \"top\"\n */\n placement?: TooltipPlacement;\n\n /**\n * Collision handling strategy\n * @default \"smart\"\n */\n collision?: TooltipCollisionStrategy;\n\n /**\n * Advanced collision configuration for power users\n * When provided, overrides the collision strategy\n */\n collisionConfig?: TooltipCollisionConfig;\n\n /**\n * Advanced positioning configuration\n */\n positioner?: TooltipPositioner;\n\n /**\n * Animation configuration\n */\n animation?: TooltipAnimation;\n\n /**\n * Delay in milliseconds before showing the tooltip\n * @default 600\n */\n delay?: number;\n\n /**\n * Delay in milliseconds before hiding the tooltip\n * @default 0\n */\n closeDelay?: number;\n\n /**\n * Whether to show the arrow pointing to the trigger\n * @default true\n */\n arrow?: boolean;\n\n /**\n * Whether the tooltip is disabled\n * When true, tooltip will not appear\n * @default false\n */\n disabled?: boolean;\n\n /**\n * Direct props passed to Base UI Root component\n * For power users who need full control\n */\n rootProps?: Partial<BaseTooltipRootProps>;\n\n /**\n * Direct props passed to Base UI Positioner component\n * For power users who need full control\n */\n positionerProps?: Partial<BaseTooltipPositionerProps>;\n\n /**\n * Test identifier for automated testing\n */\n testId?: string;\n\n ref?: React.Ref<HTMLDivElement>;\n}\n\n/**\n * Props for the Tooltip component with prettified type for better IntelliSense\n */\nexport type TooltipProps = Prettify<TooltipBaseProps>;\n\n/**\n * A tooltip component that displays contextual information on hover.\n *\n * Built on @base-ui/react for robust accessibility and positioning.\n * Provides an intuitive API similar to MUI with advanced positioning and collision handling.\n * Supports both simple text and complex React nodes for advanced tooltip content.\n *\n * @example\n * ```tsx\n * // Simple text tooltip\n * <Tooltip title=\"Save your work\">\n * <Button>Save</Button>\n * </Tooltip>\n *\n * // Intuitive positioning (MUI-style)\n * <Tooltip placement=\"bottom-start\" title=\"Menu options\">\n * <IconButton><MenuIcon /></IconButton>\n * </Tooltip>\n *\n * // Intelligent collision handling (new default)\n * <Tooltip collision=\"smart\" title=\"Intelligent positioning\">\n * <Button>Smart tooltip</Button>\n * </Tooltip>\n *\n * // Advanced collision configuration\n * <Tooltip\n * collisionConfig={{\n * side: 'flip',\n * align: 'shift',\n * fallbackAxisSide: 'start',\n * hideWhenDetached: true\n * }}\n * title=\"Custom collision handling\"\n * >\n * <Button>Advanced collision</Button>\n * </Tooltip>\n *\n * // Advanced positioning\n * <Tooltip\n * positioner={{\n * offset: 12,\n * padding: 10,\n * sticky: true,\n * boundary: '#container'\n * }}\n * title=\"Advanced positioning\"\n * >\n * <Button>Advanced</Button>\n * </Tooltip>\n *\n * // Custom animations\n * <Tooltip\n * animation={{\n * animated: true,\n * duration: 300,\n * easing: 'ease-in-out'\n * }}\n * title=\"Custom animation\"\n * >\n * <Button>Animated</Button>\n * </Tooltip>\n *\n * // Interactive tooltip\n * <Tooltip\n * hoverable={true}\n * closeDelay={300}\n * title={\n * <div>\n * <strong>Interactive Content</strong>\n * <br />\n * <button>Click me</button>\n * </div>\n * }\n * >\n * <Button>Hoverable tooltip</Button>\n * </Tooltip>\n *\n * // Full control with raw props\n * <Tooltip\n * arrow={false}\n * rootProps={{ trackCursorAxis: 'x' }}\n * positionerProps={{ arrowPadding: 20 }}\n * title=\"Full control\"\n * >\n * <Button>Power user</Button>\n * </Tooltip>\n * ```\n */\nexport const Tooltip: React.FC<TooltipProps> = ({\n children,\n title,\n placement = 'top',\n collision = 'smart',\n collisionConfig,\n positioner = {},\n animation = {},\n delay = 600,\n closeDelay = 0,\n arrow = true,\n disabled = false,\n rootProps = {},\n positionerProps = {},\n className,\n testId,\n id,\n style,\n ref,\n ...htmlProps\n}) => {\n // If disabled, return just the children without tooltip\n if (disabled) {\n return children;\n }\n\n // Parse placement into side and align\n const { side, align } = parsePlacement(placement);\n\n // Parse collision strategy or use custom config\n const collisionSettings = collisionConfig\n ? {\n collisionAvoidance: {\n side: collisionConfig.side ?? 'flip',\n align: collisionConfig.align ?? 'shift',\n fallbackAxisSide: collisionConfig.fallbackAxisSide ?? 'none',\n } as CollisionAvoidance,\n }\n : parseCollisionStrategy(collision);\n\n // Prepare positioner configuration\n const {\n offset = 8,\n padding = 8,\n sticky = false,\n boundary,\n trackCursor,\n } = positioner;\n\n // Prepare final positioner props\n const finalPositionerProps: Partial<BaseTooltipPositionerProps> = {\n side,\n align,\n sideOffset: offset,\n collisionPadding: padding,\n sticky,\n collisionAvoidance: collisionSettings.collisionAvoidance,\n ...positionerProps,\n };\n\n // Handle boundary if specified\n if (boundary) {\n if (typeof boundary === 'string') {\n const boundaryElement = document.querySelector(boundary);\n if (boundaryElement) {\n finalPositionerProps.collisionBoundary = boundaryElement;\n }\n } else {\n finalPositionerProps.collisionBoundary = boundary;\n }\n }\n\n // Merge our z-index positioner class with any caller-provided className,\n // honoring Base UI's function-className form (power-user `positionerProps`).\n const incomingPositionerClassName = finalPositionerProps.className;\n let positionerClassName: typeof incomingPositionerClassName;\n if (typeof incomingPositionerClassName === 'function') {\n positionerClassName = state =>\n cx(tooltipPositionerStyle, incomingPositionerClassName(state));\n } else {\n positionerClassName = cx(\n tooltipPositionerStyle,\n incomingPositionerClassName\n );\n }\n\n // Handle cursor tracking. Copy into a local object instead of mutating the\n // caller-owned `rootProps` prop.\n const finalRootProps: Partial<BaseTooltipRootProps> = { ...rootProps };\n if (trackCursor) {\n finalRootProps.trackCursorAxis = trackCursor;\n }\n\n // Build animation transition style. Honors `prefers-reduced-motion: reduce`\n // by collapsing to `transition: none` regardless of caller-passed config —\n // user OS-level preference always wins over component config.\n const prefersReducedMotion =\n typeof window !== 'undefined' &&\n typeof window.matchMedia === 'function' &&\n window.matchMedia('(prefers-reduced-motion: reduce)').matches;\n const animated = animation.animated !== false && !prefersReducedMotion;\n const duration = animation.duration ?? 200;\n const easing = animation.easing ?? 'ease-out';\n\n const animationStyle: React.CSSProperties = animated\n ? {\n transition: `opacity ${duration}ms ${easing}, transform ${duration}ms ${easing}`,\n }\n : { transition: 'none' };\n\n return (\n <BaseTooltip.Provider\n delay={delay}\n closeDelay={closeDelay}\n {...finalRootProps}\n >\n <BaseTooltip.Root>\n <BaseTooltip.Trigger\n render={props => (\n <div\n {...props}\n className={cx(tooltipTriggerStyle, props.className)}\n />\n )}\n >\n {children}\n </BaseTooltip.Trigger>\n\n <BaseTooltip.Portal>\n <BaseTooltip.Positioner\n {...finalPositionerProps}\n className={positionerClassName}\n >\n <BaseTooltip.Popup\n render={props => (\n <div\n {...props}\n className={cx(\n tooltipContentStyle,\n className,\n props.className\n )}\n data-testid={testId}\n id={id}\n style={{\n ...animationStyle,\n ...style,\n ...props.style,\n }}\n ref={ref}\n {...htmlProps}\n >\n {title}\n {arrow && (\n <StyledTooltipArrow>\n <ArrowSvg />\n </StyledTooltipArrow>\n )}\n </div>\n )}\n />\n </BaseTooltip.Positioner>\n </BaseTooltip.Portal>\n </BaseTooltip.Root>\n </BaseTooltip.Provider>\n );\n};\n\nTooltip.displayName = 'Tooltip';\n"],"names":[],"mappings":";;;;;;;;AAkIA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAuFG;AACI;;;AAuBH;;;;;;AAQA;AACI;AACE;AACA;AACA;AACqB;AACxB;AACH;;AAGF;;AASA;;;AAGE;AACA;;;AAGA;;;;AAKA;;;AAGI;;;;AAGF;;;;;AAMJ;AACA;AACA;AACE;;;AAGA;;;;AAQF;;AAEE;;;;;AAMF;AAEE;AACA;;AAEF;AACA;;AAGE;;AAEG;AACH;;AAqCgB;AACA;;;AAoBpB;AAEA;;"}
|
|
@@ -56,8 +56,11 @@ function categorizeChildren(children) {
|
|
|
56
56
|
return { layers, worldChildren, overlayChildren };
|
|
57
57
|
}
|
|
58
58
|
/**
|
|
59
|
-
* Viewport — pan/zoom canvas + HTML container for editor-style
|
|
60
|
-
* such as node graphs, timelines, and 2D world editors.
|
|
59
|
+
* Viewport — **2D only.** A pan/zoom canvas + HTML container for editor-style
|
|
60
|
+
* surfaces such as node graphs, timelines, and 2D world editors. Despite the
|
|
61
|
+
* name it is *not* a 3D viewport: it owns a 2D pan/zoom transform, not a camera.
|
|
62
|
+
* For 3D, host your own WebGL/WebGPU canvas (e.g. inside an `AppShell.Dock`) and
|
|
63
|
+
* reach for `Viewport` only for 2D editor surfaces.
|
|
61
64
|
*
|
|
62
65
|
* Composes:
|
|
63
66
|
* - Multiple perf-isolated `<ViewportLayer>` canvases (one draw cycle each).
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Viewport.js","sources":["src/components/primitives/viewport/Viewport.tsx"],"sourcesContent":["'use client';\n\nimport { assignInlineVars } from '@vanilla-extract/dynamic';\nimport React, {\n useCallback,\n useImperativeHandle,\n useLayoutEffect,\n useMemo,\n useRef,\n useSyncExternalStore,\n} from 'react';\n\nimport { useControlledState, useResizeObserver } from '@/hooks';\nimport { cx } from '@/utils/cx';\nimport { clamp } from '@/utils/mathUtils';\n\nimport { useViewportGestures } from './useViewportGestures';\nimport {\n viewportRootRecipe,\n viewportHeightVar,\n marqueeRectStyle,\n} from './Viewport.css';\nimport { ViewportStoreContext, useViewportStore } from './ViewportContext';\nimport { ViewportStore } from './ViewportStore';\n\nimport type { ViewportProps, ViewportTransform } from './Viewport.types';\n\nconst IDENTITY_TRANSFORM: ViewportTransform = { x: 0, y: 0, zoom: 1 };\n\n// Stable empty defaults so `useViewportGestures` does not re-compute config\n// objects on every render when the consumer passes nothing.\nconst EMPTY_PAN = {};\nconst EMPTY_ZOOM = {};\nconst EMPTY_SELECTION = {};\n\ninterface CategorizedChildren {\n layers: React.ReactElement[];\n worldChildren: React.ReactElement[];\n overlayChildren: React.ReactElement[];\n}\n\nfunction getSlotKind(\n child: React.ReactElement\n): 'layer' | 'world' | 'overlay' | null {\n const displayName = (child.type as { displayName?: string } | undefined)\n ?.displayName;\n if (displayName === 'ViewportLayer') return 'layer';\n if (displayName === 'ViewportWorld') return 'world';\n if (displayName === 'ViewportOverlay') return 'overlay';\n // Compound components rendered in the overlay slot (e.g. <ViewportMinimap />).\n if (displayName === 'ViewportMinimap') return 'overlay';\n return null;\n}\n\nfunction categorizeChildren(children: React.ReactNode): CategorizedChildren {\n const layers: React.ReactElement[] = [];\n const worldChildren: React.ReactElement[] = [];\n const overlayChildren: React.ReactElement[] = [];\n\n React.Children.forEach(children, child => {\n if (!React.isValidElement(child)) return;\n const kind = getSlotKind(child);\n if (kind === 'layer') layers.push(child);\n else if (kind === 'world') worldChildren.push(child);\n else if (kind === 'overlay') overlayChildren.push(child);\n });\n\n return { layers, worldChildren, overlayChildren };\n}\n\n/**\n * Viewport — pan/zoom canvas + HTML container for editor-style
|
|
1
|
+
{"version":3,"file":"Viewport.js","sources":["src/components/primitives/viewport/Viewport.tsx"],"sourcesContent":["'use client';\n\nimport { assignInlineVars } from '@vanilla-extract/dynamic';\nimport React, {\n useCallback,\n useImperativeHandle,\n useLayoutEffect,\n useMemo,\n useRef,\n useSyncExternalStore,\n} from 'react';\n\nimport { useControlledState, useResizeObserver } from '@/hooks';\nimport { cx } from '@/utils/cx';\nimport { clamp } from '@/utils/mathUtils';\n\nimport { useViewportGestures } from './useViewportGestures';\nimport {\n viewportRootRecipe,\n viewportHeightVar,\n marqueeRectStyle,\n} from './Viewport.css';\nimport { ViewportStoreContext, useViewportStore } from './ViewportContext';\nimport { ViewportStore } from './ViewportStore';\n\nimport type { ViewportProps, ViewportTransform } from './Viewport.types';\n\nconst IDENTITY_TRANSFORM: ViewportTransform = { x: 0, y: 0, zoom: 1 };\n\n// Stable empty defaults so `useViewportGestures` does not re-compute config\n// objects on every render when the consumer passes nothing.\nconst EMPTY_PAN = {};\nconst EMPTY_ZOOM = {};\nconst EMPTY_SELECTION = {};\n\ninterface CategorizedChildren {\n layers: React.ReactElement[];\n worldChildren: React.ReactElement[];\n overlayChildren: React.ReactElement[];\n}\n\nfunction getSlotKind(\n child: React.ReactElement\n): 'layer' | 'world' | 'overlay' | null {\n const displayName = (child.type as { displayName?: string } | undefined)\n ?.displayName;\n if (displayName === 'ViewportLayer') return 'layer';\n if (displayName === 'ViewportWorld') return 'world';\n if (displayName === 'ViewportOverlay') return 'overlay';\n // Compound components rendered in the overlay slot (e.g. <ViewportMinimap />).\n if (displayName === 'ViewportMinimap') return 'overlay';\n return null;\n}\n\nfunction categorizeChildren(children: React.ReactNode): CategorizedChildren {\n const layers: React.ReactElement[] = [];\n const worldChildren: React.ReactElement[] = [];\n const overlayChildren: React.ReactElement[] = [];\n\n React.Children.forEach(children, child => {\n if (!React.isValidElement(child)) return;\n const kind = getSlotKind(child);\n if (kind === 'layer') layers.push(child);\n else if (kind === 'world') worldChildren.push(child);\n else if (kind === 'overlay') overlayChildren.push(child);\n });\n\n return { layers, worldChildren, overlayChildren };\n}\n\n/**\n * Viewport — **2D only.** A pan/zoom canvas + HTML container for editor-style\n * surfaces such as node graphs, timelines, and 2D world editors. Despite the\n * name it is *not* a 3D viewport: it owns a 2D pan/zoom transform, not a camera.\n * For 3D, host your own WebGL/WebGPU canvas (e.g. inside an `AppShell.Dock`) and\n * reach for `Viewport` only for 2D editor surfaces.\n *\n * Composes:\n * - Multiple perf-isolated `<ViewportLayer>` canvases (one draw cycle each).\n * - `<ViewportWorld>` for HTML positioned in world coordinates.\n * - `<ViewportOverlay>` for HTML pinned to the viewport (toolbars, minimap).\n *\n * Owns pan/zoom gestures (drag-to-pan, wheel-zoom-toward-cursor, optional\n * pinch and space-to-pan) and optional marquee selection. Transform is\n * controlled or uncontrolled (`useControlledState`-style) and mirrored\n * into an internal `ViewportStore` that powers slice-level subscriptions\n * for layers, world, and overlay children.\n *\n * @example\n * ```tsx\n * <Viewport responsive selectionRect={{ enabled: true }}>\n * <ViewportLayer name=\"grid\" draw={drawGrid} invalidateOn={[gridStep]} />\n * <ViewportLayer name=\"content\" draw={drawNodes} invalidateOn={[nodes]} />\n * <ViewportWorld>\n * <NodeView style={{ position: 'absolute', left: 100, top: 200 }} />\n * </ViewportWorld>\n * <ViewportOverlay>\n * <Toolbar />\n * </ViewportOverlay>\n * </Viewport>\n * ```\n */\nexport const Viewport = ({\n transform: transformProp,\n defaultTransform,\n onTransformChange,\n minZoom = 0.1,\n maxZoom = 8,\n pan,\n zoom,\n selectionRect,\n onSelectionChange,\n onPanStart,\n onPanEnd,\n onZoomStart,\n onZoomEnd,\n responsive = false,\n height = 300,\n disabled = false,\n role = 'application',\n ariaLabel,\n ariaRoledescription,\n className,\n style,\n testId,\n id,\n children,\n ref,\n ...rest\n}: ViewportProps): React.ReactElement => {\n const rootRef = useRef<HTMLDivElement>(null);\n\n // One store per <Viewport> instance, stable for the lifetime of the component.\n const store = useMemo(() => new ViewportStore(), []);\n\n // ── Controlled / uncontrolled transform (React state) ──\n const clampTransform = useCallback(\n (t: ViewportTransform): ViewportTransform => ({\n x: t.x,\n y: t.y,\n zoom: clamp(t.zoom, minZoom, maxZoom),\n }),\n [minZoom, maxZoom]\n );\n\n const [transformRaw, setTransformRaw] = useControlledState<ViewportTransform>(\n {\n value: transformProp,\n defaultValue: defaultTransform,\n onChange: onTransformChange,\n fallback: IDENTITY_TRANSFORM,\n }\n );\n\n const transform = useMemo(\n () => clampTransform(transformRaw),\n [transformRaw, clampTransform]\n );\n const setTransform = useCallback(\n (next: ViewportTransform) => {\n setTransformRaw(clampTransform(next));\n },\n [setTransformRaw, clampTransform]\n );\n\n // Mirror React state → store before paint so layer/world subscribers see the\n // latest transform in the same frame.\n useLayoutEffect(() => {\n store.setTransform(transform);\n }, [store, transform]);\n\n // Wire the store's imperative methods (fitToContent etc.) to React's\n // setTransform and current zoom limits.\n useLayoutEffect(() => {\n store.configureImperative({ setTransform, minZoom, maxZoom });\n }, [store, setTransform, minZoom, maxZoom]);\n\n // ── Size tracking ──\n useResizeObserver(rootRef, entry => {\n store.setSize({\n width: entry.contentRect.width,\n height: entry.contentRect.height,\n });\n });\n\n // Seed size on mount in case ResizeObserver fires asynchronously.\n useLayoutEffect(() => {\n const el = rootRef.current;\n if (!el) return;\n const rect = el.getBoundingClientRect();\n store.setSize({ width: rect.width, height: rect.height });\n }, [store]);\n\n // ── Imperative ref ──\n useImperativeHandle(ref, () => store.handle, [store]);\n\n // ── Gestures ──\n const { handlers } = useViewportGestures({\n viewportRef: rootRef,\n store,\n setTransform,\n disabled,\n minZoom,\n maxZoom,\n pan: pan ?? EMPTY_PAN,\n zoom: zoom ?? EMPTY_ZOOM,\n selectionRect: selectionRect ?? EMPTY_SELECTION,\n callbacks: {\n onPanStart,\n onPanEnd,\n onZoomStart,\n onZoomEnd,\n onSelectionChange,\n },\n });\n\n // Subscribe to isPanning only — drives the cursor class on the wrapper.\n const isPanning = useSyncExternalStore(\n store.subscribeIsPanning,\n store.getIsPanning\n );\n\n // ── Children categorization ──\n const { layers, worldChildren, overlayChildren } = useMemo(\n () => categorizeChildren(children),\n [children]\n );\n\n const rootStyle = useMemo(\n () => ({\n ...style,\n ...(!responsive\n ? assignInlineVars({ [viewportHeightVar]: `${height}px` })\n : {}),\n }),\n [style, responsive, height]\n );\n\n return (\n <div\n ref={rootRef}\n id={id}\n data-testid={testId}\n role={role}\n aria-label={ariaLabel}\n aria-roledescription={ariaRoledescription}\n tabIndex={disabled ? -1 : 0}\n className={cx(\n viewportRootRecipe({\n responsive,\n disabled,\n panning: isPanning,\n }),\n className\n )}\n style={rootStyle}\n onPointerDown={handlers.onPointerDown}\n onPointerMove={handlers.onPointerMove}\n onPointerUp={handlers.onPointerUp}\n onPointerCancel={handlers.onPointerCancel}\n onPointerEnter={handlers.onPointerEnter}\n onPointerLeave={handlers.onPointerLeave}\n onContextMenu={handlers.onContextMenu}\n {...rest}\n >\n <ViewportStoreContext.Provider value={store}>\n {layers}\n {worldChildren}\n {overlayChildren}\n <MarqueeRect />\n </ViewportStoreContext.Provider>\n </div>\n );\n};\n\nViewport.displayName = 'Viewport';\n\n/**\n * Internal — renders the marquee selection rectangle. Subscribes only to\n * the marquee slice so it re-renders only when the rectangle changes,\n * never on transform/size/isPanning mutations.\n */\nconst MarqueeRect: React.FC = () => {\n const store = useViewportStore();\n const rect = useSyncExternalStore(\n store.subscribeMarquee,\n store.getMarqueeRect\n );\n if (!rect) return null;\n return (\n <div\n className={marqueeRectStyle}\n style={{\n left: rect.x,\n top: rect.y,\n width: rect.width,\n height: rect.height,\n }}\n data-viewport-marquee=\"\"\n />\n );\n};\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;AA2BA;AAEA;AACA;AACA;AACA;AACA;AAQA;AAGE;AACE;;AACmC;;AACA;;AACE;;;AAEA;AACvC;AACF;AAEA;;;;;AAMI;;AACA;;AACsB;;AACK;;AACE;AAC/B;AAEA;AACF;AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+BG;AACI;AA4BL;;AAGA;;;;;;AAQG;AAIH;AAEI;AACA;AACA;AACA;AACD;AAGH;AAIA;AAEI;AACF;;;;AAOA;AACF;;;;;;;AASA;;AAEI;AACA;AACD;AACH;;;AAIE;AACA;;AACA;AACA;AACF;;AAGA;;AAGA;AACE;;;;;;;;;AASA;;;;;;AAMC;AACF;;AAGD;;;AAWA;AAEI;;AAEE;;;AAMN;;;AAaQ;AACD;AAqBT;AAEA;AAEA;;;;AAIG;AACH;AACE;AACA;AAIA;AAAW;AACX;;;;;;AAYF;;"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import './../../../assets/src/components/shell/AppShell/AppShell.css.ts.vanilla-
|
|
1
|
+
import './../../../assets/src/components/shell/AppShell/AppShell.css.ts.vanilla-BLK2-DRL.css';
|
|
2
2
|
import { createRuntimeFn } from '@vanilla-extract/recipes/createRuntimeFn';
|
|
3
3
|
|
|
4
4
|
var dockSlot = 'AppShell_dockSlot__1m62ws2l';
|