-
-
-
+
+
+
+
@@ -50,36 +80,45 @@ const style = computed(() => ({
diff --git a/app/gui2/src/components/DocumentationEditor.vue b/app/gui2/src/components/DocumentationEditor.vue
index fcdb7e6570..3355bb347e 100644
--- a/app/gui2/src/components/DocumentationEditor.vue
+++ b/app/gui2/src/components/DocumentationEditor.vue
@@ -6,12 +6,12 @@ import { useProjectStore } from '@/stores/project'
import type { ToValue } from '@/util/reactivity'
import type { Path } from 'shared/languageServerTypes'
import { Err, Ok, mapOk, withContext, type Result } from 'shared/util/data/result'
-import { toRef, toValue } from 'vue'
+import { ref, toRef, toValue } from 'vue'
const documentation = defineModel
({ required: true })
-const _props = defineProps<{
- toolbarContainer: HTMLElement | undefined
-}>()
+const _props = defineProps<{}>()
+
+const toolbarElement = ref()
const graphStore = useGraphStore()
const projectStore = useProjectStore()
@@ -69,9 +69,36 @@ function useDocumentationImages(
-
+
+
+
diff --git a/app/gui2/src/components/ComponentBrowser/DocumentationPanel.vue b/app/gui2/src/components/DocumentationPanel.vue
similarity index 98%
rename from app/gui2/src/components/ComponentBrowser/DocumentationPanel.vue
rename to app/gui2/src/components/DocumentationPanel.vue
index 25beb686b2..4b29062546 100644
--- a/app/gui2/src/components/ComponentBrowser/DocumentationPanel.vue
+++ b/app/gui2/src/components/DocumentationPanel.vue
@@ -189,9 +189,9 @@ function openDocs(url: string) {
line-height: 160%;
color: var(--enso-docs-text-color);
background-color: var(--enso-docs-background-color);
- padding: 4px 12px 4px 4px;
+ padding: 4px 12px var(--doc-panel-bottom-clip, 0) 4px;
white-space: normal;
- clip-path: inset(0 0 4px 0);
+ clip-path: inset(0 0 var(--doc-panel-bottom-clip, 0) 0);
height: 100%;
overflow-y: auto;
display: flex;
diff --git a/app/gui2/src/components/DocumentationPanel/DocsBreadcrumb.vue b/app/gui2/src/components/DocumentationPanel/DocsBreadcrumb.vue
index 5102f0262d..e210504855 100644
--- a/app/gui2/src/components/DocumentationPanel/DocsBreadcrumb.vue
+++ b/app/gui2/src/components/DocumentationPanel/DocsBreadcrumb.vue
@@ -3,11 +3,10 @@ import SvgIcon from '@/components/SvgIcon.vue'
import type { Icon } from '@/util/iconName'
const props = defineProps<{ text: string; icon?: Icon | undefined }>()
-const emit = defineEmits<{ click: [] }>()
-
+
diff --git a/app/gui2/src/components/DocumentationPanel/DocsBreadcrumbs.vue b/app/gui2/src/components/DocumentationPanel/DocsBreadcrumbs.vue
index bee37089b8..722d6a263b 100644
--- a/app/gui2/src/components/DocumentationPanel/DocsBreadcrumbs.vue
+++ b/app/gui2/src/components/DocumentationPanel/DocsBreadcrumbs.vue
@@ -44,7 +44,9 @@ function shrinkFactor(index: number): number {
:text="breadcrumb.label"
:icon="index === props.breadcrumbs.length - 1 ? props.icon : undefined"
:style="{ 'flex-shrink': shrinkFactor(index) }"
- @click="emit('click', index)"
+ :class="{ nonInteractive: index === props.breadcrumbs.length - 1 }"
+ class="clickable"
+ @click.stop="emit('click', index)"
/>
@@ -81,4 +83,8 @@ function shrinkFactor(index: number): number {
.breadcrumbs-enter-from {
opacity: 0;
}
+
+.nonInteractive {
+ pointer-events: none;
+}
diff --git a/app/gui2/src/components/GraphEditor.vue b/app/gui2/src/components/GraphEditor.vue
index 03ab94c39a..8b2c691f99 100644
--- a/app/gui2/src/components/GraphEditor.vue
+++ b/app/gui2/src/components/GraphEditor.vue
@@ -11,6 +11,7 @@ import CodeEditor from '@/components/CodeEditor.vue'
import ComponentBrowser from '@/components/ComponentBrowser.vue'
import { type Usage } from '@/components/ComponentBrowser/input'
import { usePlacement } from '@/components/ComponentBrowser/placement'
+import ComponentDocumentation from '@/components/ComponentDocumentation.vue'
import DockPanel from '@/components/DockPanel.vue'
import DocumentationEditor from '@/components/DocumentationEditor.vue'
import GraphEdges from '@/components/GraphEditor/GraphEdges.vue'
@@ -24,6 +25,7 @@ import GraphMouse from '@/components/GraphMouse.vue'
import PlusButton from '@/components/PlusButton.vue'
import SceneScroller from '@/components/SceneScroller.vue'
import TopBar from '@/components/TopBar.vue'
+import { builtinWidgets } from '@/components/widgets'
import { useAstDocumentation } from '@/composables/astDocumentation'
import { useDoubleClick } from '@/composables/doubleClick'
import { keyboardBusy, keyboardBusyExceptIn, unrefElement, useEvent } from '@/composables/events'
@@ -37,6 +39,7 @@ import { provideGraphSelection } from '@/providers/graphSelection'
import { provideStackNavigator } from '@/providers/graphStackNavigator'
import { provideInteractionHandler } from '@/providers/interactionHandler'
import { provideKeyboard } from '@/providers/keyboard'
+import { injectVisibility } from '@/providers/visibility'
import { provideWidgetRegistry } from '@/providers/widgetRegistry'
import { provideGraphStore, type NodeId } from '@/stores/graph'
import type { RequiredImport } from '@/stores/graph/imports'
@@ -50,7 +53,7 @@ import { colorFromString } from '@/util/colors'
import { partition } from '@/util/data/array'
import { every, filterDefined } from '@/util/data/iterable'
import { Rect } from '@/util/data/rect'
-import { unwrapOr } from '@/util/data/result'
+import { Err, Ok, unwrapOr, type Result } from '@/util/data/result'
import { Vec2 } from '@/util/data/vec2'
import { computedFallback } from '@/util/reactivity'
import { until } from '@vueuse/core'
@@ -70,9 +73,6 @@ import {
type ComponentInstance,
} from 'vue'
-import { builtinWidgets } from '@/components/widgets'
-import { injectVisibility } from '@/providers/visibility'
-
const keyboard = provideKeyboard()
const projectStore = useProjectStore()
const suggestionDb = provideSuggestionDbStore(projectStore)
@@ -101,7 +101,8 @@ const graphNavigator: GraphNavigator = provideGraphNavigator(viewportNode, keybo
// === Client saved state ===
-const storedShowDocumentationEditor = ref()
+const storedShowRightDock = ref()
+const storedRightDockTab = ref()
const rightDockWidth = ref
()
/**
@@ -119,6 +120,8 @@ interface GraphStoredState {
s: number
/** Whether or not the documentation panel is open. */
doc: boolean
+ /** The selected tab in the right-side panel. */
+ rtab: string
/** Width of the right dock. */
rwidth: number | null
}
@@ -145,7 +148,8 @@ useSyncLocalStorage({
x: graphNavigator.targetCenter.x,
y: graphNavigator.targetCenter.y,
s: graphNavigator.targetScale,
- doc: storedShowDocumentationEditor.value,
+ doc: storedShowRightDock.value,
+ rtab: storedRightDockTab.value,
rwidth: rightDockWidth.value ?? null,
}
},
@@ -154,7 +158,8 @@ useSyncLocalStorage({
const pos = new Vec2(restored.x ?? 0, restored.y ?? 0)
const scale = restored.s ?? 1
graphNavigator.setCenterAndScale(pos, scale)
- storedShowDocumentationEditor.value = restored.doc ?? undefined
+ storedShowRightDock.value = restored.doc ?? undefined
+ storedRightDockTab.value = restored.rtab ?? undefined
rightDockWidth.value = restored.rwidth ?? undefined
} else {
await until(visibleAreasReady).toBe(true)
@@ -352,27 +357,22 @@ const graphBindingsHandler = graphBindings.handler({
showColorPicker.value = true
},
openDocumentation() {
- const failure = 'Unable to show node documentation'
- const selected = getSoleSelectionOrToast(failure)
- if (selected == null) return
- const suggestion = graphStore.db.nodeMainSuggestion.lookup(selected)
- const documentation = suggestion && suggestionDocumentationUrl(suggestion)
- if (documentation) {
- window.open(documentation, '_blank')
- } else {
- toasts.userActionFailed.show(`${failure}: no documentation available for selected node.`)
+ const result = tryGetSelectionDocUrl()
+ if (!result.ok) {
+ toasts.userActionFailed.show(result.error.message('Unable to show node documentation'))
+ return
}
+ window.open(result.value, '_blank')
},
})
-function getSoleSelectionOrToast(context: string) {
- if (nodeSelection.selected.size === 0) {
- toasts.userActionFailed.show(`${context}: no node selected.`)
- } else if (nodeSelection.selected.size > 1) {
- toasts.userActionFailed.show(`${context}: multiple nodes selected.`)
- } else {
- return set.first(nodeSelection.selected)
- }
+function tryGetSelectionDocUrl() {
+ const selected = nodeSelection.tryGetSoleSelection()
+ if (!selected.ok) return selected
+ const suggestion = graphStore.db.getNodeMainSuggestion(selected.value)
+ const documentation = suggestion && suggestionDocumentationUrl(suggestion)
+ if (!documentation) return Err('No external documentation available for selected component')
+ return Ok(documentation)
}
const { handleClick } = useDoubleClick(
@@ -405,15 +405,16 @@ const codeEditorHandler = codeEditorBindings.handler({
const docEditor = shallowRef>()
const documentationEditorArea = computed(() => unrefElement(docEditor))
-const showDocumentationEditor = computedFallback(
- storedShowDocumentationEditor,
+const showRightDock = computedFallback(
+ storedShowRightDock,
// Show documentation editor when documentation exists on first graph visit.
() => !!documentation.state.value,
)
+const rightDockTab = computedFallback(storedRightDockTab, () => 'docs')
const documentationEditorHandler = documentationEditorBindings.handler({
toggle() {
- showDocumentationEditor.value = !showDocumentationEditor.value
+ showRightDock.value = !showRightDock.value
},
})
@@ -662,6 +663,7 @@ const groupColors = computed(() => {
@@ -669,7 +671,6 @@ const groupColors = computed(() => {
{
v-model:recordMode="projectStore.recordMode"
v-model:showColorPicker="showColorPicker"
v-model:showCodeEditor="showCodeEditor"
- v-model:showDocumentationEditor="showDocumentationEditor"
+ v-model:showDocumentationEditor="showRightDock"
:zoomLevel="100.0 * graphNavigator.targetScale"
:componentsSelected="nodeSelection.selected.size"
- :class="{ extraRightSpace: !showDocumentationEditor }"
+ :class="{ extraRightSpace: !showRightDock }"
@fitToAllClicked="zoomToSelected"
@zoomIn="graphNavigator.stepZoom(+1)"
@zoomOut="graphNavigator.stepZoom(-1)"
@@ -719,15 +720,21 @@ const groupColors = computed(() => {
-
-
+
+
+
+
+
diff --git a/app/gui2/src/components/GraphEditor/GraphNode.vue b/app/gui2/src/components/GraphEditor/GraphNode.vue
index 5cf6cbfbbe..39655a6880 100644
--- a/app/gui2/src/components/GraphEditor/GraphNode.vue
+++ b/app/gui2/src/components/GraphEditor/GraphNode.vue
@@ -291,7 +291,7 @@ const isRecordingOverridden = computed({
const expressionInfo = computed(() => graph.db.getExpressionInfo(props.node.innerExpr.externalId))
const executionState = computed(() => expressionInfo.value?.payload.type ?? 'Unknown')
-const suggestionEntry = computed(() => graph.db.nodeMainSuggestion.lookup(nodeId.value))
+const suggestionEntry = computed(() => graph.db.getNodeMainSuggestion(nodeId.value))
const color = computed(() => graph.db.getNodeColorStyle(nodeId.value))
const documentationUrl = computed(
() => suggestionEntry.value && suggestionDocumentationUrl(suggestionEntry.value),
diff --git a/app/gui2/src/components/GraphEditor/NodeWidgetTree.vue b/app/gui2/src/components/GraphEditor/NodeWidgetTree.vue
index cf400affe9..a8a35f73a1 100644
--- a/app/gui2/src/components/GraphEditor/NodeWidgetTree.vue
+++ b/app/gui2/src/components/GraphEditor/NodeWidgetTree.vue
@@ -101,7 +101,7 @@ const widgetTree = provideWidgetTree(
)
const expressionInfo = computed(() => graph.db.getExpressionInfo(props.ast.externalId))
-const suggestionEntry = computed(() => graph.db.nodeMainSuggestion.lookup(props.nodeId))
+const suggestionEntry = computed(() => graph.db.getNodeMainSuggestion(props.nodeId))
const topLevelIcon = computed(() => {
switch (props.nodeType) {
default:
diff --git a/app/gui2/src/composables/selection.ts b/app/gui2/src/composables/selection.ts
index 0667d46e55..4dd8271eea 100644
--- a/app/gui2/src/composables/selection.ts
+++ b/app/gui2/src/composables/selection.ts
@@ -9,6 +9,7 @@ import { intersectionSize } from '@/util/data/set'
import type { Vec2 } from '@/util/data/vec2'
import { dataAttribute, elementHierarchy } from '@/util/dom'
import * as set from 'lib0/set'
+import { Err, Ok, type Result } from 'shared/util/data/result'
import { computed, ref, shallowReactive, shallowRef } from 'vue'
interface BaseSelectionOptions {
@@ -119,6 +120,17 @@ function useSelectionImpl(
setSelection(newSelection)
}
+ /** Returns the single selected component, or an error. */
+ function tryGetSoleSelection(): Result {
+ if (selected.value.size === 0) {
+ return Err('No component selected')
+ } else if (selected.value.size > 1) {
+ return Err('Multiple components selected')
+ } else {
+ return Ok(set.first(selected.value)!)
+ }
+ }
+
const selectionEventHandler = selectionMouseBindings.handler({
replace() {
setSelection(elementsToSelect.value)
@@ -215,6 +227,7 @@ function useSelectionImpl(
},
committedSelection,
setSelection,
+ tryGetSoleSelection,
// === Selection changes ===
anchor,
isChanging,
diff --git a/app/gui2/src/stores/graph/graphDatabase.ts b/app/gui2/src/stores/graph/graphDatabase.ts
index 3bd7429d02..b82b21deca 100644
--- a/app/gui2/src/stores/graph/graphDatabase.ts
+++ b/app/gui2/src/stores/graph/graphDatabase.ts
@@ -207,20 +207,24 @@ export class GraphDb {
return Array.from(ports, (port) => [id, port])
})
- nodeMainSuggestion = new ReactiveMapping(this.nodeIdToNode, (_id, entry) => {
+ nodeMainSuggestionId = new ReactiveMapping(this.nodeIdToNode, (_id, entry) => {
const expressionInfo = this.getExpressionInfo(entry.innerExpr.id)
const method = expressionInfo?.methodCall?.methodPointer
if (method == null) return
- const suggestionId = this.suggestionDb.findByMethodPointer(method)
+ return this.suggestionDb.findByMethodPointer(method)
+ })
+
+ getNodeMainSuggestion(id: NodeId) {
+ const suggestionId = this.nodeMainSuggestionId.lookup(id)
if (suggestionId == null) return
return this.suggestionDb.get(suggestionId)
- })
+ }
nodeColor = new ReactiveMapping(this.nodeIdToNode, (id, entry) => {
if (entry.colorOverride != null) return entry.colorOverride
return computeNodeColor(
() => entry.type,
- () => tryGetIndex(this.groups.value, this.nodeMainSuggestion.lookup(id)?.groupIndex),
+ () => tryGetIndex(this.groups.value, this.getNodeMainSuggestion(id)?.groupIndex),
() => this.getExpressionInfo(id)?.typename,
)
})
@@ -291,11 +295,7 @@ export class GraphDb {
}
getMethodCallInfo(id: AstId): MethodCallInfo | undefined {
- const info = this.getExpressionInfo(id)
- if (info == null) return
- const payloadFuncSchema =
- info.payload.type === 'Value' ? info.payload.functionSchema : undefined
- const methodCall = info.methodCall ?? payloadFuncSchema
+ const methodCall = this.getMethodCall(id)
if (methodCall == null) return
const suggestionId = this.suggestionDb.findByMethodPointer(methodCall.methodPointer)
if (suggestionId == null) return
diff --git a/app/ide-desktop/common/package.json b/app/ide-desktop/common/package.json
index 6e6374376e..08965d8c1b 100644
--- a/app/ide-desktop/common/package.json
+++ b/app/ide-desktop/common/package.json
@@ -17,12 +17,16 @@
"./src/utilities/data/dateTime": "./src/utilities/data/dateTime.ts",
"./src/utilities/data/newtype": "./src/utilities/data/newtype.ts",
"./src/utilities/data/object": "./src/utilities/data/object.ts",
+ "./src/utilities/style/tabBar": "./src/utilities/style/tabBar.ts",
"./src/utilities/uniqueString": "./src/utilities/uniqueString.ts",
"./src/text": "./src/text/index.ts",
"./src/utilities/permissions": "./src/utilities/permissions.ts",
"./src/services/Backend": "./src/services/Backend.ts",
"./src/types": "./src/types.d.ts"
},
+ "scripts": {
+ "test": "vitest run"
+ },
"peerDependencies": {
"@tanstack/query-core": "5.45.0",
"@tanstack/vue-query": ">= 5.45.0 < 5.46.0"
@@ -32,6 +36,7 @@
"react": "^18.3.1",
"@tanstack/query-persist-client-core": "^5.45.0",
"@tanstack/vue-query": ">= 5.45.0 < 5.46.0",
- "vue": "^3.4.19"
+ "vue": "^3.4.19",
+ "vitest": "^1.3.1"
}
}
diff --git a/app/ide-desktop/common/src/utilities/style/__tests__/tabBar.test.ts b/app/ide-desktop/common/src/utilities/style/__tests__/tabBar.test.ts
new file mode 100644
index 0000000000..75101edc76
--- /dev/null
+++ b/app/ide-desktop/common/src/utilities/style/__tests__/tabBar.test.ts
@@ -0,0 +1,50 @@
+import * as v from 'vitest'
+
+import * as tabBar from '../tabBar'
+
+interface TabClipPathInput {
+ bounds: { width: number; height: number }
+ radius: number
+ side?: 'top' | 'right'
+}
+
+const dashboardTabCases = [
+ {
+ input: {
+ bounds: { width: 164.2109375, height: 48 },
+ radius: 24,
+ },
+ expected:
+ 'path("M 0 48 A 24 24 0 0 0 24 24 L 24 24 A 24 24 0 0 1 48 0 L 116.2109375 0 A 24 24 0 0 1 140.2109375 24 L 140.2109375 24 A 24 24 0 0 0 164.2109375 48 M 0 0")',
+ },
+ {
+ input: {
+ bounds: { width: 209.6171875, height: 48 },
+ radius: 24,
+ },
+ expected:
+ 'path("M 0 48 A 24 24 0 0 0 24 24 L 24 24 A 24 24 0 0 1 48 0 L 161.6171875 0 A 24 24 0 0 1 185.6171875 24 L 185.6171875 24 A 24 24 0 0 0 209.6171875 48 M 0 0")',
+ },
+]
+
+const guiTabCases = [
+ {
+ input: {
+ bounds: { width: 44, height: 48 },
+ radius: 8,
+ side: 'right',
+ },
+ expected:
+ 'path("M 0 0 A 8 8 0 0 0 8 8 L 36 8 A 8 8 0 0 1 44 16 L 44 32 A 8 8 0 0 1 36 40 L 8 40 A 8 8 0 0 0 0 48 M 44 0")',
+ },
+]
+
+v.test.each([
+ { group: 'Dashboard', cases: dashboardTabCases },
+ { group: 'GUI', cases: guiTabCases },
+])('Tab clip path: $group', ({ group, cases }) => {
+ cases.forEach(({ input, expected }) => {
+ const result = tabBar.tabClipPath(input.bounds, input.radius, (input as TabClipPathInput)?.side)
+ v.expect(result).toBe(expected)
+ })
+})
diff --git a/app/ide-desktop/common/src/utilities/style/tabBar.ts b/app/ide-desktop/common/src/utilities/style/tabBar.ts
new file mode 100644
index 0000000000..3888a8ff89
--- /dev/null
+++ b/app/ide-desktop/common/src/utilities/style/tabBar.ts
@@ -0,0 +1,88 @@
+/** Functions that generate CSS to render the shapes in a tab bar. */
+
+// ===========
+// === Tab ===
+// ===========
+
+/** Returns a CSS clip-path for a tab of the specified dimensions. */
+export function tabClipPath(
+ bounds: Readonly<{ width: number; height: number }>,
+ radiusPx: number,
+ side: 'top' | 'right' = 'top',
+) {
+ console.log('bounds', JSON.stringify(bounds))
+ console.log('radiusPx', JSON.stringify(radiusPx))
+ console.log('side', JSON.stringify(side))
+ const sweep0 = 0
+ const sweep1 = 1
+ const xIndex = side === 'top' ? 0 : 1
+ const x1 = [bounds.width, bounds.height][xIndex]!
+ const y1 = [bounds.width, bounds.height][1 - xIndex]!
+ const orient =
+ side === 'top' ? (x: number, y: number) => [x, y] : (x: number, y: number) => [y1 - y, x]
+ const pt = (x: number, y: number) => orient(x, y).join(' ')
+ const result = path([
+ `M ${pt(0, y1)}`,
+ `A ${radiusPx} ${radiusPx} 0 0 ${sweep0} ${pt(radiusPx, y1 - radiusPx)}`,
+ `L ${pt(radiusPx, radiusPx)}`,
+ `A ${radiusPx} ${radiusPx} 0 0 ${sweep1} ${pt(radiusPx * 2, 0)}`,
+ `L ${pt(x1 - radiusPx * 2, 0)}`,
+ `A ${radiusPx} ${radiusPx} 0 0 ${sweep1} ${pt(x1 - radiusPx, radiusPx)}`,
+ `L ${pt(x1 - radiusPx, y1 - radiusPx)}`,
+ `A ${radiusPx} ${radiusPx} 0 0 ${sweep0} ${pt(x1, y1)}`,
+ `M ${pt(0, 0)}`,
+ ])
+ console.log('result', JSON.stringify(result))
+ return result
+}
+
+// ===============
+// === Tab Bar ===
+// ===============
+
+/** Returns a CSS clip-path for a tab bar of the specified dimensions and parent dimensions. */
+export function barClipPath(
+ bounds: DOMRectReadOnly,
+ rootBounds: DOMRectReadOnly,
+ radiusPx: number,
+) {
+ const tabLeft = bounds.left - rootBounds.left + radiusPx
+ const tabRight = bounds.right - rootBounds.left - radiusPx
+ const rightSegments = [
+ 'M 0 0',
+ `L ${rootBounds.width + window.outerWidth} 0`,
+ `L ${rootBounds.width + window.outerWidth} ${rootBounds.height}`,
+ `L ${tabRight + radiusPx} ${rootBounds.height}`,
+ `A ${radiusPx} ${radiusPx} 0 0 1 ${tabRight} ${rootBounds.height - radiusPx}`,
+ ]
+ const leftSegments = [
+ `A ${radiusPx} ${radiusPx} 0 0 1 ${tabLeft - radiusPx} ${rootBounds.height}`,
+ `L 0 ${rootBounds.height}`,
+ 'Z',
+ ]
+ const clipPath = path([
+ ...rightSegments,
+ `L ${tabRight} ${radiusPx}`,
+ `A ${radiusPx} ${radiusPx} 0 0 0 ${tabRight - radiusPx} 0`,
+ `L ${tabLeft + radiusPx} 0`,
+ `A ${radiusPx} ${radiusPx} 0 0 0 ${tabLeft} ${radiusPx}`,
+ `L ${tabLeft} ${rootBounds.height - radiusPx}`,
+ ...leftSegments,
+ ])
+ const rootClipPath = path([
+ ...rightSegments,
+ `A ${radiusPx} ${radiusPx} 0 0 1 ${tabRight - radiusPx} ${rootBounds.height}`,
+ `L ${tabLeft + radiusPx} ${rootBounds.height}`,
+ `A ${radiusPx} ${radiusPx} 0 0 1 ${tabLeft} ${rootBounds.height - radiusPx}`,
+ ...leftSegments,
+ ])
+ return { clipPath, rootClipPath }
+}
+
+// ===============
+// === Helpers ===
+// ===============
+
+function path(segments: string[]) {
+ return `path("${segments.join(' ')}")`
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index c1dc1bd5ae..cea26abd2a 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -700,6 +700,9 @@ importers:
react:
specifier: ^18.3.1
version: 18.3.1
+ vitest:
+ specifier: ^1.3.1
+ version: 1.6.0(@types/node@20.11.21)(jsdom@24.1.0)(lightningcss@1.25.1)
vue:
specifier: ^3.4.19
version: 3.4.31(typescript@5.5.3)