From 8741dfe8176c44e68c5b311182c6d2db06bb7472 Mon Sep 17 00:00:00 2001 From: Federico Ruggi <1081051+ruggi@users.noreply.github.com> Date: Fri, 11 Oct 2024 12:41:59 +0200 Subject: [PATCH] Insert better default grids (#6512) **Problem:** 1. When creating a grid layout from the inspector the default grid should be 2x2 with a 10px gap 2. There should be an insert menu entry in the canvas toolbar for a 3x3 grid with a 10px gap 3. There should be an entry for a grid in the floating component picker too **Fix:** Implement all of the above: https://github.com/user-attachments/assets/6214c47d-ae77-4735-b9ec-ab00198e4b90 Note: the icon for the grid is the one used for the navigator, there should be a dedicated one with the right size (16x16) for the canvas toolbar as well. Fixes #6511 --- .../convert-to-grid-strategy.ts | 28 +++++++-- .../editor/canvas-toolbar-states.tsx | 27 +++++++++ .../src/components/editor/canvas-toolbar.tsx | 12 ++++ editor/src/components/editor/defaults.ts | 14 ++++- .../src/components/editor/insert-callbacks.ts | 5 ++ .../editor/insertmenu.spec.browser2.tsx | 2 +- .../project-components.spec.ts.snap | 23 +++++++ .../components/shared/project-components.ts | 60 +++++++++++++++++++ 8 files changed, 164 insertions(+), 7 deletions(-) diff --git a/editor/src/components/common/shared-strategies/convert-to-grid-strategy.ts b/editor/src/components/common/shared-strategies/convert-to-grid-strategy.ts index 9c99dddfe74c..8299cbd6cb21 100644 --- a/editor/src/components/common/shared-strategies/convert-to-grid-strategy.ts +++ b/editor/src/components/common/shared-strategies/convert-to-grid-strategy.ts @@ -47,7 +47,10 @@ function guessLayoutInfoAlongAxis( } } -function guessMatchingGridSetup(children: Array): { +function guessMatchingGridSetup( + children: Array, + isFlexContainer: boolean, +): { gap: number numberOfColumns: number numberOfRows: number @@ -65,10 +68,12 @@ function guessMatchingGridSetup(children: Array): { (a, b) => b.frame.y - (a.frame.y + a.frame.height), ) + const minRowsOrCols = isFlexContainer ? 1 : 2 + return { gap: (horizontalData.averageGap + verticalData.averageGap) / 2, - numberOfColumns: Math.max(1, horizontalData.nChildren), - numberOfRows: Math.max(1, verticalData.nChildren), + numberOfColumns: Math.max(minRowsOrCols, horizontalData.nChildren), + numberOfRows: Math.max(minRowsOrCols, verticalData.nChildren), } } @@ -90,9 +95,16 @@ export function convertLayoutToGridCommands( frame: MetadataUtils.getFrameOrZeroRectInCanvasCoords(child, metadata), })) - const { gap, numberOfColumns, numberOfRows } = guessMatchingGridSetup(childFrames) + const isFlexContainer = MetadataUtils.isFlexLayoutedContainer( + MetadataUtils.findElementByElementPath(metadata, elementPath), + ) + + const { gap, numberOfColumns, numberOfRows } = guessMatchingGridSetup( + childFrames, + isFlexContainer, + ) - return [ + let commands = [ ...prunePropsCommands(flexContainerProps, elementPath), ...prunePropsCommands(gridContainerProps, elementPath), ...childrenPaths.flatMap((child) => [ @@ -114,5 +126,11 @@ export function convertLayoutToGridCommands( Array(numberOfRows).fill('1fr').join(' '), ), ] + + if (!isFlexContainer) { + commands.push(setProperty('always', elementPath, PP.create('style', 'gap'), 10)) + } + + return commands }) } diff --git a/editor/src/components/editor/canvas-toolbar-states.tsx b/editor/src/components/editor/canvas-toolbar-states.tsx index 091326327fbe..ed410bd250f9 100644 --- a/editor/src/components/editor/canvas-toolbar-states.tsx +++ b/editor/src/components/editor/canvas-toolbar-states.tsx @@ -8,6 +8,9 @@ import type { Optic } from '../../core/shared/optics/optics' import { fromField, fromTypeGuard } from '../../core/shared/optics/optic-creators' import { anyBy, set } from '../../core/shared/optics/optic-utilities' import type { EditorAction } from './action-types' +import { MetadataUtils } from '../../core/model/element-metadata-utils' +import { getJSXAttributesAtPath } from '../../core/shared/jsx-attribute-utils' +import { create } from '../../core/shared/property-path' // This is the data structure that governs the Canvas Toolbar's submenus and active buttons type ToolbarMode = @@ -21,6 +24,7 @@ type ToolbarMode = imageInsertionActive: boolean buttonInsertionActive: boolean conditionalInsertionActive: boolean + gridInsertionActive: boolean insertSidebarOpen: boolean } } @@ -110,6 +114,28 @@ export function useToolbarMode(): ToolbarMode { const insertionTargetConditional = editorMode.type === 'insert' && editorMode.subjects.some((subject) => subject.insertionSubjectWrapper === 'conditional') + const insertionTargetGrid = + editorMode.type === 'insert' && + editorMode.subjects.some((subject) => { + if (subject.element.name.baseVariable !== 'div') { + return false + } + + const style = subject.element.props.find( + (p) => p.type === 'JSX_ATTRIBUTES_ENTRY' && p.key === 'style', + ) + if (style == null) { + return false + } + + const display = getJSXAttributesAtPath(subject.element.props, create('style', 'display')) + return ( + style.type === 'JSX_ATTRIBUTES_ENTRY' && + style.value.type === 'ATTRIBUTE_VALUE' && + display.attribute.type === 'PART_OF_ATTRIBUTE_VALUE' && + display.attribute.value === 'grid' + ) + }) return { primary: 'insert', @@ -119,6 +145,7 @@ export function useToolbarMode(): ToolbarMode { imageInsertionActive: insertionTargetImage, buttonInsertionActive: insertionTargetButton, conditionalInsertionActive: insertionTargetConditional, + gridInsertionActive: insertionTargetGrid, insertSidebarOpen: rightMenuTab === RightMenuTab.Insert, }, } diff --git a/editor/src/components/editor/canvas-toolbar.tsx b/editor/src/components/editor/canvas-toolbar.tsx index 9ebb819f7aa5..0455691f97f2 100644 --- a/editor/src/components/editor/canvas-toolbar.tsx +++ b/editor/src/components/editor/canvas-toolbar.tsx @@ -28,6 +28,7 @@ import { useEnterDrawToInsertForButton, useEnterDrawToInsertForConditional, useEnterDrawToInsertForDiv, + useEnterDrawToInsertForGrid, useEnterDrawToInsertForImage, useEnterTextEditMode, } from './insert-callbacks' @@ -79,6 +80,7 @@ export const InsertOrEditTextButtonTestId = 'insert-or-edit-text-button' export const PlayModeButtonTestId = 'canvas-toolbar-play-mode' export const CommentModeButtonTestId = (status: string) => `canvas-toolbar-comment-mode-${status}` export const InsertConditionalButtonTestId = 'insert-mode-conditional' +export const InsertGridButtonTestId = 'insert-mode-grid' export const CanvasToolbarId = 'canvas-toolbar' export const CanvasToolbarSearchPortalId = 'canvas-toolbar-search-portal' @@ -220,6 +222,7 @@ export const CanvasToolbar = React.memo(() => { const insertTextCallback = useEnterTextEditMode() const insertButtonCallback = useEnterDrawToInsertForButton() const insertConditionalCallback = useEnterDrawToInsertForConditional() + const insertGridCallback = useEnterDrawToInsertForGrid() // Back to select mode, close the "floating" menu and turn off the forced insert mode. const dispatchSwitchToSelectModeCloseMenus = React.useCallback(() => { @@ -548,6 +551,15 @@ export const CanvasToolbar = React.memo(() => { onClick={insertDivCallback} /> + + + ) => void { + return useEnterDrawToInsertForElement(defaultGridElement) +} + export function useEnterDrawToInsertForConditional(): (event: React.MouseEvent) => void { const conditionalInsertCallback = useEnterDrawToInsertForElement(defaultDivElement) diff --git a/editor/src/components/editor/insertmenu.spec.browser2.tsx b/editor/src/components/editor/insertmenu.spec.browser2.tsx index 3dc761f4d0c3..7e70223e118f 100644 --- a/editor/src/components/editor/insertmenu.spec.browser2.tsx +++ b/editor/src/components/editor/insertmenu.spec.browser2.tsx @@ -26,7 +26,7 @@ function getInsertItems() { return screen.queryAllByTestId(/^component-picker-item-/gi) } -const allInsertItemsCount = 23 +const allInsertItemsCount = 24 function openInsertMenu(renderResult: EditorRenderResult) { return renderResult.dispatch( diff --git a/editor/src/components/shared/__snapshots__/project-components.spec.ts.snap b/editor/src/components/shared/__snapshots__/project-components.spec.ts.snap index 8f4fcbee4592..35a569f48fc9 100644 --- a/editor/src/components/shared/__snapshots__/project-components.spec.ts.snap +++ b/editor/src/components/shared/__snapshots__/project-components.spec.ts.snap @@ -336,6 +336,29 @@ Array [ "type": "SAMPLES_GROUP", }, }, + Object { + "insertableComponents": Array [ + Object { + "defaultSize": Object { + "height": 150, + "width": 150, + }, + "element": [Function], + "icon": "component", + "importsToAdd": Object {}, + "insertionCeiling": Object { + "type": "file-root", + }, + "name": "Grid", + "stylePropOptions": Array [ + "do-not-add", + ], + }, + ], + "source": Object { + "type": "HTML_GRID", + }, + }, Object { "insertableComponents": Array [], "source": Object { diff --git a/editor/src/components/shared/project-components.ts b/editor/src/components/shared/project-components.ts index aabba62f4631..1fc9ef491136 100644 --- a/editor/src/components/shared/project-components.ts +++ b/editor/src/components/shared/project-components.ts @@ -63,6 +63,7 @@ import { intrinsicHTMLElementNamesThatSupportChildren } from '../../core/shared/ import { getTopLevelElementByExportsDetail } from '../../core/model/project-file-utils' import { type Icon } from 'utopia-api' import type { FileRootPath } from '../canvas/ui-jsx-canvas' +import type { CSSProperties } from 'react' export type StylePropOption = 'do-not-add' | 'add-size' @@ -126,6 +127,14 @@ export function insertableComponentGroupDiv(): InsertableComponentGroupDiv { return { type: 'HTML_DIV' } } +export interface InsertableComponentGroupGrid { + type: 'HTML_GRID' +} + +export function insertableComponentGroupGrid(): InsertableComponentGroupGrid { + return { type: 'HTML_GRID' } +} + export function insertableComponentGroupHTML(): InsertableComponentGroupHTML { return { type: 'HTML_GROUP', @@ -207,6 +216,7 @@ export type InsertableComponentGroupType = | InsertableComponentGroupSamples | InsertableComponentGroupGroups | InsertableComponentGroupMap + | InsertableComponentGroupGrid export interface InsertableComponentGroup { source: InsertableComponentGroupType @@ -254,6 +264,8 @@ export function getInsertableGroupLabel(insertableType: InsertableComponentGroup return 'Div' case 'MAP_GROUP': return 'List' + case 'HTML_GRID': + return 'Grid' default: assertNever(insertableType) } @@ -271,6 +283,7 @@ export function getInsertableGroupPackageStatus( case 'GROUPS_GROUP': case 'HTML_DIV': case 'MAP_GROUP': + case 'HTML_GRID': return 'loaded' case 'PROJECT_DEPENDENCY_GROUP': return insertableType.dependencyStatus @@ -462,6 +475,47 @@ const divComponentGroup = { ), } +export function insertableGridStyle(): CSSProperties { + return { + position: 'absolute', + display: 'grid', + gridTemplateColumns: '1fr 1fr 1fr', + gridTemplateRows: '1fr 1fr 1fr', + gap: 10, + } +} + +const gridComponentGroup: ComponentDescriptorsForFile = { + grid: { + properties: {}, + supportsChildren: true, + preferredChildComponents: [], + source: defaultComponentDescriptor(), + variants: [ + { + insertMenuLabel: 'Grid', + elementToInsert: () => + jsxElementWithoutUID( + 'div', + jsxAttributesFromMap({ + style: jsExpressionValue( + { + ...insertableGridStyle(), + width: 150, + height: 150, + }, + emptyComments, + ), + }), + [], + ), + importsToAdd: {}, + }, + ], + ...ComponentDescriptorDefaults, + }, +} + const conditionalElementsDescriptors: ComponentDescriptorsForFile = { conditional: { properties: {}, @@ -832,6 +886,11 @@ export function getComponentGroups( // Add groups group. addDependencyDescriptor(insertableComponentGroupGroups(), groupElementsDescriptors) // TODO instead of this, use createWrapInGroupActions! + addDependencyDescriptor(insertableComponentGroupGrid(), gridComponentGroup, { + width: 150, + height: 150, + }) + // Add entries for dependencies of the project. for (const dependency of dependencies) { if (isResolvedNpmDependency(dependency)) { @@ -909,6 +968,7 @@ export function insertMenuModesForInsertableComponentGroupType( case 'SAMPLES_GROUP': case 'HTML_DIV': case 'MAP_GROUP': + case 'HTML_GRID': return insertMenuModes.all case 'GROUPS_GROUP': return insertMenuModes.onlyWrap