From 0924afe76a7c29b018174e53be52f81cf9631fb2 Mon Sep 17 00:00:00 2001 From: Balint Gabor <127662+gbalint@users.noreply.github.com> Date: Wed, 9 Oct 2024 12:06:54 +0200 Subject: [PATCH 1/4] Better grid reparent (#6499) **Problem:** One of the root causes of some major issues is that the grid reparent strategies were not created by the general reparent-metastrategy, but the grid-rearrange-move strategy itself. **Fix:** - Removed reparent strategy creation from grid-rearrange-move. After this grid-rearrange-move is always selectable when dragging from a grid, even when outside of a grid (so you can manually switch to this strategy and the child will jump back to the original grid, into the cell which is closest to the mouse). This is a behavior which is similar to flex reorder. - reparent-metastrategy can create a reparent into grid strategy. To achieve this I just had to allow `GRID_CELL_HANDLE` controls in the strategy factory. - Fortunately reparent-metastrategy does not allow reparenting into the same parent, so this automagically solved the problem that reparenting inside the original grid should just do a rearrange (because the reparent strategy is not even selectable in that case) - With this solution, when reparenting from one grid to a different grid, the grid controls were shown on both the original and the new parent grid too. Unfortunately, the grid controls do not support showing the controls on multiple grid instances at the same time (e.g. they shared the state that which cell was highlighted), so I had to switch the grid control of the rearrange strategy to `visible-only-while-active` (so it is not visible on the original grid when the reparent strategy is active) - I had to make sure the grid specific style props are removed during all kinds of reparent (earlier this was manually handled in the grid specific code) - I could remove plenty of reparent specific code lines from `grid-rearrange-move-strategy.ts` - In the earlier solution reparent to flow was the default when repareing into a flow target, but in our regular reparent flow reparent is never the default. Because of this I had to rewrite one test to select the flow strategy manually. **Manual Tests:** I hereby swear that: - [x] I opened a hydrogen project and it loaded - [x] I could navigate to various routes in Play mode --- .../strategies/drag-to-move-metastrategy.tsx | 3 +- .../grid-rearrange-move-strategy.ts | 179 +----------------- ...grid-reparent-strategies.spec.browser2.tsx | 19 +- .../strategies/grid-reparent-strategy.tsx | 6 +- .../reparent-property-changes.ts | 6 + .../strategies/reparent-metastrategy.tsx | 3 +- .../canvas/controls/grid-controls.tsx | 12 +- 7 files changed, 43 insertions(+), 185 deletions(-) diff --git a/editor/src/components/canvas/canvas-strategies/strategies/drag-to-move-metastrategy.tsx b/editor/src/components/canvas/canvas-strategies/strategies/drag-to-move-metastrategy.tsx index 67255fe562d1..c70f692fd2e8 100644 --- a/editor/src/components/canvas/canvas-strategies/strategies/drag-to-move-metastrategy.tsx +++ b/editor/src/components/canvas/canvas-strategies/strategies/drag-to-move-metastrategy.tsx @@ -58,7 +58,8 @@ export const dragToMoveMetaStrategy: MetaCanvasStrategy = ( selectedElements.length === 0 || selectedElements.some(EP.isRootElementOfInstance) || interactionSession == null || - interactionSession.activeControl.type !== 'BOUNDING_AREA' || + (interactionSession.activeControl.type !== 'BOUNDING_AREA' && + interactionSession.activeControl.type !== 'GRID_CELL_HANDLE') || interactionSession.interactionData.type !== 'DRAG' || interactionSession.interactionData.modifiers.alt ) { diff --git a/editor/src/components/canvas/canvas-strategies/strategies/grid-rearrange-move-strategy.ts b/editor/src/components/canvas/canvas-strategies/strategies/grid-rearrange-move-strategy.ts index b8696111b321..d79b46d21b00 100644 --- a/editor/src/components/canvas/canvas-strategies/strategies/grid-rearrange-move-strategy.ts +++ b/editor/src/components/canvas/canvas-strategies/strategies/grid-rearrange-move-strategy.ts @@ -77,7 +77,6 @@ export const gridRearrangeMoveStrategy: CanvasStrategyFactory = ( } const strategyToApply = getStrategyToApply( - canvasState, interactionSession.interactionData, parentGridPath, canvasState.startingMetadata, @@ -130,23 +129,11 @@ export const gridRearrangeMoveStrategy: CanvasStrategyFactory = ( ), ] - const { commands, patch, elementsToRerender } = - strategyToApply.type === 'GRID_REARRANGE' - ? getCommandsAndPatchForGridRearrange( - canvasState, - interactionSession.interactionData, - selectedElement, - ) - : getCommandsAndPatchForReparent( - strategyToApply.strategy, - canvasState, - interactionSession.interactionData, - interactionSession, - customState, - selectedElement, - strategyLifecycle, - gridFrame, - ) + const { commands, patch, elementsToRerender } = getCommandsAndPatchForGridRearrange( + canvasState, + interactionSession.interactionData, + selectedElement, + ) if (commands.length === 0) { return emptyStrategyApplicationResult @@ -188,102 +175,6 @@ function getCommandsAndPatchForGridRearrange( } } -function getCommandsAndPatchForReparent( - strategy: FindReparentStrategyResult, - canvasState: InteractionCanvasState, - interactionData: DragInteractionData, - interactionSession: InteractionSession, - customState: CustomStrategyState, - targetElement: ElementPath, - strategyLifecycle: InteractionLifecycle, - gridFrame: CanvasRectangle, -): { - commands: CanvasCommand[] - patch: CustomStrategyStatePatch - elementsToRerender: ElementPath[] -} { - if (interactionData.drag == null) { - return { commands: [], patch: {}, elementsToRerender: [] } - } - - function applyReparent() { - switch (strategy.strategy) { - case 'REPARENT_AS_ABSOLUTE': - return applyAbsoluteReparent( - canvasState, - interactionSession, - customState, - strategy.target, - [targetElement], - )(strategyLifecycle) - case 'REPARENT_AS_STATIC': - return applyStaticReparent(canvasState, interactionSession, customState, strategy.target) - case 'REPARENT_INTO_GRID': - return applyGridReparent( - canvasState, - interactionData, - interactionSession, - customState, - strategy.target, - [targetElement], - gridFrame, - )() - default: - assertNever(strategy.strategy) - } - } - const result = applyReparent() - - let commands: CanvasCommand[] = [] - - const frame = MetadataUtils.getFrameOrZeroRect(targetElement, canvasState.startingMetadata) - - if (strategy.strategy === 'REPARENT_AS_ABSOLUTE') { - // for absolute reparents, set positioning and size props - commands.push( - updateBulkProperties('always', targetElement, [ - propertyToSet(PP.create('style', 'position'), 'absolute'), - propertyToSet(PP.create('style', 'top'), frame.y + interactionData.drag.y), - propertyToSet(PP.create('style', 'left'), frame.x + interactionData.drag.x), - propertyToSet(PP.create('style', 'width'), frame.width), - propertyToSet(PP.create('style', 'height'), frame.height), - ]), - ) - } else if (strategy.strategy === 'REPARENT_AS_STATIC') { - // for static reparents, set size props - commands.push( - updateBulkProperties('always', targetElement, [ - propertyToSet(PP.create('style', 'width'), frame.width), - propertyToSet(PP.create('style', 'height'), frame.height), - ]), - ) - } - - // for absolute, static, or non-same-grid reparents, remove cell placement props - if ( - strategy.strategy !== 'REPARENT_INTO_GRID' || - !EP.pathsEqual(strategy.target.newParent.intendedParentPath, EP.parentPath(targetElement)) - ) { - commands.push( - updateBulkProperties('on-complete', targetElement, [ - propertyToDelete(PP.create('style', 'gridRow')), - propertyToDelete(PP.create('style', 'gridColumn')), - ]), - ) - } - - commands.push(...result.commands) - - return { - commands: commands, - patch: result.customStatePatch, - elementsToRerender: [ - EP.parentPath(targetElement), - strategy.target.newParent.intendedParentPath, - ], - } -} - function restoreGridTemplateFromProps(params: { columns: GridAutoOrTemplateBase rows: GridAutoOrTemplateBase @@ -342,21 +233,12 @@ function getGridTemplates(jsxMetadata: ElementInstanceMetadataMap, gridPath: Ele } } -type StrategyToApply = - | { - type: 'GRID_REARRANGE' - controlsToRender: ControlWithProps[] - name: string - } - | { - type: 'REPARENT' - controlsToRender: ControlWithProps[] - name: string - strategy: FindReparentStrategyResult - } +type StrategyToApply = { + controlsToRender: ControlWithProps[] + name: string +} function getStrategyToApply( - canvasState: InteractionCanvasState, interactionData: DragInteractionData, parentGridPath: ElementPath, jsxMetadata: ElementInstanceMetadataMap, @@ -366,46 +248,6 @@ function getStrategyToApply( return null } - const shouldReparent = interactionData.modifiers.cmd - if (shouldReparent) { - const pointOnCanvas = offsetPoint(interactionData.originalDragStart, interactionData.drag) - const reparentStrategies = findReparentStrategies( - canvasState, - true, - pointOnCanvas, - 'allow-smaller-parent', - ) - - const strategy = reparentStrategies[0] - if (strategy != null) { - switch (strategy.strategy) { - case 'REPARENT_AS_ABSOLUTE': - return { - type: 'REPARENT', - name: 'Reparent (Abs)', - controlsToRender: controlsForAbsoluteReparent(strategy.target), - strategy: strategy, - } - case 'REPARENT_AS_STATIC': - return { - type: 'REPARENT', - name: 'Reparent (Flex)', - controlsToRender: controlsForStaticReparent(strategy.target), - strategy: strategy, - } - case 'REPARENT_INTO_GRID': - return { - type: 'REPARENT', - name: 'Reparent (Grid)', - controlsToRender: controlsForGridReparent(strategy.target), - strategy: strategy, - } - default: - assertNever(strategy.strategy) - } - } - } - const element = MetadataUtils.findElementByElementPath(jsxMetadata, cell) const name = @@ -415,8 +257,7 @@ function getStrategyToApply( : 'Rearrange Grid (Move)' return { - type: 'GRID_REARRANGE', name: name, - controlsToRender: [controlsForGridPlaceholders(parentGridPath)], + controlsToRender: [controlsForGridPlaceholders(parentGridPath, 'visible-only-while-active')], } } diff --git a/editor/src/components/canvas/canvas-strategies/strategies/grid-reparent-strategies.spec.browser2.tsx b/editor/src/components/canvas/canvas-strategies/strategies/grid-reparent-strategies.spec.browser2.tsx index 96f1fa5a14a6..ad60b02414c2 100644 --- a/editor/src/components/canvas/canvas-strategies/strategies/grid-reparent-strategies.spec.browser2.tsx +++ b/editor/src/components/canvas/canvas-strategies/strategies/grid-reparent-strategies.spec.browser2.tsx @@ -14,6 +14,7 @@ import { mouseDragFromPointToPoint, mouseMoveToPoint, mouseUpAtPoint, + pressKey, } from '../../event-helpers.test-utils' import type { EditorRenderResult } from '../../ui-jsx.test-utils' import { @@ -311,8 +312,8 @@ describe('grid reparent strategies', () => { width: 79, height: 86, position: 'absolute', - top: 934, left: 1627, + top: 934, }} data-uid='dragme' data-testid='dragme' @@ -451,7 +452,7 @@ describe('grid reparent strategies', () => { ), ) }) - it('into a flow element', async () => { + it('into a flow element with flow strategy selected', async () => { const editor = await renderTestEditorWithCode( makeTestProjectCode({ insideGrid: ` @@ -519,7 +520,9 @@ describe('grid reparent strategies', () => { y: fooRect.y + fooRect.height / 2, } - await dragOut(editor, EP.fromString('sb/grid/dragme'), endPoint) + await dragOut(editor, EP.fromString('sb/grid/dragme'), endPoint, async () => { + await pressKey('3', { modifiers: cmdModifier }) // this should select the Reparent (Flow) strategy + }) expect(getPrintedUiJsCode(editor.getEditorState())).toEqual( formatTestProjectCode( @@ -755,11 +758,11 @@ describe('grid reparent strategies', () => {
Promise, ): Promise { const sourceGridCell = renderResult.renderedDOM.getByTestId(GridCellTestId(cell)) const sourceRect = sourceGridCell.getBoundingClientRect() @@ -857,6 +861,9 @@ async function dragOut( }, modifiers: cmdModifier, }) + if (midDragCallback != null) { + await midDragCallback() + } await mouseUpAtPoint(renderResult.renderedDOM.getByTestId(CanvasControlsContainerID), endPoint, { modifiers: cmdModifier, }) diff --git a/editor/src/components/canvas/canvas-strategies/strategies/grid-reparent-strategy.tsx b/editor/src/components/canvas/canvas-strategies/strategies/grid-reparent-strategy.tsx index 7e41c20e2ed8..f98ee00f7680 100644 --- a/editor/src/components/canvas/canvas-strategies/strategies/grid-reparent-strategy.tsx +++ b/editor/src/components/canvas/canvas-strategies/strategies/grid-reparent-strategy.tsx @@ -45,11 +45,7 @@ import { removeAbsolutePositioningProps } from './reparent-helpers/reparent-prop import type { ReparentTarget } from './reparent-helpers/reparent-strategy-helpers' import { getReparentOutcome, pathToReparent } from './reparent-utils' import { flattenSelection } from './shared-move-strategies-helpers' -import { - getClosestGridCellToPointFromMetadata, - getGridCellUnderMouseFromMetadata, - type GridCellCoordinates, -} from './grid-cell-bounds' +import { getClosestGridCellToPointFromMetadata, type GridCellCoordinates } from './grid-cell-bounds' export function gridReparentStrategy( reparentTarget: ReparentTarget, diff --git a/editor/src/components/canvas/canvas-strategies/strategies/reparent-helpers/reparent-property-changes.ts b/editor/src/components/canvas/canvas-strategies/strategies/reparent-helpers/reparent-property-changes.ts index 86a6f01ccc2d..307d87ad479b 100644 --- a/editor/src/components/canvas/canvas-strategies/strategies/reparent-helpers/reparent-property-changes.ts +++ b/editor/src/components/canvas/canvas-strategies/strategies/reparent-helpers/reparent-property-changes.ts @@ -73,6 +73,12 @@ const propertiesToRemove: Array = [ PP.create('style', 'top'), PP.create('style', 'right'), PP.create('style', 'bottom'), + PP.create('style', 'gridRow'), + PP.create('style', 'gridColumn'), + PP.create('style', 'gridRowStart'), + PP.create('style', 'gridRowEnd'), + PP.create('style', 'gridColumnStart'), + PP.create('style', 'gridColumnEnd'), ] export type ForcePins = 'force-pins' | 'do-not-force-pins' diff --git a/editor/src/components/canvas/canvas-strategies/strategies/reparent-metastrategy.tsx b/editor/src/components/canvas/canvas-strategies/strategies/reparent-metastrategy.tsx index a5d79e59521d..41478bff23f0 100644 --- a/editor/src/components/canvas/canvas-strategies/strategies/reparent-metastrategy.tsx +++ b/editor/src/components/canvas/canvas-strategies/strategies/reparent-metastrategy.tsx @@ -274,7 +274,8 @@ export const reparentMetaStrategy: MetaCanvasStrategy = ( if ( reparentSubjects.length === 0 || interactionSession == null || - interactionSession.activeControl.type !== 'BOUNDING_AREA' || + (interactionSession.activeControl.type !== 'BOUNDING_AREA' && + interactionSession.activeControl.type !== 'GRID_CELL_HANDLE') || interactionSession.interactionData.type !== 'DRAG' || interactionSession.interactionData.drag == null || interactionSession.interactionData.modifiers.alt diff --git a/editor/src/components/canvas/controls/grid-controls.tsx b/editor/src/components/canvas/controls/grid-controls.tsx index 2d70341e6b61..176a10dd3eea 100644 --- a/editor/src/components/canvas/controls/grid-controls.tsx +++ b/editor/src/components/canvas/controls/grid-controls.tsx @@ -59,7 +59,10 @@ import { useColorTheme, UtopiaStyles } from '../../../uuiui' import { useDispatch } from '../../editor/store/dispatch-context' import { Substores, useEditorState, useRefEditorState } from '../../editor/store/store-hook' import CanvasActions from '../canvas-actions' -import type { ControlWithProps } from '../canvas-strategies/canvas-strategy-types' +import type { + ControlWithProps, + WhenToShowControl, +} from '../canvas-strategies/canvas-strategy-types' import { controlForStrategyMemoized } from '../canvas-strategies/canvas-strategy-types' import type { GridResizeEdge, @@ -1837,12 +1840,15 @@ function gridPlaceholderWidthOrHeight(scale: number): string { return `calc(100% + ${(placeholderBorderBaseWidth * 2) / scale}px)` } -export function controlsForGridPlaceholders(gridPath: ElementPath): ControlWithProps { +export function controlsForGridPlaceholders( + gridPath: ElementPath, + whenToShow: WhenToShowControl = 'always-visible', +): ControlWithProps { return { control: GridControls, props: { targets: [gridPath] }, key: GridControlsKey(gridPath), - show: 'always-visible', + show: whenToShow, priority: 'bottom', } } From 108b2c2f5a187386596ddb5c453a7f74fb0c34a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bertalan=20K=C3=B6rmendy?= Date: Wed, 9 Oct 2024 12:11:21 +0200 Subject: [PATCH 2/4] Memoized Tailwind config evaluation (#6500) ## Problem The Tailwind config is only accessible inside `useTailwindCompilation`, which means that the upcoming element style plugins won't be able to access it ## Fix Store the last seen text contents of the Tailwind config file along with the last evaluation result. If the new contents of the Tailwind config file are the same as the last seen contents, the last evaluation result, otherwise, re-evaluate the Tailwind config file, and store it, and return it --- editor/package.json | 1 + editor/pnpm-lock.yaml | 39 +---- .../property-controls-local.ts | 146 ++++++++++-------- .../src/core/tailwind/tailwind-compilation.ts | 27 +++- .../core/tailwind/tailwind.spec.browser2.tsx | 84 +--------- editor/src/core/tailwind/tailwind.spec.ts | 72 +++++++++ .../src/core/tailwind/tailwind.test-utils.ts | 85 ++++++++++ editor/webpack.config.js | 1 + 8 files changed, 274 insertions(+), 181 deletions(-) create mode 100644 editor/src/core/tailwind/tailwind.spec.ts create mode 100644 editor/src/core/tailwind/tailwind.test-utils.ts diff --git a/editor/package.json b/editor/package.json index 9da259561b23..1d4fa750356a 100644 --- a/editor/package.json +++ b/editor/package.json @@ -424,6 +424,7 @@ "source-map-loader": "0.2.3", "string-replace-loader": "2.2.0", "style-loader": "0.18.2", + "tailwindcss": "^3.4.13", "tar": "6.0.5", "terser-webpack-plugin": "5.3.9", "three": "0.140.2", diff --git a/editor/pnpm-lock.yaml b/editor/pnpm-lock.yaml index 9c062e509ccd..fe2bc6505a00 100644 --- a/editor/pnpm-lock.yaml +++ b/editor/pnpm-lock.yaml @@ -318,6 +318,7 @@ specifiers: string-replace-loader: 2.2.0 strip-ansi: 6.0.0 style-loader: 0.18.2 + tailwindcss: ^3.4.13 tar: 6.0.5 terser-webpack-plugin: 5.3.9 three: 0.140.2 @@ -643,6 +644,7 @@ devDependencies: source-map-loader: 0.2.3 string-replace-loader: 2.2.0_webpack@5.88.2 style-loader: 0.18.2 + tailwindcss: 3.4.13 tar: 6.0.5 terser-webpack-plugin: 5.3.9_webpack@5.88.2 three: 0.140.2 @@ -669,7 +671,6 @@ packages: /@alloc/quick-lru/5.2.0: resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} engines: {node: '>=10'} - dev: false /@alloc/types/1.3.0: resolution: {integrity: sha512-mH7LiFiq9g6rX2tvt1LtwsclfG5hnsmtIfkZiauAGrm1AwXhoRS0sF2WrN9JGN7eV5vFXqNaB0eXZ3IvMsVi9g==} @@ -4649,7 +4650,7 @@ packages: dependencies: '@types/estree': 0.0.39 estree-walker: 1.0.1 - picomatch: 2.3.0 + picomatch: 2.3.1 dev: true /@rollup/pluginutils/4.2.1: @@ -6428,7 +6429,6 @@ packages: /arg/5.0.2: resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} - dev: false /argparse/1.0.10: resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} @@ -7459,7 +7459,6 @@ packages: /camelcase-css/2.0.1: resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} engines: {node: '>= 6'} - dev: false /camelcase/5.3.1: resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} @@ -8831,7 +8830,6 @@ packages: /didyoumean/1.2.2: resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} - dev: false /diff-match-patch/1.0.5: resolution: {integrity: sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==} @@ -8887,7 +8885,6 @@ packages: /dlv/1.1.3: resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} - dev: false /dnd-core/16.0.1: resolution: {integrity: sha512-HK294sl7tbw6F6IeuK16YSBUoorvHpY8RHO+9yFfaJyCDVb6n7PRcezrOEOa2SBCqiYpemh5Jx20ZcjKdFAVng==} @@ -10875,7 +10872,6 @@ packages: engines: {node: '>=10.13.0'} dependencies: is-glob: 4.0.3 - dev: false /glob-to-regexp/0.4.1: resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==} @@ -10902,7 +10898,6 @@ packages: minipass: 7.1.2 package-json-from-dist: 1.0.0 path-scurry: 1.11.1 - dev: false /glob/3.1.21: resolution: {integrity: sha512-ANhy2V2+tFpRajE3wN4DhkNQ08KDr0Ir1qL12/cUe5+a7STEK8jkW4onUYuY8/06qAFuT5je7mjAqzx0eKI2tQ==} @@ -12334,7 +12329,6 @@ packages: '@isaacs/cliui': 8.0.2 optionalDependencies: '@pkgjs/parseargs': 0.11.0 - dev: false /jake/10.8.2: resolution: {integrity: sha512-eLpKyrfG3mzvGE2Du8VoPbeSkRry093+tyNjdYaBbJS9v17knImYGNXQCUV0gLxQtF82m3E8iRb/wdSQZLoq7A==} @@ -12972,7 +12966,6 @@ packages: /jiti/1.21.6: resolution: {integrity: sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==} hasBin: true - dev: false /jotai-devtools/0.6.2_hgjqizhc26gc665cnflpub44vy: resolution: {integrity: sha512-iHKYt8V2T2Gh2DtGRpvpv2daVoFoHRJXqk5/LHnhFkJy9rMQuIGo4XgVu/v1ZMSvMzwDXdU3hDOQkfQWlDErUQ==} @@ -13477,12 +13470,10 @@ packages: /lilconfig/2.1.0: resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==} engines: {node: '>=10'} - dev: false /lilconfig/3.1.2: resolution: {integrity: sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow==} engines: {node: '>=14'} - dev: false /lines-and-columns/1.1.6: resolution: {integrity: sha512-8ZmlJFVK9iCmtLz19HpSsR8HaAMWBT284VMNednLwlIMDP2hJDCIhUp0IZ2xUcZ+Ob6BM0VvCSJwzASDM45NLQ==} @@ -14255,7 +14246,6 @@ packages: any-promise: 1.3.0 object-assign: 4.1.1 thenify-all: 1.6.0 - dev: false /nanoid/3.1.20: resolution: {integrity: sha512-a1cQNyczgKbLX9jwbS/+d7W8fX/RfgYR7lVWwWOGIPNgK2m0MWvrGF6/m4kk6U3QcFMnZf3RIhL0v2Jgh/0Uxw==} @@ -14537,7 +14527,6 @@ packages: /object-hash/3.0.0: resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} engines: {node: '>= 6'} - dev: false /object-inspect/1.11.0: resolution: {integrity: sha512-jp7ikS6Sd3GxQfZJPyH3cjcbJF6GZPClgdV+EFygjFLQ5FmW/dRUnTd9PQ9k0JhoNDabWFbpF1yCdSWCC6gexg==} @@ -14851,7 +14840,6 @@ packages: /package-json-from-dist/1.0.0: resolution: {integrity: sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==} - dev: false /pako/0.2.9: resolution: {integrity: sha1-8/dSL073gjSNqBYbrZ7P1Rv4OnU=} @@ -14997,6 +14985,7 @@ packages: /path-parse/1.0.6: resolution: {integrity: sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==} + dev: false /path-parse/1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} @@ -15065,11 +15054,6 @@ packages: /picocolors/1.0.0: resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} - /picomatch/2.3.0: - resolution: {integrity: sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==} - engines: {node: '>=8.6'} - dev: true - /picomatch/2.3.1: resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} engines: {node: '>=8.6'} @@ -15207,7 +15191,6 @@ packages: postcss-value-parser: 4.2.0 read-cache: 1.0.0 resolve: 1.22.2 - dev: false /postcss-js/4.0.1_postcss@8.4.27: resolution: {integrity: sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==} @@ -15217,7 +15200,6 @@ packages: dependencies: camelcase-css: 2.0.1 postcss: 8.4.27 - dev: false /postcss-load-config/4.0.2_postcss@8.4.27: resolution: {integrity: sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==} @@ -15234,7 +15216,6 @@ packages: lilconfig: 3.1.2 postcss: 8.4.27 yaml: 2.5.1 - dev: false /postcss-merge-idents/2.1.7: resolution: {integrity: sha1-TFUwMTwI4dWzu/PSu8dH4njuonA=} @@ -15342,7 +15323,6 @@ packages: dependencies: postcss: 8.4.27 postcss-selector-parser: 6.1.2 - dev: false /postcss-normalize-charset/1.1.1: resolution: {integrity: sha1-757nEhLX/nWceO0WL2HtYrXLk/E=} @@ -15401,7 +15381,6 @@ packages: dependencies: cssesc: 3.0.0 util-deprecate: 1.0.2 - dev: false /postcss-svgo/2.1.6: resolution: {integrity: sha1-tt8YqmE7Zm4TPwittSGcJoSsEI0=} @@ -15426,7 +15405,6 @@ packages: /postcss-value-parser/4.2.0: resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} - dev: false /postcss-zindex/2.2.0: resolution: {integrity: sha1-0hCd3AVbka9n/EyzsCWUZjnSryI=} @@ -16997,7 +16975,6 @@ packages: resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==} dependencies: pify: 2.3.0 - dev: false /read-only-stream/2.0.0: resolution: {integrity: sha1-JyT9aoET1zdkrCiNQ4YnDB2/F/A=} @@ -17262,7 +17239,7 @@ packages: resolution: {integrity: sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==} dependencies: is-core-module: 2.12.1 - path-parse: 1.0.6 + path-parse: 1.0.7 /resolve/1.22.2: resolution: {integrity: sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==} @@ -18475,7 +18452,6 @@ packages: mz: 2.7.0 pirates: 4.0.6 ts-interface-checker: 0.1.13 - dev: false /superstruct/0.8.4: resolution: {integrity: sha512-48Ors8IVWZm/tMr8r0Si6+mJiB7mkD7jqvIzktjJ4+EnP5tBp0qOpiM1J8sCUorKx+TXWrfb3i1UcjdD1YK/wA==} @@ -18621,7 +18597,6 @@ packages: sucrase: 3.35.0 transitivePeerDependencies: - ts-node - dev: false /tapable/1.1.3: resolution: {integrity: sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==} @@ -18737,13 +18712,11 @@ packages: engines: {node: '>=0.8'} dependencies: thenify: 3.3.1 - dev: false /thenify/3.3.1: resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} dependencies: any-promise: 1.3.0 - dev: false /three/0.139.2: resolution: {integrity: sha512-gV7q7QY8rogu7HLFZR9cWnOQAUedUhu2WXAnpr2kdXZP9YDKsG/0ychwQvWkZN5PlNw9mv5MoCTin6zNTXoONg==} @@ -18938,7 +18911,6 @@ packages: /ts-interface-checker/0.1.13: resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} - dev: false /ts-loader/5.3.3_typescript@5.5.4: resolution: {integrity: sha512-KwF1SplmOJepnoZ4eRIloH/zXL195F51skt7reEsS6jvDqzgc/YSbz9b8E07GxIUwLXdcD4ssrJu6v8CwaTafA==} @@ -20139,7 +20111,6 @@ packages: resolution: {integrity: sha512-bLQOjaX/ADgQ20isPJRvF0iRUHIxVhYvr53Of7wGcWlO2jvtUlH5m87DsmulFVxRpNLOnI4tB6p/oh8D7kpn9Q==} engines: {node: '>= 14'} hasBin: true - dev: false /yargs-parser/20.2.4: resolution: {integrity: sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==} diff --git a/editor/src/core/property-controls/property-controls-local.ts b/editor/src/core/property-controls/property-controls-local.ts index c4a4beac7048..ffd05dff6e82 100644 --- a/editor/src/core/property-controls/property-controls-local.ts +++ b/editor/src/core/property-controls/property-controls-local.ts @@ -76,7 +76,7 @@ import { right, sequenceEither, } from '../shared/either' -import { assertNever } from '../shared/utils' +import { assertNever, identity } from '../shared/utils' import type { Imports, ParsedTextFile, @@ -157,73 +157,93 @@ function extendExportsWithInfo(exports: any, toImport: string): any { return exports } -export type ModuleEvaluator = (moduleName: string) => any -export function createModuleEvaluator(editor: EditorState): ModuleEvaluator { - return (moduleName: string) => { - let mutableContextRef: { current: MutableUtopiaCtxRefData } = { current: {} } - let topLevelComponentRendererComponents: { - current: MapLike> - } = { current: {} } - const emptyMetadataContext: UiJsxCanvasContextData = { - current: { - spyValues: { - allElementProps: {}, - metadata: {}, - variablesInScope: {}, - }, +export const createRequireFn = ( + editor: EditorState, + moduleName: string, + transform: (result: any, absoluteFilenameOrPackage: string) => any = identity, +) => { + let mutableContextRef: { current: MutableUtopiaCtxRefData } = { current: {} } + let topLevelComponentRendererComponents: { + current: MapLike> + } = { current: {} } + const emptyMetadataContext: UiJsxCanvasContextData = { + current: { + spyValues: { + allElementProps: {}, + metadata: {}, + variablesInScope: {}, }, - } + }, + } - let resolvedFiles: MapLike> = {} - let resolvedFileNames: Array = [moduleName] + let resolvedFiles: MapLike> = {} + let resolvedFileNames: Array = [moduleName] - const requireFn = editor.codeResultCache.curriedRequireFn(editor.projectContents) - const resolve = editor.codeResultCache.curriedResolveFn(editor.projectContents) + const requireFn = editor.codeResultCache.curriedRequireFn(editor.projectContents) + const resolve = editor.codeResultCache.curriedResolveFn(editor.projectContents) - const customRequire = (importOrigin: string, toImport: string) => { - if (resolvedFiles[importOrigin] == null) { - resolvedFiles[importOrigin] = [] - } - let resolvedFromThisOrigin = resolvedFiles[importOrigin] - - const alreadyResolved = resolvedFromThisOrigin[toImport] !== undefined - const filePathResolveResult = alreadyResolved - ? left('Already resolved') - : resolve(importOrigin, toImport) - - forEachRight(filePathResolveResult, (filepath) => resolvedFileNames.push(filepath)) - - const resolvedParseSuccess: Either> = attemptToResolveParsedComponents( - resolvedFromThisOrigin, - toImport, - editor.projectContents, - customRequire, - mutableContextRef, - topLevelComponentRendererComponents, - moduleName, - editor.canvas.base64Blobs, - editor.hiddenInstances, - editor.displayNoneInstances, - emptyMetadataContext, - NO_OP, - false, - filePathResolveResult, - null, - ) - const result = foldEither( - () => { - // We did not find a ParseSuccess, fallback to standard require Fn - return requireFn(importOrigin, toImport, false) - }, - (scope) => { - // Return an artificial exports object that contains our ComponentRendererComponents - return scope - }, - resolvedParseSuccess, - ) - const absoluteFilenameOrPackage = defaultEither(toImport, filePathResolveResult) - return extendExportsWithInfo(result, absoluteFilenameOrPackage) + const customRequire = (importOrigin: string, toImport: string) => { + if (resolvedFiles[importOrigin] == null) { + resolvedFiles[importOrigin] = [] } + let resolvedFromThisOrigin = resolvedFiles[importOrigin] + + const alreadyResolved = resolvedFromThisOrigin[toImport] !== undefined + const filePathResolveResult = alreadyResolved + ? left('Already resolved') + : resolve(importOrigin, toImport) + + forEachRight(filePathResolveResult, (filepath) => resolvedFileNames.push(filepath)) + + const resolvedParseSuccess: Either> = attemptToResolveParsedComponents( + resolvedFromThisOrigin, + toImport, + editor.projectContents, + customRequire, + mutableContextRef, + topLevelComponentRendererComponents, + moduleName, + editor.canvas.base64Blobs, + editor.hiddenInstances, + editor.displayNoneInstances, + emptyMetadataContext, + NO_OP, + false, + filePathResolveResult, + null, + ) + const result = foldEither( + () => { + // We did not find a ParseSuccess, fallback to standard require Fn + return requireFn(importOrigin, toImport, false) + }, + (scope) => { + // Return an artificial exports object that contains our ComponentRendererComponents + return scope + }, + resolvedParseSuccess, + ) + const absoluteFilenameOrPackage = defaultEither(toImport, filePathResolveResult) + return transform(result, absoluteFilenameOrPackage) + } + + return { + customRequire, + emptyMetadataContext, + mutableContextRef, + topLevelComponentRendererComponents, + } +} + +export type ModuleEvaluator = (moduleName: string) => any +export function createModuleEvaluator(editor: EditorState): ModuleEvaluator { + return (moduleName: string) => { + const { + customRequire, + emptyMetadataContext, + mutableContextRef, + topLevelComponentRendererComponents, + } = createRequireFn(editor, moduleName, extendExportsWithInfo) return createExecutionScope( moduleName, customRequire, diff --git a/editor/src/core/tailwind/tailwind-compilation.ts b/editor/src/core/tailwind/tailwind-compilation.ts index 9e030e1b83ad..78f5ef83eae0 100644 --- a/editor/src/core/tailwind/tailwind-compilation.ts +++ b/editor/src/core/tailwind/tailwind-compilation.ts @@ -1,7 +1,7 @@ import React from 'react' import type { TailwindConfig, Tailwindcss } from '@mhsdesign/jit-browser-tailwindcss' import { createTailwindcss } from '@mhsdesign/jit-browser-tailwindcss' -import type { ProjectContentTreeRoot } from 'utopia-shared/src/types' +import type { ProjectContentTreeRoot, TextFile, TextFileContents } from 'utopia-shared/src/types' import { getProjectFileByFilePath, walkContentsTree } from '../../components/assets' import { interactionSessionIsActive } from '../../components/canvas/canvas-strategies/interaction-state' import { CanvasContainerID } from '../../components/canvas/canvas-types' @@ -16,6 +16,31 @@ import type { RequireFn } from '../shared/npm-dependency-types' import { TailwindConfigPath } from './tailwind-config' import { ElementsToRerenderGLOBAL } from '../../components/canvas/ui-jsx-canvas' import { isFeatureEnabled } from '../../utils/feature-switches' +import type { Config } from 'tailwindcss/types/config' +import type { EditorState } from '../../components/editor/store/editor-state' +import { createRequireFn } from '../property-controls/property-controls-local' + +const LatestConfig: { current: { code: string; config: Config } | null } = { current: null } +export function getTailwindConfigCached(editorState: EditorState): Config | null { + const tailwindConfig = getProjectFileByFilePath(editorState.projectContents, TailwindConfigPath) + if (tailwindConfig == null || tailwindConfig.type !== 'TEXT_FILE') { + return null + } + const cached = + LatestConfig.current == null || LatestConfig.current.code !== tailwindConfig.fileContents.code + ? null + : LatestConfig.current.config + + if (cached != null) { + return cached + } + // FIXME this should use a shared long-lived require function instead of creating a brand new one + const { customRequire } = createRequireFn(editorState, TailwindConfigPath) + const config = importDefault(customRequire('/', TailwindConfigPath)) as Config + LatestConfig.current = { code: tailwindConfig.fileContents.code, config: config } + + return config +} const TAILWIND_INSTANCE: { current: Tailwindcss | null } = { current: null } diff --git a/editor/src/core/tailwind/tailwind.spec.browser2.tsx b/editor/src/core/tailwind/tailwind.spec.browser2.tsx index 8040db905fdc..b95956046c32 100644 --- a/editor/src/core/tailwind/tailwind.spec.browser2.tsx +++ b/editor/src/core/tailwind/tailwind.spec.browser2.tsx @@ -1,88 +1,6 @@ import { renderTestEditorWithModel } from '../../components/canvas/ui-jsx.test-utils' -import { createModifiedProject } from '../../sample-projects/sample-project-utils.test-utils' import { setFeatureForBrowserTestsUseInDescribeBlockOnly } from '../../utils/utils.test-utils' -import { wait } from '../model/performance-scripts' - -const Project = createModifiedProject({ - '/utopia/storyboard.js': `import { Scene, Storyboard } from 'utopia-api' - -export var storyboard = ( - - -
-
-
-
-
-
-
-
-
-
-
-
-
-
- Text Shadow - This is a medium text shadow example - This is a large text shadow example - This has no text shadow -
- - -) -`, - '/src/app.css': ` -@tailwind base; -@tailwind components; -@tailwind utilities; -`, - 'tailwind.config.js': ` -const Tailwind = { - theme: { - colors: { - transparent: 'transparent', - current: 'currentColor', - white: '#ffffff', - purple: '#3f3cbb', - midnight: '#121063', - metal: '#565584', - tahiti: '#3ab7bf', - silver: '#ecebff', - 'bubble-gum': '#ff77e9', - bermuda: '#78dcca', - }, - }, - plugins: [ - function ({ addUtilities }) { - const newUtilities = { - '.text-shadow': { - textShadow: '2px 2px 4px rgba(0, 0, 0, 0.1)', - }, - '.text-shadow-md': { - textShadow: '3px 3px 6px rgba(0, 0, 0, 0.2)', - }, - '.text-shadow-lg': { - textShadow: '4px 4px 8px rgba(0, 0, 0, 0.3)', - }, - '.text-shadow-none': { - textShadow: 'none', - }, - } - - addUtilities(newUtilities, ['responsive', 'hover']) - }, - ], -} -export default Tailwind -`, -}) +import { Project } from './tailwind.test-utils' describe('rendering tailwind projects in the editor', () => { setFeatureForBrowserTestsUseInDescribeBlockOnly('Tailwind', true) diff --git a/editor/src/core/tailwind/tailwind.spec.ts b/editor/src/core/tailwind/tailwind.spec.ts new file mode 100644 index 000000000000..dcaa26fb51c2 --- /dev/null +++ b/editor/src/core/tailwind/tailwind.spec.ts @@ -0,0 +1,72 @@ +import { Project, TailwindConfigFileContents } from './tailwind.test-utils' +import { renderTestEditorWithModel } from '../../components/canvas/ui-jsx.test-utils' +import { TailwindConfigPath } from './tailwind-config' +import { updateFromCodeEditor } from '../../components/editor/actions/actions-from-vscode' +import { getTailwindConfigCached } from './tailwind-compilation' + +describe('tailwind config file in the editor', () => { + it('is set during editor load', async () => { + const editor = await renderTestEditorWithModel(Project, 'await-first-dom-report') + + expect(getTailwindConfigCached(editor.getEditorState().editor)).toMatchInlineSnapshot(` + Object { + "plugins": Array [ + [Function], + ], + "theme": Object { + "colors": Object { + "bermuda": "#78dcca", + "bubble-gum": "#ff77e9", + "current": "currentColor", + "metal": "#565584", + "midnight": "#121063", + "purple": "#3f3cbb", + "silver": "#ecebff", + "tahiti": "#3ab7bf", + "transparent": "transparent", + "white": "#ffffff", + }, + }, + } + `) + }) + it('is updated in the editor state when the tailwind config is updated', async () => { + const editor = await renderTestEditorWithModel(Project, 'await-first-dom-report') + + await editor.dispatch( + [ + updateFromCodeEditor( + TailwindConfigPath, + TailwindConfigFileContents, + ` +const Tailwind = { + theme: { + colors: { + transparent: 'transparent', + current: 'currentColor', + white: '#ffffff', + }, + }, + plugins: [ ], + } + export default Tailwind +`, + ), + ], + true, + ) + + expect(getTailwindConfigCached(editor.getEditorState().editor)).toMatchInlineSnapshot(` + Object { + "plugins": Array [], + "theme": Object { + "colors": Object { + "current": "currentColor", + "transparent": "transparent", + "white": "#ffffff", + }, + }, + } + `) + }) +}) diff --git a/editor/src/core/tailwind/tailwind.test-utils.ts b/editor/src/core/tailwind/tailwind.test-utils.ts new file mode 100644 index 000000000000..80b5952ef0ab --- /dev/null +++ b/editor/src/core/tailwind/tailwind.test-utils.ts @@ -0,0 +1,85 @@ +import { createModifiedProject } from '../../sample-projects/sample-project-utils.test-utils' +import { TailwindConfigPath } from './tailwind-config' + +export const TailwindConfigFileContents = ` +const Tailwind = { + theme: { + colors: { + transparent: 'transparent', + current: 'currentColor', + white: '#ffffff', + purple: '#3f3cbb', + midnight: '#121063', + metal: '#565584', + tahiti: '#3ab7bf', + silver: '#ecebff', + 'bubble-gum': '#ff77e9', + bermuda: '#78dcca', + }, + }, + plugins: [ + function ({ addUtilities }) { + const newUtilities = { + '.text-shadow': { + textShadow: '2px 2px 4px rgba(0, 0, 0, 0.1)', + }, + '.text-shadow-md': { + textShadow: '3px 3px 6px rgba(0, 0, 0, 0.2)', + }, + '.text-shadow-lg': { + textShadow: '4px 4px 8px rgba(0, 0, 0, 0.3)', + }, + '.text-shadow-none': { + textShadow: 'none', + }, + } + + addUtilities(newUtilities, ['responsive', 'hover']) + }, + ], + } + export default Tailwind +` + +export const Project = createModifiedProject({ + '/utopia/storyboard.js': `import { Scene, Storyboard } from 'utopia-api' + + export var storyboard = ( + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Text Shadow + This is a medium text shadow example + This is a large text shadow example + This has no text shadow +
+ + + ) + `, + '/src/app.css': ` + @tailwind base; + @tailwind components; + @tailwind utilities; + `, + [TailwindConfigPath]: TailwindConfigFileContents, +}) diff --git a/editor/webpack.config.js b/editor/webpack.config.js index f0c7d9af5995..c236883873db 100644 --- a/editor/webpack.config.js +++ b/editor/webpack.config.js @@ -189,6 +189,7 @@ const config = { extensions: ['.ts', '.tsx', '.js', '.json', '.ttf'], symlinks: true, // We set this to false as we have symlinked some common code from the website project alias: { + 'tailwindcss/resolveConfig': 'tailwindcss/resolveConfig.js', uuiui: srcPath('uuiui'), 'worker-imports': path.resolve(__dirname, 'src/core/workers/worker-import-utils.ts'), 'uuiui-deps': srcPath('uuiui-deps'), From 4cdb407e2ef3928182253ac7b57c386e3827e00f Mon Sep 17 00:00:00 2001 From: McKayla Lankau Date: Wed, 9 Oct 2024 06:12:16 -0400 Subject: [PATCH 3/4] Grid controls design (#6489) Making some improvements to the visual design of canvas grid controls - grid gap handles and canvas labels, are orange to match the orange overlay pattern - grid row/column resize handles are now labels with their width (replacing the <-> arrows) - grid row/column area names are now inside the highlight overlay when being resized - grid/row highlight is now a blue pattern instead of pink ![image](https://github.com/user-attachments/assets/7dd6256c-bfa9-4da1-93d1-a454fbc52c8a) ![image](https://github.com/user-attachments/assets/09fe10ed-0af5-469a-9a20-f208403f7033) ![image](https://github.com/user-attachments/assets/918ecc27-6d16-453a-a87d-edb90ecd6c89) ![image](https://github.com/user-attachments/assets/88de5103-0f81-4025-aa85-6b1caa94b80a) --------- Co-authored-by: Balazs Bajorics <2226774+balazsbajorics@users.noreply.github.com> --- .../strategies/set-flex-gap-strategy.tsx | 2 +- .../strategies/set-grid-gap-strategy.tsx | 2 +- .../canvas/controls/grid-controls.tsx | 78 ++++++++----------- .../controls/select-mode/controls-common.tsx | 2 +- .../controls/select-mode/flex-gap-control.tsx | 4 +- .../controls/select-mode/grid-gap-control.tsx | 4 +- editor/src/uuiui/styles/theme/dark.ts | 4 +- editor/src/uuiui/styles/theme/light.ts | 4 +- 8 files changed, 45 insertions(+), 55 deletions(-) diff --git a/editor/src/components/canvas/canvas-strategies/strategies/set-flex-gap-strategy.tsx b/editor/src/components/canvas/canvas-strategies/strategies/set-flex-gap-strategy.tsx index 15b012d2b451..e247092ba7be 100644 --- a/editor/src/components/canvas/canvas-strategies/strategies/set-flex-gap-strategy.tsx +++ b/editor/src/components/canvas/canvas-strategies/strategies/set-flex-gap-strategy.tsx @@ -143,7 +143,7 @@ export const setFlexGapStrategy: CanvasStrategyFactory = ( control: FloatingIndicator, props: { ...props, - color: colorTheme.brandNeonPink.value, + color: colorTheme.brandNeonOrange.value, }, key: 'padding-value-indicator-control', show: 'visible-except-when-other-strategy-is-active', diff --git a/editor/src/components/canvas/canvas-strategies/strategies/set-grid-gap-strategy.tsx b/editor/src/components/canvas/canvas-strategies/strategies/set-grid-gap-strategy.tsx index e20678016012..484d19455ef5 100644 --- a/editor/src/components/canvas/canvas-strategies/strategies/set-grid-gap-strategy.tsx +++ b/editor/src/components/canvas/canvas-strategies/strategies/set-grid-gap-strategy.tsx @@ -132,7 +132,7 @@ export const setGridGapStrategy: CanvasStrategyFactory = ( control: FloatingIndicator, props: { ...maybeIndicatorProps, - color: colorTheme.brandNeonPink.value, + color: colorTheme.brandNeonOrange.value, }, key: 'padding-value-indicator-control', show: 'visible-except-when-other-strategy-is-active', diff --git a/editor/src/components/canvas/controls/grid-controls.tsx b/editor/src/components/canvas/controls/grid-controls.tsx index 176a10dd3eea..0a2ab0de5350 100644 --- a/editor/src/components/canvas/controls/grid-controls.tsx +++ b/editor/src/components/canvas/controls/grid-controls.tsx @@ -254,34 +254,22 @@ export const GridResizingControl = React.memo((props: GridResizingControlProps) data-testid={labelId} style={{ zoom: 1 / scale, - width: GRID_RESIZE_HANDLE_SIZE, height: GRID_RESIZE_HANDLE_SIZE, - borderRadius: '100%', - border: `1px solid ${colorTheme.border0.value}`, - boxShadow: `${colorTheme.canvasControlsSizeBoxShadowColor50.value} 0px 0px - 1px, ${colorTheme.canvasControlsSizeBoxShadowColor20.value} 0px 1px 2px 2px`, - background: colorTheme.white.value, + borderRadius: 3, + padding: '0 4px', + border: `.1px solid ${colorTheme.white.value}`, + background: colorTheme.primary.value, + color: colorTheme.white.value, display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: gridEdgeToCSSCursor(props.axis === 'column' ? 'column-start' : 'row-start'), - fontSize: 8, pointerEvents: 'initial', }} - css={{ - opacity: props.resizing !== 'not-resizing' ? 1 : 0.5, - ':hover': { - opacity: 1, - }, - }} onMouseDown={mouseDownHandler} onMouseMove={onMouseMove} > - {props.axis === 'row' ? '↕' : '↔'} - {when( - props.dimension.areaName != null, - {props.dimension.areaName}, - )} + {getLabelForAxis(props.dimension, props.dimensionIndex, props.fromPropsAxisValues)}
{when( props.resizing !== 'not-resizing', @@ -305,37 +293,34 @@ export const GridResizingControl = React.memo((props: GridResizingControlProps) justifyContent: 'center', border: `1px solid ${ props.resizeLocked - ? colorTheme.brandNeonPink10.value + ? colorTheme.primary10.value : props.resizing === 'resize-target' - ? colorTheme.brandNeonPink.value - : colorTheme.brandNeonPink60.value + ? colorTheme.primary.value + : colorTheme.primary50.value }`, ...(props.resizeLocked - ? UtopiaStyles.backgrounds.stripedBackground(colorTheme.brandNeonPink10.value, scale) + ? UtopiaStyles.backgrounds.stripedBackground(colorTheme.primary10.value, scale) : props.resizing === 'resize-target' - ? UtopiaStyles.backgrounds.stripedBackground(colorTheme.brandNeonPink60.value, scale) - : UtopiaStyles.backgrounds.stripedBackground( - colorTheme.brandNeonPink10.value, - scale, - )), + ? UtopiaStyles.backgrounds.stripedBackground(colorTheme.primary50.value, scale) + : UtopiaStyles.backgrounds.stripedBackground(colorTheme.primary10.value, scale)), }} > - + {when( + props.dimension.areaName != null, +
+ {props.dimension.areaName} +
, + )}
, )}
@@ -948,7 +933,8 @@ export const GridControl = React.memo(({ grid }) => { const countedColumn = Math.floor(cell % grid.columns) + 1 const id = gridCellTargetId(grid.elementPath, countedRow, countedColumn) const borderID = `${id}-border` - const dotgridColor = activelyDraggingOrResizingCell != null ? `#00000033` : 'transparent' + const dotgridColor = + activelyDraggingOrResizingCell != null ? colorTheme.blackOpacity35.value : 'transparent' const isActiveCell = countedColumn === currentHoveredCell?.column && countedRow === currentHoveredCell?.row @@ -956,7 +942,7 @@ export const GridControl = React.memo(({ grid }) => { const borderColor = isActiveCell && targetsAreCellsWithPositioning ? colorTheme.brandNeonPink.value - : `#00000033` + : colorTheme.blackOpacity35.value return (
((props) => { , )} @@ -391,7 +391,7 @@ const GapControlSegment = React.memo((props) => {
diff --git a/editor/src/components/canvas/controls/select-mode/grid-gap-control.tsx b/editor/src/components/canvas/controls/select-mode/grid-gap-control.tsx index 5cf6af9f6779..adca71944cc3 100644 --- a/editor/src/components/canvas/controls/select-mode/grid-gap-control.tsx +++ b/editor/src/components/canvas/controls/select-mode/grid-gap-control.tsx @@ -452,7 +452,7 @@ function GridGapHandle({ , )} @@ -460,7 +460,7 @@ function GridGapHandle({
diff --git a/editor/src/uuiui/styles/theme/dark.ts b/editor/src/uuiui/styles/theme/dark.ts index 3081003c09c7..63c1564223f5 100644 --- a/editor/src/uuiui/styles/theme/dark.ts +++ b/editor/src/uuiui/styles/theme/dark.ts @@ -20,6 +20,7 @@ const darkBase = { brandNeonPink10: createUtopiColor('oklch(78.64% 0.237 327.81 / 10%)'), brandNeonPink60: createUtopiColor('oklch(78.64% 0.237 327.81 / 50%)'), brandNeonGreen: createUtopiColor('oklch(86.6% 0.27 158.6)'), + brandNeonOrange: createUtopiColor('oklch(79% 0.19 70)'), green: createUtopiColor('oklch(88% 0.2535 150)'), green10: createUtopiColor('oklch(88% 0.2535 150 / 10%)'), green20: createUtopiColor('oklch(88% 0.2535 150 / 20%)'), @@ -133,6 +134,7 @@ const colorsWithOpacity = { whiteOpacity20: createUtopiColor('oklch(100% 0 0 /20%)'), whiteOpacity30: createUtopiColor('oklch(100% 0 0 /30%)'), whiteOpacity35: createUtopiColor('oklch(100% 0 0 /35%)'), + blackOpacity35: createUtopiColor('oklch(0% 0 0 / 35%)'), canvasControlsSizeBoxShadowColor20: createUtopiColor('rgba(255,255,255,0.20)'), canvasControlsSizeBoxShadowColor50: createUtopiColor('rgba(255,255,255,0.5)'), neutralInvertedBackground10: createUtopiColor('rgba(217, 220, 227, 0.1)'), @@ -266,7 +268,7 @@ const darkTheme: typeof light = { codeEditorGrid: createUtopiColor('#6d705b'), // Gap controls - gapControlsBg: darkBase.brandNeonGreen, + gapControlsBg: darkBase.brandNeonOrange, } export const dark = enforceUtopiColorTheme(darkTheme) diff --git a/editor/src/uuiui/styles/theme/light.ts b/editor/src/uuiui/styles/theme/light.ts index 2c14f9b169d5..0063da12508d 100644 --- a/editor/src/uuiui/styles/theme/light.ts +++ b/editor/src/uuiui/styles/theme/light.ts @@ -20,6 +20,7 @@ const lightBase = { brandNeonPink10: createUtopiColor('oklch(72.53% 0.353 331.69 / 10%)'), brandNeonPink60: createUtopiColor('oklch(72.53% 0.353 331.69 / 30%)'), brandNeonGreen: createUtopiColor('oklch(86.6% 0.27 158.6)'), + brandNeonOrange: createUtopiColor('oklch(79% 0.19 70)'), green: createUtopiColor('oklch(64.6% 0.17 150.6)'), green10: createUtopiColor('oklch(64.6% 0.17 150.6 / 10%)'), green20: createUtopiColor('oklch(64.6% 0.17 150.6 / 20%)'), @@ -134,6 +135,7 @@ const colorsWithOpacity = { whiteOpacity20: createUtopiColor('oklch(100% 0 0 /20%)'), whiteOpacity30: createUtopiColor('oklch(100% 0 0 /30%)'), whiteOpacity35: createUtopiColor('oklch(100% 0 0 /35%)'), + blackOpacity35: createUtopiColor('oklch(0% 0 0 / 35%)'), canvasControlsSizeBoxShadowColor20: createUtopiColor('rgba(0,0,0,0.20)'), canvasControlsSizeBoxShadowColor50: createUtopiColor('rgba(0,0,0,0.5)'), neutralInvertedBackground10: createUtopiColor('hsla(0,0%,0%,0.1)'), @@ -266,7 +268,7 @@ const lightTheme = { codeEditorGrid: createUtopiColor('#6d705b'), // Gap controls - gapControlsBg: createUtopiColor('#FFA500'), + gapControlsBg: lightBase.brandNeonOrange, } // all values in light must be of the type UtopiColor! This will break if you made a mistake. From 37f1e75a0ef855aa3625f8c7f290df2eded04414 Mon Sep 17 00:00:00 2001 From: Federico Ruggi <1081051+ruggi@users.noreply.github.com> Date: Wed, 9 Oct 2024 13:40:12 +0200 Subject: [PATCH 4/4] Use `auto` as value for Auto Flow dropdown (#6504) **Problem:** The `Auto Flow` dropdown should show `Auto` as its label instead of `Unset` when the dropdown is closed, but still show `Unset` if the dropdown is open _and_ the current value is not `auto` (or not set). **Fix:** https://github.com/user-attachments/assets/99bbfe1e-78f2-4512-afbb-a598985b0a0f Fixes #6503 --- .../src/components/inspector/flex-section.tsx | 6 ++-- editor/src/uuiui/radix-components.tsx | 35 ++++++++++++++++--- 2 files changed, 34 insertions(+), 7 deletions(-) diff --git a/editor/src/components/inspector/flex-section.tsx b/editor/src/components/inspector/flex-section.tsx index 6eed0980a59a..8afca94bc45f 100644 --- a/editor/src/components/inspector/flex-section.tsx +++ b/editor/src/components/inspector/flex-section.tsx @@ -967,8 +967,8 @@ function selectOption(value: GridAutoFlow) { } const unsetSelectOption = regularRadixSelectOption({ - label: 'unset', - value: 'unset', + label: (isOpen, currentValue) => (isOpen && currentValue !== 'auto' ? 'unset' : 'auto'), + value: 'auto', placeholder: true, }) @@ -1026,7 +1026,7 @@ const AutoFlowControl = React.memo(() => { selectededViewsRef.current.map((path) => applyCommandsAction([ updateBulkProperties('always', path, [ - value === 'unset' + value === 'auto' ? propertyToDelete(PP.create('style', 'gridAutoFlow')) : propertyToSet(PP.create('style', 'gridAutoFlow'), value), ]), diff --git a/editor/src/uuiui/radix-components.tsx b/editor/src/uuiui/radix-components.tsx index ad8787c65c6d..86ed787124af 100644 --- a/editor/src/uuiui/radix-components.tsx +++ b/editor/src/uuiui/radix-components.tsx @@ -228,7 +228,7 @@ Separator.displayName = 'Separator' type RegularRadixSelectOption = { type: 'REGULAR' value: string - label: string + label: string | ((isOpen: boolean, currentValue: string | null) => string) icon?: IcnProps placeholder?: boolean } @@ -254,6 +254,20 @@ export function separatorRadixSelectOption(): Separator { export type RadixSelectOption = RegularRadixSelectOption | Separator +function optionLabelToString( + option: RegularRadixSelectOption | null, + isOpen: boolean, + currentValue: string | null, +): string | null { + if (option == null) { + return null + } + + const label = typeof option.label === 'string' ? option.label : option.label(isOpen, currentValue) + + return `${label.charAt(0).toUpperCase()}${label.slice(1)}` +} + export const RadixSelect = React.memo( (props: { id: string @@ -269,11 +283,24 @@ export const RadixSelect = React.memo( e.stopPropagation() }, []) + const { onOpenChange: propsOnOpenChange } = props + + const [isOpen, setIsOpen] = React.useState(false) + const onOpenChange = React.useCallback( + (open: boolean) => { + setIsOpen(open) + propsOnOpenChange?.(open) + }, + [propsOnOpenChange], + ) + + const valueLabel = optionLabelToString(props.value ?? null, isOpen, props.value?.value ?? null) + return ( - + @@ -339,7 +366,7 @@ export const RadixSelect = React.memo( ) } - const label = `${option.label.charAt(0).toUpperCase()}${option.label.slice(1)}` + const label = optionLabelToString(option, isOpen, props.value?.value ?? null) return (