Skip to content
This repository was archived by the owner on Feb 1, 2026. It is now read-only.

Commit 307e1d2

Browse files
committed
feat(inspector): enhance copy functionality in Props and Tree components
1 parent 6a3284e commit 307e1d2

2 files changed

Lines changed: 191 additions & 85 deletions

File tree

client/components/inspector/Props.vue

Lines changed: 88 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ function getValueClass(value: unknown): string {
167167
<template>
168168
<div class="text-sm text-gray-500 pt-2 pb-8">
169169
<!-- Object Type Header - compact -->
170-
<div class="flex items-center gap-1 py-0.5 mb-1 hover:bg-gray-50 group">
170+
<div class="flex items-center gap-1 py-0.5 mb-4 hover:bg-gray-50 group">
171171
<UIcon
172172
:name="titleIcon"
173173
class="w-4 h-4 text-gray-600 flex-shrink-0"
@@ -176,7 +176,6 @@ function getValueClass(value: unknown): string {
176176
<UBadge
177177
v-if="object.name"
178178
variant="soft"
179-
color="gray"
180179
size="xs"
181180
class="ml-2"
182181
>
@@ -185,72 +184,99 @@ function getValueClass(value: unknown): string {
185184
</div>
186185

187186
<!-- Key Properties - following inspector tree pattern -->
188-
<div class="flex flex-col gap-1">
187+
<div class="pl-5 flex flex-col gap-1">
189188
<div
190189
v-for="prop in keyProperties"
191190
:key="prop.key"
192-
class="flex items-center py-0.5 hover:bg-gray-50 group"
191+
class="flex items-center justify-between py-0.5 hover:bg-gray-50 group"
193192
>
194-
<span>{{ prop.key }} :</span>
195-
196-
<!-- Vector3 values with individual badges -->
197-
<template v-if="isVector3(prop.value)">
198-
<div class="flex items-center gap-1 ml-1">
199-
<EditableNumber
200-
v-model="prop.value.x"
201-
@update:model-value="(val) => emit('update-value', prop.key + '.x', val)"
202-
/>
203-
<EditableNumber
204-
v-model="prop.value.y"
205-
@update:model-value="(val) => emit('update-value', prop.key + '.y', val)"
206-
/>
207-
<EditableNumber
208-
v-model="prop.value.z"
209-
@update:model-value="(val) => emit('update-value', prop.key + '.z', val)"
193+
<div class="flex items-center gap-1 min-w-0">
194+
<span>{{ prop.key }} :</span>
195+
196+
<!-- Vector3 values with individual badges -->
197+
<template v-if="isVector3(prop.value)">
198+
<div class="flex items-center gap-1 ml-1">
199+
<EditableNumber
200+
v-model="prop.value.x"
201+
@update:model-value="(val) => emit('update-value', prop.key + '.x', val)"
202+
/>
203+
<EditableNumber
204+
v-model="prop.value.y"
205+
@update:model-value="(val) => emit('update-value', prop.key + '.y', val)"
206+
/>
207+
<EditableNumber
208+
v-model="prop.value.z"
209+
@update:model-value="(val) => emit('update-value', prop.key + '.z', val)"
210+
/>
211+
</div>
212+
</template>
213+
214+
<!-- Material values with color preview -->
215+
<MaterialBadge
216+
v-else-if="isMaterial(prop.value)"
217+
:material="prop.value"
218+
:display-value="prop.displayValue"
219+
/>
220+
221+
<!-- Geometry values with icon -->
222+
<GeometryBadge
223+
v-else-if="isGeometry(prop.value)"
224+
:geometry="prop.value"
225+
:display-value="prop.displayValue"
226+
/>
227+
228+
<UButton
229+
v-else-if="prop.key === 'visible'"
230+
size="xs"
231+
class="ml-1 mr-1"
232+
variant="soft"
233+
:icon="prop.value ? 'i-lucide-eye' : 'i-lucide-eye-closed'"
234+
@click="() => emit('update-value', prop.key, !prop.value)"
235+
/>
236+
237+
<!-- Color value with preview dot -->
238+
<template v-else-if="prop.key === 'color' && prop.value && typeof prop.value === 'object' && 'getHexString' in prop.value">
239+
<div
240+
class="w-3 h-3 rounded-full border border-gray-300 dark:border-gray-600 ml-1 mr-1"
241+
:style="{ backgroundColor: '#' + (prop.value as { getHexString: () => string }).getHexString() }"
210242
/>
211-
</div>
212-
</template>
213-
214-
<!-- Material values with color preview -->
215-
<MaterialBadge
216-
v-else-if="isMaterial(prop.value)"
217-
:material="prop.value"
218-
:display-value="prop.displayValue"
219-
/>
220-
221-
<!-- Geometry values with icon -->
222-
<GeometryBadge
223-
v-else-if="isGeometry(prop.value)"
224-
:geometry="prop.value"
225-
:display-value="prop.displayValue"
226-
/>
227-
228-
<UButton
229-
v-if="typeof prop.value === 'boolean'"
230-
size="xs"
231-
class="ml-1 mr-1"
232-
variant="soft"
233-
:icon="prop.value ? 'i-lucide-eye' : 'i-lucide-eye-off'"
234-
@click="() => emit('update-value', prop.key, !prop.value)"
235-
/>
236-
237-
<!-- Color value with preview dot -->
238-
<template v-else-if="prop.key === 'color' && prop.value && typeof prop.value === 'object' && 'getHexString' in prop.value">
239-
<div
240-
class="w-3 h-3 rounded-full border border-gray-300 dark:border-gray-600 ml-1 mr-1"
241-
:style="{ backgroundColor: '#' + (prop.value as { getHexString: () => string }).getHexString() }"
243+
<span :class="getValueClass(prop.value)">{{ prop.displayValue }}</span>
244+
</template>
245+
246+
<!-- Regular values -->
247+
<template v-else>
248+
<span
249+
:class="[getValueClass(prop.value), 'ml-1']"
250+
>
251+
{{ prop.displayValue }}
252+
</span>
253+
</template>
254+
</div>
255+
256+
<div
257+
class="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity ml-auto"
258+
@click.stop
259+
>
260+
<!-- Copy value button -->
261+
<UButton
262+
size="xs"
263+
variant="ghost"
264+
color="gray"
265+
icon="i-tabler:copy"
266+
title="Copy value"
267+
@click.stop="copyValue(node.value)"
268+
/>
269+
270+
<!-- Copy path button -->
271+
<UButton
272+
size="xs"
273+
variant="ghost"
274+
color="gray"
275+
icon="i-tabler:link"
276+
title="Copy path"
277+
@click.stop="copyPath(node.path)"
242278
/>
243-
<span :class="getValueClass(prop.value)">{{ prop.displayValue }}</span>
244-
</template>
245-
246-
<!-- Regular values -->
247-
<template v-else>
248-
<span
249-
:class="[getValueClass(prop.value), 'ml-1']"
250-
>
251-
{{ prop.displayValue }}
252-
</span>
253-
</template>
279+
</div>
254280
</div>
255281
</div>
256282
</div>

client/components/inspector/Tree.vue

Lines changed: 103 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
<script setup lang="ts">
2+
import { Node } from 'three/webgpu'
23
import { computed, ref } from 'vue'
34
import type { InspectorNode } from '~/client/types'
45
@@ -46,6 +47,37 @@ const getLabel = (value: unknown): string => {
4647
}
4748
4849
// Copy functionality
50+
51+
const copyPath = async (path: string): Promise<void> => {
52+
try {
53+
await navigator.clipboard.writeText(path)
54+
}
55+
catch (error) {
56+
console.error('Failed to copy path:', error)
57+
}
58+
}
59+
60+
const copyProp = async (node: InspectorNode): Promise<void> => {
61+
try {
62+
const propString = `:${node.path.replace(/\./g, '-')}="${typeof node.value === 'string' ? node.value : JSON.stringify(node.value)}"`
63+
await navigator.clipboard.writeText(propString)
64+
}
65+
catch (error) {
66+
console.error('Failed to copy prop:', error)
67+
}
68+
}
69+
70+
const copyPropAsArray = async (node: InspectorNode): Promise<void> => {
71+
try {
72+
const arrayValue = node.children?.map(child => child.value) || []
73+
const propString = `:${node.path.replace(/\./g, '-')}='[${arrayValue.map(v => (typeof v === 'string' ? `"${v}"` : v)).join(', ')}]'`
74+
await navigator.clipboard.writeText(propString)
75+
}
76+
catch (error) {
77+
console.error('Failed to copy prop as array:', error)
78+
}
79+
}
80+
4981
const copyValue = async (value: unknown): Promise<void> => {
5082
try {
5183
const stringValue = typeof value === 'string' ? value : JSON.stringify(value)
@@ -56,15 +88,41 @@ const copyValue = async (value: unknown): Promise<void> => {
5688
}
5789
}
5890
59-
const copyPath = async (path: string): Promise<void> => {
91+
const copyValueAsVector3 = async (node: InspectorNode): Promise<void> => {
6092
try {
61-
await navigator.clipboard.writeText(path)
93+
await navigator.clipboard.writeText(`new Vector3(${node.children[0].value}, ${node.children[1].value}, ${node.children[2].value})`)
6294
}
6395
catch (error) {
64-
console.error('Failed to copy path:', error)
96+
console.error('Failed to copy prop as Vector3:', error)
97+
}
98+
}
99+
100+
const copyValueAsArray = async (node: InspectorNode): Promise<void> => {
101+
try {
102+
const arrayValue = node.children?.map(child => child.value) || []
103+
const propString = `[${arrayValue.map(v => (typeof v === 'string' ? `"${v}"` : v)).join(', ')}]`
104+
await navigator.clipboard.writeText(propString)
105+
}
106+
catch (error) {
107+
console.error('Failed to copy prop as array:', error)
65108
}
66109
}
67110
111+
const copyValueAsJSON = async (node: InspectorNode): Promise<void> => {
112+
try {
113+
let object = {}
114+
if (node.children && node.children.length > 0) {
115+
object = node.children.reduce((acc, child) => {
116+
acc[child.label] = child.value
117+
return acc
118+
}, {} as Record<string, unknown>)
119+
}
120+
await navigator.clipboard.writeText(JSON.stringify(object, null, 2))
121+
}
122+
catch (error) {
123+
console.error('Failed to copy value as JSON:', error)
124+
}
125+
}
68126
// Value modification
69127
const incrementValue = (): void => {
70128
if (typeof props.node.value === 'number') {
@@ -171,6 +229,29 @@ const indentStyle = computed(() => ({ paddingLeft: `${props.level * 16}px` }))
171229
>
172230
{{ node.label }} : <span class="text-gray-600 font-semibold">{{ node.value }}</span>
173231
</span>
232+
233+
<UDropdownMenu
234+
v-if="node.type !== 'array'"
235+
size="xs"
236+
:items="[
237+
{ label: 'Copy Path', icon: 'i-lucide:link', onSelect: () => copyPath(node.path) },
238+
{ label: 'Copy value as Array', icon: 'i-material-symbols:data-array', onSelect: () => copyValueAsArray(node) },
239+
node.value === '_Vector3' ? { label: 'Copy value as Vector3', icon: 'i-lucide:pen-line', onSelect: () => copyValueAsVector3(node) } : null,
240+
{ label: 'Copy value as JSON', icon: 'i-material-symbols:data-object', onSelect: () => copyValueAsJSON(node) },
241+
node.value === '_Vector3' || node.value === '_Euler' ? { label: 'Copy as Prop', icon: 'i-lucide:code', onSelect: () => copyPropAsArray(node) } : null,
242+
].filter(Boolean)"
243+
:ui="{
244+
content: 'w-48',
245+
}"
246+
>
247+
<UButton
248+
size="xs"
249+
variant="ghost"
250+
color="gray"
251+
icon="i-lucide-ellipsis-vertical"
252+
title="Copy value"
253+
/>
254+
</UDropdownMenu>
174255
</template>
175256

176257
<!-- Primitive value display -->
@@ -233,26 +314,6 @@ const indentStyle = computed(() => ({ paddingLeft: `${props.level * 16}px` }))
233314
class="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity ml-auto"
234315
@click.stop
235316
>
236-
<!-- Copy value button -->
237-
<UButton
238-
size="xs"
239-
variant="ghost"
240-
color="gray"
241-
icon="i-tabler:copy"
242-
title="Copy value"
243-
@click.stop="copyValue(node.value)"
244-
/>
245-
246-
<!-- Copy path button -->
247-
<UButton
248-
size="xs"
249-
variant="ghost"
250-
color="gray"
251-
icon="i-tabler:link"
252-
title="Copy path"
253-
@click.stop="copyPath(node.path)"
254-
/>
255-
256317
<!-- Number controls -->
257318
<template v-if="typeof node.value === 'number'">
258319
<UButton
@@ -272,6 +333,25 @@ const indentStyle = computed(() => ({ paddingLeft: `${props.level * 16}px` }))
272333
@click.stop="incrementValue"
273334
/>
274335
</template>
336+
<UDropdownMenu
337+
size="xs"
338+
:items="[
339+
{ label: 'Copy Value', icon: 'i-lucide:copy', onSelect: () => copyValue(node.value) },
340+
{ label: 'Copy Path', icon: 'i-lucide:link', onSelect: () => copyPath(node.path) },
341+
{ label: 'Copy as prop', icon: 'i-lucide:pen-line', onSelect: () => copyProp(node) },
342+
]"
343+
:ui="{
344+
content: 'w-48',
345+
}"
346+
>
347+
<UButton
348+
size="xs"
349+
variant="ghost"
350+
color="gray"
351+
icon="i-lucide-ellipsis-vertical"
352+
title="Copy value"
353+
/>
354+
</UDropdownMenu>
275355
</div>
276356
</div>
277357
</div>

0 commit comments

Comments
 (0)