diff --git a/VERSION b/VERSION index 1451d48..1e79b04 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.7.9 \ No newline at end of file +0.7.10 \ No newline at end of file diff --git a/package.json b/package.json index 8233794..4250e2d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "craftbase", - "version": "0.7.9", + "version": "0.7.10", "private": true, "main": "src/lib.ts", "module": "src/lib.ts", @@ -55,12 +55,15 @@ }, "resolutions": { "micromatch": ">=4.0.8", - "form-data": ">=3.0.4", + "form-data": "^4.0.6", "brace-expansion": ">=5.0.6", "lodash": ">=4.18.0", "postcss": ">=8.5.10", "shell-quote": ">=1.8.4", - "vite": "~6.4.3" + "vite": "~6.4.3", + "js-yaml": "^4.2.0", + "@babel/core": "^7.29.6", + "subscriptions-transport-ws/ws": "^7.5.11" }, "eslintConfig": { "extends": "react-app/base" diff --git a/src/App.css b/src/App.css index 7a8a9b2..ee99a61 100644 --- a/src/App.css +++ b/src/App.css @@ -53,6 +53,10 @@ code { #main-two-root svg { background-color: #f5f0e8; background-image: radial-gradient(circle, #c4b89a 1px, transparent 1px); + /* First-paint default. background-size + background-position are driven live + by syncBackgroundToCamera() in newCanvas.tsx so the dot grid scales/pans + with the Two.js camera. Base tile (24px) is BG_TILE_BASE there — keep in + sync. */ background-size: 24px 24px; z-index: -1; } diff --git a/src/components/ZoomControls.tsx b/src/components/ZoomControls.tsx index ade5f08..00a6e03 100644 --- a/src/components/ZoomControls.tsx +++ b/src/components/ZoomControls.tsx @@ -4,8 +4,11 @@ import { useBoardContext } from '../views/Board/boardContext' import zoomInIcon from '../assets/zoom-in.svg' import zoomOutIcon from '../assets/zoom-out.svg' -// eslint-disable-next-line @typescript-eslint/no-explicit-any -type ZuiWrapperLike = { zui: any } | null +type ZuiWrapperLike = { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + zui: any + syncBackgroundToCamera?: () => void +} | null const ZoomControls = (): ReactElement => { const { zuiInBoard, twoJSInstance, scaleToDisplay } = useBoardContext() @@ -31,6 +34,7 @@ const ZoomControls = (): ReactElement => { if (!zui || !twoJSInstance) return zui.zui.zoomBy(delta, window.innerWidth / 2, window.innerHeight / 2) twoJSInstance.update() + zui.syncBackgroundToCamera?.() window.dispatchEvent( new CustomEvent('zoomChanged', { detail: { scale: zui.zui.scale }, diff --git a/src/components/sidebar/shareLinkPopup.tsx b/src/components/sidebar/shareLinkPopup.tsx index 5a00134..d713928 100644 --- a/src/components/sidebar/shareLinkPopup.tsx +++ b/src/components/sidebar/shareLinkPopup.tsx @@ -15,8 +15,13 @@ const ShareLinkPopup = (): ReactElement => { const [showConfirmModal, setShowConfirmModal] = useState(false) const [isPersisting, setIsPersisting] = useState(false) const [shareUrl, setShareUrl] = useState(null) - const { isPersisted, persistBoard, backgroundBoardId, stateRefForComponentStore } = - useBoardContext() + const { + isPersisted, + persistBoard, + backgroundBoardId, + stateRefForComponentStore, + twoJSInstance, + } = useBoardContext() // Whether there's anything to share. Gate on the actual component store, // NOT backgroundBoardId: that id is only minted by ensureBackgroundBoard() @@ -57,10 +62,22 @@ const ShareLinkPopup = (): ReactElement => { const serverBoardId = await persistBoard() await updateBoardVisibility({ variables: { id: serverBoardId } }) const url = `${window.location.origin}/board/${serverBoardId}` + // The shared/copied link stays clean (no params) — params only ride + // on the auto-opened tab to hand off the current '/' viewport. setShareUrl(url) setShowConfirmModal(false) setShowLink(true) - window.open(url, '_blank', 'noopener,noreferrer') + // Carry the live '/' viewport (pan + zoom) to the freshly-created + // board via query params so the opened tab lands on the same view + // instead of the origin. Read the live scene rather than the + // debounced localStorage entry so the last pan before clicking Share + // is included. The land side (newCanvas) consumes these once, seeds + // this board's viewport localStorage key(s), then strips the params. + const scene = twoJSInstance?.scene + const openUrl = scene + ? `${url}?vx=${scene.translation.x}&vy=${scene.translation.y}&vs=${scene.scale}` + : url + window.open(openUrl, '_blank', 'noopener,noreferrer') } finally { setIsPersisting(false) } diff --git a/src/newCanvas.tsx b/src/newCanvas.tsx index 82f06c3..be30566 100644 --- a/src/newCanvas.tsx +++ b/src/newCanvas.tsx @@ -385,6 +385,42 @@ function addZUI( zui.clientToSurface(e.clientX, e.clientY) zui.addLimits(0.06, 8) + // Parchment dot-grid camera sync. The grid is a CSS radial-gradient painted + // on the renderer's SVG element, which lives in screen space (Two.js + // transforms the scene group inside it, never the SVG itself). To make the + // grid feel glued to the canvas we mirror the camera onto the CSS background: + // scale the tile by the zoom and offset it by the scene translation. CSS + // tiling makes it an infinite grid for free, and the dots re-rasterize sharp + // at every zoom. Base tile is 24px (kept in sync with App.css). + const BG_TILE_BASE = 24 + // Keep the on-screen dot spacing inside a comfortable band by stepping the + // tile through power-of-2 "octaves". A plain `BG_TILE_BASE * scale` shrinks + // the tile as you zoom out, crowding the dots into a dense mush below ~50% + // (and into a sparse field when zoomed far in). Doubling/halving the tile at + // the band edges keeps apparent spacing roughly constant. MAX must be 2*MIN + // so each octave wrap lands back inside the band. + const BG_TILE_MIN = 16 + const BG_TILE_MAX = 32 + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let bgSvgEl: any = null + const syncBackgroundToCamera = () => { + if (!bgSvgEl) { + const root = document.getElementById('main-two-root') + bgSvgEl = root?.querySelector('svg') || null + } + if (!bgSvgEl) return + const scale = two.scene.scale || 1 + const tx = two.scene.translation.x + const ty = two.scene.translation.y + let size = BG_TILE_BASE * scale + if (size > 0) { + while (size < BG_TILE_MIN) size *= 2 + while (size > BG_TILE_MAX) size /= 2 + } + bgSvgEl.style.backgroundSize = `${size}px ${size}px` + bgSvgEl.style.backgroundPosition = `${tx}px ${ty}px` + } + const setRootCursor = (cursor: string) => { const root = document.getElementById('main-two-root') if (root) root.style.cursor = cursor @@ -555,9 +591,14 @@ function addZUI( }, commit: (id, patch) => { updateComponentBulkPropertiesInLocalStore(id, patch) + const g = selectionController.currentGroup + // Keep elementData (read by copy/paste and other consumers) in step + // with the store + rendered shape after a resize — otherwise a clone + // reads stale draw-time width/height. Mirrors the font-resize + // metadata sync in selectionController's resize-end path. + if (g?.elementData) Object.assign(g.elementData, patch) // Resize ended — persist any connectors whose ports tracked the // shape so their new tail/head survive a reload. - const g = selectionController.currentGroup if (g) persistBoundArrows(g) }, onTransform: (group) => { @@ -2240,6 +2281,7 @@ function addZUI( if (dx !== 0 || dy !== 0) { zui.translateSurface(dx, dy) two.update() + syncBackgroundToCamera() onCameraChangeRef?.current?.({ scale: two.scene.scale, tx: two.scene.translation.x, @@ -3249,6 +3291,7 @@ function addZUI( } two.update() + syncBackgroundToCamera() onCameraChangeRef?.current?.({ scale: two.scene.scale, @@ -3378,6 +3421,7 @@ function addZUI( if (dx !== 0 || dy !== 0) { zui.translateSurface(dx, dy) two.update() + syncBackgroundToCamera() onCameraChangeRef?.current?.({ scale: two.scene.scale, tx: two.scene.translation.x, @@ -3581,6 +3625,7 @@ function addZUI( distance = newDist two.update() + syncBackgroundToCamera() onCameraChangeRef?.current?.({ scale: two.scene.scale, @@ -3591,6 +3636,7 @@ function addZUI( return { zui, + syncBackgroundToCamera, mousemove, resetDragState: () => { dragging = false @@ -3761,6 +3807,47 @@ const Canvas: React.FC = (props) => { } } + // First-land seed: a board opened from Share carries the originating + // '/' viewport (pan + zoom) in the URL (vx/vy/vs). Clone it into this + // board's viewport localStorage key(s) so restoreViewport below lands + // on the same view instead of the origin. Seed both desktop+mobile keys + // (sharer and opener may be on different devices), then strip the params + // so a later reload uses the save-as-you-go value rather than the frozen + // param — i.e. subsequent reloads honour the last pan/zoom for this board. + if (props.boardId) { + const params = new URLSearchParams(window.location.search) + if (params.has('vx') && params.has('vy') && params.has('vs')) { + const tx = Number(params.get('vx')) + const ty = Number(params.get('vy')) + const scale = Number(params.get('vs')) + // Guard against malformed/tampered params — a NaN here would + // feed zoomSet/translateSurface and corrupt the scene. + if ( + Number.isFinite(tx) && + Number.isFinite(ty) && + Number.isFinite(scale) && + scale > 0 + ) { + const seeded = JSON.stringify({ + tx, + ty, + scale, + savedAt: Date.now(), + }) + localStorage.setItem( + `${VIEWPORT_KEY_PREFIX}${props.boardId}`, + seeded + ) + localStorage.setItem( + `${MOBILE_VIEWPORT_KEY_PREFIX}${props.boardId}`, + seeded + ) + } + // Strip the params regardless so a reload never re-seeds. + window.history.replaceState({}, '', window.location.pathname) + } + } + if (isMobile && props.boardId) { restoreViewport(`${MOBILE_VIEWPORT_KEY_PREFIX}${props.boardId}`) } @@ -3769,6 +3856,10 @@ const Canvas: React.FC = (props) => { restoreViewport(`${VIEWPORT_KEY_PREFIX}${props.boardId}`) } + // Seed the parchment grid from the restored (or default) camera so it + // lands aligned on first paint, not just on the first pan/zoom. + zui_instance.syncBackgroundToCamera() + onCameraChangeRef.current?.({ scale: two.scene.scale as number, tx: two.scene.translation.x, diff --git a/src/schema/generated.ts b/src/schema/generated.ts index 1926aa5..b09114d 100644 --- a/src/schema/generated.ts +++ b/src/schema/generated.ts @@ -554,6 +554,7 @@ export type Components_Component = { createdBy: Scalars['String']['output']; fill: Scalars['String']['output']; headEdge?: Maybe; + headPortIndex: Scalars['Int']['output']; headShapeId?: Maybe; height: Scalars['float8']['output']; iconStroke?: Maybe; @@ -568,6 +569,7 @@ export type Components_Component = { stroke?: Maybe; strokeType?: Maybe; tailEdge?: Maybe; + tailPortIndex: Scalars['Int']['output']; tailShapeId?: Maybe; textColor?: Maybe; updatedBy?: Maybe; @@ -958,11 +960,13 @@ export type Components_Component_Append_Input = { export type Components_Component_Avg_Fields = { __typename?: 'components_component_avg_fields'; createdAt?: Maybe; + headPortIndex?: Maybe; height?: Maybe; linewidth?: Maybe; opacity?: Maybe; position?: Maybe; radius?: Maybe; + tailPortIndex?: Maybe; width?: Maybe; x?: Maybe; x1?: Maybe; @@ -986,6 +990,7 @@ export type Components_Component_Bool_Exp = { createdBy?: InputMaybe; fill?: InputMaybe; headEdge?: InputMaybe; + headPortIndex?: InputMaybe; headShapeId?: InputMaybe; height?: InputMaybe; iconStroke?: InputMaybe; @@ -1000,6 +1005,7 @@ export type Components_Component_Bool_Exp = { stroke?: InputMaybe; strokeType?: InputMaybe; tailEdge?: InputMaybe; + tailPortIndex?: InputMaybe; tailShapeId?: InputMaybe; textColor?: InputMaybe; updatedBy?: InputMaybe; @@ -1040,11 +1046,13 @@ export type Components_Component_Delete_Key_Input = { /** input type for incrementing numeric columns in table "components.component" */ export type Components_Component_Inc_Input = { createdAt?: InputMaybe; + headPortIndex?: InputMaybe; height?: InputMaybe; linewidth?: InputMaybe; opacity?: InputMaybe; position?: InputMaybe; radius?: InputMaybe; + tailPortIndex?: InputMaybe; width?: InputMaybe; x?: InputMaybe; x1?: InputMaybe; @@ -1065,6 +1073,7 @@ export type Components_Component_Insert_Input = { createdBy?: InputMaybe; fill?: InputMaybe; headEdge?: InputMaybe; + headPortIndex?: InputMaybe; headShapeId?: InputMaybe; height?: InputMaybe; iconStroke?: InputMaybe; @@ -1079,6 +1088,7 @@ export type Components_Component_Insert_Input = { stroke?: InputMaybe; strokeType?: InputMaybe; tailEdge?: InputMaybe; + tailPortIndex?: InputMaybe; tailShapeId?: InputMaybe; textColor?: InputMaybe; updatedBy?: InputMaybe; @@ -1101,6 +1111,7 @@ export type Components_Component_Max_Fields = { createdBy?: Maybe; fill?: Maybe; headEdge?: Maybe; + headPortIndex?: Maybe; headShapeId?: Maybe; height?: Maybe; iconStroke?: Maybe; @@ -1113,6 +1124,7 @@ export type Components_Component_Max_Fields = { stroke?: Maybe; strokeType?: Maybe; tailEdge?: Maybe; + tailPortIndex?: Maybe; tailShapeId?: Maybe; textColor?: Maybe; updatedBy?: Maybe; @@ -1135,6 +1147,7 @@ export type Components_Component_Min_Fields = { createdBy?: Maybe; fill?: Maybe; headEdge?: Maybe; + headPortIndex?: Maybe; headShapeId?: Maybe; height?: Maybe; iconStroke?: Maybe; @@ -1147,6 +1160,7 @@ export type Components_Component_Min_Fields = { stroke?: Maybe; strokeType?: Maybe; tailEdge?: Maybe; + tailPortIndex?: Maybe; tailShapeId?: Maybe; textColor?: Maybe; updatedBy?: Maybe; @@ -1186,6 +1200,7 @@ export type Components_Component_Order_By = { createdBy?: InputMaybe; fill?: InputMaybe; headEdge?: InputMaybe; + headPortIndex?: InputMaybe; headShapeId?: InputMaybe; height?: InputMaybe; iconStroke?: InputMaybe; @@ -1200,6 +1215,7 @@ export type Components_Component_Order_By = { stroke?: InputMaybe; strokeType?: InputMaybe; tailEdge?: InputMaybe; + tailPortIndex?: InputMaybe; tailShapeId?: InputMaybe; textColor?: InputMaybe; updatedBy?: InputMaybe; @@ -1242,6 +1258,8 @@ export type Components_Component_Select_Column = /** column name */ | 'headEdge' /** column name */ + | 'headPortIndex' + /** column name */ | 'headShapeId' /** column name */ | 'height' @@ -1270,6 +1288,8 @@ export type Components_Component_Select_Column = /** column name */ | 'tailEdge' /** column name */ + | 'tailPortIndex' + /** column name */ | 'tailShapeId' /** column name */ | 'textColor' @@ -1300,6 +1320,7 @@ export type Components_Component_Set_Input = { createdBy?: InputMaybe; fill?: InputMaybe; headEdge?: InputMaybe; + headPortIndex?: InputMaybe; headShapeId?: InputMaybe; height?: InputMaybe; iconStroke?: InputMaybe; @@ -1314,6 +1335,7 @@ export type Components_Component_Set_Input = { stroke?: InputMaybe; strokeType?: InputMaybe; tailEdge?: InputMaybe; + tailPortIndex?: InputMaybe; tailShapeId?: InputMaybe; textColor?: InputMaybe; updatedBy?: InputMaybe; @@ -1330,11 +1352,13 @@ export type Components_Component_Set_Input = { export type Components_Component_Stddev_Fields = { __typename?: 'components_component_stddev_fields'; createdAt?: Maybe; + headPortIndex?: Maybe; height?: Maybe; linewidth?: Maybe; opacity?: Maybe; position?: Maybe; radius?: Maybe; + tailPortIndex?: Maybe; width?: Maybe; x?: Maybe; x1?: Maybe; @@ -1348,11 +1372,13 @@ export type Components_Component_Stddev_Fields = { export type Components_Component_Stddev_Pop_Fields = { __typename?: 'components_component_stddev_pop_fields'; createdAt?: Maybe; + headPortIndex?: Maybe; height?: Maybe; linewidth?: Maybe; opacity?: Maybe; position?: Maybe; radius?: Maybe; + tailPortIndex?: Maybe; width?: Maybe; x?: Maybe; x1?: Maybe; @@ -1366,11 +1392,13 @@ export type Components_Component_Stddev_Pop_Fields = { export type Components_Component_Stddev_Samp_Fields = { __typename?: 'components_component_stddev_samp_fields'; createdAt?: Maybe; + headPortIndex?: Maybe; height?: Maybe; linewidth?: Maybe; opacity?: Maybe; position?: Maybe; radius?: Maybe; + tailPortIndex?: Maybe; width?: Maybe; x?: Maybe; x1?: Maybe; @@ -1398,6 +1426,7 @@ export type Components_Component_Stream_Cursor_Value_Input = { createdBy?: InputMaybe; fill?: InputMaybe; headEdge?: InputMaybe; + headPortIndex?: InputMaybe; headShapeId?: InputMaybe; height?: InputMaybe; iconStroke?: InputMaybe; @@ -1412,6 +1441,7 @@ export type Components_Component_Stream_Cursor_Value_Input = { stroke?: InputMaybe; strokeType?: InputMaybe; tailEdge?: InputMaybe; + tailPortIndex?: InputMaybe; tailShapeId?: InputMaybe; textColor?: InputMaybe; updatedBy?: InputMaybe; @@ -1428,11 +1458,13 @@ export type Components_Component_Stream_Cursor_Value_Input = { export type Components_Component_Sum_Fields = { __typename?: 'components_component_sum_fields'; createdAt?: Maybe; + headPortIndex?: Maybe; height?: Maybe; linewidth?: Maybe; opacity?: Maybe; position?: Maybe; radius?: Maybe; + tailPortIndex?: Maybe; width?: Maybe; x?: Maybe; x1?: Maybe; @@ -1461,6 +1493,8 @@ export type Components_Component_Update_Column = /** column name */ | 'headEdge' /** column name */ + | 'headPortIndex' + /** column name */ | 'headShapeId' /** column name */ | 'height' @@ -1489,6 +1523,8 @@ export type Components_Component_Update_Column = /** column name */ | 'tailEdge' /** column name */ + | 'tailPortIndex' + /** column name */ | 'tailShapeId' /** column name */ | 'textColor' @@ -1532,11 +1568,13 @@ export type Components_Component_Updates = { export type Components_Component_Var_Pop_Fields = { __typename?: 'components_component_var_pop_fields'; createdAt?: Maybe; + headPortIndex?: Maybe; height?: Maybe; linewidth?: Maybe; opacity?: Maybe; position?: Maybe; radius?: Maybe; + tailPortIndex?: Maybe; width?: Maybe; x?: Maybe; x1?: Maybe; @@ -1550,11 +1588,13 @@ export type Components_Component_Var_Pop_Fields = { export type Components_Component_Var_Samp_Fields = { __typename?: 'components_component_var_samp_fields'; createdAt?: Maybe; + headPortIndex?: Maybe; height?: Maybe; linewidth?: Maybe; opacity?: Maybe; position?: Maybe; radius?: Maybe; + tailPortIndex?: Maybe; width?: Maybe; x?: Maybe; x1?: Maybe; @@ -1568,11 +1608,13 @@ export type Components_Component_Var_Samp_Fields = { export type Components_Component_Variance_Fields = { __typename?: 'components_component_variance_fields'; createdAt?: Maybe; + headPortIndex?: Maybe; height?: Maybe; linewidth?: Maybe; opacity?: Maybe; position?: Maybe; radius?: Maybe; + tailPortIndex?: Maybe; width?: Maybe; x?: Maybe; x1?: Maybe; @@ -3585,7 +3627,7 @@ export type GetComponentsForBoardQueryVariables = Exact<{ }>; -export type GetComponentsForBoardQuery = { __typename?: 'query_root', components: Array<{ __typename?: 'components_component', id: string, componentType: string, objectClass: string, children?: unknown | null, metadata?: unknown | null, x: any, x1: any, x2: any, y: any, y1: any, y2: any, fill: string, width: any, height: any, iconStroke?: string | null, stroke?: string | null, linewidth?: any | null, strokeType?: string | null, textColor?: string | null, opacity?: number | null, position: number }> }; +export type GetComponentsForBoardQuery = { __typename?: 'query_root', components: Array<{ __typename?: 'components_component', id: string, componentType: string, objectClass: string, children?: unknown | null, metadata?: unknown | null, x: any, x1: any, x2: any, y: any, y1: any, y2: any, fill: string, width: any, height: any, iconStroke?: string | null, stroke?: string | null, linewidth?: any | null, strokeType?: string | null, textColor?: string | null, opacity?: number | null, position: number, tailShapeId?: string | null, tailEdge?: string | null, headShapeId?: string | null, headEdge?: string | null, tailPortIndex: number, headPortIndex: number }> }; export type GetComponentInfoQueryQueryVariables = Exact<{ id?: InputMaybe; @@ -3642,7 +3684,7 @@ export const UpdateBoardVisibilityDocument = {"kind":"Document","definitions":[{ export const UpdateUserRevisitCountDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"updateUserRevisitCount"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"userId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"lastVisit"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"timestamptz"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"update_users_user_revisits_by_pk"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"pk_columns"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"user_id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"userId"}}}]}},{"kind":"Argument","name":{"kind":"Name","value":"_inc"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"count"},"value":{"kind":"StringValue","value":"1","block":false}}]}},{"kind":"Argument","name":{"kind":"Name","value":"_set"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"last_visit"},"value":{"kind":"Variable","name":{"kind":"Name","value":"lastVisit"}}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"count"}},{"kind":"Field","name":{"kind":"Name","value":"user_id"}},{"kind":"Field","name":{"kind":"Name","value":"last_visit"}}]}}]}}]} as unknown as DocumentNode; export const MyQueryDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"MyQuery"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}},"defaultValue":{"kind":"StringValue","value":"","block":false}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","alias":{"kind":"Name","value":"users"},"name":{"kind":"Name","value":"users_user"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"where"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"id"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"_eq"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}]}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"firstName"}},{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]} as unknown as DocumentNode; export const GetComponentTypesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"getComponentTypes"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","alias":{"kind":"Name","value":"componentTypes"},"name":{"kind":"Name","value":"components_componentType"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"label"}},{"kind":"Field","name":{"kind":"Name","value":"metadata"}},{"kind":"Field","name":{"kind":"Name","value":"logo"}},{"kind":"Field","name":{"kind":"Name","value":"width"}},{"kind":"Field","name":{"kind":"Name","value":"height"}},{"kind":"Field","name":{"kind":"Name","value":"fill"}},{"kind":"Field","name":{"kind":"Name","value":"textColor"}}]}}]}}]} as unknown as DocumentNode; -export const GetComponentsForBoardDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"getComponentsForBoard"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"boardId"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"uuid"}},"defaultValue":{"kind":"StringValue","value":"","block":false}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","alias":{"kind":"Name","value":"components"},"name":{"kind":"Name","value":"components_component"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"where"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"boardId"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"_eq"},"value":{"kind":"Variable","name":{"kind":"Name","value":"boardId"}}}]}}]}},{"kind":"Argument","name":{"kind":"Name","value":"order_by"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"position"},"value":{"kind":"EnumValue","value":"asc"}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"componentType"}},{"kind":"Field","name":{"kind":"Name","value":"objectClass"}},{"kind":"Field","name":{"kind":"Name","value":"children"}},{"kind":"Field","name":{"kind":"Name","value":"metadata"}},{"kind":"Field","name":{"kind":"Name","value":"x"}},{"kind":"Field","name":{"kind":"Name","value":"x1"}},{"kind":"Field","name":{"kind":"Name","value":"x2"}},{"kind":"Field","name":{"kind":"Name","value":"y"}},{"kind":"Field","name":{"kind":"Name","value":"y1"}},{"kind":"Field","name":{"kind":"Name","value":"y2"}},{"kind":"Field","name":{"kind":"Name","value":"fill"}},{"kind":"Field","name":{"kind":"Name","value":"width"}},{"kind":"Field","name":{"kind":"Name","value":"height"}},{"kind":"Field","name":{"kind":"Name","value":"iconStroke"}},{"kind":"Field","name":{"kind":"Name","value":"stroke"}},{"kind":"Field","name":{"kind":"Name","value":"linewidth"}},{"kind":"Field","name":{"kind":"Name","value":"strokeType"}},{"kind":"Field","name":{"kind":"Name","value":"textColor"}},{"kind":"Field","name":{"kind":"Name","value":"opacity"}},{"kind":"Field","name":{"kind":"Name","value":"position"}}]}}]}}]} as unknown as DocumentNode; +export const GetComponentsForBoardDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"getComponentsForBoard"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"boardId"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"uuid"}},"defaultValue":{"kind":"StringValue","value":"","block":false}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","alias":{"kind":"Name","value":"components"},"name":{"kind":"Name","value":"components_component"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"where"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"boardId"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"_eq"},"value":{"kind":"Variable","name":{"kind":"Name","value":"boardId"}}}]}}]}},{"kind":"Argument","name":{"kind":"Name","value":"order_by"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"position"},"value":{"kind":"EnumValue","value":"asc"}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"componentType"}},{"kind":"Field","name":{"kind":"Name","value":"objectClass"}},{"kind":"Field","name":{"kind":"Name","value":"children"}},{"kind":"Field","name":{"kind":"Name","value":"metadata"}},{"kind":"Field","name":{"kind":"Name","value":"x"}},{"kind":"Field","name":{"kind":"Name","value":"x1"}},{"kind":"Field","name":{"kind":"Name","value":"x2"}},{"kind":"Field","name":{"kind":"Name","value":"y"}},{"kind":"Field","name":{"kind":"Name","value":"y1"}},{"kind":"Field","name":{"kind":"Name","value":"y2"}},{"kind":"Field","name":{"kind":"Name","value":"fill"}},{"kind":"Field","name":{"kind":"Name","value":"width"}},{"kind":"Field","name":{"kind":"Name","value":"height"}},{"kind":"Field","name":{"kind":"Name","value":"iconStroke"}},{"kind":"Field","name":{"kind":"Name","value":"stroke"}},{"kind":"Field","name":{"kind":"Name","value":"linewidth"}},{"kind":"Field","name":{"kind":"Name","value":"strokeType"}},{"kind":"Field","name":{"kind":"Name","value":"textColor"}},{"kind":"Field","name":{"kind":"Name","value":"opacity"}},{"kind":"Field","name":{"kind":"Name","value":"position"}},{"kind":"Field","name":{"kind":"Name","value":"tailShapeId"}},{"kind":"Field","name":{"kind":"Name","value":"tailEdge"}},{"kind":"Field","name":{"kind":"Name","value":"headShapeId"}},{"kind":"Field","name":{"kind":"Name","value":"headEdge"}},{"kind":"Field","name":{"kind":"Name","value":"tailPortIndex"}},{"kind":"Field","name":{"kind":"Name","value":"headPortIndex"}}]}}]}}]} as unknown as DocumentNode; export const GetComponentInfoQueryDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"getComponentInfoQuery"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"uuid"}},"defaultValue":{"kind":"StringValue","value":"","block":false}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","alias":{"kind":"Name","value":"component"},"name":{"kind":"Name","value":"components_component_by_pk"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"metadata"}},{"kind":"Field","name":{"kind":"Name","value":"width"}},{"kind":"Field","name":{"kind":"Name","value":"height"}},{"kind":"Field","name":{"kind":"Name","value":"fill"}},{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"stroke"}},{"kind":"Field","name":{"kind":"Name","value":"linewidth"}},{"kind":"Field","name":{"kind":"Name","value":"strokeType"}},{"kind":"Field","name":{"kind":"Name","value":"x"}},{"kind":"Field","name":{"kind":"Name","value":"y"}},{"kind":"Field","name":{"kind":"Name","value":"x1"}},{"kind":"Field","name":{"kind":"Name","value":"y1"}},{"kind":"Field","name":{"kind":"Name","value":"x2"}},{"kind":"Field","name":{"kind":"Name","value":"y2"}},{"kind":"Field","name":{"kind":"Name","value":"componentType"}},{"kind":"Field","name":{"kind":"Name","value":"children"}},{"kind":"Field","name":{"kind":"Name","value":"updatedBy"}},{"kind":"Field","name":{"kind":"Name","value":"iconStroke"}},{"kind":"Field","name":{"kind":"Name","value":"textColor"}},{"kind":"Field","name":{"kind":"Name","value":"opacity"}}]}}]}}]} as unknown as DocumentNode; export const GetBoardComponentsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"getBoardComponents"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"boardId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"uuid"}}},"defaultValue":{"kind":"StringValue","value":"","block":false}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","alias":{"kind":"Name","value":"components"},"name":{"kind":"Name","value":"components_component"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"where"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"boardId"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"_eq"},"value":{"kind":"Variable","name":{"kind":"Name","value":"boardId"}}}]}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"componentType"}}]}}]}}]} as unknown as DocumentNode; export const UserDetailsSubscriptionDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"subscription","name":{"kind":"Name","value":"userDetailsSubscription"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}},"defaultValue":{"kind":"StringValue","value":"","block":false}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","alias":{"kind":"Name","value":"users"},"name":{"kind":"Name","value":"users_user"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"where"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"id"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"_eq"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}]}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"firstName"}},{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]} as unknown as DocumentNode; diff --git a/src/schema/queries/index.ts b/src/schema/queries/index.ts index f12e9b1..cbeea99 100644 --- a/src/schema/queries/index.ts +++ b/src/schema/queries/index.ts @@ -72,6 +72,12 @@ export const GET_COMPONENTS_FOR_BOARD_QUERY: TypedDocumentNode< textColor opacity position + tailShapeId + tailEdge + headShapeId + headEdge + tailPortIndex + headPortIndex } } ` diff --git a/tests/e2e/copy-paste.spec.js b/tests/e2e/copy-paste.spec.js index 3949b37..5499781 100644 --- a/tests/e2e/copy-paste.spec.js +++ b/tests/e2e/copy-paste.spec.js @@ -16,6 +16,55 @@ function safeArea(box) { return { cx, cy } } +// --- Resize + copy/paste dimension-preservation helpers --- + +// A shape's body renders as a path (rectangle/diamond) or ellipse (circle). +const GEOM_SELECTOR = 'ellipse, path, rect, circle' +// Mirrors SELECTION_PADDING in src/canvas/selectionController.ts. At the default +// scale (1) surface units ≈ screen px, so a corner handle sits this many px +// outside the shape's rendered corner. +const SELECTION_PADDING = 5 +// Clones render identically; allow a couple px for subpixel/stroke rounding. +const SIZE_TOL = 2 + +// Reads the rendered geometry box (screen px) of a shape's body node. Size is +// position-independent, so it lets us compare an original against its pasted +// clone regardless of where paste drops the copy. +async function geomRect(handle) { + return handle.$eval(GEOM_SELECTOR, (el) => { + const r = el.getBoundingClientRect() + return { + left: r.left, + top: r.top, + right: r.right, + bottom: r.bottom, + width: r.width, + height: r.height, + } + }) +} + +// Clicks a shape's current center to select it, waiting for the selection +// chrome (.shape-selected on the canvas root) so the corner handles exist. +async function selectShape(page, handle) { + const r = await geomRect(handle) + await page.mouse.click(r.left + r.width / 2, r.top + r.height / 2) + await page.waitForSelector('.shape-selected') +} + +// Drags the SE corner handle of an already-selected shape by (dx, dy) to grow +// its width/height. The handle sits ~SELECTION_PADDING outside the rendered +// SE corner. +async function resizeFromSECorner(page, handle, dx, dy) { + const r = await geomRect(handle) + const startX = r.right + SELECTION_PADDING + const startY = r.bottom + SELECTION_PADDING + await page.mouse.move(startX, startY) + await page.mouse.down() + await page.mouse.move(startX + dx, startY + dy, { steps: 10 }) + await page.mouse.up() +} + test.describe('Copy-paste', () => { test.beforeEach(async ({ page }) => { await setupLocalBoard(page) @@ -262,4 +311,109 @@ test.describe('Copy-paste', () => { ) expect(pastedFill).toBe(NEW_FILL) }) + + // Resizing commits new width/height (the radius/diameter for circles) to the + // shape. These guard the regression risk that the copy handler reads stale + // pre-resize geometry — the same class of bug as the rectangle-with-text + // staleness above — so the pasted clone must carry the *resized* dimensions, + // not the draw-time ones. One test per shape, including circle. + const RESIZE_CASES = [ + { + type: 'rectangle', + name: 'pasted rectangle preserves width/height set by a prior resize', + draw: (cx, cy) => ({ + startX: cx - 100, + startY: cy - 60, + endX: cx + 100, + endY: cy + 60, + }), + }, + { + type: 'circle', + name: 'pasted circle preserves size (radius) set by a prior resize', + draw: (cx, cy) => ({ + startX: cx - 60, + startY: cy - 60, + endX: cx + 60, + endY: cy + 60, + }), + }, + { + type: 'diamond', + name: 'pasted diamond preserves width/height set by a prior resize', + draw: (cx, cy) => ({ + startX: cx - 70, + startY: cy - 70, + endX: cx + 70, + endY: cy + 70, + }), + }, + ] + + for (const shapeCase of RESIZE_CASES) { + test(shapeCase.name, async ({ page }) => { + const box = await getCanvasBox(page) + const { cx, cy } = safeArea(box) + // Empty canvas point (left of the shape, clear of the top toolbar + // and the left Defaults panel) used for blur and re-blur clicks. + const emptyPoint = { + x: box.x + box.width * 0.3, + y: box.y + box.height * 0.85, + } + + const handle = await drawShape( + page, + shapeCase.type, + shapeCase.draw(cx, cy) + ) + + // Blur: click empty canvas to drop any post-draw selection. + await page.mouse.click(emptyPoint.x, emptyPoint.y) + + // Re-select, then grow the shape via its SE corner handle. + await selectShape(page, handle) + const beforeResize = await geomRect(handle) + await resizeFromSECorner(page, handle, 80, 80) + const afterResize = await geomRect(handle) + + // Sanity: the resize actually enlarged the shape. + expect(afterResize.width).toBeGreaterThan(beforeResize.width + 20) + expect(afterResize.height).toBeGreaterThan(beforeResize.height + 20) + + // Deselect → re-select → copy (mirrors the proven select-before-copy + // sequence above), then paste on empty canvas. + await page.mouse.click(emptyPoint.x, emptyPoint.y) + await selectShape(page, handle) + await page.keyboard.press('Meta+c') + await page.mouse.move(cx - 300, cy) + await page.keyboard.press('Meta+v') + + await page.waitForFunction( + () => + document.querySelectorAll('[data-component-id]').length >= 2 + ) + + // The pasted clone is the element whose id differs from the original. + const originalId = await handle.getAttribute('id') + const allGroups = await page.$$('[data-component-id]') + let pastedHandle = null + for (const group of allGroups) { + const id = await group.getAttribute('id') + if (id !== originalId) { + pastedHandle = group + break + } + } + expect(pastedHandle).not.toBeNull() + + // The pasted clone must carry the resized dimensions, not pre-resize. + const pasted = await geomRect(pastedHandle) + expect( + Math.abs(pasted.width - afterResize.width) + ).toBeLessThanOrEqual(SIZE_TOL) + expect( + Math.abs(pasted.height - afterResize.height) + ).toBeLessThanOrEqual(SIZE_TOL) + }) + } }) diff --git a/yarn.lock b/yarn.lock index 1bd190e..fc6d86a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -61,25 +61,34 @@ js-tokens "^4.0.0" picocolors "^1.1.1" -"@babel/compat-data@^7.28.6": - version "7.29.0" - resolved "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz" - integrity sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg== - -"@babel/core@^7.21.3", "@babel/core@^7.28.0", "@babel/core@^7.28.6": - version "7.29.0" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.29.0.tgz#5286ad785df7f79d656e88ce86e650d16ca5f322" - integrity sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA== +"@babel/code-frame@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.29.7.tgz#f2fbbfea87c44a21590ec515b778b2c26d8866e7" + integrity sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw== dependencies: - "@babel/code-frame" "^7.29.0" - "@babel/generator" "^7.29.0" - "@babel/helper-compilation-targets" "^7.28.6" - "@babel/helper-module-transforms" "^7.28.6" - "@babel/helpers" "^7.28.6" - "@babel/parser" "^7.29.0" - "@babel/template" "^7.28.6" - "@babel/traverse" "^7.29.0" - "@babel/types" "^7.29.0" + "@babel/helper-validator-identifier" "^7.29.7" + js-tokens "^4.0.0" + picocolors "^1.1.1" + +"@babel/compat-data@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.29.7.tgz#6f0237f0f36d2e51c0570a636faed9d2d0efe629" + integrity sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg== + +"@babel/core@^7.21.3", "@babel/core@^7.28.0", "@babel/core@^7.28.6", "@babel/core@^7.29.6": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.29.7.tgz#80c10b17248082968b57a857b91640971f2070f7" + integrity sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA== + dependencies: + "@babel/code-frame" "^7.29.7" + "@babel/generator" "^7.29.7" + "@babel/helper-compilation-targets" "^7.29.7" + "@babel/helper-module-transforms" "^7.29.7" + "@babel/helpers" "^7.29.7" + "@babel/parser" "^7.29.7" + "@babel/template" "^7.29.7" + "@babel/traverse" "^7.29.7" + "@babel/types" "^7.29.7" "@jridgewell/remapping" "^2.3.5" convert-source-map "^2.0.0" debug "^4.1.0" @@ -98,13 +107,24 @@ "@jridgewell/trace-mapping" "^0.3.28" jsesc "^3.0.2" -"@babel/helper-compilation-targets@^7.28.6": - version "7.28.6" - resolved "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz" - integrity sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA== +"@babel/generator@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.29.7.tgz#cca0b8827e6bcf3ba176788e7f3b180ad6db2fa3" + integrity sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ== dependencies: - "@babel/compat-data" "^7.28.6" - "@babel/helper-validator-option" "^7.27.1" + "@babel/parser" "^7.29.7" + "@babel/types" "^7.29.7" + "@jridgewell/gen-mapping" "^0.3.12" + "@jridgewell/trace-mapping" "^0.3.28" + jsesc "^3.0.2" + +"@babel/helper-compilation-targets@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz#7a1def704302401c47f64fa85589e974ae217042" + integrity sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g== + dependencies: + "@babel/compat-data" "^7.29.7" + "@babel/helper-validator-option" "^7.29.7" browserslist "^4.24.0" lru-cache "^5.1.1" semver "^6.3.1" @@ -114,22 +134,27 @@ resolved "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz" integrity sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw== -"@babel/helper-module-imports@^7.28.6": - version "7.28.6" - resolved "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz" - integrity sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw== +"@babel/helper-globals@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/helper-globals/-/helper-globals-7.29.7.tgz#f04a96fbd8473241b1079243f5b3f03a3010ab7b" + integrity sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA== + +"@babel/helper-module-imports@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz#ef25048a518e828d7393fac5882ddd73921d7396" + integrity sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g== dependencies: - "@babel/traverse" "^7.28.6" - "@babel/types" "^7.28.6" + "@babel/traverse" "^7.29.7" + "@babel/types" "^7.29.7" -"@babel/helper-module-transforms@^7.28.6": - version "7.28.6" - resolved "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz" - integrity sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA== +"@babel/helper-module-transforms@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz#b062747a5997ba138637201328bbff77960574ae" + integrity sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg== dependencies: - "@babel/helper-module-imports" "^7.28.6" - "@babel/helper-validator-identifier" "^7.28.5" - "@babel/traverse" "^7.28.6" + "@babel/helper-module-imports" "^7.29.7" + "@babel/helper-validator-identifier" "^7.29.7" + "@babel/traverse" "^7.29.7" "@babel/helper-plugin-utils@^7.20.2", "@babel/helper-plugin-utils@^7.27.1", "@babel/helper-plugin-utils@^7.28.6": version "7.28.6" @@ -141,23 +166,33 @@ resolved "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz" integrity sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA== +"@babel/helper-string-parser@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz#7f0871d99824d23137d60f86fcf6130fd5a1b51f" + integrity sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw== + "@babel/helper-validator-identifier@^7.28.5": version "7.28.5" resolved "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz" integrity sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q== -"@babel/helper-validator-option@^7.27.1": - version "7.27.1" - resolved "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz" - integrity sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg== +"@babel/helper-validator-identifier@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz#bd87084ced0c796ec46bda492de6e83d29e89fc2" + integrity sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg== -"@babel/helpers@^7.28.6": - version "7.29.2" - resolved "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz" - integrity sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw== +"@babel/helper-validator-option@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz#cf315be940213b354eb4abcc0bd01ebe3f73bc2a" + integrity sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw== + +"@babel/helpers@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.29.7.tgz#45abfde7548997e34376c3e69feb475cffb4a607" + integrity sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg== dependencies: - "@babel/template" "^7.28.6" - "@babel/types" "^7.29.0" + "@babel/template" "^7.29.7" + "@babel/types" "^7.29.7" "@babel/parser@^7.1.0", "@babel/parser@^7.20.7", "@babel/parser@^7.28.6", "@babel/parser@^7.29.0": version "7.29.2" @@ -173,6 +208,13 @@ dependencies: "@babel/types" "^7.29.0" +"@babel/parser@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.29.7.tgz#837b87387cbf5ec5530cb634b3c622f68edb9334" + integrity sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg== + dependencies: + "@babel/types" "^7.29.7" + "@babel/plugin-syntax-import-assertions@^7.26.0": version "7.28.6" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.28.6.tgz#ae9bc1923a6ba527b70104dd2191b0cd872c8507" @@ -208,7 +250,16 @@ "@babel/parser" "^7.28.6" "@babel/types" "^7.28.6" -"@babel/traverse@^7.26.10", "@babel/traverse@^7.28.6", "@babel/traverse@^7.29.0": +"@babel/template@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.29.7.tgz#4d9d4004f645cdd304de958c725162784ecac700" + integrity sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg== + dependencies: + "@babel/code-frame" "^7.29.7" + "@babel/parser" "^7.29.7" + "@babel/types" "^7.29.7" + +"@babel/traverse@^7.26.10": version "7.29.0" resolved "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz" integrity sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA== @@ -221,6 +272,19 @@ "@babel/types" "^7.29.0" debug "^4.3.1" +"@babel/traverse@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.29.7.tgz#c47b07a41b95da0907d026b5dd894d98de7d2f2d" + integrity sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw== + dependencies: + "@babel/code-frame" "^7.29.7" + "@babel/generator" "^7.29.7" + "@babel/helper-globals" "^7.29.7" + "@babel/parser" "^7.29.7" + "@babel/template" "^7.29.7" + "@babel/types" "^7.29.7" + debug "^4.3.1" + "@babel/types@^7.0.0", "@babel/types@^7.18.13", "@babel/types@^7.20.7", "@babel/types@^7.21.3", "@babel/types@^7.26.10", "@babel/types@^7.28.6", "@babel/types@^7.29.0": version "7.29.0" resolved "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz" @@ -229,6 +293,14 @@ "@babel/helper-string-parser" "^7.27.1" "@babel/helper-validator-identifier" "^7.28.5" +"@babel/types@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.29.7.tgz#8005e31d82712ee7adaef6e23c63b71a62770a92" + integrity sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA== + dependencies: + "@babel/helper-string-parser" "^7.29.7" + "@babel/helper-validator-identifier" "^7.29.7" + "@csstools/color-helpers@^5.1.0": version "5.1.0" resolved "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz" @@ -2502,16 +2574,16 @@ foreground-child@^3.1.0: cross-spawn "^7.0.0" signal-exit "^4.0.1" -form-data@>=3.0.4, form-data@^4.0.0: - version "4.0.5" - resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.5.tgz#b49e48858045ff4cbf6b03e1805cebcad3679053" - integrity sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w== +form-data@^4.0.0, form-data@^4.0.6: + version "4.0.6" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.6.tgz#28e864e1b786dbebb68db1f452f9635278665827" + integrity sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ== dependencies: asynckit "^0.4.0" combined-stream "^1.0.8" es-set-tostringtag "^2.1.0" - hasown "^2.0.2" - mime-types "^2.1.12" + hasown "^2.0.4" + mime-types "^2.1.35" formdata-polyfill@^4.0.10: version "4.0.10" @@ -2700,6 +2772,13 @@ hasown@^2.0.2: dependencies: function-bind "^1.1.2" +hasown@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.4.tgz#8c62d8cb90beb2aad5d0a5b67581ad9854c3f003" + integrity sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A== + dependencies: + function-bind "^1.1.2" + header-case@^2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/header-case/-/header-case-2.0.4.tgz#5a42e63b55177349cf405beb8d775acabb92c063" @@ -2996,10 +3075,10 @@ js-tokens@^9.0.1: resolved "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz" integrity sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ== -js-yaml@^4.0.0, js-yaml@^4.1.0: - version "4.1.1" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.1.tgz#854c292467705b699476e1a2decc0c8a3458806b" - integrity sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA== +js-yaml@^4.0.0, js-yaml@^4.1.0, js-yaml@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.2.0.tgz#2bd9e85682dd91bd469afb809d816043b3d49524" + integrity sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw== dependencies: argparse "^2.0.1" @@ -3195,9 +3274,9 @@ mime-db@1.52.0: resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz" integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== -mime-types@^2.1.12: +mime-types@^2.1.35: version "2.1.35" - resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== dependencies: mime-db "1.52.0" @@ -4520,10 +4599,10 @@ wrap-ansi@^8.1.0: string-width "^5.0.1" strip-ansi "^7.0.1" -"ws@^5.2.0 || ^6.0.0 || ^7.0.0": - version "7.5.10" - resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.10.tgz#58b5c20dc281633f6c19113f39b349bd8bd558d9" - integrity sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ== +"ws@^5.2.0 || ^6.0.0 || ^7.0.0", ws@^7.5.11: + version "7.5.11" + resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.11.tgz#9460daf1812bb81a423c5b9eac746941a86310fa" + integrity sha512-zS54Oen9bITtp7kp2XM3AydrCIq1D+HwJOuH+c+e4LfpL/lotP5osijd+UoMnxwAam1GN8R4KtLAyIrIcBNpiA== ws@^8.17.1, ws@^8.18.0, ws@^8.18.3, ws@^8.20.0: version "8.21.0"