diff --git a/editor/package.json b/editor/package.json index 5b639fa0640a..91b7e6b6f694 100644 --- a/editor/package.json +++ b/editor/package.json @@ -154,6 +154,7 @@ "@liveblocks/yjs": "1.10.0", "@popperjs/core": "2.4.4", "@radix-ui/react-dropdown-menu": "2.1.1", + "@radix-ui/react-select": "2.1.1", "@remix-run/react": "2.0.1", "@remix-run/server-runtime": "2.3.1", "@root/encoding": "1.0.1", diff --git a/editor/pnpm-lock.yaml b/editor/pnpm-lock.yaml index 6fbb279035e9..a624524d7631 100644 --- a/editor/pnpm-lock.yaml +++ b/editor/pnpm-lock.yaml @@ -69,6 +69,7 @@ specifiers: '@peculiar/webcrypto': 1.4.3 '@popperjs/core': 2.4.4 '@radix-ui/react-dropdown-menu': 2.1.1 + '@radix-ui/react-select': 2.1.1 '@react-three/fiber': 8.0.14 '@relative-ci/agent': ^2.1.0 '@remix-run/react': 2.0.1 @@ -371,6 +372,7 @@ dependencies: '@liveblocks/yjs': 1.10.0_yjs@13.6.8 '@popperjs/core': 2.4.4 '@radix-ui/react-dropdown-menu': 2.1.1_wgqxz3kjpz4ixw4iz4mbg2tk4i + '@radix-ui/react-select': 2.1.1_wgqxz3kjpz4ixw4iz4mbg2tk4i '@remix-run/react': 2.0.1_4uzd4uhfxjy6hfd2xmixhrzbqy_jrnwecbkcjj5lllk4vji4c6ehm '@remix-run/server-runtime': 2.3.1_typescript@5.5.4 '@root/encoding': 1.0.1 @@ -3258,6 +3260,10 @@ packages: '@babel/runtime': 7.20.13 dev: false + /@radix-ui/number/1.1.0: + resolution: {integrity: sha512-V3gRzhVNU1ldS5XhAPTom1fOIo4ccrjjJgmE+LI2h/WaFpHmx0MQApT+KZHnx8abG6Avtfcz4WoEciMnpFT3HQ==} + dev: false + /@radix-ui/primitive/1.0.0: resolution: {integrity: sha512-3e7rn8FDMin4CgeL7Z/49smCA3rFYY3Ha2rUQ7HRWFadS5iCRw08ZgVT1LaNTCNqgvrUiyczLflrVrF0SRQtNA==} dependencies: @@ -3938,6 +3944,46 @@ packages: react-dom: 18.1.0_abari7w75zfr6mrhvamxwmfpxm_react@18.1.0 dev: false + /@radix-ui/react-select/2.1.1_wgqxz3kjpz4ixw4iz4mbg2tk4i: + resolution: {integrity: sha512-8iRDfyLtzxlprOo9IicnzvpsO1wNCkuwzzCM+Z5Rb5tNOpCdMvcc2AkzX0Fz+Tz9v6NJ5B/7EEgyZveo4FBRfQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@radix-ui/number': 1.1.0 + '@radix-ui/primitive': 1.1.0 + '@radix-ui/react-collection': 1.1.0_wgqxz3kjpz4ixw4iz4mbg2tk4i + '@radix-ui/react-compose-refs': 1.1.0_7cpxmzzodpxnolj5zcc5cr63ji + '@radix-ui/react-context': 1.1.0_7cpxmzzodpxnolj5zcc5cr63ji + '@radix-ui/react-direction': 1.1.0_7cpxmzzodpxnolj5zcc5cr63ji + '@radix-ui/react-dismissable-layer': 1.1.0_wgqxz3kjpz4ixw4iz4mbg2tk4i + '@radix-ui/react-focus-guards': 1.1.0_7cpxmzzodpxnolj5zcc5cr63ji + '@radix-ui/react-focus-scope': 1.1.0_wgqxz3kjpz4ixw4iz4mbg2tk4i + '@radix-ui/react-id': 1.1.0_7cpxmzzodpxnolj5zcc5cr63ji + '@radix-ui/react-popper': 1.2.0_wgqxz3kjpz4ixw4iz4mbg2tk4i + '@radix-ui/react-portal': 1.1.1_wgqxz3kjpz4ixw4iz4mbg2tk4i + '@radix-ui/react-primitive': 2.0.0_wgqxz3kjpz4ixw4iz4mbg2tk4i + '@radix-ui/react-slot': 1.1.0_7cpxmzzodpxnolj5zcc5cr63ji + '@radix-ui/react-use-callback-ref': 1.1.0_7cpxmzzodpxnolj5zcc5cr63ji + '@radix-ui/react-use-controllable-state': 1.1.0_7cpxmzzodpxnolj5zcc5cr63ji + '@radix-ui/react-use-layout-effect': 1.1.0_7cpxmzzodpxnolj5zcc5cr63ji + '@radix-ui/react-use-previous': 1.1.0_7cpxmzzodpxnolj5zcc5cr63ji + '@radix-ui/react-visually-hidden': 1.1.0_wgqxz3kjpz4ixw4iz4mbg2tk4i + '@types/react': 18.0.9 + '@types/react-dom': 18.0.3 + aria-hidden: 1.2.3 + react: 18.1.0_47cciibm4ysmleigs33s763fqu + react-dom: 18.1.0_abari7w75zfr6mrhvamxwmfpxm_react@18.1.0 + react-remove-scroll: 2.5.7_7cpxmzzodpxnolj5zcc5cr63ji + dev: false + /@radix-ui/react-slot/1.0.1_react@18.1.0: resolution: {integrity: sha512-avutXAFL1ehGvAXtPquu0YK5oz6ctS474iM3vNGQIkswrVhdrS52e3uoMQBzZhNRAIE0jBnUyXWNmSjGHhCFcw==} peerDependencies: @@ -4162,6 +4208,19 @@ packages: react: 18.1.0_47cciibm4ysmleigs33s763fqu dev: false + /@radix-ui/react-use-previous/1.1.0_7cpxmzzodpxnolj5zcc5cr63ji: + resolution: {integrity: sha512-Z/e78qg2YFnnXcW88A4JmTtm4ADckLno6F7OXotmkQfeuCVaKuYzqAATPhVzl3delXE7CxIV8shofPn3jPc5Og==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@types/react': 18.0.9 + react: 18.1.0_47cciibm4ysmleigs33s763fqu + dev: false + /@radix-ui/react-use-rect/1.0.1_7cpxmzzodpxnolj5zcc5cr63ji: resolution: {integrity: sha512-Cq5DLuSiuYVKNU8orzJMbl15TXilTnJKUCltMVQg53BQOF1/C5toAaGrowkgksdBQ9H+SRL23g0HDmg9tvmxXw==} peerDependencies: @@ -4241,6 +4300,26 @@ packages: react-dom: 18.1.0_abari7w75zfr6mrhvamxwmfpxm_react@18.1.0 dev: false + /@radix-ui/react-visually-hidden/1.1.0_wgqxz3kjpz4ixw4iz4mbg2tk4i: + resolution: {integrity: sha512-N8MDZqtgCgG5S3aV60INAB475osJousYpZ4cTJ2cFbMpdHS5Y6loLTH8LPtkj2QN0x93J30HT/M3qJXM0+lyeQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@radix-ui/react-primitive': 2.0.0_wgqxz3kjpz4ixw4iz4mbg2tk4i + '@types/react': 18.0.9 + '@types/react-dom': 18.0.3 + react: 18.1.0_47cciibm4ysmleigs33s763fqu + react-dom: 18.1.0_abari7w75zfr6mrhvamxwmfpxm_react@18.1.0 + dev: false + /@radix-ui/rect/1.0.1: resolution: {integrity: sha512-fyrgCaedtvMg9NK3en0pnOYJdtfwxUcNolezkNPUsoX57X8oQk+NkqcvzHXD2uKNij6GXmWU9NDru2IWjrO4BQ==} dependencies: diff --git a/editor/src/components/inspector/common/css-utils.ts b/editor/src/components/inspector/common/css-utils.ts index 12ebc42a49f4..405a1e1a24a9 100644 --- a/editor/src/components/inspector/common/css-utils.ts +++ b/editor/src/components/inspector/common/css-utils.ts @@ -88,6 +88,7 @@ import type { ParseError } from '../../../utils/value-parser-utils' import { descriptionParseError } from '../../../utils/value-parser-utils' import * as csstree from 'css-tree' import { expandCssTreeNodeValue, parseCssTreeNodeValue } from './css-tree-utils' +import type { IcnProps } from '../../../uuiui' var combineRegExp = function (regexpList: Array, flags?: string) { let source: string = '' @@ -982,9 +983,35 @@ export function parseGridPosition( } } -export const GridAutoFlowValues = ['row', 'column', 'dense', 'row dense', 'column dense'] as const +export const GridAutoFlowValues = ['column', 'column dense', 'row', 'row dense', 'dense'] as const export type GridAutoFlow = (typeof GridAutoFlowValues)[number] +export function gridAutoFlowIcon(value: GridAutoFlow): IcnProps { + switch (value) { + case 'column': + case 'column dense': + return { + category: 'inspector-element', + type: 'arrowDown', + color: 'black', + width: 16, + height: 16, + } + case 'dense': + case 'row': + case 'row dense': + return { + category: 'inspector-element', + type: 'arrowRight', + color: 'black', + width: 16, + height: 16, + } + default: + assertNever(value) + } +} + export function parseGridAutoFlow(rawValue: string): GridAutoFlow | null { if (GridAutoFlowValues.some((v) => v === rawValue)) { return rawValue as GridAutoFlow diff --git a/editor/src/components/inspector/flex-section.tsx b/editor/src/components/inspector/flex-section.tsx index fb607befd110..33f12ae182e4 100644 --- a/editor/src/components/inspector/flex-section.tsx +++ b/editor/src/components/inspector/flex-section.tsx @@ -23,7 +23,6 @@ import type { DetectedLayoutSystem } from 'utopia-shared/src/types' import { NO_OP } from '../../core/shared/utils' import { assertNever } from '../../core/shared/utils' import { - PopupList, FlexRow, Icons, InspectorSectionIcons, @@ -44,6 +43,7 @@ import { cssKeyword, cssNumber, cssNumberToString, + gridAutoFlowIcon, gridCSSKeyword, gridCSSNumber, isCSSKeyword, @@ -54,11 +54,7 @@ import { isValidGridDimensionKeyword, type GridDimension, } from './common/css-utils' -import { - applyCommandsAction, - setProp_UNSAFE, - transientActions, -} from '../editor/actions/action-creators' +import { applyCommandsAction, transientActions } from '../editor/actions/action-creators' import type { PropertyToUpdate } from '../canvas/commands/set-property-command' import { propertyToDelete, @@ -73,19 +69,22 @@ import type { GridPosition, } from '../../core/shared/element-template' import { - emptyComments, gridPositionValue, - jsExpressionValue, type ElementInstanceMetadata, type GridElementProperties, } from '../../core/shared/element-template' import { setGridPropsCommands } from '../canvas/canvas-strategies/strategies/grid-helpers' import { type CanvasCommand } from '../canvas/commands/commands' import type { DropdownMenuItem } from '../../uuiui/radix-components' -import { DropdownMenu, regularDropdownMenuItem } from '../../uuiui/radix-components' +import { + DropdownMenu, + RadixSelect, + regularDropdownMenuItem, + regularRadixSelectOption, + separatorRadixSelectOption, +} from '../../uuiui/radix-components' import { useInspectorLayoutInfo, useInspectorStyleInfo } from './common/property-path-hooks' import { NumberOrKeywordControl } from '../../uuiui/inputs/number-or-keyword-control' -import type { SelectOption } from './controls/select-control' import { optionalMap } from '../../core/shared/optional-utils' import { cssNumberEqual } from '../canvas/controls/select-mode/controls-common' import type { EditorAction } from '../editor/action-types' @@ -825,12 +824,25 @@ GapRowColumnControl.displayName = 'GapRowColumnControl' const AutoFlowPopupId = 'auto-flow-control' -const selectOption = (value: GridAutoFlow | 'unset'): SelectOption => ({ - label: value, - value: value, +function selectOption(value: GridAutoFlow) { + return regularRadixSelectOption({ + label: value, + value: value, + icon: gridAutoFlowIcon(value), + }) +} + +const unsetSelectOption = regularRadixSelectOption({ + label: 'unset', + value: 'unset', + placeholder: true, }) -const GRID_AUTO_FLOW_DROPDOWN_OPTIONS: Array = GridAutoFlowValues.map(selectOption) +const autoflowOptions = [ + unsetSelectOption, + separatorRadixSelectOption(), + ...GridAutoFlowValues.map(selectOption), +] const AutoFlowControl = React.memo(() => { const dispatch = useDispatch() @@ -853,28 +865,30 @@ const AutoFlowControl = React.memo(() => { 'AutoFlowControl gridAutoFlowValue', ) - const { controlStyles, controlStatus } = useInspectorStyleInfo('gridAutoFlow') + const { controlStatus } = useInspectorStyleInfo('gridAutoFlow') const currentValue = React.useMemo( () => controlStatus === 'detected' - ? selectOption('unset') - : optionalMap((v) => selectOption(v), gridAutoFlowValue) ?? undefined, + ? unsetSelectOption + : optionalMap(selectOption, gridAutoFlowValue) ?? undefined, [controlStatus, gridAutoFlowValue], ) const onSubmit = React.useCallback( - (option: SelectOption) => { + (value: string) => { if (selectededViewsRef.current.length === 0) { return } dispatch( - selectededViewsRef.current.map((target) => - setProp_UNSAFE( - target, - PP.create('style', 'gridAutoFlow'), - jsExpressionValue(option.value, emptyComments), - ), + selectededViewsRef.current.map((path) => + applyCommandsAction([ + updateBulkProperties('always', path, [ + value === 'unset' + ? propertyToDelete(PP.create('style', 'gridAutoFlow')) + : propertyToSet(PP.create('style', 'gridAutoFlow'), value), + ]), + ]), ), ) }, @@ -884,16 +898,12 @@ const AutoFlowControl = React.memo(() => { return (
Auto Flow
-
) diff --git a/editor/src/uuiui/radix-components.tsx b/editor/src/uuiui/radix-components.tsx index cadd738b4f08..928ea1240429 100644 --- a/editor/src/uuiui/radix-components.tsx +++ b/editor/src/uuiui/radix-components.tsx @@ -2,12 +2,14 @@ /** @jsx jsx */ import { jsx } from '@emotion/react' import * as RadixDropdownMenu from '@radix-ui/react-dropdown-menu' +import * as Select from '@radix-ui/react-select' import { styled } from '@stitches/react' import type { CSSProperties } from 'react' import React from 'react' import { colorTheme, UtopiaStyles } from './styles/theme' -import { Icons } from './icons' +import { Icons, SmallerIcons } from './icons' import { when } from '../utils/react-conditionals' +import { Icn, type IcnProps } from './icn' const RadixItemContainer = styled(RadixDropdownMenu.Item, { minWidth: 128, @@ -190,3 +192,150 @@ const Separator = React.memo(() => { ) }) Separator.displayName = 'Separator' + +type RegularRadixSelectOption = { + type: 'REGULAR' + value: string + label: string + icon?: IcnProps + placeholder?: boolean +} + +export function regularRadixSelectOption( + params: Omit, +): RegularRadixSelectOption { + return { + type: 'REGULAR', + ...params, + } +} + +type Separator = { + type: 'SEPARATOR' +} + +export function separatorRadixSelectOption(): Separator { + return { + type: 'SEPARATOR', + } +} + +export type RadixSelectOption = RegularRadixSelectOption | Separator + +export const RadixSelect = React.memo( + (props: { + id: string + value: RegularRadixSelectOption | null + options: RadixSelectOption[] + style?: CSSProperties + onValueChange?: (value: string) => void + }) => { + const stopPropagation = React.useCallback((e: React.KeyboardEvent) => { + e.stopPropagation() + }, []) + + return ( + + + + + + + + + + + + + + {props.options.map((option, index) => { + if (option.type === 'SEPARATOR') { + return ( + + ) + } + + const label = `${option.label.charAt(0).toUpperCase()}${option.label.slice(1)}` + return ( + + {props.value?.value === option.value ? ( + + ) : ( +
+ )} + {option.icon != null ? ( + + ) : null} + {label} + + ) + })} + + + + + + + + ) + }, +) +RadixSelect.displayName = 'RadixSelect'