@autumnsgrove/groveengine 0.8.5 → 0.9.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/WispPanel.svelte +0 -1
- package/dist/components/admin/GutterManager.svelte +213 -101
- package/dist/components/admin/MarkdownEditor.svelte +6 -3
- package/dist/components/custom/ContentWithGutter.svelte +7 -13
- package/dist/components/custom/GutterItem.svelte +8 -2
- package/dist/components/quota/UpgradePrompt.svelte +1 -0
- package/dist/config/domain-blocklist.d.ts +59 -0
- package/dist/config/domain-blocklist.js +731 -0
- package/dist/config/index.d.ts +3 -1
- package/dist/config/index.js +2 -1
- package/dist/config/offensive-blocklist.d.ts +44 -0
- package/dist/config/offensive-blocklist.js +751 -0
- package/dist/config/terrarium.d.ts +109 -0
- package/dist/config/terrarium.js +125 -0
- package/dist/styles/tokens.css +90 -0
- package/dist/types/dom-to-image-more.d.ts +39 -0
- package/dist/ui/components/chrome/Footer.svelte +137 -0
- package/dist/ui/components/chrome/Footer.svelte.d.ts +11 -0
- package/dist/ui/components/chrome/FooterMinimal.svelte +75 -0
- package/dist/ui/components/chrome/FooterMinimal.svelte.d.ts +10 -0
- package/dist/ui/components/chrome/Header.svelte +113 -0
- package/dist/ui/components/chrome/Header.svelte.d.ts +11 -0
- package/dist/ui/components/chrome/HeaderMinimal.svelte +68 -0
- package/dist/ui/components/chrome/HeaderMinimal.svelte.d.ts +9 -0
- package/dist/ui/components/chrome/MobileMenu.svelte +145 -0
- package/dist/ui/components/chrome/MobileMenu.svelte.d.ts +9 -0
- package/dist/ui/components/chrome/ThemeToggle.svelte +34 -0
- package/dist/ui/components/chrome/ThemeToggle.svelte.d.ts +3 -0
- package/dist/ui/components/chrome/defaults.d.ts +6 -0
- package/dist/ui/components/chrome/defaults.js +65 -0
- package/dist/ui/components/chrome/index.d.ts +13 -0
- package/dist/ui/components/chrome/index.js +14 -0
- package/dist/ui/components/chrome/types.d.ts +19 -0
- package/dist/ui/components/chrome/types.js +8 -0
- package/dist/ui/components/content/RoadmapPreview.svelte +2 -1
- package/dist/ui/components/forms/ContentSearch.svelte +406 -0
- package/dist/ui/components/forms/ContentSearch.svelte.d.ts +71 -0
- package/dist/ui/components/forms/SearchInput.svelte +0 -1
- package/dist/ui/components/forms/filterUtils.d.ts +138 -0
- package/dist/ui/components/forms/filterUtils.js +240 -0
- package/dist/ui/components/forms/index.d.ts +2 -0
- package/dist/ui/components/forms/index.js +5 -1
- package/dist/ui/components/gallery/ImageGallery.svelte +17 -3
- package/dist/ui/components/gallery/Lightbox.svelte +11 -3
- package/dist/ui/components/gallery/ZoomableImage.svelte +13 -2
- package/dist/ui/components/icons/index.d.ts +2 -1
- package/dist/ui/components/icons/index.js +14 -3
- package/dist/ui/components/icons/lucide.d.ts +213 -0
- package/dist/ui/components/icons/lucide.js +224 -0
- package/dist/ui/components/terrarium/AssetPalette.svelte +207 -0
- package/dist/ui/components/terrarium/AssetPalette.svelte.d.ts +7 -0
- package/dist/ui/components/terrarium/Canvas.svelte +231 -0
- package/dist/ui/components/terrarium/Canvas.svelte.d.ts +14 -0
- package/dist/ui/components/terrarium/ExportDialog.svelte +307 -0
- package/dist/ui/components/terrarium/ExportDialog.svelte.d.ts +18 -0
- package/dist/ui/components/terrarium/PaletteItem.svelte +169 -0
- package/dist/ui/components/terrarium/PaletteItem.svelte.d.ts +9 -0
- package/dist/ui/components/terrarium/PlacedAsset.svelte +222 -0
- package/dist/ui/components/terrarium/PlacedAsset.svelte.d.ts +11 -0
- package/dist/ui/components/terrarium/Terrarium.svelte +266 -0
- package/dist/ui/components/terrarium/Terrarium.svelte.d.ts +3 -0
- package/dist/ui/components/terrarium/Toolbar.svelte +299 -0
- package/dist/ui/components/terrarium/Toolbar.svelte.d.ts +24 -0
- package/dist/ui/components/terrarium/index.d.ts +31 -0
- package/dist/ui/components/terrarium/index.js +33 -0
- package/dist/ui/components/terrarium/terrariumState.svelte.d.ts +45 -0
- package/dist/ui/components/terrarium/terrariumState.svelte.js +291 -0
- package/dist/ui/components/terrarium/types.d.ts +139 -0
- package/dist/ui/components/terrarium/types.js +43 -0
- package/dist/ui/components/terrarium/utils/export.d.ts +48 -0
- package/dist/ui/components/terrarium/utils/export.js +148 -0
- package/dist/ui/components/typography/index.d.ts +0 -10
- package/dist/ui/components/typography/index.js +1 -12
- package/dist/ui/components/ui/CollapsibleSection.svelte +12 -0
- package/dist/ui/components/ui/GlassConfirmDialog.svelte +9 -0
- package/dist/ui/components/ui/GlassOverlay.svelte +2 -1
- package/dist/ui/components/ui/Input.svelte +9 -1
- package/dist/ui/components/ui/Input.svelte.d.ts +2 -0
- package/dist/ui/components/ui/Textarea.svelte +9 -1
- package/dist/ui/components/ui/Textarea.svelte.d.ts +2 -0
- package/dist/ui/stores/index.d.ts +6 -0
- package/dist/ui/stores/index.js +6 -0
- package/dist/ui/stores/season.d.ts +14 -0
- package/dist/ui/stores/season.js +65 -0
- package/dist/ui/tokens/fonts.d.ts +1 -1
- package/dist/ui/tokens/fonts.js +0 -126
- package/package.json +46 -22
- package/static/fonts/alagard.ttf +0 -0
- package/LICENSE +0 -378
- package/dist/ui/components/typography/BodoniModa.svelte +0 -17
- package/dist/ui/components/typography/BodoniModa.svelte.d.ts +0 -10
- package/dist/ui/components/typography/Cormorant.svelte +0 -17
- package/dist/ui/components/typography/Cormorant.svelte.d.ts +0 -10
- package/dist/ui/components/typography/EBGaramond.svelte +0 -17
- package/dist/ui/components/typography/EBGaramond.svelte.d.ts +0 -10
- package/dist/ui/components/typography/Fraunces.svelte +0 -17
- package/dist/ui/components/typography/Fraunces.svelte.d.ts +0 -10
- package/dist/ui/components/typography/InstrumentSans.svelte +0 -17
- package/dist/ui/components/typography/InstrumentSans.svelte.d.ts +0 -10
- package/dist/ui/components/typography/Lora.svelte +0 -17
- package/dist/ui/components/typography/Lora.svelte.d.ts +0 -10
- package/dist/ui/components/typography/Luciole.svelte +0 -17
- package/dist/ui/components/typography/Luciole.svelte.d.ts +0 -10
- package/dist/ui/components/typography/Manrope.svelte +0 -17
- package/dist/ui/components/typography/Manrope.svelte.d.ts +0 -10
- package/dist/ui/components/typography/Merriweather.svelte +0 -17
- package/dist/ui/components/typography/Merriweather.svelte.d.ts +0 -10
- package/dist/ui/components/typography/Nunito.svelte +0 -17
- package/dist/ui/components/typography/Nunito.svelte.d.ts +0 -10
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
Grove — A place to Be
|
|
3
|
+
Copyright (c) 2025 Autumn Brown
|
|
4
|
+
|
|
5
|
+
This program is free software: you can redistribute it and/or modify
|
|
6
|
+
it under the terms of the GNU Affero General Public License as published
|
|
7
|
+
by the Free Software Foundation, either version 3 of the License, or
|
|
8
|
+
(at your option) any later version.
|
|
9
|
+
|
|
10
|
+
This program is distributed in the hope that it will be useful,
|
|
11
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
12
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
13
|
+
GNU Affero General Public License for more details.
|
|
14
|
+
|
|
15
|
+
You should have received a copy of the GNU Affero General Public License
|
|
16
|
+
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
17
|
+
-->
|
|
18
|
+
|
|
19
|
+
<script lang="ts">
|
|
20
|
+
import { X, Loader2, Download } from 'lucide-svelte';
|
|
21
|
+
import { cn } from '../../utils';
|
|
22
|
+
import GlassCard from '../ui/GlassCard.svelte';
|
|
23
|
+
import GlassButton from '../ui/GlassButton.svelte';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Terrarium Export Dialog
|
|
27
|
+
*
|
|
28
|
+
* Modal dialog for exporting scenes as PNG images.
|
|
29
|
+
* Provides scale and background options with estimated file size.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
interface Props {
|
|
33
|
+
open: boolean;
|
|
34
|
+
sceneName: string;
|
|
35
|
+
onClose: () => void;
|
|
36
|
+
onExport: (options: { scale: number; includeBackground: boolean }) => Promise<void>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
let { open = $bindable(), sceneName, onClose, onExport }: Props = $props();
|
|
40
|
+
|
|
41
|
+
let scale = $state(2);
|
|
42
|
+
let includeBackground = $state(true);
|
|
43
|
+
let isExporting = $state(false);
|
|
44
|
+
|
|
45
|
+
let dialogElement: HTMLDivElement | null = $state(null);
|
|
46
|
+
let firstFocusableElement: HTMLElement | null = $state(null);
|
|
47
|
+
let lastFocusableElement: HTMLElement | null = $state(null);
|
|
48
|
+
|
|
49
|
+
// Scale options
|
|
50
|
+
const scaleOptions = [
|
|
51
|
+
{ value: 1, label: '1x (Original)' },
|
|
52
|
+
{ value: 2, label: '2x (Retina)' },
|
|
53
|
+
{ value: 3, label: '3x (High-res)' }
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
// Estimated dimensions and file size
|
|
57
|
+
const estimatedWidth = $derived(1200 * scale);
|
|
58
|
+
const estimatedHeight = $derived(800 * scale);
|
|
59
|
+
const estimatedSizeMB = $derived(
|
|
60
|
+
((estimatedWidth * estimatedHeight * 4) / (1024 * 1024) * 0.3).toFixed(1)
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
// Handle escape key
|
|
64
|
+
function handleKeydown(e: KeyboardEvent) {
|
|
65
|
+
if (!open) return;
|
|
66
|
+
|
|
67
|
+
if (e.key === 'Escape') {
|
|
68
|
+
e.preventDefault();
|
|
69
|
+
handleClose();
|
|
70
|
+
} else if (e.key === 'Tab') {
|
|
71
|
+
// Focus trap
|
|
72
|
+
if (!firstFocusableElement || !lastFocusableElement) return;
|
|
73
|
+
|
|
74
|
+
if (e.shiftKey) {
|
|
75
|
+
// Shift+Tab
|
|
76
|
+
if (document.activeElement === firstFocusableElement) {
|
|
77
|
+
e.preventDefault();
|
|
78
|
+
lastFocusableElement.focus();
|
|
79
|
+
}
|
|
80
|
+
} else {
|
|
81
|
+
// Tab
|
|
82
|
+
if (document.activeElement === lastFocusableElement) {
|
|
83
|
+
e.preventDefault();
|
|
84
|
+
firstFocusableElement.focus();
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Handle close
|
|
91
|
+
function handleClose() {
|
|
92
|
+
if (!isExporting) {
|
|
93
|
+
onClose();
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Handle export
|
|
98
|
+
async function handleExport() {
|
|
99
|
+
if (isExporting) return;
|
|
100
|
+
|
|
101
|
+
isExporting = true;
|
|
102
|
+
try {
|
|
103
|
+
await onExport({ scale, includeBackground });
|
|
104
|
+
onClose();
|
|
105
|
+
} catch (error) {
|
|
106
|
+
console.error('Export failed:', error);
|
|
107
|
+
} finally {
|
|
108
|
+
isExporting = false;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Update focusable elements when dialog opens
|
|
113
|
+
$effect(() => {
|
|
114
|
+
if (open && dialogElement) {
|
|
115
|
+
const focusableSelector =
|
|
116
|
+
'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
|
|
117
|
+
const focusableElements = dialogElement.querySelectorAll(focusableSelector);
|
|
118
|
+
|
|
119
|
+
if (focusableElements.length > 0) {
|
|
120
|
+
firstFocusableElement = focusableElements[0] as HTMLElement;
|
|
121
|
+
lastFocusableElement = focusableElements[
|
|
122
|
+
focusableElements.length - 1
|
|
123
|
+
] as HTMLElement;
|
|
124
|
+
|
|
125
|
+
// Focus first element after a tick
|
|
126
|
+
setTimeout(() => {
|
|
127
|
+
firstFocusableElement?.focus();
|
|
128
|
+
}, 0);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
</script>
|
|
133
|
+
|
|
134
|
+
<svelte:window onkeydown={handleKeydown} />
|
|
135
|
+
|
|
136
|
+
{#if open}
|
|
137
|
+
<!-- Backdrop -->
|
|
138
|
+
<div
|
|
139
|
+
class={cn(
|
|
140
|
+
'fixed inset-0 z-50',
|
|
141
|
+
'bg-black/50 backdrop-blur-sm',
|
|
142
|
+
'flex items-center justify-center p-4',
|
|
143
|
+
'animate-in fade-in duration-200'
|
|
144
|
+
)}
|
|
145
|
+
onclick={(e) => {
|
|
146
|
+
if (e.target === e.currentTarget) handleClose();
|
|
147
|
+
}}
|
|
148
|
+
role="presentation"
|
|
149
|
+
>
|
|
150
|
+
<!-- Dialog -->
|
|
151
|
+
<div bind:this={dialogElement} role="dialog" aria-modal="true" aria-labelledby="export-title">
|
|
152
|
+
<GlassCard variant="frosted" class="w-full max-w-lg">
|
|
153
|
+
{#snippet header()}
|
|
154
|
+
<div class="flex items-center justify-between">
|
|
155
|
+
<div>
|
|
156
|
+
<h2 id="export-title" class="text-xl font-semibold text-foreground">
|
|
157
|
+
Export Scene
|
|
158
|
+
</h2>
|
|
159
|
+
<p class="text-sm text-muted-foreground mt-1">
|
|
160
|
+
Export "{sceneName}" as PNG
|
|
161
|
+
</p>
|
|
162
|
+
</div>
|
|
163
|
+
<button
|
|
164
|
+
onclick={handleClose}
|
|
165
|
+
disabled={isExporting}
|
|
166
|
+
class={cn(
|
|
167
|
+
'inline-flex items-center justify-center',
|
|
168
|
+
'h-8 w-8 rounded-lg',
|
|
169
|
+
'text-muted-foreground hover:text-foreground',
|
|
170
|
+
'hover:bg-white/40 dark:hover:bg-emerald-950/25',
|
|
171
|
+
'transition-colors duration-200',
|
|
172
|
+
'disabled:opacity-50 disabled:pointer-events-none',
|
|
173
|
+
'[&_svg]:w-4 [&_svg]:h-4'
|
|
174
|
+
)}
|
|
175
|
+
aria-label="Close dialog"
|
|
176
|
+
>
|
|
177
|
+
<X />
|
|
178
|
+
</button>
|
|
179
|
+
</div>
|
|
180
|
+
{/snippet}
|
|
181
|
+
|
|
182
|
+
<!-- Options -->
|
|
183
|
+
<div class="space-y-6">
|
|
184
|
+
<!-- Scale Selection -->
|
|
185
|
+
<div>
|
|
186
|
+
<label for="scale-select" class="block text-sm font-medium text-foreground mb-2">
|
|
187
|
+
Export Scale
|
|
188
|
+
</label>
|
|
189
|
+
<select
|
|
190
|
+
id="scale-select"
|
|
191
|
+
bind:value={scale}
|
|
192
|
+
disabled={isExporting}
|
|
193
|
+
class={cn(
|
|
194
|
+
'w-full h-10 px-3 rounded-lg',
|
|
195
|
+
'bg-white/60 dark:bg-emerald-950/25',
|
|
196
|
+
'border border-white/40 dark:border-emerald-800/25',
|
|
197
|
+
'text-foreground text-sm',
|
|
198
|
+
'hover:bg-white/75 dark:hover:bg-emerald-950/35',
|
|
199
|
+
'transition-all duration-200',
|
|
200
|
+
'backdrop-blur-md',
|
|
201
|
+
'focus:outline-none focus:ring-2 focus:ring-accent/50',
|
|
202
|
+
'disabled:opacity-50 disabled:pointer-events-none',
|
|
203
|
+
'cursor-pointer'
|
|
204
|
+
)}
|
|
205
|
+
>
|
|
206
|
+
{#each scaleOptions as option}
|
|
207
|
+
<option value={option.value}>{option.label}</option>
|
|
208
|
+
{/each}
|
|
209
|
+
</select>
|
|
210
|
+
</div>
|
|
211
|
+
|
|
212
|
+
<!-- Include Background -->
|
|
213
|
+
<div class="flex items-center gap-3">
|
|
214
|
+
<input
|
|
215
|
+
id="include-background"
|
|
216
|
+
type="checkbox"
|
|
217
|
+
bind:checked={includeBackground}
|
|
218
|
+
disabled={isExporting}
|
|
219
|
+
class={cn(
|
|
220
|
+
'h-4 w-4 rounded',
|
|
221
|
+
'border border-white/40 dark:border-emerald-800/25',
|
|
222
|
+
'bg-white/60 dark:bg-emerald-950/25',
|
|
223
|
+
'text-accent focus:ring-accent/50',
|
|
224
|
+
'disabled:opacity-50 disabled:pointer-events-none',
|
|
225
|
+
'cursor-pointer'
|
|
226
|
+
)}
|
|
227
|
+
/>
|
|
228
|
+
<label
|
|
229
|
+
for="include-background"
|
|
230
|
+
class="text-sm font-medium text-foreground cursor-pointer"
|
|
231
|
+
>
|
|
232
|
+
Include background
|
|
233
|
+
</label>
|
|
234
|
+
</div>
|
|
235
|
+
|
|
236
|
+
<!-- Estimated File Info -->
|
|
237
|
+
<div
|
|
238
|
+
class={cn(
|
|
239
|
+
'p-4 rounded-lg',
|
|
240
|
+
'bg-white/40 dark:bg-emerald-950/20',
|
|
241
|
+
'border border-white/30 dark:border-emerald-800/20'
|
|
242
|
+
)}
|
|
243
|
+
>
|
|
244
|
+
<h3 class="text-sm font-medium text-foreground mb-2">Estimated Output</h3>
|
|
245
|
+
<dl class="space-y-1 text-sm">
|
|
246
|
+
<div class="flex justify-between">
|
|
247
|
+
<dt class="text-muted-foreground">Dimensions:</dt>
|
|
248
|
+
<dd class="font-medium text-foreground">
|
|
249
|
+
{estimatedWidth} × {estimatedHeight}px
|
|
250
|
+
</dd>
|
|
251
|
+
</div>
|
|
252
|
+
<div class="flex justify-between">
|
|
253
|
+
<dt class="text-muted-foreground">File size:</dt>
|
|
254
|
+
<dd class="font-medium text-foreground">~{estimatedSizeMB} MB</dd>
|
|
255
|
+
</div>
|
|
256
|
+
<div class="flex justify-between">
|
|
257
|
+
<dt class="text-muted-foreground">Format:</dt>
|
|
258
|
+
<dd class="font-medium text-foreground">PNG</dd>
|
|
259
|
+
</div>
|
|
260
|
+
</dl>
|
|
261
|
+
</div>
|
|
262
|
+
|
|
263
|
+
<!-- Export Progress -->
|
|
264
|
+
{#if isExporting}
|
|
265
|
+
<div
|
|
266
|
+
class={cn(
|
|
267
|
+
'flex items-center gap-3 p-4 rounded-lg',
|
|
268
|
+
'bg-accent/10 dark:bg-accent/5',
|
|
269
|
+
'border border-accent/30 dark:border-accent/20'
|
|
270
|
+
)}
|
|
271
|
+
>
|
|
272
|
+
<Loader2 class="w-5 h-5 text-accent animate-spin" />
|
|
273
|
+
<div>
|
|
274
|
+
<p class="text-sm font-medium text-foreground">Exporting...</p>
|
|
275
|
+
<p class="text-xs text-muted-foreground mt-0.5">
|
|
276
|
+
This may take a few seconds
|
|
277
|
+
</p>
|
|
278
|
+
</div>
|
|
279
|
+
</div>
|
|
280
|
+
{/if}
|
|
281
|
+
</div>
|
|
282
|
+
|
|
283
|
+
{#snippet footer()}
|
|
284
|
+
<div class="flex items-center justify-end gap-3">
|
|
285
|
+
<GlassButton variant="ghost" onclick={handleClose} disabled={isExporting}>
|
|
286
|
+
Cancel
|
|
287
|
+
</GlassButton>
|
|
288
|
+
<GlassButton
|
|
289
|
+
variant="accent"
|
|
290
|
+
onclick={handleExport}
|
|
291
|
+
disabled={isExporting}
|
|
292
|
+
class="min-w-[120px]"
|
|
293
|
+
>
|
|
294
|
+
{#if isExporting}
|
|
295
|
+
<Loader2 class="w-4 h-4 animate-spin" />
|
|
296
|
+
<span>Exporting...</span>
|
|
297
|
+
{:else}
|
|
298
|
+
<Download />
|
|
299
|
+
<span>Export</span>
|
|
300
|
+
{/if}
|
|
301
|
+
</GlassButton>
|
|
302
|
+
</div>
|
|
303
|
+
{/snippet}
|
|
304
|
+
</GlassCard>
|
|
305
|
+
</div>
|
|
306
|
+
</div>
|
|
307
|
+
{/if}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terrarium Export Dialog
|
|
3
|
+
*
|
|
4
|
+
* Modal dialog for exporting scenes as PNG images.
|
|
5
|
+
* Provides scale and background options with estimated file size.
|
|
6
|
+
*/
|
|
7
|
+
interface Props {
|
|
8
|
+
open: boolean;
|
|
9
|
+
sceneName: string;
|
|
10
|
+
onClose: () => void;
|
|
11
|
+
onExport: (options: {
|
|
12
|
+
scale: number;
|
|
13
|
+
includeBackground: boolean;
|
|
14
|
+
}) => Promise<void>;
|
|
15
|
+
}
|
|
16
|
+
declare const ExportDialog: import("svelte").Component<Props, {}, "open">;
|
|
17
|
+
type ExportDialog = ReturnType<typeof ExportDialog>;
|
|
18
|
+
export default ExportDialog;
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
Grove — A place to Be
|
|
3
|
+
Copyright (c) 2025 Autumn Brown
|
|
4
|
+
Licensed under AGPL-3.0
|
|
5
|
+
-->
|
|
6
|
+
<script lang="ts">
|
|
7
|
+
import type { AssetCategory } from './types';
|
|
8
|
+
import type { Component as SvelteComponent } from 'svelte';
|
|
9
|
+
|
|
10
|
+
interface Props {
|
|
11
|
+
name: string;
|
|
12
|
+
category: AssetCategory;
|
|
13
|
+
onSelect: (name: string, category: AssetCategory) => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
let { name, category, onSelect }: Props = $props();
|
|
17
|
+
|
|
18
|
+
// Convert component name to display name (e.g., "TreePine" -> "Pine Tree")
|
|
19
|
+
const displayName = $derived(() => {
|
|
20
|
+
// Handle special cases
|
|
21
|
+
const specialCases: Record<string, string> = {
|
|
22
|
+
LatticeWithVine: 'Lattice with Vine'
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
if (specialCases[name]) {
|
|
26
|
+
return specialCases[name];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Split on capital letters and reverse tree names
|
|
30
|
+
const words = name.split(/(?=[A-Z])/).filter(Boolean);
|
|
31
|
+
|
|
32
|
+
// For trees, put the type first (e.g., TreePine -> Pine Tree)
|
|
33
|
+
if (words[0] === 'Tree' && words.length > 1) {
|
|
34
|
+
return `${words.slice(1).join(' ')} Tree`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// For everything else, just join with spaces
|
|
38
|
+
return words.join(' ');
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// Dynamically import the component for preview
|
|
42
|
+
let ComponentPreview: SvelteComponent | null = $state(null);
|
|
43
|
+
let isLoading = $state(true);
|
|
44
|
+
let loadError = $state(false);
|
|
45
|
+
|
|
46
|
+
// Load the component based on category
|
|
47
|
+
async function loadComponent() {
|
|
48
|
+
try {
|
|
49
|
+
isLoading = true;
|
|
50
|
+
loadError = false;
|
|
51
|
+
|
|
52
|
+
let module;
|
|
53
|
+
switch (category) {
|
|
54
|
+
case 'trees':
|
|
55
|
+
module = await import(`../nature/trees/${name}.svelte`);
|
|
56
|
+
break;
|
|
57
|
+
case 'creatures':
|
|
58
|
+
module = await import(`../nature/creatures/${name}.svelte`);
|
|
59
|
+
break;
|
|
60
|
+
case 'botanical':
|
|
61
|
+
module = await import(`../nature/botanical/${name}.svelte`);
|
|
62
|
+
break;
|
|
63
|
+
case 'ground':
|
|
64
|
+
module = await import(`../nature/ground/${name}.svelte`);
|
|
65
|
+
break;
|
|
66
|
+
case 'structural':
|
|
67
|
+
module = await import(`../nature/structural/${name}.svelte`);
|
|
68
|
+
break;
|
|
69
|
+
default:
|
|
70
|
+
throw new Error(`Unknown category: ${category}`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
ComponentPreview = module.default;
|
|
74
|
+
} catch (err) {
|
|
75
|
+
console.error(`Failed to load component ${name}:`, err);
|
|
76
|
+
loadError = true;
|
|
77
|
+
} finally {
|
|
78
|
+
isLoading = false;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Load component on mount
|
|
83
|
+
$effect(() => {
|
|
84
|
+
loadComponent();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// Handle selection
|
|
88
|
+
function handleClick() {
|
|
89
|
+
onSelect(name, category);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function handleKeydown(e: KeyboardEvent) {
|
|
93
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
94
|
+
e.preventDefault();
|
|
95
|
+
onSelect(name, category);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Handle drag start for drag-to-canvas
|
|
100
|
+
function handleDragStart(e: DragEvent) {
|
|
101
|
+
if (!e.dataTransfer) return;
|
|
102
|
+
|
|
103
|
+
e.dataTransfer.effectAllowed = 'copy';
|
|
104
|
+
e.dataTransfer.setData(
|
|
105
|
+
'application/json',
|
|
106
|
+
JSON.stringify({
|
|
107
|
+
name,
|
|
108
|
+
category
|
|
109
|
+
})
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
</script>
|
|
113
|
+
|
|
114
|
+
<button
|
|
115
|
+
class="palette-item group relative flex flex-col items-center gap-2 p-3 rounded-lg
|
|
116
|
+
bg-white/40 dark:bg-emerald-950/20 backdrop-blur-sm
|
|
117
|
+
border border-white/30 dark:border-emerald-800/20
|
|
118
|
+
hover:bg-white/60 dark:hover:bg-emerald-950/30
|
|
119
|
+
hover:border-white/50 dark:hover:border-emerald-700/30
|
|
120
|
+
hover:shadow-md
|
|
121
|
+
focus:outline-none focus:ring-2 focus:ring-accent focus:ring-offset-2
|
|
122
|
+
transition-all duration-200
|
|
123
|
+
cursor-pointer"
|
|
124
|
+
draggable="true"
|
|
125
|
+
tabindex="0"
|
|
126
|
+
onclick={handleClick}
|
|
127
|
+
onkeydown={handleKeydown}
|
|
128
|
+
ondragstart={handleDragStart}
|
|
129
|
+
aria-label={`Add ${displayName()} to canvas`}
|
|
130
|
+
>
|
|
131
|
+
<!-- Preview container -->
|
|
132
|
+
<div
|
|
133
|
+
class="w-16 h-16 flex items-center justify-center rounded-md
|
|
134
|
+
bg-gradient-to-b from-sky-100/50 to-emerald-50/50
|
|
135
|
+
dark:from-sky-950/30 dark:to-emerald-950/30
|
|
136
|
+
group-hover:from-sky-100/70 group-hover:to-emerald-50/70
|
|
137
|
+
dark:group-hover:from-sky-950/40 dark:group-hover:to-emerald-950/40
|
|
138
|
+
transition-colors duration-200"
|
|
139
|
+
>
|
|
140
|
+
{#if isLoading}
|
|
141
|
+
<div class="w-4 h-4 border-2 border-accent border-t-transparent rounded-full animate-spin"></div>
|
|
142
|
+
{:else if loadError}
|
|
143
|
+
<span class="text-xs text-red-500">Error</span>
|
|
144
|
+
{:else if ComponentPreview}
|
|
145
|
+
<svelte:component this={ComponentPreview} class="w-12 h-12" color="#2D5F3F" />
|
|
146
|
+
{/if}
|
|
147
|
+
</div>
|
|
148
|
+
|
|
149
|
+
<!-- Display name -->
|
|
150
|
+
<span
|
|
151
|
+
class="text-xs font-medium text-slate-700 dark:text-slate-300
|
|
152
|
+
group-hover:text-slate-900 dark:group-hover:text-slate-100
|
|
153
|
+
text-center leading-tight"
|
|
154
|
+
>
|
|
155
|
+
{displayName()}
|
|
156
|
+
</span>
|
|
157
|
+
</button>
|
|
158
|
+
|
|
159
|
+
<style>
|
|
160
|
+
.palette-item {
|
|
161
|
+
-moz-user-select: none;
|
|
162
|
+
user-select: none;
|
|
163
|
+
-webkit-user-select: none;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
.palette-item:active {
|
|
167
|
+
transform: scale(0.98);
|
|
168
|
+
}
|
|
169
|
+
</style>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { AssetCategory } from './types';
|
|
2
|
+
interface Props {
|
|
3
|
+
name: string;
|
|
4
|
+
category: AssetCategory;
|
|
5
|
+
onSelect: (name: string, category: AssetCategory) => void;
|
|
6
|
+
}
|
|
7
|
+
declare const PaletteItem: import("svelte").Component<Props, {}, "">;
|
|
8
|
+
type PaletteItem = ReturnType<typeof PaletteItem>;
|
|
9
|
+
export default PaletteItem;
|