@selvajs/ui 6.6.0-beta.6 → 6.6.0-beta.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/ErrorScreen.svelte +1 -1
- package/dist/components/compute/AppLayout.svelte +63 -26
- package/dist/components/layout/AppShell.svelte +10 -6
- package/dist/components/layout/PageHeader.svelte +11 -7
- package/dist/components/layout/SectionHeader.svelte +4 -2
- package/dist/components/layout/SideNav.svelte +19 -11
- package/dist/components/preview/ChartOutput.svelte +2 -2
- package/dist/components/preview/ImageOutput.svelte +1 -1
- package/dist/components/preview/OutputDisplay.svelte +1 -1
- package/dist/components/primitives/DataTable.svelte +38 -4
- package/dist/components/primitives/DataTable.svelte.d.ts +2 -0
- package/dist/components/primitives/alert-dialog/alert-dialog-content.svelte +1 -1
- package/dist/components/primitives/dialog/dialog-content.svelte +1 -1
- package/dist/components/primitives/drawer/Drawer.svelte +1 -1
- package/dist/components/viewer/MeshMetadataPanel.svelte +119 -0
- package/dist/components/viewer/MeshMetadataPanel.svelte.d.ts +9 -0
- package/dist/components/viewer/Viewer.svelte +59 -24
- package/dist/styles/base.css +21 -1
- package/package.json +5 -5
- package/src/lib/components/ErrorScreen.svelte +1 -1
- package/src/lib/components/compute/AppLayout.svelte +63 -26
- package/src/lib/components/layout/AppShell.svelte +10 -6
- package/src/lib/components/layout/PageHeader.svelte +11 -7
- package/src/lib/components/layout/SectionHeader.svelte +4 -2
- package/src/lib/components/layout/SideNav.svelte +19 -11
- package/src/lib/components/preview/ChartOutput.svelte +2 -2
- package/src/lib/components/preview/ImageOutput.svelte +1 -1
- package/src/lib/components/preview/OutputDisplay.svelte +1 -1
- package/src/lib/components/primitives/DataTable.svelte +38 -4
- package/src/lib/components/primitives/alert-dialog/alert-dialog-content.svelte +1 -1
- package/src/lib/components/primitives/dialog/dialog-content.svelte +1 -1
- package/src/lib/components/primitives/drawer/Drawer.svelte +1 -1
- package/src/lib/components/viewer/MeshMetadataPanel.svelte +119 -0
- package/src/lib/components/viewer/Viewer.svelte +59 -24
- package/src/lib/styles/base.css +21 -1
- package/dist/components/viewer/MeshMetadataDialog.svelte +0 -126
- package/dist/components/viewer/MeshMetadataDialog.svelte.d.ts +0 -10
- package/src/lib/components/viewer/MeshMetadataDialog.svelte +0 -126
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
bind:ref
|
|
20
20
|
data-slot="alert-dialog-content"
|
|
21
21
|
class={cn(
|
|
22
|
-
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 gap-4 p-6 shadow-lg sm:max-w-lg fixed start-1/2 top-1/2 z-50 grid
|
|
22
|
+
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 gap-4 p-6 shadow-lg sm:max-w-lg fixed start-1/2 top-1/2 z-50 grid max-h-dialog w-screen-inset -translate-x-1/2 -translate-y-1/2 overflow-y-auto rounded-lg border bg-background duration-200',
|
|
23
23
|
className
|
|
24
24
|
)}
|
|
25
25
|
{...restProps}
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
data-slot="dialog-content"
|
|
33
33
|
class={cn(
|
|
34
34
|
variant === 'flush' ? 'gap-0' : 'gap-4 p-6',
|
|
35
|
-
'shadow-lg data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 sm:max-w-lg fixed top-1/2 left-1/2 z-50 grid max-h-dialog w-
|
|
35
|
+
'shadow-lg data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 sm:max-w-lg fixed top-1/2 left-1/2 z-50 grid max-h-dialog w-screen-inset -translate-x-1/2 -translate-y-1/2 overflow-y-auto rounded-lg border bg-background duration-200',
|
|
36
36
|
className
|
|
37
37
|
)}
|
|
38
38
|
{...restProps}
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
onkeydown={handleKey}
|
|
27
27
|
></div>
|
|
28
28
|
<div
|
|
29
|
-
class="animate-in slide-in-from-right-4 top-0 right-0 fixed z-50 flex h-full border-border bg-background {width} flex-col overflow-y-auto border-l duration-150"
|
|
29
|
+
class="animate-in slide-in-from-right-4 top-0 right-0 fixed z-50 flex h-full max-w-full border-border bg-background {width} flex-col overflow-y-auto border-l duration-150"
|
|
30
30
|
>
|
|
31
31
|
{@render children()}
|
|
32
32
|
</div>
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { X } from '@lucide/svelte';
|
|
3
|
+
import { Button } from '../primitives/button/index.js';
|
|
4
|
+
import { getLocaleContext } from '$lib/i18n/localeContext.svelte';
|
|
5
|
+
|
|
6
|
+
const locale = getLocaleContext();
|
|
7
|
+
const t = $derived(locale.messages);
|
|
8
|
+
|
|
9
|
+
interface Props {
|
|
10
|
+
metadata: Record<string, any>;
|
|
11
|
+
meshName: string | null;
|
|
12
|
+
onClose: () => void;
|
|
13
|
+
class?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const EXCLUDED_KEYS = new Set(['name', 'layer', 'id']);
|
|
17
|
+
|
|
18
|
+
let { metadata, meshName, onClose, class: className = '' }: Props = $props();
|
|
19
|
+
|
|
20
|
+
const filteredMetadata = $derived.by(() => {
|
|
21
|
+
const result: Record<string, unknown> = {};
|
|
22
|
+
for (const [key, value] of Object.entries(metadata)) {
|
|
23
|
+
if (EXCLUDED_KEYS.has(key)) continue;
|
|
24
|
+
if (
|
|
25
|
+
key === 'metadata' &&
|
|
26
|
+
typeof value === 'object' &&
|
|
27
|
+
value !== null &&
|
|
28
|
+
!Array.isArray(value)
|
|
29
|
+
) {
|
|
30
|
+
Object.assign(result, value);
|
|
31
|
+
} else {
|
|
32
|
+
result[key] = value;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return result;
|
|
36
|
+
});
|
|
37
|
+
</script>
|
|
38
|
+
|
|
39
|
+
<!-- Not a modal: a modal locks the page, and on touch the tap that should dismiss it can race the
|
|
40
|
+
dialog's own dismiss handler, leaving the viewer frozen. A card lets the canvas keep orbiting. -->
|
|
41
|
+
<section
|
|
42
|
+
aria-label={meshName ?? t.objectFallbackName}
|
|
43
|
+
class="min-h-0 shadow-lg backdrop-blur-sm flex flex-col overflow-hidden rounded-lg border bg-background/95 {className}"
|
|
44
|
+
>
|
|
45
|
+
<header class="gap-2 py-1 pr-1 pl-4 flex items-center border-b">
|
|
46
|
+
<h2 class="text-sm font-semibold min-w-0 flex-1 truncate">
|
|
47
|
+
{meshName ?? t.objectFallbackName}
|
|
48
|
+
</h2>
|
|
49
|
+
<Button
|
|
50
|
+
variant="ghost"
|
|
51
|
+
size="icon-sm"
|
|
52
|
+
class="pointer-coarse:size-10 shrink-0"
|
|
53
|
+
aria-label={t.close}
|
|
54
|
+
title={t.close}
|
|
55
|
+
onclick={onClose}
|
|
56
|
+
>
|
|
57
|
+
<X />
|
|
58
|
+
</Button>
|
|
59
|
+
</header>
|
|
60
|
+
|
|
61
|
+
<div class="min-h-0 overflow-y-auto overscroll-contain">
|
|
62
|
+
{#if Object.keys(filteredMetadata).length === 0}
|
|
63
|
+
<p class="px-4 py-3 text-xs text-muted-foreground">{t.noMetadata}</p>
|
|
64
|
+
{:else}
|
|
65
|
+
<table class="text-xs w-full border-collapse">
|
|
66
|
+
<tbody>
|
|
67
|
+
{#each Object.entries(filteredMetadata) as [key, value] (key)}
|
|
68
|
+
<tr class="border-b border-border/60 last:border-b-0">
|
|
69
|
+
<th
|
|
70
|
+
scope="row"
|
|
71
|
+
class="px-4 py-2 font-normal text-left align-top whitespace-nowrap text-muted-foreground"
|
|
72
|
+
>{key}</th
|
|
73
|
+
>
|
|
74
|
+
<td class="px-4 py-2 text-right align-top text-foreground">
|
|
75
|
+
{#if typeof value === 'object' && value !== null}
|
|
76
|
+
{#if Array.isArray(value)}
|
|
77
|
+
<div class="space-y-1 text-left">
|
|
78
|
+
{#each value as item, index (index)}
|
|
79
|
+
{#if typeof item === 'object'}
|
|
80
|
+
<div class="space-y-0.5">
|
|
81
|
+
{#each Object.entries(item) as [k, v] (k)}
|
|
82
|
+
<div class="gap-2 flex">
|
|
83
|
+
<span class="shrink-0 text-muted-foreground">{k}:</span>
|
|
84
|
+
<span>{v}</span>
|
|
85
|
+
</div>
|
|
86
|
+
{/each}
|
|
87
|
+
</div>
|
|
88
|
+
{:else}
|
|
89
|
+
<div>{item}</div>
|
|
90
|
+
{/if}
|
|
91
|
+
{/each}
|
|
92
|
+
</div>
|
|
93
|
+
{:else}
|
|
94
|
+
<div class="space-y-0.5 text-left">
|
|
95
|
+
{#each Object.entries(value) as [k, v] (k)}
|
|
96
|
+
<div class="gap-2 flex">
|
|
97
|
+
<span class="shrink-0 text-muted-foreground">{k}:</span>
|
|
98
|
+
<span>{v}</span>
|
|
99
|
+
</div>
|
|
100
|
+
{/each}
|
|
101
|
+
</div>
|
|
102
|
+
{/if}
|
|
103
|
+
{:else if typeof value === 'number'}
|
|
104
|
+
<span class="font-mono">{value}</span>
|
|
105
|
+
{:else if typeof value === 'boolean'}
|
|
106
|
+
<span class={value ? 'text-success' : 'text-muted-foreground'}
|
|
107
|
+
>{value ? 'true' : 'false'}</span
|
|
108
|
+
>
|
|
109
|
+
{:else}
|
|
110
|
+
<span class="break-all">{value}</span>
|
|
111
|
+
{/if}
|
|
112
|
+
</td>
|
|
113
|
+
</tr>
|
|
114
|
+
{/each}
|
|
115
|
+
</tbody>
|
|
116
|
+
</table>
|
|
117
|
+
{/if}
|
|
118
|
+
</div>
|
|
119
|
+
</section>
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
import type { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
|
|
35
35
|
import { createSceneOutliner, type SceneOutliner } from '@selvajs/visualization/scene';
|
|
36
36
|
import SceneManager from './SceneManager.svelte';
|
|
37
|
-
import
|
|
37
|
+
import MeshMetadataPanel from './MeshMetadataPanel.svelte';
|
|
38
38
|
import * as Resizable from '$lib/components/primitives/resizable/index.js';
|
|
39
39
|
import * as DropdownMenu from '$lib/components/primitives/dropdown-menu/index.js';
|
|
40
40
|
import { Button } from '$lib/components/primitives/button/index.js';
|
|
@@ -122,6 +122,7 @@
|
|
|
122
122
|
let updateGridScale: (() => void) | null = null;
|
|
123
123
|
let updateShadowBounds: (() => void) | null = null;
|
|
124
124
|
let fitToView: (() => void) | null = null;
|
|
125
|
+
let clearSelection: (() => void) | null = null;
|
|
125
126
|
let viewerInitialized = false;
|
|
126
127
|
let sceneVersion = $state(0);
|
|
127
128
|
let hideButton = $state(false);
|
|
@@ -194,6 +195,10 @@
|
|
|
194
195
|
grid: { enabled: config.showToolsMenu && config.showGridToggle },
|
|
195
196
|
measure: { enabled: config.showToolsMenu },
|
|
196
197
|
events: {
|
|
198
|
+
// Fires before onMeshMetadataClicked, so picking an object without metadata closes the
|
|
199
|
+
// previous object's panel instead of leaving it describing the wrong thing.
|
|
200
|
+
onObjectSelected: closeMetadata,
|
|
201
|
+
onBackgroundClicked: closeMetadata,
|
|
197
202
|
onMeshMetadataClicked: config.enableMeshClick
|
|
198
203
|
? (metadata: Record<string, unknown>) => {
|
|
199
204
|
if (hasUsefulMetadata(metadata)) {
|
|
@@ -224,6 +229,7 @@
|
|
|
224
229
|
updateGridScale = init.updateGridScale;
|
|
225
230
|
updateShadowBounds = init.updateShadowBounds;
|
|
226
231
|
fitToView = init.fitToView;
|
|
232
|
+
clearSelection = init.clearSelection;
|
|
227
233
|
projection = init.cameraController.getProjection();
|
|
228
234
|
|
|
229
235
|
const renderer = init.renderer;
|
|
@@ -375,6 +381,29 @@
|
|
|
375
381
|
}
|
|
376
382
|
});
|
|
377
383
|
|
|
384
|
+
function closeMetadata() {
|
|
385
|
+
selectedMeshMetadata = null;
|
|
386
|
+
selectedMeshName = null;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function dismissMetadata() {
|
|
390
|
+
closeMetadata();
|
|
391
|
+
clearSelection?.();
|
|
392
|
+
invalidate?.();
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// The panel describes an object from the previous solve once new meshes arrive.
|
|
396
|
+
$effect(() => {
|
|
397
|
+
void meshes;
|
|
398
|
+
untrack(closeMetadata);
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
// Dialogs portal to <body>, so one opened over the fullscreen viewer needs lifting above it.
|
|
402
|
+
$effect(() => {
|
|
403
|
+
document.body.toggleAttribute('data-viewer-fullscreen', isFullscreen);
|
|
404
|
+
return () => document.body.removeAttribute('data-viewer-fullscreen');
|
|
405
|
+
});
|
|
406
|
+
|
|
378
407
|
function toggleFullscreen() {
|
|
379
408
|
isFullscreen = !isFullscreen;
|
|
380
409
|
}
|
|
@@ -450,12 +479,9 @@
|
|
|
450
479
|
{/if}
|
|
451
480
|
|
|
452
481
|
{#if logoUrl}
|
|
453
|
-
<!--
|
|
454
|
-
Always pointer-events:none so it never intercepts canvas drags. -->
|
|
482
|
+
<!-- pointer-events:none so it never intercepts canvas drags. -->
|
|
455
483
|
<div
|
|
456
|
-
class="right-4 {
|
|
457
|
-
? 'bottom-4'
|
|
458
|
-
: 'bottom-16 sm:bottom-4'} pointer-events-none absolute z-20 {hideButton
|
|
484
|
+
class="right-4 bottom-4 pointer-events-none absolute z-20 {hideButton
|
|
459
485
|
? 'opacity-0'
|
|
460
486
|
: ''}"
|
|
461
487
|
>
|
|
@@ -467,11 +493,18 @@
|
|
|
467
493
|
</div>
|
|
468
494
|
{/if}
|
|
469
495
|
|
|
496
|
+
{#if selectedMeshMetadata}
|
|
497
|
+
<MeshMetadataPanel
|
|
498
|
+
metadata={selectedMeshMetadata}
|
|
499
|
+
meshName={selectedMeshName}
|
|
500
|
+
onClose={dismissMetadata}
|
|
501
|
+
class="top-3 right-3 left-3 sm:right-auto sm:max-h-2/3 sm:w-72 absolute z-30 max-h-1/2"
|
|
502
|
+
/>
|
|
503
|
+
{/if}
|
|
504
|
+
|
|
470
505
|
{#if config.showToolsMenu}
|
|
471
506
|
<div
|
|
472
|
-
class="left-4 {
|
|
473
|
-
? 'bottom-4'
|
|
474
|
-
: 'bottom-16 sm:bottom-4'} shadow-lg absolute z-50 flex items-center rounded-lg {hideButton
|
|
507
|
+
class="left-4 bottom-4 gap-2 *:shadow-lg absolute z-50 flex items-center *:rounded-md {hideButton
|
|
475
508
|
? 'pointer-events-none opacity-0'
|
|
476
509
|
: ''}"
|
|
477
510
|
>
|
|
@@ -594,6 +627,18 @@
|
|
|
594
627
|
{/if}
|
|
595
628
|
</DropdownMenu.Content>
|
|
596
629
|
</DropdownMenu.Root>
|
|
630
|
+
<!-- Touch has no F key, and fit-to-view is the way back from a framed part. -->
|
|
631
|
+
<div class="hidden pointer-coarse:block">
|
|
632
|
+
<Button
|
|
633
|
+
variant="outline"
|
|
634
|
+
size="icon-lg"
|
|
635
|
+
title={t.fitToView}
|
|
636
|
+
aria-label={t.fitToView}
|
|
637
|
+
onclick={() => fitToView?.()}
|
|
638
|
+
>
|
|
639
|
+
<Frame />
|
|
640
|
+
</Button>
|
|
641
|
+
</div>
|
|
597
642
|
</div>
|
|
598
643
|
{/if}
|
|
599
644
|
</div>
|
|
@@ -608,22 +653,12 @@
|
|
|
608
653
|
</Resizable.PaneGroup>
|
|
609
654
|
</div>
|
|
610
655
|
|
|
611
|
-
<MeshMetadataDialog
|
|
612
|
-
open={selectedMeshMetadata !== null}
|
|
613
|
-
metadata={selectedMeshMetadata}
|
|
614
|
-
meshName={selectedMeshName}
|
|
615
|
-
{isFullscreen}
|
|
616
|
-
onOpenChange={(open: boolean) => {
|
|
617
|
-
if (!open) {
|
|
618
|
-
setTimeout(() => {
|
|
619
|
-
selectedMeshMetadata = null;
|
|
620
|
-
selectedMeshName = null;
|
|
621
|
-
}, 200);
|
|
622
|
-
}
|
|
623
|
-
}}
|
|
624
|
-
/>
|
|
625
|
-
|
|
626
656
|
<style>
|
|
657
|
+
:global([data-viewer-fullscreen] [data-slot='dialog-content']),
|
|
658
|
+
:global([data-viewer-fullscreen] [data-slot='dialog-overlay']) {
|
|
659
|
+
z-index: 10001;
|
|
660
|
+
}
|
|
661
|
+
|
|
627
662
|
.fullscreen-viewer {
|
|
628
663
|
position: fixed;
|
|
629
664
|
inset: 0;
|
package/src/lib/styles/base.css
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
:root {
|
|
4
4
|
--radius: 0.625rem;
|
|
5
5
|
--header-h: 3.5rem;
|
|
6
|
-
--page-px:
|
|
6
|
+
--page-px: 1rem;
|
|
7
7
|
--page-py: 1.75rem;
|
|
8
8
|
--background: oklch(0.99 0 0);
|
|
9
9
|
--foreground: oklch(0.22 0 0);
|
|
@@ -180,6 +180,26 @@
|
|
|
180
180
|
border-radius: inherit;
|
|
181
181
|
}
|
|
182
182
|
|
|
183
|
+
/* Phones keep a narrower gutter: at 390px every 8px of side padding is 2% of the width. */
|
|
184
|
+
@media (min-width: 40rem) {
|
|
185
|
+
:root {
|
|
186
|
+
--page-px: 1.5rem;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/* A native select draws its arrow inside the right padding, so with the `px-2`/`px-3` every
|
|
191
|
+
caller uses it sits on the border. A drawn chevron with its own gutter fixes them all. Unlayered
|
|
192
|
+
on purpose: the gutter has to beat the callers' `px-*` utilities. Mid-grey reads on both themes,
|
|
193
|
+
since a data URI can't take `currentColor`. */
|
|
194
|
+
select:not([multiple]):not([size]) {
|
|
195
|
+
appearance: none;
|
|
196
|
+
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23888' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
|
|
197
|
+
background-repeat: no-repeat;
|
|
198
|
+
background-position: right 0.625rem center;
|
|
199
|
+
background-size: 1.1em;
|
|
200
|
+
padding-inline-end: calc(1.1em + 1rem);
|
|
201
|
+
}
|
|
202
|
+
|
|
183
203
|
@layer base {
|
|
184
204
|
* {
|
|
185
205
|
@apply border-border outline-ring;
|
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import * as Dialog from '../primitives/dialog/index.js';
|
|
3
|
-
import { getLocaleContext } from '../../i18n/localeContext.svelte';
|
|
4
|
-
|
|
5
|
-
const locale = getLocaleContext();
|
|
6
|
-
const t = $derived(locale.messages);
|
|
7
|
-
|
|
8
|
-
interface Props {
|
|
9
|
-
open: boolean;
|
|
10
|
-
metadata: Record<string, any> | null;
|
|
11
|
-
meshName: string | null;
|
|
12
|
-
onOpenChange: (open: boolean) => void;
|
|
13
|
-
isFullscreen?: boolean;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
const EXCLUDED_KEYS = new Set(['name', 'layer', 'id']);
|
|
17
|
-
|
|
18
|
-
let {
|
|
19
|
-
open = $bindable(),
|
|
20
|
-
metadata,
|
|
21
|
-
meshName,
|
|
22
|
-
onOpenChange,
|
|
23
|
-
isFullscreen = false
|
|
24
|
-
}: Props = $props();
|
|
25
|
-
|
|
26
|
-
$effect(() => {
|
|
27
|
-
document.body.toggleAttribute('data-viewer-fullscreen', isFullscreen);
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
const getFilteredMetadata = () => {
|
|
31
|
-
if (!metadata) return {};
|
|
32
|
-
const entries = Object.entries(metadata).filter(([key]) => !EXCLUDED_KEYS.has(key));
|
|
33
|
-
const result: Record<string, unknown> = {};
|
|
34
|
-
for (const [key, value] of entries) {
|
|
35
|
-
if (
|
|
36
|
-
key === 'metadata' &&
|
|
37
|
-
typeof value === 'object' &&
|
|
38
|
-
value !== null &&
|
|
39
|
-
!Array.isArray(value)
|
|
40
|
-
) {
|
|
41
|
-
Object.assign(result, value);
|
|
42
|
-
} else {
|
|
43
|
-
result[key] = value;
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
return result;
|
|
47
|
-
};
|
|
48
|
-
</script>
|
|
49
|
-
|
|
50
|
-
<Dialog.Root bind:open {onOpenChange}>
|
|
51
|
-
<Dialog.Content variant="flush" class="min-w-60 max-w-xs w-auto">
|
|
52
|
-
{#if meshName}
|
|
53
|
-
<div class="px-4 pt-3 pb-2 border-b">
|
|
54
|
-
<Dialog.Title size="sm">{meshName}</Dialog.Title>
|
|
55
|
-
</div>
|
|
56
|
-
{:else}
|
|
57
|
-
<Dialog.Title class="sr-only">{t.objectFallbackName}</Dialog.Title>
|
|
58
|
-
{/if}
|
|
59
|
-
|
|
60
|
-
<div class="max-h-80 overflow-y-auto">
|
|
61
|
-
{#if !metadata || Object.keys(getFilteredMetadata()).length === 0}
|
|
62
|
-
<p class="px-4 py-3 text-xs text-muted-foreground">{t.noMetadata}</p>
|
|
63
|
-
{:else}
|
|
64
|
-
<table class="text-xs w-full border-collapse">
|
|
65
|
-
<tbody>
|
|
66
|
-
{#each Object.entries(getFilteredMetadata()) as [key, value] (key)}
|
|
67
|
-
<tr class="border-b border-border/60 transition-colors hover:bg-muted/40">
|
|
68
|
-
<th
|
|
69
|
-
scope="row"
|
|
70
|
-
class="px-4 py-2 font-normal text-left align-top whitespace-nowrap text-muted-foreground"
|
|
71
|
-
>{key}</th
|
|
72
|
-
>
|
|
73
|
-
<td class="px-4 py-2 text-right align-top text-foreground">
|
|
74
|
-
{#if typeof value === 'object' && value !== null}
|
|
75
|
-
{#if Array.isArray(value)}
|
|
76
|
-
<div class="space-y-1 text-left">
|
|
77
|
-
{#each value as item, index (index)}
|
|
78
|
-
{#if typeof item === 'object'}
|
|
79
|
-
<div class="space-y-0.5">
|
|
80
|
-
{#each Object.entries(item) as [k, v] (k)}
|
|
81
|
-
<div class="gap-2 flex">
|
|
82
|
-
<span class="shrink-0 text-muted-foreground">{k}:</span>
|
|
83
|
-
<span>{v}</span>
|
|
84
|
-
</div>
|
|
85
|
-
{/each}
|
|
86
|
-
</div>
|
|
87
|
-
{:else}
|
|
88
|
-
<div>{item}</div>
|
|
89
|
-
{/if}
|
|
90
|
-
{/each}
|
|
91
|
-
</div>
|
|
92
|
-
{:else}
|
|
93
|
-
<div class="space-y-0.5 text-left">
|
|
94
|
-
{#each Object.entries(value) as [k, v] (k)}
|
|
95
|
-
<div class="gap-2 flex">
|
|
96
|
-
<span class="shrink-0 text-muted-foreground">{k}:</span>
|
|
97
|
-
<span>{v}</span>
|
|
98
|
-
</div>
|
|
99
|
-
{/each}
|
|
100
|
-
</div>
|
|
101
|
-
{/if}
|
|
102
|
-
{:else if typeof value === 'number'}
|
|
103
|
-
<span class="font-mono">{value}</span>
|
|
104
|
-
{:else if typeof value === 'boolean'}
|
|
105
|
-
<span class={value ? 'text-success' : 'text-muted-foreground'}
|
|
106
|
-
>{value ? 'true' : 'false'}</span
|
|
107
|
-
>
|
|
108
|
-
{:else}
|
|
109
|
-
<span class="break-all">{value}</span>
|
|
110
|
-
{/if}
|
|
111
|
-
</td>
|
|
112
|
-
</tr>
|
|
113
|
-
{/each}
|
|
114
|
-
</tbody>
|
|
115
|
-
</table>
|
|
116
|
-
{/if}
|
|
117
|
-
</div>
|
|
118
|
-
</Dialog.Content>
|
|
119
|
-
</Dialog.Root>
|
|
120
|
-
|
|
121
|
-
<style>
|
|
122
|
-
:global([data-viewer-fullscreen] [data-slot='dialog-content']),
|
|
123
|
-
:global([data-viewer-fullscreen] [data-slot='dialog-overlay']) {
|
|
124
|
-
z-index: 10001;
|
|
125
|
-
}
|
|
126
|
-
</style>
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
interface Props {
|
|
2
|
-
open: boolean;
|
|
3
|
-
metadata: Record<string, any> | null;
|
|
4
|
-
meshName: string | null;
|
|
5
|
-
onOpenChange: (open: boolean) => void;
|
|
6
|
-
isFullscreen?: boolean;
|
|
7
|
-
}
|
|
8
|
-
declare const MeshMetadataDialog: import("svelte").Component<Props, {}, "open">;
|
|
9
|
-
type MeshMetadataDialog = ReturnType<typeof MeshMetadataDialog>;
|
|
10
|
-
export default MeshMetadataDialog;
|
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import * as Dialog from '../primitives/dialog/index.js';
|
|
3
|
-
import { getLocaleContext } from '$lib/i18n/localeContext.svelte';
|
|
4
|
-
|
|
5
|
-
const locale = getLocaleContext();
|
|
6
|
-
const t = $derived(locale.messages);
|
|
7
|
-
|
|
8
|
-
interface Props {
|
|
9
|
-
open: boolean;
|
|
10
|
-
metadata: Record<string, any> | null;
|
|
11
|
-
meshName: string | null;
|
|
12
|
-
onOpenChange: (open: boolean) => void;
|
|
13
|
-
isFullscreen?: boolean;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
const EXCLUDED_KEYS = new Set(['name', 'layer', 'id']);
|
|
17
|
-
|
|
18
|
-
let {
|
|
19
|
-
open = $bindable(),
|
|
20
|
-
metadata,
|
|
21
|
-
meshName,
|
|
22
|
-
onOpenChange,
|
|
23
|
-
isFullscreen = false
|
|
24
|
-
}: Props = $props();
|
|
25
|
-
|
|
26
|
-
$effect(() => {
|
|
27
|
-
document.body.toggleAttribute('data-viewer-fullscreen', isFullscreen);
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
const getFilteredMetadata = () => {
|
|
31
|
-
if (!metadata) return {};
|
|
32
|
-
const entries = Object.entries(metadata).filter(([key]) => !EXCLUDED_KEYS.has(key));
|
|
33
|
-
const result: Record<string, unknown> = {};
|
|
34
|
-
for (const [key, value] of entries) {
|
|
35
|
-
if (
|
|
36
|
-
key === 'metadata' &&
|
|
37
|
-
typeof value === 'object' &&
|
|
38
|
-
value !== null &&
|
|
39
|
-
!Array.isArray(value)
|
|
40
|
-
) {
|
|
41
|
-
Object.assign(result, value);
|
|
42
|
-
} else {
|
|
43
|
-
result[key] = value;
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
return result;
|
|
47
|
-
};
|
|
48
|
-
</script>
|
|
49
|
-
|
|
50
|
-
<Dialog.Root bind:open {onOpenChange}>
|
|
51
|
-
<Dialog.Content variant="flush" class="min-w-60 max-w-xs w-auto">
|
|
52
|
-
{#if meshName}
|
|
53
|
-
<div class="px-4 pt-3 pb-2 border-b">
|
|
54
|
-
<Dialog.Title size="sm">{meshName}</Dialog.Title>
|
|
55
|
-
</div>
|
|
56
|
-
{:else}
|
|
57
|
-
<Dialog.Title class="sr-only">{t.objectFallbackName}</Dialog.Title>
|
|
58
|
-
{/if}
|
|
59
|
-
|
|
60
|
-
<div class="max-h-80 overflow-y-auto">
|
|
61
|
-
{#if !metadata || Object.keys(getFilteredMetadata()).length === 0}
|
|
62
|
-
<p class="px-4 py-3 text-xs text-muted-foreground">{t.noMetadata}</p>
|
|
63
|
-
{:else}
|
|
64
|
-
<table class="text-xs w-full border-collapse">
|
|
65
|
-
<tbody>
|
|
66
|
-
{#each Object.entries(getFilteredMetadata()) as [key, value] (key)}
|
|
67
|
-
<tr class="border-b border-border/60 transition-colors hover:bg-muted/40">
|
|
68
|
-
<th
|
|
69
|
-
scope="row"
|
|
70
|
-
class="px-4 py-2 font-normal text-left align-top whitespace-nowrap text-muted-foreground"
|
|
71
|
-
>{key}</th
|
|
72
|
-
>
|
|
73
|
-
<td class="px-4 py-2 text-right align-top text-foreground">
|
|
74
|
-
{#if typeof value === 'object' && value !== null}
|
|
75
|
-
{#if Array.isArray(value)}
|
|
76
|
-
<div class="space-y-1 text-left">
|
|
77
|
-
{#each value as item, index (index)}
|
|
78
|
-
{#if typeof item === 'object'}
|
|
79
|
-
<div class="space-y-0.5">
|
|
80
|
-
{#each Object.entries(item) as [k, v] (k)}
|
|
81
|
-
<div class="gap-2 flex">
|
|
82
|
-
<span class="shrink-0 text-muted-foreground">{k}:</span>
|
|
83
|
-
<span>{v}</span>
|
|
84
|
-
</div>
|
|
85
|
-
{/each}
|
|
86
|
-
</div>
|
|
87
|
-
{:else}
|
|
88
|
-
<div>{item}</div>
|
|
89
|
-
{/if}
|
|
90
|
-
{/each}
|
|
91
|
-
</div>
|
|
92
|
-
{:else}
|
|
93
|
-
<div class="space-y-0.5 text-left">
|
|
94
|
-
{#each Object.entries(value) as [k, v] (k)}
|
|
95
|
-
<div class="gap-2 flex">
|
|
96
|
-
<span class="shrink-0 text-muted-foreground">{k}:</span>
|
|
97
|
-
<span>{v}</span>
|
|
98
|
-
</div>
|
|
99
|
-
{/each}
|
|
100
|
-
</div>
|
|
101
|
-
{/if}
|
|
102
|
-
{:else if typeof value === 'number'}
|
|
103
|
-
<span class="font-mono">{value}</span>
|
|
104
|
-
{:else if typeof value === 'boolean'}
|
|
105
|
-
<span class={value ? 'text-success' : 'text-muted-foreground'}
|
|
106
|
-
>{value ? 'true' : 'false'}</span
|
|
107
|
-
>
|
|
108
|
-
{:else}
|
|
109
|
-
<span class="break-all">{value}</span>
|
|
110
|
-
{/if}
|
|
111
|
-
</td>
|
|
112
|
-
</tr>
|
|
113
|
-
{/each}
|
|
114
|
-
</tbody>
|
|
115
|
-
</table>
|
|
116
|
-
{/if}
|
|
117
|
-
</div>
|
|
118
|
-
</Dialog.Content>
|
|
119
|
-
</Dialog.Root>
|
|
120
|
-
|
|
121
|
-
<style>
|
|
122
|
-
:global([data-viewer-fullscreen] [data-slot='dialog-content']),
|
|
123
|
-
:global([data-viewer-fullscreen] [data-slot='dialog-overlay']) {
|
|
124
|
-
z-index: 10001;
|
|
125
|
-
}
|
|
126
|
-
</style>
|