@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.
Files changed (109) hide show
  1. package/dist/components/WispPanel.svelte +0 -1
  2. package/dist/components/admin/GutterManager.svelte +213 -101
  3. package/dist/components/admin/MarkdownEditor.svelte +6 -3
  4. package/dist/components/custom/ContentWithGutter.svelte +7 -13
  5. package/dist/components/custom/GutterItem.svelte +8 -2
  6. package/dist/components/quota/UpgradePrompt.svelte +1 -0
  7. package/dist/config/domain-blocklist.d.ts +59 -0
  8. package/dist/config/domain-blocklist.js +731 -0
  9. package/dist/config/index.d.ts +3 -1
  10. package/dist/config/index.js +2 -1
  11. package/dist/config/offensive-blocklist.d.ts +44 -0
  12. package/dist/config/offensive-blocklist.js +751 -0
  13. package/dist/config/terrarium.d.ts +109 -0
  14. package/dist/config/terrarium.js +125 -0
  15. package/dist/styles/tokens.css +90 -0
  16. package/dist/types/dom-to-image-more.d.ts +39 -0
  17. package/dist/ui/components/chrome/Footer.svelte +137 -0
  18. package/dist/ui/components/chrome/Footer.svelte.d.ts +11 -0
  19. package/dist/ui/components/chrome/FooterMinimal.svelte +75 -0
  20. package/dist/ui/components/chrome/FooterMinimal.svelte.d.ts +10 -0
  21. package/dist/ui/components/chrome/Header.svelte +113 -0
  22. package/dist/ui/components/chrome/Header.svelte.d.ts +11 -0
  23. package/dist/ui/components/chrome/HeaderMinimal.svelte +68 -0
  24. package/dist/ui/components/chrome/HeaderMinimal.svelte.d.ts +9 -0
  25. package/dist/ui/components/chrome/MobileMenu.svelte +145 -0
  26. package/dist/ui/components/chrome/MobileMenu.svelte.d.ts +9 -0
  27. package/dist/ui/components/chrome/ThemeToggle.svelte +34 -0
  28. package/dist/ui/components/chrome/ThemeToggle.svelte.d.ts +3 -0
  29. package/dist/ui/components/chrome/defaults.d.ts +6 -0
  30. package/dist/ui/components/chrome/defaults.js +65 -0
  31. package/dist/ui/components/chrome/index.d.ts +13 -0
  32. package/dist/ui/components/chrome/index.js +14 -0
  33. package/dist/ui/components/chrome/types.d.ts +19 -0
  34. package/dist/ui/components/chrome/types.js +8 -0
  35. package/dist/ui/components/content/RoadmapPreview.svelte +2 -1
  36. package/dist/ui/components/forms/ContentSearch.svelte +406 -0
  37. package/dist/ui/components/forms/ContentSearch.svelte.d.ts +71 -0
  38. package/dist/ui/components/forms/SearchInput.svelte +0 -1
  39. package/dist/ui/components/forms/filterUtils.d.ts +138 -0
  40. package/dist/ui/components/forms/filterUtils.js +240 -0
  41. package/dist/ui/components/forms/index.d.ts +2 -0
  42. package/dist/ui/components/forms/index.js +5 -1
  43. package/dist/ui/components/gallery/ImageGallery.svelte +17 -3
  44. package/dist/ui/components/gallery/Lightbox.svelte +11 -3
  45. package/dist/ui/components/gallery/ZoomableImage.svelte +13 -2
  46. package/dist/ui/components/icons/index.d.ts +2 -1
  47. package/dist/ui/components/icons/index.js +14 -3
  48. package/dist/ui/components/icons/lucide.d.ts +213 -0
  49. package/dist/ui/components/icons/lucide.js +224 -0
  50. package/dist/ui/components/terrarium/AssetPalette.svelte +207 -0
  51. package/dist/ui/components/terrarium/AssetPalette.svelte.d.ts +7 -0
  52. package/dist/ui/components/terrarium/Canvas.svelte +231 -0
  53. package/dist/ui/components/terrarium/Canvas.svelte.d.ts +14 -0
  54. package/dist/ui/components/terrarium/ExportDialog.svelte +307 -0
  55. package/dist/ui/components/terrarium/ExportDialog.svelte.d.ts +18 -0
  56. package/dist/ui/components/terrarium/PaletteItem.svelte +169 -0
  57. package/dist/ui/components/terrarium/PaletteItem.svelte.d.ts +9 -0
  58. package/dist/ui/components/terrarium/PlacedAsset.svelte +222 -0
  59. package/dist/ui/components/terrarium/PlacedAsset.svelte.d.ts +11 -0
  60. package/dist/ui/components/terrarium/Terrarium.svelte +266 -0
  61. package/dist/ui/components/terrarium/Terrarium.svelte.d.ts +3 -0
  62. package/dist/ui/components/terrarium/Toolbar.svelte +299 -0
  63. package/dist/ui/components/terrarium/Toolbar.svelte.d.ts +24 -0
  64. package/dist/ui/components/terrarium/index.d.ts +31 -0
  65. package/dist/ui/components/terrarium/index.js +33 -0
  66. package/dist/ui/components/terrarium/terrariumState.svelte.d.ts +45 -0
  67. package/dist/ui/components/terrarium/terrariumState.svelte.js +291 -0
  68. package/dist/ui/components/terrarium/types.d.ts +139 -0
  69. package/dist/ui/components/terrarium/types.js +43 -0
  70. package/dist/ui/components/terrarium/utils/export.d.ts +48 -0
  71. package/dist/ui/components/terrarium/utils/export.js +148 -0
  72. package/dist/ui/components/typography/index.d.ts +0 -10
  73. package/dist/ui/components/typography/index.js +1 -12
  74. package/dist/ui/components/ui/CollapsibleSection.svelte +12 -0
  75. package/dist/ui/components/ui/GlassConfirmDialog.svelte +9 -0
  76. package/dist/ui/components/ui/GlassOverlay.svelte +2 -1
  77. package/dist/ui/components/ui/Input.svelte +9 -1
  78. package/dist/ui/components/ui/Input.svelte.d.ts +2 -0
  79. package/dist/ui/components/ui/Textarea.svelte +9 -1
  80. package/dist/ui/components/ui/Textarea.svelte.d.ts +2 -0
  81. package/dist/ui/stores/index.d.ts +6 -0
  82. package/dist/ui/stores/index.js +6 -0
  83. package/dist/ui/stores/season.d.ts +14 -0
  84. package/dist/ui/stores/season.js +65 -0
  85. package/dist/ui/tokens/fonts.d.ts +1 -1
  86. package/dist/ui/tokens/fonts.js +0 -126
  87. package/package.json +46 -22
  88. package/static/fonts/alagard.ttf +0 -0
  89. package/LICENSE +0 -378
  90. package/dist/ui/components/typography/BodoniModa.svelte +0 -17
  91. package/dist/ui/components/typography/BodoniModa.svelte.d.ts +0 -10
  92. package/dist/ui/components/typography/Cormorant.svelte +0 -17
  93. package/dist/ui/components/typography/Cormorant.svelte.d.ts +0 -10
  94. package/dist/ui/components/typography/EBGaramond.svelte +0 -17
  95. package/dist/ui/components/typography/EBGaramond.svelte.d.ts +0 -10
  96. package/dist/ui/components/typography/Fraunces.svelte +0 -17
  97. package/dist/ui/components/typography/Fraunces.svelte.d.ts +0 -10
  98. package/dist/ui/components/typography/InstrumentSans.svelte +0 -17
  99. package/dist/ui/components/typography/InstrumentSans.svelte.d.ts +0 -10
  100. package/dist/ui/components/typography/Lora.svelte +0 -17
  101. package/dist/ui/components/typography/Lora.svelte.d.ts +0 -10
  102. package/dist/ui/components/typography/Luciole.svelte +0 -17
  103. package/dist/ui/components/typography/Luciole.svelte.d.ts +0 -10
  104. package/dist/ui/components/typography/Manrope.svelte +0 -17
  105. package/dist/ui/components/typography/Manrope.svelte.d.ts +0 -10
  106. package/dist/ui/components/typography/Merriweather.svelte +0 -17
  107. package/dist/ui/components/typography/Merriweather.svelte.d.ts +0 -10
  108. package/dist/ui/components/typography/Nunito.svelte +0 -17
  109. 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;