@selvajs/ui 4.7.0 → 4.8.0
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/viewer/MeshMetadataDialog.svelte +6 -2
- package/dist/components/viewer/SceneManager.svelte +15 -9
- package/dist/components/viewer/Viewer.svelte +44 -23
- package/dist/components/viewer/Viewer.svelte.d.ts +8 -0
- package/dist/i18n/localeContext.svelte.d.ts +18 -0
- package/dist/i18n/localeContext.svelte.js +50 -0
- package/dist/i18n/messages.d.ts +38 -0
- package/dist/i18n/messages.js +82 -0
- package/dist/public.d.ts +4 -0
- package/dist/public.js +7 -0
- package/package.json +3 -3
- package/src/lib/components/viewer/MeshMetadataDialog.svelte +6 -2
- package/src/lib/components/viewer/SceneManager.svelte +15 -9
- package/src/lib/components/viewer/Viewer.svelte +44 -23
- package/src/lib/i18n/localeContext.svelte.ts +63 -0
- package/src/lib/i18n/messages.ts +136 -0
- package/src/lib/public.ts +18 -0
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
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);
|
|
3
7
|
|
|
4
8
|
interface Props {
|
|
5
9
|
open: boolean;
|
|
@@ -50,12 +54,12 @@
|
|
|
50
54
|
<Dialog.Title class="text-sm font-semibold">{meshName}</Dialog.Title>
|
|
51
55
|
</div>
|
|
52
56
|
{:else}
|
|
53
|
-
<Dialog.Title class="sr-only">
|
|
57
|
+
<Dialog.Title class="sr-only">{t.objectFallbackName}</Dialog.Title>
|
|
54
58
|
{/if}
|
|
55
59
|
|
|
56
60
|
<div class="max-h-80 overflow-y-auto">
|
|
57
61
|
{#if !metadata || Object.keys(getFilteredMetadata()).length === 0}
|
|
58
|
-
<p class="px-4 py-3 text-xs text-muted-foreground">
|
|
62
|
+
<p class="px-4 py-3 text-xs text-muted-foreground">{t.noMetadata}</p>
|
|
59
63
|
{:else}
|
|
60
64
|
<table class="text-xs w-full border-collapse">
|
|
61
65
|
<tbody>
|
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
import * as THREE from 'three';
|
|
3
3
|
import { Eye, EyeOff, ChevronRight, Search, X } from '@lucide/svelte';
|
|
4
4
|
import { SvelteSet, SvelteMap } from 'svelte/reactivity';
|
|
5
|
+
import { getLocaleContext } from '../../i18n/localeContext.svelte';
|
|
6
|
+
|
|
7
|
+
const locale = getLocaleContext();
|
|
8
|
+
const t = $derived(locale.messages);
|
|
5
9
|
|
|
6
10
|
interface Props {
|
|
7
11
|
scene: THREE.Scene;
|
|
@@ -165,11 +169,11 @@
|
|
|
165
169
|
<Search class="h-3.5 w-3.5 shrink-0 text-muted-foreground/50" />
|
|
166
170
|
<input
|
|
167
171
|
class="min-w-0 text-xs flex-1 bg-transparent text-foreground outline-none placeholder:text-muted-foreground/50"
|
|
168
|
-
placeholder=
|
|
172
|
+
placeholder={t.searchObjects}
|
|
169
173
|
bind:value={searchQuery}
|
|
170
174
|
/>
|
|
171
175
|
{#if searchQuery}
|
|
172
|
-
<button onclick={() => (searchQuery = '')} aria-label=
|
|
176
|
+
<button onclick={() => (searchQuery = '')} aria-label={t.clearSearch}>
|
|
173
177
|
<X
|
|
174
178
|
class="h-3.5 w-3.5 text-muted-foreground/50 transition-colors hover:text-muted-foreground"
|
|
175
179
|
/>
|
|
@@ -187,7 +191,7 @@
|
|
|
187
191
|
<button
|
|
188
192
|
class="rounded p-0.5 shrink-0 text-muted-foreground transition-colors hover:text-muted-foreground"
|
|
189
193
|
onclick={() => toggleLayerCollapsed(layerName)}
|
|
190
|
-
aria-label={collapsed ?
|
|
194
|
+
aria-label={collapsed ? t.expandLayer : t.collapseLayer}
|
|
191
195
|
>
|
|
192
196
|
<ChevronRight
|
|
193
197
|
class="h-3.5 w-3.5 transition-transform duration-150 {collapsed ? '' : 'rotate-90'}"
|
|
@@ -197,8 +201,8 @@
|
|
|
197
201
|
<button
|
|
198
202
|
class="rounded p-1 shrink-0 transition-colors hover:bg-muted"
|
|
199
203
|
onclick={() => toggleLayer(layerName, objects)}
|
|
200
|
-
title={layerHidden ?
|
|
201
|
-
aria-label={layerHidden ?
|
|
204
|
+
title={layerHidden ? t.showLayer : t.hideLayer}
|
|
205
|
+
aria-label={layerHidden ? t.showLayer : t.hideLayer}
|
|
202
206
|
>
|
|
203
207
|
{#if layerHidden}
|
|
204
208
|
<EyeOff class="h-3.5 w-3.5 text-muted-foreground/40" />
|
|
@@ -249,8 +253,8 @@
|
|
|
249
253
|
e.stopPropagation();
|
|
250
254
|
toggleObject(object);
|
|
251
255
|
}}
|
|
252
|
-
title={hidden ?
|
|
253
|
-
aria-label={hidden ?
|
|
256
|
+
title={hidden ? t.showObject : t.hideObject}
|
|
257
|
+
aria-label={hidden ? t.showObject : t.hideObject}
|
|
254
258
|
>
|
|
255
259
|
{#if hidden}
|
|
256
260
|
<EyeOff class="h-3 w-3 text-muted-foreground/60" />
|
|
@@ -284,12 +288,14 @@
|
|
|
284
288
|
{#if getSceneObjects().length === 0}
|
|
285
289
|
<div class="py-12 flex flex-col items-center justify-center text-center">
|
|
286
290
|
<EyeOff class="mb-2 h-5 w-5 text-muted-foreground/30" />
|
|
287
|
-
<p class="text-xs text-muted-foreground">
|
|
291
|
+
<p class="text-xs text-muted-foreground">{t.noObjects}</p>
|
|
288
292
|
</div>
|
|
289
293
|
{:else if getFilteredLayerGroups().size === 0}
|
|
290
294
|
<div class="py-12 flex flex-col items-center justify-center text-center">
|
|
291
295
|
<Search class="mb-2 h-5 w-5 text-muted-foreground/30" />
|
|
292
|
-
<p class="text-xs text-muted-foreground">
|
|
296
|
+
<p class="text-xs text-muted-foreground">
|
|
297
|
+
{t.noResultsFor.replace('{query}', searchQuery)}
|
|
298
|
+
</p>
|
|
293
299
|
</div>
|
|
294
300
|
{/if}
|
|
295
301
|
</div>
|
|
@@ -31,6 +31,8 @@
|
|
|
31
31
|
import SceneManager from './SceneManager.svelte';
|
|
32
32
|
import MeshMetadataDialog from './MeshMetadataDialog.svelte';
|
|
33
33
|
import * as Resizable from '../primitives/resizable/index.js';
|
|
34
|
+
import type { Locale } from '../../i18n/messages';
|
|
35
|
+
import { setLocaleContext, getLocaleContext } from '../../i18n/localeContext.svelte';
|
|
34
36
|
|
|
35
37
|
export interface ViewerConfig {
|
|
36
38
|
showScreenshotButton?: boolean;
|
|
@@ -50,6 +52,13 @@
|
|
|
50
52
|
isBlurred?: boolean;
|
|
51
53
|
drawerOpen?: boolean;
|
|
52
54
|
viewerConfig?: ViewerConfig;
|
|
55
|
+
/**
|
|
56
|
+
* UI language for the viewer's own chrome (tools menu, panels, dialogs).
|
|
57
|
+
* When set, the viewer provides it to its subtree. When omitted, the viewer
|
|
58
|
+
* reads the nearest locale context (set by the host app), defaulting to
|
|
59
|
+
* English. Does not translate Grasshopper-sourced names/metadata.
|
|
60
|
+
*/
|
|
61
|
+
lang?: Locale;
|
|
53
62
|
}
|
|
54
63
|
|
|
55
64
|
const defaultViewerConfig: Required<ViewerConfig> = {
|
|
@@ -68,11 +77,23 @@
|
|
|
68
77
|
isSolving = false,
|
|
69
78
|
isBlurred = false,
|
|
70
79
|
drawerOpen = false,
|
|
71
|
-
viewerConfig = {}
|
|
80
|
+
viewerConfig = {},
|
|
81
|
+
lang
|
|
72
82
|
}: Props = $props();
|
|
73
83
|
|
|
74
84
|
const config = $derived({ ...defaultViewerConfig, ...viewerConfig });
|
|
75
85
|
|
|
86
|
+
// Read any host-provided locale before we (maybe) override it for our subtree.
|
|
87
|
+
const hostLocale = getLocaleContext();
|
|
88
|
+
|
|
89
|
+
// Resolution order: explicit `lang` prop → host locale context → default.
|
|
90
|
+
// Provide the resolved value to our subtree so the scene manager and metadata
|
|
91
|
+
// dialog read the same locale. The getter is re-read reactively, so switching
|
|
92
|
+
// the `lang` prop (or the host's locale) updates the chrome live.
|
|
93
|
+
setLocaleContext(() => lang ?? hostLocale.locale);
|
|
94
|
+
const locale = getLocaleContext();
|
|
95
|
+
const t = $derived(locale.messages);
|
|
96
|
+
|
|
76
97
|
let canvas: HTMLCanvasElement;
|
|
77
98
|
let scene: THREE.Scene | null = $state(null);
|
|
78
99
|
let camera: THREE.PerspectiveCamera | null = null;
|
|
@@ -91,14 +112,14 @@
|
|
|
91
112
|
let selectedMeshMetadata: Record<string, any> | null = $state(null);
|
|
92
113
|
let selectedMeshName: string | null = $state(null);
|
|
93
114
|
|
|
94
|
-
const VIEW_PRESETS: { preset: ViewPreset; label: string }[] = [
|
|
95
|
-
{ preset: 'top', label:
|
|
96
|
-
{ preset: 'front', label:
|
|
97
|
-
{ preset: 'right', label:
|
|
98
|
-
{ preset: 'back', label:
|
|
99
|
-
{ preset: 'left', label:
|
|
100
|
-
{ preset: 'bottom', label:
|
|
101
|
-
{ preset: 'iso', label:
|
|
115
|
+
const VIEW_PRESETS: { preset: ViewPreset; label: () => string }[] = [
|
|
116
|
+
{ preset: 'top', label: () => t.viewTop },
|
|
117
|
+
{ preset: 'front', label: () => t.viewFront },
|
|
118
|
+
{ preset: 'right', label: () => t.viewRight },
|
|
119
|
+
{ preset: 'back', label: () => t.viewBack },
|
|
120
|
+
{ preset: 'left', label: () => t.viewLeft },
|
|
121
|
+
{ preset: 'bottom', label: () => t.viewBottom },
|
|
122
|
+
{ preset: 'iso', label: () => t.viewIso }
|
|
102
123
|
];
|
|
103
124
|
|
|
104
125
|
// Hide button instantly when expanding, show after animation when collapsing
|
|
@@ -128,7 +149,7 @@
|
|
|
128
149
|
? (metadata: Record<string, string>) => {
|
|
129
150
|
if (hasUsefulMetadata(metadata)) {
|
|
130
151
|
selectedMeshMetadata = metadata;
|
|
131
|
-
selectedMeshName = metadata?.name ||
|
|
152
|
+
selectedMeshName = metadata?.name || t.objectFallbackName;
|
|
132
153
|
}
|
|
133
154
|
}
|
|
134
155
|
: undefined
|
|
@@ -265,8 +286,8 @@
|
|
|
265
286
|
<DropdownMenu.Root>
|
|
266
287
|
<DropdownMenu.Trigger
|
|
267
288
|
class="h-10 w-10 shadow-lg hover:shadow-xl flex items-center justify-center rounded-lg border border-border bg-card/90 transition-all hover:bg-card active:scale-95 data-[state=open]:bg-secondary/60"
|
|
268
|
-
title=
|
|
269
|
-
aria-label=
|
|
289
|
+
title={t.toolsMenu}
|
|
290
|
+
aria-label={t.toolsMenu}
|
|
270
291
|
>
|
|
271
292
|
<Settings2 class="h-5 w-5 text-card-foreground" />
|
|
272
293
|
</DropdownMenu.Trigger>
|
|
@@ -280,22 +301,22 @@
|
|
|
280
301
|
<DropdownMenu.Item class={itemClass} onSelect={toggleProjection}>
|
|
281
302
|
{#if projection === 'perspective'}
|
|
282
303
|
<Square class="h-4 w-4" />
|
|
283
|
-
|
|
304
|
+
{t.switchTo2D}
|
|
284
305
|
{:else}
|
|
285
306
|
<Box class="h-4 w-4" />
|
|
286
|
-
|
|
307
|
+
{t.switchTo3D}
|
|
287
308
|
{/if}
|
|
288
309
|
</DropdownMenu.Item>
|
|
289
310
|
|
|
290
311
|
<DropdownMenu.Item class={itemClass} onSelect={() => fitToView?.()}>
|
|
291
312
|
<Frame class="h-4 w-4" />
|
|
292
|
-
|
|
313
|
+
{t.fitToView}
|
|
293
314
|
</DropdownMenu.Item>
|
|
294
315
|
|
|
295
316
|
<DropdownMenu.Sub>
|
|
296
317
|
<DropdownMenu.SubTrigger class="{itemClass} data-[state=open]:bg-muted">
|
|
297
318
|
<Box class="h-4 w-4" />
|
|
298
|
-
<span class="flex-1">
|
|
319
|
+
<span class="flex-1">{t.views}</span>
|
|
299
320
|
<ChevronRight class="h-4 w-4 text-muted-foreground" />
|
|
300
321
|
</DropdownMenu.SubTrigger>
|
|
301
322
|
<DropdownMenu.SubContent
|
|
@@ -307,7 +328,7 @@
|
|
|
307
328
|
class="px-2 py-1.5 text-sm flex cursor-pointer items-center rounded-sm transition-colors outline-none select-none hover:bg-muted focus:bg-muted"
|
|
308
329
|
onSelect={() => setView(preset)}
|
|
309
330
|
>
|
|
310
|
-
{label}
|
|
331
|
+
{label()}
|
|
311
332
|
</DropdownMenu.Item>
|
|
312
333
|
{/each}
|
|
313
334
|
</DropdownMenu.SubContent>
|
|
@@ -319,7 +340,7 @@
|
|
|
319
340
|
onSelect={toggleMeasure}
|
|
320
341
|
>
|
|
321
342
|
<Ruler class="h-4 w-4" />
|
|
322
|
-
<span class="flex-1">
|
|
343
|
+
<span class="flex-1">{t.measure}</span>
|
|
323
344
|
{#if measureActive}
|
|
324
345
|
<Check class="h-4 w-4" />
|
|
325
346
|
{/if}
|
|
@@ -332,7 +353,7 @@
|
|
|
332
353
|
onSelect={toggleGrid}
|
|
333
354
|
>
|
|
334
355
|
<Grid3x3 class="h-4 w-4" />
|
|
335
|
-
<span class="flex-1">
|
|
356
|
+
<span class="flex-1">{t.grid}</span>
|
|
336
357
|
{#if gridVisible}
|
|
337
358
|
<Check class="h-4 w-4" />
|
|
338
359
|
{/if}
|
|
@@ -351,7 +372,7 @@
|
|
|
351
372
|
onSelect={() => (sceneManagerOpen = !sceneManagerOpen)}
|
|
352
373
|
>
|
|
353
374
|
<Layers class="h-4 w-4" />
|
|
354
|
-
<span class="flex-1">
|
|
375
|
+
<span class="flex-1">{t.sceneManager}</span>
|
|
355
376
|
{#if sceneManagerOpen}
|
|
356
377
|
<Check class="h-4 w-4" />
|
|
357
378
|
{/if}
|
|
@@ -361,7 +382,7 @@
|
|
|
361
382
|
{#if config.showScreenshotButton}
|
|
362
383
|
<DropdownMenu.Item class={itemClass} onSelect={downloadScreenshot}>
|
|
363
384
|
<Camera class="h-4 w-4" />
|
|
364
|
-
|
|
385
|
+
{t.screenshot}
|
|
365
386
|
</DropdownMenu.Item>
|
|
366
387
|
{/if}
|
|
367
388
|
|
|
@@ -369,10 +390,10 @@
|
|
|
369
390
|
<DropdownMenu.Item class={itemClass} onSelect={toggleFullscreen}>
|
|
370
391
|
{#if isFullscreen}
|
|
371
392
|
<Minimize class="h-4 w-4" />
|
|
372
|
-
|
|
393
|
+
{t.exitFullscreen}
|
|
373
394
|
{:else}
|
|
374
395
|
<Maximize class="h-4 w-4" />
|
|
375
|
-
|
|
396
|
+
{t.fullscreen}
|
|
376
397
|
{/if}
|
|
377
398
|
</DropdownMenu.Item>
|
|
378
399
|
{/if}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Locale } from '../../i18n/messages';
|
|
1
2
|
export interface ViewerConfig {
|
|
2
3
|
showScreenshotButton?: boolean;
|
|
3
4
|
showFullscreenButton?: boolean;
|
|
@@ -15,6 +16,13 @@ interface Props {
|
|
|
15
16
|
isBlurred?: boolean;
|
|
16
17
|
drawerOpen?: boolean;
|
|
17
18
|
viewerConfig?: ViewerConfig;
|
|
19
|
+
/**
|
|
20
|
+
* UI language for the viewer's own chrome (tools menu, panels, dialogs).
|
|
21
|
+
* When set, the viewer provides it to its subtree. When omitted, the viewer
|
|
22
|
+
* reads the nearest locale context (set by the host app), defaulting to
|
|
23
|
+
* English. Does not translate Grasshopper-sourced names/metadata.
|
|
24
|
+
*/
|
|
25
|
+
lang?: Locale;
|
|
18
26
|
}
|
|
19
27
|
declare const Viewer: import("svelte").Component<Props, {}, "isFullscreen">;
|
|
20
28
|
type Viewer = ReturnType<typeof Viewer>;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type Locale, type ViewerMessages } from './messages';
|
|
2
|
+
export interface LocaleContext {
|
|
3
|
+
/** Current locale. Called reactively — return reactive state to enable live switching. */
|
|
4
|
+
readonly locale: Locale;
|
|
5
|
+
/** Resolved message catalog for the current locale. */
|
|
6
|
+
readonly messages: ViewerMessages;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Provide the locale to descendants. Pass a getter so a reactive source (a
|
|
10
|
+
* `$state`, a store, the app's Paraglide locale) keeps consumers in sync.
|
|
11
|
+
*/
|
|
12
|
+
export declare function setLocaleContext(getLocale: () => Locale | undefined): void;
|
|
13
|
+
/**
|
|
14
|
+
* Read the locale context. Falls back to an English-only context when no
|
|
15
|
+
* provider exists (e.g. a primitive used in isolation), so consumers never
|
|
16
|
+
* need a null check.
|
|
17
|
+
*/
|
|
18
|
+
export declare function getLocaleContext(): LocaleContext;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { getContext, setContext } from 'svelte';
|
|
2
|
+
import { messagesFor, DEFAULT_LOCALE } from './messages';
|
|
3
|
+
// ============================================================================
|
|
4
|
+
// Viewer locale context
|
|
5
|
+
// ============================================================================
|
|
6
|
+
//
|
|
7
|
+
// Carries the current UI locale down to the viewer and its panels without
|
|
8
|
+
// threading a `lang` prop through every layer. The value is a getter so the
|
|
9
|
+
// host can back it with reactive state — flip the language and the viewer
|
|
10
|
+
// re-renders live.
|
|
11
|
+
//
|
|
12
|
+
// Resolution order for any consuming component:
|
|
13
|
+
// explicit `lang` prop → nearest locale context → English default
|
|
14
|
+
//
|
|
15
|
+
// Two ways to provide it:
|
|
16
|
+
// - Standalone: <Viewer lang="de" /> — Viewer provides the context itself.
|
|
17
|
+
// - In an app: call setLocaleContext(() => app.locale) once at the root; the
|
|
18
|
+
// viewer (and anything else) reads it. selva later wires its Paraglide
|
|
19
|
+
// locale in here.
|
|
20
|
+
const LOCALE_CONTEXT_KEY = Symbol('viewer-locale-context');
|
|
21
|
+
/**
|
|
22
|
+
* Provide the locale to descendants. Pass a getter so a reactive source (a
|
|
23
|
+
* `$state`, a store, the app's Paraglide locale) keeps consumers in sync.
|
|
24
|
+
*/
|
|
25
|
+
export function setLocaleContext(getLocale) {
|
|
26
|
+
const ctx = {
|
|
27
|
+
get locale() {
|
|
28
|
+
return getLocale() ?? DEFAULT_LOCALE;
|
|
29
|
+
},
|
|
30
|
+
get messages() {
|
|
31
|
+
return messagesFor(getLocale());
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
setContext(LOCALE_CONTEXT_KEY, ctx);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Read the locale context. Falls back to an English-only context when no
|
|
38
|
+
* provider exists (e.g. a primitive used in isolation), so consumers never
|
|
39
|
+
* need a null check.
|
|
40
|
+
*/
|
|
41
|
+
export function getLocaleContext() {
|
|
42
|
+
return (getContext(LOCALE_CONTEXT_KEY) ?? {
|
|
43
|
+
get locale() {
|
|
44
|
+
return DEFAULT_LOCALE;
|
|
45
|
+
},
|
|
46
|
+
get messages() {
|
|
47
|
+
return messagesFor(DEFAULT_LOCALE);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export type Locale = 'en' | 'de';
|
|
2
|
+
export interface ViewerMessages {
|
|
3
|
+
toolsMenu: string;
|
|
4
|
+
switchTo2D: string;
|
|
5
|
+
switchTo3D: string;
|
|
6
|
+
fitToView: string;
|
|
7
|
+
views: string;
|
|
8
|
+
measure: string;
|
|
9
|
+
grid: string;
|
|
10
|
+
sceneManager: string;
|
|
11
|
+
screenshot: string;
|
|
12
|
+
fullscreen: string;
|
|
13
|
+
exitFullscreen: string;
|
|
14
|
+
viewTop: string;
|
|
15
|
+
viewFront: string;
|
|
16
|
+
viewRight: string;
|
|
17
|
+
viewBack: string;
|
|
18
|
+
viewLeft: string;
|
|
19
|
+
viewBottom: string;
|
|
20
|
+
viewIso: string;
|
|
21
|
+
searchObjects: string;
|
|
22
|
+
clearSearch: string;
|
|
23
|
+
expandLayer: string;
|
|
24
|
+
collapseLayer: string;
|
|
25
|
+
showLayer: string;
|
|
26
|
+
hideLayer: string;
|
|
27
|
+
showObject: string;
|
|
28
|
+
hideObject: string;
|
|
29
|
+
noObjects: string;
|
|
30
|
+
/** `{query}` is replaced with the current search text. */
|
|
31
|
+
noResultsFor: string;
|
|
32
|
+
objectFallbackName: string;
|
|
33
|
+
noMetadata: string;
|
|
34
|
+
}
|
|
35
|
+
export declare const VIEWER_MESSAGES: Record<Locale, ViewerMessages>;
|
|
36
|
+
export declare const DEFAULT_LOCALE: Locale;
|
|
37
|
+
/** Resolve a catalog for a locale, falling back to English for unknown locales. */
|
|
38
|
+
export declare function messagesFor(locale: Locale | undefined): ViewerMessages;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// Viewer message catalog (library-owned strings)
|
|
3
|
+
// ============================================================================
|
|
4
|
+
//
|
|
5
|
+
// These are the strings @selvajs/ui renders itself in the 3D viewer and its
|
|
6
|
+
// panels — tool menu, view presets, scene manager, metadata dialog. They are
|
|
7
|
+
// NOT the strings that come from a Grasshopper definition (mesh/layer/metadata
|
|
8
|
+
// names): those live in the user's .gh file and can't be translated here.
|
|
9
|
+
//
|
|
10
|
+
// The library ships English + German. A host app can switch locale at runtime
|
|
11
|
+
// via the locale context (see ./localeContext.svelte.ts) — e.g. selva feeds its
|
|
12
|
+
// own Paraglide locale in. With no provider, components fall back to English.
|
|
13
|
+
const en = {
|
|
14
|
+
toolsMenu: 'Viewer tools',
|
|
15
|
+
switchTo2D: 'Switch to 2D',
|
|
16
|
+
switchTo3D: 'Switch to 3D',
|
|
17
|
+
fitToView: 'Fit to view',
|
|
18
|
+
views: 'Views',
|
|
19
|
+
measure: 'Measure',
|
|
20
|
+
grid: 'Grid',
|
|
21
|
+
sceneManager: 'Scene manager',
|
|
22
|
+
screenshot: 'Screenshot',
|
|
23
|
+
fullscreen: 'Fullscreen',
|
|
24
|
+
exitFullscreen: 'Exit fullscreen',
|
|
25
|
+
viewTop: 'Top',
|
|
26
|
+
viewFront: 'Front',
|
|
27
|
+
viewRight: 'Right',
|
|
28
|
+
viewBack: 'Back',
|
|
29
|
+
viewLeft: 'Left',
|
|
30
|
+
viewBottom: 'Bottom',
|
|
31
|
+
viewIso: 'Isometric',
|
|
32
|
+
searchObjects: 'Search objects...',
|
|
33
|
+
clearSearch: 'Clear search',
|
|
34
|
+
expandLayer: 'Expand layer',
|
|
35
|
+
collapseLayer: 'Collapse layer',
|
|
36
|
+
showLayer: 'Show layer',
|
|
37
|
+
hideLayer: 'Hide layer',
|
|
38
|
+
showObject: 'Show object',
|
|
39
|
+
hideObject: 'Hide object',
|
|
40
|
+
noObjects: 'No objects',
|
|
41
|
+
noResultsFor: 'No results for "{query}"',
|
|
42
|
+
objectFallbackName: 'Object',
|
|
43
|
+
noMetadata: 'No metadata'
|
|
44
|
+
};
|
|
45
|
+
const de = {
|
|
46
|
+
toolsMenu: 'Viewer-Werkzeuge',
|
|
47
|
+
switchTo2D: 'Zu 2D wechseln',
|
|
48
|
+
switchTo3D: 'Zu 3D wechseln',
|
|
49
|
+
fitToView: 'Ansicht anpassen',
|
|
50
|
+
views: 'Ansichten',
|
|
51
|
+
measure: 'Messen',
|
|
52
|
+
grid: 'Raster',
|
|
53
|
+
sceneManager: 'Szenen-Manager',
|
|
54
|
+
screenshot: 'Screenshot',
|
|
55
|
+
fullscreen: 'Vollbild',
|
|
56
|
+
exitFullscreen: 'Vollbild beenden',
|
|
57
|
+
viewTop: 'Oben',
|
|
58
|
+
viewFront: 'Vorne',
|
|
59
|
+
viewRight: 'Rechts',
|
|
60
|
+
viewBack: 'Hinten',
|
|
61
|
+
viewLeft: 'Links',
|
|
62
|
+
viewBottom: 'Unten',
|
|
63
|
+
viewIso: 'Isometrisch',
|
|
64
|
+
searchObjects: 'Objekte suchen...',
|
|
65
|
+
clearSearch: 'Suche löschen',
|
|
66
|
+
expandLayer: 'Ebene aufklappen',
|
|
67
|
+
collapseLayer: 'Ebene zuklappen',
|
|
68
|
+
showLayer: 'Ebene einblenden',
|
|
69
|
+
hideLayer: 'Ebene ausblenden',
|
|
70
|
+
showObject: 'Objekt einblenden',
|
|
71
|
+
hideObject: 'Objekt ausblenden',
|
|
72
|
+
noObjects: 'Keine Objekte',
|
|
73
|
+
noResultsFor: 'Keine Ergebnisse für „{query}“',
|
|
74
|
+
objectFallbackName: 'Objekt',
|
|
75
|
+
noMetadata: 'Keine Metadaten'
|
|
76
|
+
};
|
|
77
|
+
export const VIEWER_MESSAGES = { en, de };
|
|
78
|
+
export const DEFAULT_LOCALE = 'en';
|
|
79
|
+
/** Resolve a catalog for a locale, falling back to English for unknown locales. */
|
|
80
|
+
export function messagesFor(locale) {
|
|
81
|
+
return VIEWER_MESSAGES[locale ?? DEFAULT_LOCALE] ?? VIEWER_MESSAGES[DEFAULT_LOCALE];
|
|
82
|
+
}
|
package/dist/public.d.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
export { default as AppLayout } from './components/compute/AppLayout.svelte';
|
|
2
2
|
export { default as ComputeApp } from './components/compute/ComputeApp.svelte';
|
|
3
|
+
export { default as Viewer, type ViewerConfig } from './components/viewer/Viewer.svelte';
|
|
4
|
+
export type { Locale, ViewerMessages } from './i18n/messages';
|
|
5
|
+
export { VIEWER_MESSAGES, DEFAULT_LOCALE, messagesFor } from './i18n/messages';
|
|
6
|
+
export { setLocaleContext, getLocaleContext, type LocaleContext } from './i18n/localeContext.svelte';
|
|
3
7
|
export { default as ErrorScreen } from './components/ErrorScreen.svelte';
|
|
4
8
|
export { createSolveSession, createRequestResponseDriver, type SolveSession, type SolveSessionArgs, type SolveDriver, type SolveReporter } from './compute/createSolveSession.svelte';
|
|
5
9
|
export type { ClientSlotArgs, ClientSlot } from './contexts/clientSlotContext.svelte';
|
package/dist/public.js
CHANGED
|
@@ -20,6 +20,13 @@
|
|
|
20
20
|
// Compute app (schema + viewer + solve controls composed into a runnable app)
|
|
21
21
|
export { default as AppLayout } from './components/compute/AppLayout.svelte';
|
|
22
22
|
export { default as ComputeApp } from './components/compute/ComputeApp.svelte';
|
|
23
|
+
// Standalone 3D viewer. Render Grasshopper meshes on their own, outside a
|
|
24
|
+
// ComputeApp host — external apps drive it directly with a `meshes` array and
|
|
25
|
+
// an optional `viewerConfig`. Pass `lang` to localize its chrome, or provide a
|
|
26
|
+
// locale context (setLocaleContext) at the host root to drive it app-wide.
|
|
27
|
+
export { default as Viewer } from './components/viewer/Viewer.svelte';
|
|
28
|
+
export { VIEWER_MESSAGES, DEFAULT_LOCALE, messagesFor } from './i18n/messages';
|
|
29
|
+
export { setLocaleContext, getLocaleContext } from './i18n/localeContext.svelte';
|
|
23
30
|
// Full-screen states a host app renders
|
|
24
31
|
export { default as ErrorScreen } from './components/ErrorScreen.svelte';
|
|
25
32
|
// Solve Session seam (transport-agnostic value/lifecycle state machine + its
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@selvajs/ui",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.8.0",
|
|
4
4
|
"description": "Shared UI components and utilities for Selva applications",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"publishConfig": {
|
|
@@ -72,8 +72,8 @@
|
|
|
72
72
|
"svelte": "5.55.5",
|
|
73
73
|
"tailwind-variants": "^3.2.2",
|
|
74
74
|
"vitest": "^3.2.6",
|
|
75
|
-
"@selvajs/
|
|
76
|
-
"@selvajs/
|
|
75
|
+
"@selvajs/schemas": "4.5.0",
|
|
76
|
+
"@selvajs/config": "0.0.1"
|
|
77
77
|
},
|
|
78
78
|
"scripts": {
|
|
79
79
|
"dev": "vite dev",
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
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);
|
|
3
7
|
|
|
4
8
|
interface Props {
|
|
5
9
|
open: boolean;
|
|
@@ -50,12 +54,12 @@
|
|
|
50
54
|
<Dialog.Title class="text-sm font-semibold">{meshName}</Dialog.Title>
|
|
51
55
|
</div>
|
|
52
56
|
{:else}
|
|
53
|
-
<Dialog.Title class="sr-only">
|
|
57
|
+
<Dialog.Title class="sr-only">{t.objectFallbackName}</Dialog.Title>
|
|
54
58
|
{/if}
|
|
55
59
|
|
|
56
60
|
<div class="max-h-80 overflow-y-auto">
|
|
57
61
|
{#if !metadata || Object.keys(getFilteredMetadata()).length === 0}
|
|
58
|
-
<p class="px-4 py-3 text-xs text-muted-foreground">
|
|
62
|
+
<p class="px-4 py-3 text-xs text-muted-foreground">{t.noMetadata}</p>
|
|
59
63
|
{:else}
|
|
60
64
|
<table class="text-xs w-full border-collapse">
|
|
61
65
|
<tbody>
|
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
import * as THREE from 'three';
|
|
3
3
|
import { Eye, EyeOff, ChevronRight, Search, X } from '@lucide/svelte';
|
|
4
4
|
import { SvelteSet, SvelteMap } from 'svelte/reactivity';
|
|
5
|
+
import { getLocaleContext } from '$lib/i18n/localeContext.svelte';
|
|
6
|
+
|
|
7
|
+
const locale = getLocaleContext();
|
|
8
|
+
const t = $derived(locale.messages);
|
|
5
9
|
|
|
6
10
|
interface Props {
|
|
7
11
|
scene: THREE.Scene;
|
|
@@ -165,11 +169,11 @@
|
|
|
165
169
|
<Search class="h-3.5 w-3.5 shrink-0 text-muted-foreground/50" />
|
|
166
170
|
<input
|
|
167
171
|
class="min-w-0 text-xs flex-1 bg-transparent text-foreground outline-none placeholder:text-muted-foreground/50"
|
|
168
|
-
placeholder=
|
|
172
|
+
placeholder={t.searchObjects}
|
|
169
173
|
bind:value={searchQuery}
|
|
170
174
|
/>
|
|
171
175
|
{#if searchQuery}
|
|
172
|
-
<button onclick={() => (searchQuery = '')} aria-label=
|
|
176
|
+
<button onclick={() => (searchQuery = '')} aria-label={t.clearSearch}>
|
|
173
177
|
<X
|
|
174
178
|
class="h-3.5 w-3.5 text-muted-foreground/50 transition-colors hover:text-muted-foreground"
|
|
175
179
|
/>
|
|
@@ -187,7 +191,7 @@
|
|
|
187
191
|
<button
|
|
188
192
|
class="rounded p-0.5 shrink-0 text-muted-foreground transition-colors hover:text-muted-foreground"
|
|
189
193
|
onclick={() => toggleLayerCollapsed(layerName)}
|
|
190
|
-
aria-label={collapsed ?
|
|
194
|
+
aria-label={collapsed ? t.expandLayer : t.collapseLayer}
|
|
191
195
|
>
|
|
192
196
|
<ChevronRight
|
|
193
197
|
class="h-3.5 w-3.5 transition-transform duration-150 {collapsed ? '' : 'rotate-90'}"
|
|
@@ -197,8 +201,8 @@
|
|
|
197
201
|
<button
|
|
198
202
|
class="rounded p-1 shrink-0 transition-colors hover:bg-muted"
|
|
199
203
|
onclick={() => toggleLayer(layerName, objects)}
|
|
200
|
-
title={layerHidden ?
|
|
201
|
-
aria-label={layerHidden ?
|
|
204
|
+
title={layerHidden ? t.showLayer : t.hideLayer}
|
|
205
|
+
aria-label={layerHidden ? t.showLayer : t.hideLayer}
|
|
202
206
|
>
|
|
203
207
|
{#if layerHidden}
|
|
204
208
|
<EyeOff class="h-3.5 w-3.5 text-muted-foreground/40" />
|
|
@@ -249,8 +253,8 @@
|
|
|
249
253
|
e.stopPropagation();
|
|
250
254
|
toggleObject(object);
|
|
251
255
|
}}
|
|
252
|
-
title={hidden ?
|
|
253
|
-
aria-label={hidden ?
|
|
256
|
+
title={hidden ? t.showObject : t.hideObject}
|
|
257
|
+
aria-label={hidden ? t.showObject : t.hideObject}
|
|
254
258
|
>
|
|
255
259
|
{#if hidden}
|
|
256
260
|
<EyeOff class="h-3 w-3 text-muted-foreground/60" />
|
|
@@ -284,12 +288,14 @@
|
|
|
284
288
|
{#if getSceneObjects().length === 0}
|
|
285
289
|
<div class="py-12 flex flex-col items-center justify-center text-center">
|
|
286
290
|
<EyeOff class="mb-2 h-5 w-5 text-muted-foreground/30" />
|
|
287
|
-
<p class="text-xs text-muted-foreground">
|
|
291
|
+
<p class="text-xs text-muted-foreground">{t.noObjects}</p>
|
|
288
292
|
</div>
|
|
289
293
|
{:else if getFilteredLayerGroups().size === 0}
|
|
290
294
|
<div class="py-12 flex flex-col items-center justify-center text-center">
|
|
291
295
|
<Search class="mb-2 h-5 w-5 text-muted-foreground/30" />
|
|
292
|
-
<p class="text-xs text-muted-foreground">
|
|
296
|
+
<p class="text-xs text-muted-foreground">
|
|
297
|
+
{t.noResultsFor.replace('{query}', searchQuery)}
|
|
298
|
+
</p>
|
|
293
299
|
</div>
|
|
294
300
|
{/if}
|
|
295
301
|
</div>
|
|
@@ -31,6 +31,8 @@
|
|
|
31
31
|
import SceneManager from './SceneManager.svelte';
|
|
32
32
|
import MeshMetadataDialog from './MeshMetadataDialog.svelte';
|
|
33
33
|
import * as Resizable from '$lib/components/primitives/resizable/index.js';
|
|
34
|
+
import type { Locale } from '$lib/i18n/messages';
|
|
35
|
+
import { setLocaleContext, getLocaleContext } from '$lib/i18n/localeContext.svelte';
|
|
34
36
|
|
|
35
37
|
export interface ViewerConfig {
|
|
36
38
|
showScreenshotButton?: boolean;
|
|
@@ -50,6 +52,13 @@
|
|
|
50
52
|
isBlurred?: boolean;
|
|
51
53
|
drawerOpen?: boolean;
|
|
52
54
|
viewerConfig?: ViewerConfig;
|
|
55
|
+
/**
|
|
56
|
+
* UI language for the viewer's own chrome (tools menu, panels, dialogs).
|
|
57
|
+
* When set, the viewer provides it to its subtree. When omitted, the viewer
|
|
58
|
+
* reads the nearest locale context (set by the host app), defaulting to
|
|
59
|
+
* English. Does not translate Grasshopper-sourced names/metadata.
|
|
60
|
+
*/
|
|
61
|
+
lang?: Locale;
|
|
53
62
|
}
|
|
54
63
|
|
|
55
64
|
const defaultViewerConfig: Required<ViewerConfig> = {
|
|
@@ -68,11 +77,23 @@
|
|
|
68
77
|
isSolving = false,
|
|
69
78
|
isBlurred = false,
|
|
70
79
|
drawerOpen = false,
|
|
71
|
-
viewerConfig = {}
|
|
80
|
+
viewerConfig = {},
|
|
81
|
+
lang
|
|
72
82
|
}: Props = $props();
|
|
73
83
|
|
|
74
84
|
const config = $derived({ ...defaultViewerConfig, ...viewerConfig });
|
|
75
85
|
|
|
86
|
+
// Read any host-provided locale before we (maybe) override it for our subtree.
|
|
87
|
+
const hostLocale = getLocaleContext();
|
|
88
|
+
|
|
89
|
+
// Resolution order: explicit `lang` prop → host locale context → default.
|
|
90
|
+
// Provide the resolved value to our subtree so the scene manager and metadata
|
|
91
|
+
// dialog read the same locale. The getter is re-read reactively, so switching
|
|
92
|
+
// the `lang` prop (or the host's locale) updates the chrome live.
|
|
93
|
+
setLocaleContext(() => lang ?? hostLocale.locale);
|
|
94
|
+
const locale = getLocaleContext();
|
|
95
|
+
const t = $derived(locale.messages);
|
|
96
|
+
|
|
76
97
|
let canvas: HTMLCanvasElement;
|
|
77
98
|
let scene: THREE.Scene | null = $state(null);
|
|
78
99
|
let camera: THREE.PerspectiveCamera | null = null;
|
|
@@ -91,14 +112,14 @@
|
|
|
91
112
|
let selectedMeshMetadata: Record<string, any> | null = $state(null);
|
|
92
113
|
let selectedMeshName: string | null = $state(null);
|
|
93
114
|
|
|
94
|
-
const VIEW_PRESETS: { preset: ViewPreset; label: string }[] = [
|
|
95
|
-
{ preset: 'top', label:
|
|
96
|
-
{ preset: 'front', label:
|
|
97
|
-
{ preset: 'right', label:
|
|
98
|
-
{ preset: 'back', label:
|
|
99
|
-
{ preset: 'left', label:
|
|
100
|
-
{ preset: 'bottom', label:
|
|
101
|
-
{ preset: 'iso', label:
|
|
115
|
+
const VIEW_PRESETS: { preset: ViewPreset; label: () => string }[] = [
|
|
116
|
+
{ preset: 'top', label: () => t.viewTop },
|
|
117
|
+
{ preset: 'front', label: () => t.viewFront },
|
|
118
|
+
{ preset: 'right', label: () => t.viewRight },
|
|
119
|
+
{ preset: 'back', label: () => t.viewBack },
|
|
120
|
+
{ preset: 'left', label: () => t.viewLeft },
|
|
121
|
+
{ preset: 'bottom', label: () => t.viewBottom },
|
|
122
|
+
{ preset: 'iso', label: () => t.viewIso }
|
|
102
123
|
];
|
|
103
124
|
|
|
104
125
|
// Hide button instantly when expanding, show after animation when collapsing
|
|
@@ -128,7 +149,7 @@
|
|
|
128
149
|
? (metadata: Record<string, string>) => {
|
|
129
150
|
if (hasUsefulMetadata(metadata)) {
|
|
130
151
|
selectedMeshMetadata = metadata;
|
|
131
|
-
selectedMeshName = metadata?.name ||
|
|
152
|
+
selectedMeshName = metadata?.name || t.objectFallbackName;
|
|
132
153
|
}
|
|
133
154
|
}
|
|
134
155
|
: undefined
|
|
@@ -265,8 +286,8 @@
|
|
|
265
286
|
<DropdownMenu.Root>
|
|
266
287
|
<DropdownMenu.Trigger
|
|
267
288
|
class="h-10 w-10 shadow-lg hover:shadow-xl flex items-center justify-center rounded-lg border border-border bg-card/90 transition-all hover:bg-card active:scale-95 data-[state=open]:bg-secondary/60"
|
|
268
|
-
title=
|
|
269
|
-
aria-label=
|
|
289
|
+
title={t.toolsMenu}
|
|
290
|
+
aria-label={t.toolsMenu}
|
|
270
291
|
>
|
|
271
292
|
<Settings2 class="h-5 w-5 text-card-foreground" />
|
|
272
293
|
</DropdownMenu.Trigger>
|
|
@@ -280,22 +301,22 @@
|
|
|
280
301
|
<DropdownMenu.Item class={itemClass} onSelect={toggleProjection}>
|
|
281
302
|
{#if projection === 'perspective'}
|
|
282
303
|
<Square class="h-4 w-4" />
|
|
283
|
-
|
|
304
|
+
{t.switchTo2D}
|
|
284
305
|
{:else}
|
|
285
306
|
<Box class="h-4 w-4" />
|
|
286
|
-
|
|
307
|
+
{t.switchTo3D}
|
|
287
308
|
{/if}
|
|
288
309
|
</DropdownMenu.Item>
|
|
289
310
|
|
|
290
311
|
<DropdownMenu.Item class={itemClass} onSelect={() => fitToView?.()}>
|
|
291
312
|
<Frame class="h-4 w-4" />
|
|
292
|
-
|
|
313
|
+
{t.fitToView}
|
|
293
314
|
</DropdownMenu.Item>
|
|
294
315
|
|
|
295
316
|
<DropdownMenu.Sub>
|
|
296
317
|
<DropdownMenu.SubTrigger class="{itemClass} data-[state=open]:bg-muted">
|
|
297
318
|
<Box class="h-4 w-4" />
|
|
298
|
-
<span class="flex-1">
|
|
319
|
+
<span class="flex-1">{t.views}</span>
|
|
299
320
|
<ChevronRight class="h-4 w-4 text-muted-foreground" />
|
|
300
321
|
</DropdownMenu.SubTrigger>
|
|
301
322
|
<DropdownMenu.SubContent
|
|
@@ -307,7 +328,7 @@
|
|
|
307
328
|
class="px-2 py-1.5 text-sm flex cursor-pointer items-center rounded-sm transition-colors outline-none select-none hover:bg-muted focus:bg-muted"
|
|
308
329
|
onSelect={() => setView(preset)}
|
|
309
330
|
>
|
|
310
|
-
{label}
|
|
331
|
+
{label()}
|
|
311
332
|
</DropdownMenu.Item>
|
|
312
333
|
{/each}
|
|
313
334
|
</DropdownMenu.SubContent>
|
|
@@ -319,7 +340,7 @@
|
|
|
319
340
|
onSelect={toggleMeasure}
|
|
320
341
|
>
|
|
321
342
|
<Ruler class="h-4 w-4" />
|
|
322
|
-
<span class="flex-1">
|
|
343
|
+
<span class="flex-1">{t.measure}</span>
|
|
323
344
|
{#if measureActive}
|
|
324
345
|
<Check class="h-4 w-4" />
|
|
325
346
|
{/if}
|
|
@@ -332,7 +353,7 @@
|
|
|
332
353
|
onSelect={toggleGrid}
|
|
333
354
|
>
|
|
334
355
|
<Grid3x3 class="h-4 w-4" />
|
|
335
|
-
<span class="flex-1">
|
|
356
|
+
<span class="flex-1">{t.grid}</span>
|
|
336
357
|
{#if gridVisible}
|
|
337
358
|
<Check class="h-4 w-4" />
|
|
338
359
|
{/if}
|
|
@@ -351,7 +372,7 @@
|
|
|
351
372
|
onSelect={() => (sceneManagerOpen = !sceneManagerOpen)}
|
|
352
373
|
>
|
|
353
374
|
<Layers class="h-4 w-4" />
|
|
354
|
-
<span class="flex-1">
|
|
375
|
+
<span class="flex-1">{t.sceneManager}</span>
|
|
355
376
|
{#if sceneManagerOpen}
|
|
356
377
|
<Check class="h-4 w-4" />
|
|
357
378
|
{/if}
|
|
@@ -361,7 +382,7 @@
|
|
|
361
382
|
{#if config.showScreenshotButton}
|
|
362
383
|
<DropdownMenu.Item class={itemClass} onSelect={downloadScreenshot}>
|
|
363
384
|
<Camera class="h-4 w-4" />
|
|
364
|
-
|
|
385
|
+
{t.screenshot}
|
|
365
386
|
</DropdownMenu.Item>
|
|
366
387
|
{/if}
|
|
367
388
|
|
|
@@ -369,10 +390,10 @@
|
|
|
369
390
|
<DropdownMenu.Item class={itemClass} onSelect={toggleFullscreen}>
|
|
370
391
|
{#if isFullscreen}
|
|
371
392
|
<Minimize class="h-4 w-4" />
|
|
372
|
-
|
|
393
|
+
{t.exitFullscreen}
|
|
373
394
|
{:else}
|
|
374
395
|
<Maximize class="h-4 w-4" />
|
|
375
|
-
|
|
396
|
+
{t.fullscreen}
|
|
376
397
|
{/if}
|
|
377
398
|
</DropdownMenu.Item>
|
|
378
399
|
{/if}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { getContext, setContext } from 'svelte';
|
|
2
|
+
import { type Locale, type ViewerMessages, messagesFor, DEFAULT_LOCALE } from './messages';
|
|
3
|
+
|
|
4
|
+
// ============================================================================
|
|
5
|
+
// Viewer locale context
|
|
6
|
+
// ============================================================================
|
|
7
|
+
//
|
|
8
|
+
// Carries the current UI locale down to the viewer and its panels without
|
|
9
|
+
// threading a `lang` prop through every layer. The value is a getter so the
|
|
10
|
+
// host can back it with reactive state — flip the language and the viewer
|
|
11
|
+
// re-renders live.
|
|
12
|
+
//
|
|
13
|
+
// Resolution order for any consuming component:
|
|
14
|
+
// explicit `lang` prop → nearest locale context → English default
|
|
15
|
+
//
|
|
16
|
+
// Two ways to provide it:
|
|
17
|
+
// - Standalone: <Viewer lang="de" /> — Viewer provides the context itself.
|
|
18
|
+
// - In an app: call setLocaleContext(() => app.locale) once at the root; the
|
|
19
|
+
// viewer (and anything else) reads it. selva later wires its Paraglide
|
|
20
|
+
// locale in here.
|
|
21
|
+
|
|
22
|
+
const LOCALE_CONTEXT_KEY = Symbol('viewer-locale-context');
|
|
23
|
+
|
|
24
|
+
export interface LocaleContext {
|
|
25
|
+
/** Current locale. Called reactively — return reactive state to enable live switching. */
|
|
26
|
+
readonly locale: Locale;
|
|
27
|
+
/** Resolved message catalog for the current locale. */
|
|
28
|
+
readonly messages: ViewerMessages;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Provide the locale to descendants. Pass a getter so a reactive source (a
|
|
33
|
+
* `$state`, a store, the app's Paraglide locale) keeps consumers in sync.
|
|
34
|
+
*/
|
|
35
|
+
export function setLocaleContext(getLocale: () => Locale | undefined): void {
|
|
36
|
+
const ctx: LocaleContext = {
|
|
37
|
+
get locale() {
|
|
38
|
+
return getLocale() ?? DEFAULT_LOCALE;
|
|
39
|
+
},
|
|
40
|
+
get messages() {
|
|
41
|
+
return messagesFor(getLocale());
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
setContext(LOCALE_CONTEXT_KEY, ctx);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Read the locale context. Falls back to an English-only context when no
|
|
49
|
+
* provider exists (e.g. a primitive used in isolation), so consumers never
|
|
50
|
+
* need a null check.
|
|
51
|
+
*/
|
|
52
|
+
export function getLocaleContext(): LocaleContext {
|
|
53
|
+
return (
|
|
54
|
+
getContext<LocaleContext | undefined>(LOCALE_CONTEXT_KEY) ?? {
|
|
55
|
+
get locale() {
|
|
56
|
+
return DEFAULT_LOCALE;
|
|
57
|
+
},
|
|
58
|
+
get messages() {
|
|
59
|
+
return messagesFor(DEFAULT_LOCALE);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
);
|
|
63
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// Viewer message catalog (library-owned strings)
|
|
3
|
+
// ============================================================================
|
|
4
|
+
//
|
|
5
|
+
// These are the strings @selvajs/ui renders itself in the 3D viewer and its
|
|
6
|
+
// panels — tool menu, view presets, scene manager, metadata dialog. They are
|
|
7
|
+
// NOT the strings that come from a Grasshopper definition (mesh/layer/metadata
|
|
8
|
+
// names): those live in the user's .gh file and can't be translated here.
|
|
9
|
+
//
|
|
10
|
+
// The library ships English + German. A host app can switch locale at runtime
|
|
11
|
+
// via the locale context (see ./localeContext.svelte.ts) — e.g. selva feeds its
|
|
12
|
+
// own Paraglide locale in. With no provider, components fall back to English.
|
|
13
|
+
|
|
14
|
+
export type Locale = 'en' | 'de';
|
|
15
|
+
|
|
16
|
+
export interface ViewerMessages {
|
|
17
|
+
// Tools menu
|
|
18
|
+
toolsMenu: string;
|
|
19
|
+
switchTo2D: string;
|
|
20
|
+
switchTo3D: string;
|
|
21
|
+
fitToView: string;
|
|
22
|
+
views: string;
|
|
23
|
+
measure: string;
|
|
24
|
+
grid: string;
|
|
25
|
+
sceneManager: string;
|
|
26
|
+
screenshot: string;
|
|
27
|
+
fullscreen: string;
|
|
28
|
+
exitFullscreen: string;
|
|
29
|
+
|
|
30
|
+
// View presets
|
|
31
|
+
viewTop: string;
|
|
32
|
+
viewFront: string;
|
|
33
|
+
viewRight: string;
|
|
34
|
+
viewBack: string;
|
|
35
|
+
viewLeft: string;
|
|
36
|
+
viewBottom: string;
|
|
37
|
+
viewIso: string;
|
|
38
|
+
|
|
39
|
+
// Scene manager
|
|
40
|
+
searchObjects: string;
|
|
41
|
+
clearSearch: string;
|
|
42
|
+
expandLayer: string;
|
|
43
|
+
collapseLayer: string;
|
|
44
|
+
showLayer: string;
|
|
45
|
+
hideLayer: string;
|
|
46
|
+
showObject: string;
|
|
47
|
+
hideObject: string;
|
|
48
|
+
noObjects: string;
|
|
49
|
+
/** `{query}` is replaced with the current search text. */
|
|
50
|
+
noResultsFor: string;
|
|
51
|
+
|
|
52
|
+
// Metadata dialog
|
|
53
|
+
objectFallbackName: string;
|
|
54
|
+
noMetadata: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const en: ViewerMessages = {
|
|
58
|
+
toolsMenu: 'Viewer tools',
|
|
59
|
+
switchTo2D: 'Switch to 2D',
|
|
60
|
+
switchTo3D: 'Switch to 3D',
|
|
61
|
+
fitToView: 'Fit to view',
|
|
62
|
+
views: 'Views',
|
|
63
|
+
measure: 'Measure',
|
|
64
|
+
grid: 'Grid',
|
|
65
|
+
sceneManager: 'Scene manager',
|
|
66
|
+
screenshot: 'Screenshot',
|
|
67
|
+
fullscreen: 'Fullscreen',
|
|
68
|
+
exitFullscreen: 'Exit fullscreen',
|
|
69
|
+
|
|
70
|
+
viewTop: 'Top',
|
|
71
|
+
viewFront: 'Front',
|
|
72
|
+
viewRight: 'Right',
|
|
73
|
+
viewBack: 'Back',
|
|
74
|
+
viewLeft: 'Left',
|
|
75
|
+
viewBottom: 'Bottom',
|
|
76
|
+
viewIso: 'Isometric',
|
|
77
|
+
|
|
78
|
+
searchObjects: 'Search objects...',
|
|
79
|
+
clearSearch: 'Clear search',
|
|
80
|
+
expandLayer: 'Expand layer',
|
|
81
|
+
collapseLayer: 'Collapse layer',
|
|
82
|
+
showLayer: 'Show layer',
|
|
83
|
+
hideLayer: 'Hide layer',
|
|
84
|
+
showObject: 'Show object',
|
|
85
|
+
hideObject: 'Hide object',
|
|
86
|
+
noObjects: 'No objects',
|
|
87
|
+
noResultsFor: 'No results for "{query}"',
|
|
88
|
+
|
|
89
|
+
objectFallbackName: 'Object',
|
|
90
|
+
noMetadata: 'No metadata'
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const de: ViewerMessages = {
|
|
94
|
+
toolsMenu: 'Viewer-Werkzeuge',
|
|
95
|
+
switchTo2D: 'Zu 2D wechseln',
|
|
96
|
+
switchTo3D: 'Zu 3D wechseln',
|
|
97
|
+
fitToView: 'Ansicht anpassen',
|
|
98
|
+
views: 'Ansichten',
|
|
99
|
+
measure: 'Messen',
|
|
100
|
+
grid: 'Raster',
|
|
101
|
+
sceneManager: 'Szenen-Manager',
|
|
102
|
+
screenshot: 'Screenshot',
|
|
103
|
+
fullscreen: 'Vollbild',
|
|
104
|
+
exitFullscreen: 'Vollbild beenden',
|
|
105
|
+
|
|
106
|
+
viewTop: 'Oben',
|
|
107
|
+
viewFront: 'Vorne',
|
|
108
|
+
viewRight: 'Rechts',
|
|
109
|
+
viewBack: 'Hinten',
|
|
110
|
+
viewLeft: 'Links',
|
|
111
|
+
viewBottom: 'Unten',
|
|
112
|
+
viewIso: 'Isometrisch',
|
|
113
|
+
|
|
114
|
+
searchObjects: 'Objekte suchen...',
|
|
115
|
+
clearSearch: 'Suche löschen',
|
|
116
|
+
expandLayer: 'Ebene aufklappen',
|
|
117
|
+
collapseLayer: 'Ebene zuklappen',
|
|
118
|
+
showLayer: 'Ebene einblenden',
|
|
119
|
+
hideLayer: 'Ebene ausblenden',
|
|
120
|
+
showObject: 'Objekt einblenden',
|
|
121
|
+
hideObject: 'Objekt ausblenden',
|
|
122
|
+
noObjects: 'Keine Objekte',
|
|
123
|
+
noResultsFor: 'Keine Ergebnisse für „{query}“',
|
|
124
|
+
|
|
125
|
+
objectFallbackName: 'Objekt',
|
|
126
|
+
noMetadata: 'Keine Metadaten'
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
export const VIEWER_MESSAGES: Record<Locale, ViewerMessages> = { en, de };
|
|
130
|
+
|
|
131
|
+
export const DEFAULT_LOCALE: Locale = 'en';
|
|
132
|
+
|
|
133
|
+
/** Resolve a catalog for a locale, falling back to English for unknown locales. */
|
|
134
|
+
export function messagesFor(locale: Locale | undefined): ViewerMessages {
|
|
135
|
+
return VIEWER_MESSAGES[locale ?? DEFAULT_LOCALE] ?? VIEWER_MESSAGES[DEFAULT_LOCALE];
|
|
136
|
+
}
|
package/src/lib/public.ts
CHANGED
|
@@ -22,6 +22,24 @@
|
|
|
22
22
|
export { default as AppLayout } from './components/compute/AppLayout.svelte';
|
|
23
23
|
export { default as ComputeApp } from './components/compute/ComputeApp.svelte';
|
|
24
24
|
|
|
25
|
+
// Standalone 3D viewer. Render Grasshopper meshes on their own, outside a
|
|
26
|
+
// ComputeApp host — external apps drive it directly with a `meshes` array and
|
|
27
|
+
// an optional `viewerConfig`. Pass `lang` to localize its chrome, or provide a
|
|
28
|
+
// locale context (setLocaleContext) at the host root to drive it app-wide.
|
|
29
|
+
export { default as Viewer, type ViewerConfig } from './components/viewer/Viewer.svelte';
|
|
30
|
+
|
|
31
|
+
// Viewer localization. The library renders English + German chrome; switch at
|
|
32
|
+
// runtime by passing `lang` to <Viewer> or by setting a reactive locale context
|
|
33
|
+
// once at the host root (e.g. feed in an app-wide Paraglide locale). Does not
|
|
34
|
+
// translate Grasshopper-sourced names/metadata.
|
|
35
|
+
export type { Locale, ViewerMessages } from './i18n/messages';
|
|
36
|
+
export { VIEWER_MESSAGES, DEFAULT_LOCALE, messagesFor } from './i18n/messages';
|
|
37
|
+
export {
|
|
38
|
+
setLocaleContext,
|
|
39
|
+
getLocaleContext,
|
|
40
|
+
type LocaleContext
|
|
41
|
+
} from './i18n/localeContext.svelte';
|
|
42
|
+
|
|
25
43
|
// Full-screen states a host app renders
|
|
26
44
|
export { default as ErrorScreen } from './components/ErrorScreen.svelte';
|
|
27
45
|
|