diff --git a/editor/resources/editor/icons/light/component/remix-link-aqua-18x18@2x.png b/editor/resources/editor/icons/light/component/remix-link-aqua-18x18@2x.png index 843d12856f74..a3e6386cf0f0 100644 Binary files a/editor/resources/editor/icons/light/component/remix-link-aqua-18x18@2x.png and b/editor/resources/editor/icons/light/component/remix-link-aqua-18x18@2x.png differ diff --git a/editor/resources/editor/icons/light/component/remix-link-black-18x18@2x.png b/editor/resources/editor/icons/light/component/remix-link-black-18x18@2x.png index eca982a023e9..d42f13a8b927 100644 Binary files a/editor/resources/editor/icons/light/component/remix-link-black-18x18@2x.png and b/editor/resources/editor/icons/light/component/remix-link-black-18x18@2x.png differ diff --git a/editor/resources/editor/icons/light/component/remix-link-blue-18x18@2x.png b/editor/resources/editor/icons/light/component/remix-link-blue-18x18@2x.png index 466955f60c47..651329f89aa3 100644 Binary files a/editor/resources/editor/icons/light/component/remix-link-blue-18x18@2x.png and b/editor/resources/editor/icons/light/component/remix-link-blue-18x18@2x.png differ diff --git a/editor/resources/editor/icons/light/component/remix-link-darkgray-18x18@2x.png b/editor/resources/editor/icons/light/component/remix-link-darkgray-18x18@2x.png index 057f2051ffd4..825f0aed043e 100644 Binary files a/editor/resources/editor/icons/light/component/remix-link-darkgray-18x18@2x.png and b/editor/resources/editor/icons/light/component/remix-link-darkgray-18x18@2x.png differ diff --git a/editor/resources/editor/icons/light/component/remix-link-gray-18x18@2x.png b/editor/resources/editor/icons/light/component/remix-link-gray-18x18@2x.png index b33b905c24af..564c05936473 100644 Binary files a/editor/resources/editor/icons/light/component/remix-link-gray-18x18@2x.png and b/editor/resources/editor/icons/light/component/remix-link-gray-18x18@2x.png differ diff --git a/editor/resources/editor/icons/light/component/remix-link-lightaqua-18x18@2x.png b/editor/resources/editor/icons/light/component/remix-link-lightaqua-18x18@2x.png index e040d6f38735..869a249849d6 100644 Binary files a/editor/resources/editor/icons/light/component/remix-link-lightaqua-18x18@2x.png and b/editor/resources/editor/icons/light/component/remix-link-lightaqua-18x18@2x.png differ diff --git a/editor/resources/editor/icons/light/component/remix-link-lightblue-18x18@2x.png b/editor/resources/editor/icons/light/component/remix-link-lightblue-18x18@2x.png index 88751ffbec76..60049123841e 100644 Binary files a/editor/resources/editor/icons/light/component/remix-link-lightblue-18x18@2x.png and b/editor/resources/editor/icons/light/component/remix-link-lightblue-18x18@2x.png differ diff --git a/editor/resources/editor/icons/light/component/remix-link-lightgray-18x18@2x.png b/editor/resources/editor/icons/light/component/remix-link-lightgray-18x18@2x.png index 5d9e3331b226..f5b29ecd46e7 100644 Binary files a/editor/resources/editor/icons/light/component/remix-link-lightgray-18x18@2x.png and b/editor/resources/editor/icons/light/component/remix-link-lightgray-18x18@2x.png differ diff --git a/editor/resources/editor/icons/light/component/remix-link-lightorange-18x18@2x.png b/editor/resources/editor/icons/light/component/remix-link-lightorange-18x18@2x.png index 2cac9ab46e4b..51c33e9c4ced 100644 Binary files a/editor/resources/editor/icons/light/component/remix-link-lightorange-18x18@2x.png and b/editor/resources/editor/icons/light/component/remix-link-lightorange-18x18@2x.png differ diff --git a/editor/resources/editor/icons/light/component/remix-link-lightpurple-18x18@2x.png b/editor/resources/editor/icons/light/component/remix-link-lightpurple-18x18@2x.png index d742c8a8dbd8..cc1dca1092d6 100644 Binary files a/editor/resources/editor/icons/light/component/remix-link-lightpurple-18x18@2x.png and b/editor/resources/editor/icons/light/component/remix-link-lightpurple-18x18@2x.png differ diff --git a/editor/resources/editor/icons/light/component/remix-link-orange-18x18@2x.png b/editor/resources/editor/icons/light/component/remix-link-orange-18x18@2x.png index 6a80dd557e9e..ee725118445c 100644 Binary files a/editor/resources/editor/icons/light/component/remix-link-orange-18x18@2x.png and b/editor/resources/editor/icons/light/component/remix-link-orange-18x18@2x.png differ diff --git a/editor/resources/editor/icons/light/component/remix-link-purple-18x18@2x.png b/editor/resources/editor/icons/light/component/remix-link-purple-18x18@2x.png index b46e9d845fd7..f1971fc4bb09 100644 Binary files a/editor/resources/editor/icons/light/component/remix-link-purple-18x18@2x.png and b/editor/resources/editor/icons/light/component/remix-link-purple-18x18@2x.png differ diff --git a/editor/resources/editor/icons/light/component/remix-link-red-18x18@2x.png b/editor/resources/editor/icons/light/component/remix-link-red-18x18@2x.png index 237779e435c4..c6c41471a7c4 100644 Binary files a/editor/resources/editor/icons/light/component/remix-link-red-18x18@2x.png and b/editor/resources/editor/icons/light/component/remix-link-red-18x18@2x.png differ diff --git a/editor/resources/editor/icons/light/component/remix-link-white-18x18@2x.png b/editor/resources/editor/icons/light/component/remix-link-white-18x18@2x.png index 57f63af79e42..c61c98e75b78 100644 Binary files a/editor/resources/editor/icons/light/component/remix-link-white-18x18@2x.png and b/editor/resources/editor/icons/light/component/remix-link-white-18x18@2x.png differ diff --git a/editor/src/components/canvas/remix/remix-navigator.spec.browser2.tsx b/editor/src/components/canvas/remix/remix-navigator.spec.browser2.tsx index 048f1afbb88f..3f2c35c80804 100644 --- a/editor/src/components/canvas/remix/remix-navigator.spec.browser2.tsx +++ b/editor/src/components/canvas/remix/remix-navigator.spec.browser2.tsx @@ -217,7 +217,7 @@ describe('Remix navigator', () => { expect(navigatorItemElement.style.color).toEqual('var(--utopitheme-fg0)') }) - it('Remix Outlet navigator item label contains route component name', async () => { + it('Remix Outlet navigator item label contains the suffix of the full path', async () => { const project = createModifiedProject({ [StoryboardFilePath]: `import * as React from 'react' import { RemixScene, Storyboard } from 'utopia-api' @@ -277,7 +277,7 @@ describe('Remix navigator', () => { ), ), ) - expect(outletItemElement.textContent).toEqual('Outlet: Index') + expect(outletItemElement.textContent).toEqual('Outlet: (home)') }) }) describe('Reparenting in Remix projects in the navigator', () => { diff --git a/editor/src/components/canvas/remix/utopia-remix-root-component.tsx b/editor/src/components/canvas/remix/utopia-remix-root-component.tsx index f807eb9c77f6..24a6757d0919 100644 --- a/editor/src/components/canvas/remix/utopia-remix-root-component.tsx +++ b/editor/src/components/canvas/remix/utopia-remix-root-component.tsx @@ -27,7 +27,7 @@ interface RemixNavigationContext { entries: Array } -interface RemixNavigationAtomData { +export interface RemixNavigationAtomData { [pathString: string]: RemixNavigationContext | undefined } diff --git a/editor/src/components/navigator/navigator-drag-layer.tsx b/editor/src/components/navigator/navigator-drag-layer.tsx index 7f99b644f195..8425de04c07b 100644 --- a/editor/src/components/navigator/navigator-drag-layer.tsx +++ b/editor/src/components/navigator/navigator-drag-layer.tsx @@ -94,6 +94,7 @@ export const NavigatorDragLayer = React.memo(() => { selected={selected} dispatch={NO_OP} inputVisible={false} + remixItemType={'none'} /> diff --git a/editor/src/components/navigator/navigator-item/item-label.tsx b/editor/src/components/navigator/navigator-item/item-label.tsx index f2a9498c15aa..b55d89c35f42 100644 --- a/editor/src/components/navigator/navigator-item/item-label.tsx +++ b/editor/src/components/navigator/navigator-item/item-label.tsx @@ -13,11 +13,18 @@ import { getConditionalFlag, } from '../../../core/model/conditionals' import { colorTheme, flexRowStyle, StringInput } from '../../../uuiui' -import type { EditorDispatch } from '../../editor/action-types' +import type { EditorDispatch, ElementPaste } from '../../editor/action-types' import * as EditorActions from '../../editor/actions/action-creators' import { Substores, useEditorState } from '../../editor/store/store-hook' import { renameComponent } from '../actions' +import type { RemixItemType } from './navigator-item' import { NavigatorItemTestId } from './navigator-item' +import { useAtom } from 'jotai' +import type { RemixNavigationAtomData } from '../../canvas/remix/utopia-remix-root-component' +import { RemixNavigationAtom } from '../../canvas/remix/utopia-remix-root-component' +import * as EP from '../../../core/shared/element-path' +import type { ElementPath } from '../../../core/shared/project-file-types' +import type { Location } from 'react-router' export function itemLabelTestIdForEntry(navigatorEntry: NavigatorEntry): string { return `${NavigatorItemTestId(varSafeNavigatorEntryToKey(navigatorEntry))}-label` @@ -33,6 +40,7 @@ interface ItemLabelProps { suffix?: string inputVisible: boolean style?: CSSProperties + remixItemType: RemixItemType } export const ItemLabel = React.memo((props: ItemLabelProps) => { @@ -87,9 +95,35 @@ export const ItemLabel = React.memo((props: ItemLabelProps) => { } }, [inputVisible, testId]) - const value = React.useMemo(() => { - return suffix == null ? name : `${name} ${suffix}` - }, [suffix, name]) + const maybeLinkTarget = useEditorState( + Substores.metadata, + (store) => { + if (props.remixItemType !== 'link') { + return null + } + return store.editor.allElementProps[EP.toString(props.target.elementPath)]?.['to'] ?? null + }, + 'ItemLabel maybeLinkTarget', + ) + + const [remixNavigationData] = useAtom(RemixNavigationAtom) + + const maybePathForOutlet = React.useMemo(() => { + if (props.remixItemType !== 'outlet') { + return null + } + return remixSceneLocationFromOutletPath(props.target.elementPath, remixNavigationData)?.pathname + }, [props.remixItemType, props.target.elementPath, remixNavigationData]) + + const label = React.useMemo(() => { + if (maybeLinkTarget != null) { + return maybeLinkTarget + } + if (maybePathForOutlet != null) { + return maybePathForOutlet === '/' ? 'Outlet: (home)' : `Outlet: ${maybePathForOutlet}` + } + return suffix == null ? name : `Outlet: ${name} ${suffix}` + }, [maybeLinkTarget, maybePathForOutlet, suffix, name]) const cancelRename = React.useCallback(() => { setName(name) @@ -225,9 +259,7 @@ export const ItemLabel = React.memo((props: ItemLabelProps) => { key='item-label' data-testid={itemLabelTestIdForEntry(target)} style={{ - backgroundColor: 'transparent', - paddingTop: 3, - paddingBottom: 3, + padding: '3px 0px', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', @@ -245,9 +277,20 @@ export const ItemLabel = React.memo((props: ItemLabelProps) => { } }} > - {value} + {label} )} ) }) + +// here +function remixSceneLocationFromOutletPath( + outletPath: ElementPath, + remixNavigationData: RemixNavigationAtomData, +): Location | null { + return EP.findAmongAncestorsOfPath( + outletPath, + (p) => remixNavigationData[EP.toString(p)]?.location ?? null, + ) +} diff --git a/editor/src/components/navigator/navigator-item/navigator-item-components.tsx b/editor/src/components/navigator/navigator-item/navigator-item-components.tsx index e2460c75ce0b..c8c105b18a94 100644 --- a/editor/src/components/navigator/navigator-item/navigator-item-components.tsx +++ b/editor/src/components/navigator/navigator-item/navigator-item-components.tsx @@ -20,11 +20,17 @@ import type { SelectionLocked } from '../../canvas/canvas-types' export const NavigatorHintCircleDiameter = 8 +const outletAwareBackgroundColor = ( + colorTheme: ReturnType, + isOutlet: boolean, +) => (isOutlet ? colorTheme.aqua.value : colorTheme.navigatorResizeHintBorder.value) + interface NavigatorHintProps { testId: string shouldBeShown: boolean shouldAcceptMouseEvents: boolean margin: number + isOutletOrDescendantOfOutlet: boolean } export const NavigatorHintTop = React.forwardRef( @@ -61,7 +67,10 @@ export const NavigatorHintTop = React.forwardRef
@@ -118,7 +130,10 @@ export const NavigatorHintBottom = React.forwardRef
@@ -338,7 +356,7 @@ export const NavigatorItemActionSheet: React.FunctionComponent< return ( x, + (metadata, path) => { + return isDescendantOfOutletOrOutlet(path, metadata) + }, +)((_, x) => EP.toString(x)) + function onHoverDropTargetLine( propsOfDraggedItem: NavigatorItemDragAndDropWrapperProps, propsOfDropTargetItem: NavigatorItemDragAndDropWrapperProps, @@ -800,6 +813,12 @@ export const NavigatorItemContainer = React.memo((props: NavigatorItemDragAndDro [props.elementPath], ) + const isOutletOrDescendantOfOutlet = useEditorState( + Substores.metadata, + (store) => isDescendantOfOutletOrOutletSelector(store, props.elementPath), + 'NavigatorItemContainer isOutlet', + ) + const mouseEventStopPropagation = React.useCallback((event: React.MouseEvent) => { // Prevent mouse events from passing through this element so that the same event // on a containing element will only trigger de-selection if the event doesn't hit @@ -826,6 +845,7 @@ export const NavigatorItemContainer = React.memo((props: NavigatorItemDragAndDro shouldBeShown={shouldShowTopHint} shouldAcceptMouseEvents={shouldTopDropLineInterceptMouseEvents} margin={margin} + isOutletOrDescendantOfOutlet={isOutletOrDescendantOfOutlet} />, )}
) @@ -1002,6 +1024,7 @@ export const SyntheticNavigatorItemContainer = React.memo( selected={props.selected} parentOutline={parentOutline} visibleNavigatorTargets={props.visibleNavigatorTargets} + isOutletOrDescendantOfOutlet={props.isOutletOrDescendantOfOutlet} />
@@ -1048,6 +1071,7 @@ export const ConditionalClauseNavigatorItemContainer = React.memo( selected={props.selected} parentOutline={'none'} visibleNavigatorTargets={props.visibleNavigatorTargets} + isOutletOrDescendantOfOutlet={props.isOutletOrDescendantOfOutlet} /> @@ -1092,8 +1116,20 @@ export const ErrorNavigatorItemContainer = React.memo((props: ErrorNavigatorItem selected={props.selected} parentOutline={'none'} visibleNavigatorTargets={props.visibleNavigatorTargets} + isOutletOrDescendantOfOutlet={props.isOutletOrDescendantOfOutlet} /> ) }) + +function isDescendantOfOutletOrOutlet( + path: ElementPath, + metadata: ElementInstanceMetadataMap, +): boolean { + return ( + EP.findAmongAncestorsOfPath(path, (p) => + MetadataUtils.isProbablyRemixOutlet(metadata, p) ? true : null, + ) === true + ) +} diff --git a/editor/src/components/navigator/navigator-item/navigator-item-wrapper.tsx b/editor/src/components/navigator/navigator-item/navigator-item-wrapper.tsx index 0e8ddae4ba90..af307bba7ede 100644 --- a/editor/src/components/navigator/navigator-item/navigator-item-wrapper.tsx +++ b/editor/src/components/navigator/navigator-item/navigator-item-wrapper.tsx @@ -350,6 +350,7 @@ export const NavigatorItemWrapper: React.FunctionComponent< ...navigatorItemProps, childOrAttribute: props.navigatorEntry.childOrAttribute, elementPath: props.navigatorEntry.elementPath, + isOutletOrDescendantOfOutlet: false, } return } @@ -358,6 +359,7 @@ export const NavigatorItemWrapper: React.FunctionComponent< const entryProps: ConditionalClauseNavigatorItemContainerProps = { ...navigatorItemProps, navigatorEntry: props.navigatorEntry, + isOutletOrDescendantOfOutlet: false, } return } @@ -366,6 +368,7 @@ export const NavigatorItemWrapper: React.FunctionComponent< const entryProps: ErrorNavigatorItemContainerProps = { ...navigatorItemProps, navigatorEntry: props.navigatorEntry, + isOutletOrDescendantOfOutlet: false, } return diff --git a/editor/src/components/navigator/navigator-item/navigator-item.tsx b/editor/src/components/navigator/navigator-item/navigator-item.tsx index 0b7fbb895229..3fcd299e6bc6 100644 --- a/editor/src/components/navigator/navigator-item/navigator-item.tsx +++ b/editor/src/components/navigator/navigator-item/navigator-item.tsx @@ -57,6 +57,7 @@ import { NavigatorItemActionSheet } from './navigator-item-components' import { assertNever } from '../../../core/shared/utils' import type { ElementPathTrees } from '../../../core/shared/element-path-tree' import { MapCounter } from './map-counter' +import { Outlet } from 'react-router' export function getItemHeight(navigatorEntry: NavigatorEntry): number { if (isConditionalClauseNavigatorEntry(navigatorEntry)) { @@ -392,6 +393,7 @@ const elementWarningsSelector = createCachedSelector( )((_, navigatorEntry) => navigatorEntryToKey(navigatorEntry)) type CodeItemType = 'conditional' | 'map' | 'code' | 'none' | 'remix' +export type RemixItemType = 'scene' | 'outlet' | 'link' | 'none' export interface NavigatorItemInnerProps { navigatorEntry: NavigatorEntry @@ -406,6 +408,7 @@ export interface NavigatorItemInnerProps { renamingTarget: ElementPath | null selected: boolean parentOutline: ParentOutline + isOutletOrDescendantOfOutlet: boolean visibleNavigatorTargets: Array } @@ -419,6 +422,7 @@ export const NavigatorItem: React.FunctionComponent< selected, collapsed, navigatorEntry, + isOutletOrDescendantOfOutlet, getSelectedViewsInRange, index, } = props @@ -551,7 +555,8 @@ export const NavigatorItem: React.FunctionComponent< } if ( MetadataUtils.isProbablyRemixOutletFromMetadata(elementMetadata) || - MetadataUtils.isProbablyRemixSceneFromMetadata(elementMetadata) + MetadataUtils.isProbablyRemixSceneFromMetadata(elementMetadata) || + MetadataUtils.isProbablyRemixLinkFromMetadata(elementMetadata) ) { return 'remix' } @@ -560,6 +565,27 @@ export const NavigatorItem: React.FunctionComponent< 'NavigatorItem codeItemType', ) + const remixItemType: RemixItemType = useEditorState( + Substores.metadata, + (store) => { + const elementMetadata = MetadataUtils.findElementByElementPath( + store.editor.jsxMetadata, + props.navigatorEntry.elementPath, + ) + if (MetadataUtils.isProbablyRemixSceneFromMetadata(elementMetadata)) { + return 'scene' + } + if (MetadataUtils.isProbablyRemixOutletFromMetadata(elementMetadata)) { + return 'outlet' + } + if (MetadataUtils.isProbablyRemixLinkFromMetadata(elementMetadata)) { + return 'link' + } + return 'none' + }, + 'NavigatorItem remixItemType', + ) + const isConditional = codeItemType === 'conditional' const isRemixItem = codeItemType === 'remix' const isCodeItem = codeItemType !== 'none' @@ -740,7 +766,9 @@ export const NavigatorItem: React.FunctionComponent<
{ const isComponentScene = useIsProbablyScene(props.navigatorEntry) && props.childComponentCount === 1 + const isRemixScene = props.remixItemType === 'scene' + const isOutlet = props.remixItemType === 'outlet' + const isLink = props.remixItemType === 'link' + + const backgroundLozengeColor = + isCodeItem && !props.selected + ? colorTheme.dynamicBlue10.value + : isOutlet && !props.selected + ? colorTheme.aqua10.value + : 'transparent' + + const textColor = isCodeItem + ? colorTheme.dynamicBlue.value + : isRemixItem + ? colorTheme.aqua.value + : isComponentScene + ? colorTheme.componentPurple.value + : undefined + return (
{ height: 22, paddingLeft: 10, paddingRight: props.codeItemType === 'map' ? 0 : 10, - backgroundColor: - isCodeItem && !props.selected ? colorTheme.dynamicBlue10.value : 'transparent', - color: isCodeItem - ? colorTheme.dynamicBlue.value - : isRemixItem - ? colorTheme.aqua.value - : isComponentScene - ? colorTheme.componentPurple.value - : undefined, + backgroundColor: backgroundLozengeColor, + color: textColor, textTransform: isCodeItem ? 'uppercase' : undefined, }} > @@ -895,6 +937,7 @@ export const NavigatorRowLabel = React.memo((props: NavigatorRowLabelProps) => { selected={props.selected} dispatch={props.dispatch} inputVisible={EP.pathsEqual(props.renamingTarget, props.navigatorEntry.elementPath)} + remixItemType={props.remixItemType} /> ( + path: ElementPath, + fn: (ancestor: ElementPath) => T | null, +): T | null { + const ancestors = getAncestors(path) + for (const ancestor of ancestors) { + const maybeFound = fn(ancestor) + if (maybeFound != null) { + return maybeFound + } + } + return null +} + export function isDescendantOf(target: ElementPath, maybeAncestor: ElementPath): boolean { const targetElementPath = target.parts const maybeAncestorElementPath = maybeAncestor.parts diff --git a/editor/src/uuiui/styles/theme/dark.ts b/editor/src/uuiui/styles/theme/dark.ts index 8699f627fbf5..9bae5e56b928 100644 --- a/editor/src/uuiui/styles/theme/dark.ts +++ b/editor/src/uuiui/styles/theme/dark.ts @@ -34,6 +34,7 @@ const darkBase = { unavailableGrey: createUtopiColor('oklch(100% 0 0 / 22%)'), unavailableGrey10: createUtopiColor('oklch(100% 0 0 / 10%)'), aqua: createUtopiColor('oklch(86% 0.135 208.71)'), + aqua10: createUtopiColor('oklch(86% 0.135 208.71 / 10%)'), aqua05solid: createUtopiColor('oklch(0.41 0.03 238.48)'), bg510solid: createUtopiColor('oklch(0.41 0.02 269.74)'), bg0: createUtopiColor('#000000'), diff --git a/editor/src/uuiui/styles/theme/light.ts b/editor/src/uuiui/styles/theme/light.ts index d4a8da90c3c6..751b4d156845 100644 --- a/editor/src/uuiui/styles/theme/light.ts +++ b/editor/src/uuiui/styles/theme/light.ts @@ -1,4 +1,3 @@ -import { MapLike } from 'typescript' import { UtopiColor, createUtopiColor, enforceUtopiColorTheme } from '../utopi-color-helpers' import type { dark } from './dark' @@ -35,6 +34,7 @@ const lightBase = { unavailableGrey: createUtopiColor('oklch(0% 0 0 / 22%)'), unavailableGrey10: createUtopiColor('oklch(0% 0 0 / 10%)'), aqua: createUtopiColor('oklch(0.74 0.18 218.28)'), + aqua10: createUtopiColor('oklch(0.74 0.18 218.28 / 10%)'), aqua05solid: createUtopiColor('oklch(0.95 0.02 208.7)'), bg510solid: createUtopiColor('oklch(0.95 0 0)'), bg0: createUtopiColor('hsl(0,0%,100%)'),