@autumnsgrove/groveengine 0.8.6 → 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 (80) hide show
  1. package/dist/components/admin/GutterManager.svelte +213 -101
  2. package/dist/components/admin/MarkdownEditor.svelte +6 -3
  3. package/dist/components/custom/GutterItem.svelte +8 -2
  4. package/dist/components/quota/UpgradePrompt.svelte +1 -0
  5. package/dist/config/domain-blocklist.d.ts +59 -0
  6. package/dist/config/domain-blocklist.js +731 -0
  7. package/dist/config/index.d.ts +3 -1
  8. package/dist/config/index.js +2 -1
  9. package/dist/config/offensive-blocklist.d.ts +44 -0
  10. package/dist/config/offensive-blocklist.js +751 -0
  11. package/dist/config/terrarium.d.ts +109 -0
  12. package/dist/config/terrarium.js +125 -0
  13. package/dist/styles/tokens.css +90 -0
  14. package/dist/types/dom-to-image-more.d.ts +39 -0
  15. package/dist/ui/components/chrome/Footer.svelte +137 -0
  16. package/dist/ui/components/chrome/Footer.svelte.d.ts +11 -0
  17. package/dist/ui/components/chrome/FooterMinimal.svelte +75 -0
  18. package/dist/ui/components/chrome/FooterMinimal.svelte.d.ts +10 -0
  19. package/dist/ui/components/chrome/Header.svelte +113 -0
  20. package/dist/ui/components/chrome/Header.svelte.d.ts +11 -0
  21. package/dist/ui/components/chrome/HeaderMinimal.svelte +68 -0
  22. package/dist/ui/components/chrome/HeaderMinimal.svelte.d.ts +9 -0
  23. package/dist/ui/components/chrome/MobileMenu.svelte +145 -0
  24. package/dist/ui/components/chrome/MobileMenu.svelte.d.ts +9 -0
  25. package/dist/ui/components/chrome/ThemeToggle.svelte +34 -0
  26. package/dist/ui/components/chrome/ThemeToggle.svelte.d.ts +3 -0
  27. package/dist/ui/components/chrome/defaults.d.ts +6 -0
  28. package/dist/ui/components/chrome/defaults.js +65 -0
  29. package/dist/ui/components/chrome/index.d.ts +13 -0
  30. package/dist/ui/components/chrome/index.js +14 -0
  31. package/dist/ui/components/chrome/types.d.ts +19 -0
  32. package/dist/ui/components/chrome/types.js +8 -0
  33. package/dist/ui/components/content/RoadmapPreview.svelte +2 -1
  34. package/dist/ui/components/forms/ContentSearch.svelte +406 -0
  35. package/dist/ui/components/forms/ContentSearch.svelte.d.ts +71 -0
  36. package/dist/ui/components/forms/filterUtils.d.ts +138 -0
  37. package/dist/ui/components/forms/filterUtils.js +240 -0
  38. package/dist/ui/components/forms/index.d.ts +2 -0
  39. package/dist/ui/components/forms/index.js +5 -1
  40. package/dist/ui/components/gallery/ImageGallery.svelte +3 -0
  41. package/dist/ui/components/gallery/Lightbox.svelte +3 -0
  42. package/dist/ui/components/gallery/ZoomableImage.svelte +1 -0
  43. package/dist/ui/components/icons/index.d.ts +2 -1
  44. package/dist/ui/components/icons/index.js +14 -3
  45. package/dist/ui/components/icons/lucide.d.ts +213 -0
  46. package/dist/ui/components/icons/lucide.js +224 -0
  47. package/dist/ui/components/terrarium/AssetPalette.svelte +207 -0
  48. package/dist/ui/components/terrarium/AssetPalette.svelte.d.ts +7 -0
  49. package/dist/ui/components/terrarium/Canvas.svelte +231 -0
  50. package/dist/ui/components/terrarium/Canvas.svelte.d.ts +14 -0
  51. package/dist/ui/components/terrarium/ExportDialog.svelte +307 -0
  52. package/dist/ui/components/terrarium/ExportDialog.svelte.d.ts +18 -0
  53. package/dist/ui/components/terrarium/PaletteItem.svelte +169 -0
  54. package/dist/ui/components/terrarium/PaletteItem.svelte.d.ts +9 -0
  55. package/dist/ui/components/terrarium/PlacedAsset.svelte +222 -0
  56. package/dist/ui/components/terrarium/PlacedAsset.svelte.d.ts +11 -0
  57. package/dist/ui/components/terrarium/Terrarium.svelte +266 -0
  58. package/dist/ui/components/terrarium/Terrarium.svelte.d.ts +3 -0
  59. package/dist/ui/components/terrarium/Toolbar.svelte +299 -0
  60. package/dist/ui/components/terrarium/Toolbar.svelte.d.ts +24 -0
  61. package/dist/ui/components/terrarium/index.d.ts +31 -0
  62. package/dist/ui/components/terrarium/index.js +33 -0
  63. package/dist/ui/components/terrarium/terrariumState.svelte.d.ts +45 -0
  64. package/dist/ui/components/terrarium/terrariumState.svelte.js +291 -0
  65. package/dist/ui/components/terrarium/types.d.ts +139 -0
  66. package/dist/ui/components/terrarium/types.js +43 -0
  67. package/dist/ui/components/terrarium/utils/export.d.ts +48 -0
  68. package/dist/ui/components/terrarium/utils/export.js +148 -0
  69. package/dist/ui/components/ui/CollapsibleSection.svelte +2 -0
  70. package/dist/ui/components/ui/GlassConfirmDialog.svelte +9 -0
  71. package/dist/ui/components/ui/GlassOverlay.svelte +2 -1
  72. package/dist/ui/components/ui/Input.svelte +9 -1
  73. package/dist/ui/components/ui/Input.svelte.d.ts +2 -0
  74. package/dist/ui/components/ui/Textarea.svelte +9 -1
  75. package/dist/ui/components/ui/Textarea.svelte.d.ts +2 -0
  76. package/dist/ui/stores/index.d.ts +6 -0
  77. package/dist/ui/stores/index.js +6 -0
  78. package/dist/ui/stores/season.d.ts +14 -0
  79. package/dist/ui/stores/season.js +65 -0
  80. package/package.json +27 -4
@@ -0,0 +1,224 @@
1
+ /**
2
+ * Shared Lucide icon registry for Grove Platform.
3
+ * Single source of truth for commonly used icons across all Grove apps.
4
+ *
5
+ * DO: Import icons from '@autumnsgrove/groveengine/ui/icons'
6
+ * DON'T: Import directly from 'lucide-svelte' in app components
7
+ *
8
+ * @example
9
+ * ```svelte
10
+ * import { stateIcons, navIcons } from '@autumnsgrove/groveengine/ui/icons';
11
+ *
12
+ * <svelte:component this={stateIcons.check} class="w-5 h-5" />
13
+ * ```
14
+ */
15
+ import {
16
+ // Navigation
17
+ Home, Info, Telescope, MapPin, CircleDollarSign, BookOpen, Trees, PenLine, ArrowRight, ArrowLeft, ChevronRight, ChevronLeft, ChevronDown, ExternalLink, LogIn,
18
+ // Features & Content
19
+ Mail, HardDrive, Palette, ShieldCheck, Shield, Cloud, SearchCode, Archive, Upload, MessagesSquare, MessageCircle, FileText, Tag, Rss, Eye, Github, Layers,
20
+ // Nature/Growth (Grove themed)
21
+ Sprout, Heart, Leaf, Flower2, TreeDeciduous, Crown,
22
+ // States & Feedback
23
+ Check, CheckCircle, X, Loader2, AlertTriangle, HelpCircle, Info as InfoIcon, Circle, Lock,
24
+ // Phases & Special
25
+ Gem, Sparkles, Star, Moon, Sun,
26
+ // Actions
27
+ Compass, Megaphone, Lightbulb, Download, Settings, Menu,
28
+ // Metrics
29
+ Clock, TrendingUp, TrendingDown, Activity, Users, ShieldUser, BarChart3,
30
+ // Pricing
31
+ Globe, CalendarDays, LifeBuoy, } from 'lucide-svelte';
32
+ // ============================================================================
33
+ // NAVIGATION ICONS
34
+ // ============================================================================
35
+ /** Icons for main navigation items */
36
+ export const navIcons = {
37
+ home: Home,
38
+ about: Info,
39
+ vision: Telescope,
40
+ roadmap: MapPin,
41
+ pricing: CircleDollarSign,
42
+ knowledge: BookOpen,
43
+ forest: Trees,
44
+ blog: PenLine,
45
+ arrow: ArrowRight,
46
+ arrowLeft: ArrowLeft,
47
+ chevron: ChevronRight,
48
+ chevronLeft: ChevronLeft,
49
+ chevronDown: ChevronDown,
50
+ external: ExternalLink,
51
+ login: LogIn,
52
+ github: Github,
53
+ };
54
+ // ============================================================================
55
+ // STATE & FEEDBACK ICONS
56
+ // ============================================================================
57
+ /** Icons for states: success, error, loading, etc. */
58
+ export const stateIcons = {
59
+ check: Check,
60
+ checkcircle: CheckCircle,
61
+ x: X,
62
+ loader: Loader2,
63
+ warning: AlertTriangle,
64
+ help: HelpCircle,
65
+ info: InfoIcon,
66
+ circle: Circle,
67
+ lock: Lock,
68
+ };
69
+ // ============================================================================
70
+ // PRICING & TIER ICONS
71
+ // ============================================================================
72
+ /** Icons for pricing tiers and feature comparison */
73
+ export const pricingIcons = {
74
+ // Tier icons (growth progression)
75
+ sprout: Sprout,
76
+ treedeciduous: TreeDeciduous,
77
+ trees: Trees,
78
+ crown: Crown,
79
+ // Feature row icons
80
+ penline: PenLine,
81
+ filetext: FileText,
82
+ harddrive: HardDrive,
83
+ palette: Palette,
84
+ flower2: Flower2,
85
+ messagecircle: MessageCircle,
86
+ globe: Globe,
87
+ searchcode: SearchCode,
88
+ mail: Mail,
89
+ lifebuoy: LifeBuoy,
90
+ calendardays: CalendarDays,
91
+ clock: Clock,
92
+ // Checkmark for feature availability
93
+ check: Check,
94
+ };
95
+ // ============================================================================
96
+ // CONTENT & FEATURE ICONS
97
+ // ============================================================================
98
+ /** Icons for features, tools, and content types */
99
+ export const featureIcons = {
100
+ mail: Mail,
101
+ harddrive: HardDrive,
102
+ palette: Palette,
103
+ shieldcheck: ShieldCheck,
104
+ shield: Shield,
105
+ cloud: Cloud,
106
+ searchcode: SearchCode,
107
+ archive: Archive,
108
+ upload: Upload,
109
+ messagessquare: MessagesSquare,
110
+ externallink: ExternalLink,
111
+ filetext: FileText,
112
+ tag: Tag,
113
+ rss: Rss,
114
+ eye: Eye,
115
+ download: Download,
116
+ layers: Layers,
117
+ };
118
+ // ============================================================================
119
+ // GROWTH & NATURE ICONS
120
+ // ============================================================================
121
+ /** Icons representing growth and nature (Grove themed) */
122
+ export const growthIcons = {
123
+ sprout: Sprout,
124
+ heart: Heart,
125
+ leaf: Leaf,
126
+ flower2: Flower2,
127
+ trees: Trees,
128
+ treedeciduous: TreeDeciduous,
129
+ };
130
+ // ============================================================================
131
+ // PHASE & DREAM ICONS
132
+ // ============================================================================
133
+ /** Icons for phases, refinement, and mystical/future content */
134
+ export const phaseIcons = {
135
+ gem: Gem,
136
+ sparkles: Sparkles,
137
+ star: Star,
138
+ moon: Moon,
139
+ sun: Sun,
140
+ sprout: Sprout,
141
+ };
142
+ // ============================================================================
143
+ // ACTION ICONS
144
+ // ============================================================================
145
+ /** Icons for user actions and processes */
146
+ export const actionIcons = {
147
+ compass: Compass,
148
+ megaphone: Megaphone,
149
+ lightbulb: Lightbulb,
150
+ download: Download,
151
+ settings: Settings,
152
+ menu: Menu,
153
+ trend: TrendingUp,
154
+ trenddown: TrendingDown,
155
+ arrow: ArrowRight,
156
+ };
157
+ // ============================================================================
158
+ // METRICS ICONS
159
+ // ============================================================================
160
+ /** Icons for analytics and metrics display */
161
+ export const metricsIcons = {
162
+ clock: Clock,
163
+ trending: TrendingUp,
164
+ trenddown: TrendingDown,
165
+ activity: Activity,
166
+ users: Users,
167
+ shield: ShieldUser,
168
+ barchart: BarChart3,
169
+ };
170
+ // ============================================================================
171
+ // UNIFIED EXPORT
172
+ // ============================================================================
173
+ /** All icons in one map (use specific maps above when possible) */
174
+ export const allIcons = {
175
+ ...navIcons,
176
+ ...stateIcons,
177
+ ...featureIcons,
178
+ ...growthIcons,
179
+ ...phaseIcons,
180
+ ...actionIcons,
181
+ ...metricsIcons,
182
+ };
183
+ // ============================================================================
184
+ // UTILITY FUNCTIONS
185
+ // ============================================================================
186
+ /**
187
+ * Get an icon from a specific map by key
188
+ * @example
189
+ * ```ts
190
+ * const icon = getIcon(stateIcons, 'check');
191
+ * ```
192
+ */
193
+ export function getIcon(map, key) {
194
+ return map[key];
195
+ }
196
+ /**
197
+ * Get an icon from the unified map
198
+ * @example
199
+ * ```ts
200
+ * const icon = getIconFromAll('check');
201
+ * ```
202
+ */
203
+ export function getIconFromAll(key) {
204
+ return allIcons[key];
205
+ }
206
+ // ============================================================================
207
+ // DIRECT EXPORTS (for convenience)
208
+ // ============================================================================
209
+ // Re-export commonly used icons directly for simple imports
210
+ export {
211
+ // Most commonly used
212
+ Check, CheckCircle, X, ArrowRight, ArrowLeft, MapPin,
213
+ // Growth icons
214
+ Sprout, Trees, TreeDeciduous, Crown, Flower2, Leaf, Heart,
215
+ // Navigation
216
+ Home, Menu, Settings, ExternalLink, ChevronDown, LogIn, Github,
217
+ // Features
218
+ Mail, HardDrive, Palette, Shield, Download, Rss, Eye, MessageCircle, Layers,
219
+ // States
220
+ Loader2, AlertTriangle, HelpCircle, Lock,
221
+ // Phase/Special
222
+ Sparkles,
223
+ // Metrics
224
+ Clock, TrendingUp, Users, Activity, };
@@ -0,0 +1,207 @@
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 { TERRARIUM_CONFIG } from '../../../config/terrarium';
8
+ import type { AssetCategory } from './types';
9
+ import PaletteItem from './PaletteItem.svelte';
10
+ import { Trees, Bug, Leaf, Mountain, Landmark, ChevronDown } from 'lucide-svelte';
11
+ import type { ComponentType } from 'svelte';
12
+
13
+ interface Props {
14
+ onAssetSelect: (name: string, category: AssetCategory) => void;
15
+ }
16
+
17
+ let { onAssetSelect }: Props = $props();
18
+
19
+ // Define category metadata
20
+ interface CategoryMeta {
21
+ name: string;
22
+ icon: ComponentType;
23
+ assets: readonly string[];
24
+ }
25
+
26
+ // Map starter assets to their categories
27
+ const categoriesMap: Record<AssetCategory, CategoryMeta> = {
28
+ trees: {
29
+ name: 'Trees',
30
+ icon: Trees,
31
+ assets: ['TreeAspen', 'TreeBirch']
32
+ },
33
+ creatures: {
34
+ name: 'Creatures',
35
+ icon: Bug,
36
+ assets: ['Butterfly', 'Firefly']
37
+ },
38
+ botanical: {
39
+ name: 'Botanical',
40
+ icon: Leaf,
41
+ assets: ['Vine']
42
+ },
43
+ ground: {
44
+ name: 'Ground',
45
+ icon: Mountain,
46
+ assets: ['Mushroom', 'Rock']
47
+ },
48
+ structural: {
49
+ name: 'Structural',
50
+ icon: Landmark,
51
+ assets: ['Lattice', 'LatticeWithVine', 'Lantern']
52
+ },
53
+ sky: {
54
+ name: 'Sky',
55
+ icon: Landmark,
56
+ assets: []
57
+ },
58
+ water: {
59
+ name: 'Water',
60
+ icon: Landmark,
61
+ assets: []
62
+ },
63
+ weather: {
64
+ name: 'Weather',
65
+ icon: Landmark,
66
+ assets: []
67
+ }
68
+ };
69
+
70
+ // Filter to only categories that have starter assets
71
+ const categories = $derived(
72
+ (Object.entries(categoriesMap) as [AssetCategory, CategoryMeta][])
73
+ .filter(([_, meta]) => meta.assets.length > 0)
74
+ .map(([key, meta]) => ({ key, ...meta }))
75
+ );
76
+
77
+ // Track which categories are expanded (all start expanded)
78
+ let expandedCategories = $state<Set<AssetCategory>>(
79
+ new Set(categories.map((c) => c.key))
80
+ );
81
+
82
+ function toggleCategory(category: AssetCategory) {
83
+ const newExpanded = new Set(expandedCategories);
84
+ if (newExpanded.has(category)) {
85
+ newExpanded.delete(category);
86
+ } else {
87
+ newExpanded.add(category);
88
+ }
89
+ expandedCategories = newExpanded;
90
+ }
91
+
92
+ function handleCategoryKeydown(e: KeyboardEvent, category: AssetCategory) {
93
+ if (e.key === 'Enter' || e.key === ' ') {
94
+ e.preventDefault();
95
+ toggleCategory(category);
96
+ }
97
+ }
98
+ </script>
99
+
100
+ <aside
101
+ class="asset-palette flex flex-col h-full w-full
102
+ bg-white/50 dark:bg-emerald-950/25
103
+ backdrop-blur-md
104
+ border-r border-white/40 dark:border-emerald-800/25
105
+ shadow-lg"
106
+ aria-label="Asset palette"
107
+ >
108
+ <!-- Header -->
109
+ <div class="flex-shrink-0 px-4 py-4 border-b border-white/40 dark:border-emerald-800/25">
110
+ <h2 class="text-lg font-semibold text-slate-900 dark:text-slate-100">Assets</h2>
111
+ <p class="text-xs text-slate-600 dark:text-slate-400 mt-1">
112
+ Drag or click to add to canvas
113
+ </p>
114
+ </div>
115
+
116
+ <!-- Scrollable categories -->
117
+ <div class="flex-1 overflow-y-auto overflow-x-hidden px-2 py-3">
118
+ <nav aria-label="Asset categories">
119
+ {#each categories as { key, name, icon: Icon, assets }}
120
+ <section class="mb-3" aria-labelledby={`category-${key}`}>
121
+ <!-- Category header -->
122
+ <button
123
+ id={`category-${key}`}
124
+ class="w-full flex items-center justify-between px-3 py-2 rounded-lg
125
+ text-sm font-medium text-slate-700 dark:text-slate-300
126
+ hover:bg-white/40 dark:hover:bg-emerald-950/30
127
+ focus:outline-none focus:ring-2 focus:ring-accent focus:ring-offset-1
128
+ transition-colors duration-150"
129
+ onclick={() => toggleCategory(key)}
130
+ onkeydown={(e) => handleCategoryKeydown(e, key)}
131
+ aria-expanded={expandedCategories.has(key)}
132
+ aria-controls={`category-content-${key}`}
133
+ >
134
+ <span class="flex items-center gap-2">
135
+ <Icon class="w-4 h-4" />
136
+ <span>{name}</span>
137
+ <span class="text-xs text-slate-500 dark:text-slate-500">
138
+ ({assets.length})
139
+ </span>
140
+ </span>
141
+ <ChevronDown
142
+ class="w-4 h-4 transition-transform duration-200 {expandedCategories.has(key)
143
+ ? 'rotate-180'
144
+ : ''}"
145
+ />
146
+ </button>
147
+
148
+ <!-- Category content -->
149
+ {#if expandedCategories.has(key)}
150
+ <div
151
+ id={`category-content-${key}`}
152
+ class="grid grid-cols-2 gap-2 px-2 py-2"
153
+ role="group"
154
+ aria-label={`${name} assets`}
155
+ >
156
+ {#each assets as assetName}
157
+ <PaletteItem name={assetName} category={key} onSelect={onAssetSelect} />
158
+ {/each}
159
+ </div>
160
+ {/if}
161
+ </section>
162
+ {/each}
163
+ </nav>
164
+ </div>
165
+
166
+ <!-- Footer hint -->
167
+ <div
168
+ class="flex-shrink-0 px-4 py-3 border-t border-white/40 dark:border-emerald-800/25
169
+ bg-white/30 dark:bg-emerald-950/20"
170
+ >
171
+ <p class="text-xs text-slate-600 dark:text-slate-400 text-center">
172
+ Use Tab and Arrow keys to navigate
173
+ </p>
174
+ </div>
175
+ </aside>
176
+
177
+ <style>
178
+ .asset-palette {
179
+ /* Ensure the palette is always visible and scrollable */
180
+ min-width: 280px;
181
+ max-width: 320px;
182
+ }
183
+
184
+ /* Custom scrollbar styling */
185
+ .asset-palette ::-webkit-scrollbar {
186
+ width: 8px;
187
+ }
188
+
189
+ .asset-palette ::-webkit-scrollbar-track {
190
+ background: transparent;
191
+ }
192
+
193
+ .asset-palette ::-webkit-scrollbar-thumb {
194
+ background: rgba(148, 163, 184, 0.3);
195
+ border-radius: 4px;
196
+ }
197
+
198
+ .asset-palette ::-webkit-scrollbar-thumb:hover {
199
+ background: rgba(148, 163, 184, 0.5);
200
+ }
201
+
202
+ /* Firefox scrollbar */
203
+ .asset-palette {
204
+ scrollbar-width: thin;
205
+ scrollbar-color: rgba(148, 163, 184, 0.3) transparent;
206
+ }
207
+ </style>
@@ -0,0 +1,7 @@
1
+ import type { AssetCategory } from './types';
2
+ interface Props {
3
+ onAssetSelect: (name: string, category: AssetCategory) => void;
4
+ }
5
+ declare const AssetPalette: import("svelte").Component<Props, {}, "">;
6
+ type AssetPalette = ReturnType<typeof AssetPalette>;
7
+ export default AssetPalette;
@@ -0,0 +1,231 @@
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 { TerrariumScene, PlacedAsset, Point } from './types';
8
+ import PlacedAssetComponent from './PlacedAsset.svelte';
9
+
10
+ interface Props {
11
+ scene: TerrariumScene;
12
+ selectedAssetId: string | null;
13
+ animationsEnabled?: boolean;
14
+ panOffset?: Point;
15
+ onAssetSelect?: (assetId: string | null) => void;
16
+ onAssetMove?: (assetId: string, position: Point) => void;
17
+ onCanvasClick?: () => void;
18
+ onPan?: (offset: Point) => void;
19
+ }
20
+
21
+ let {
22
+ scene,
23
+ selectedAssetId,
24
+ animationsEnabled = true,
25
+ panOffset = { x: 0, y: 0 },
26
+ onAssetSelect,
27
+ onAssetMove,
28
+ onCanvasClick,
29
+ onPan
30
+ }: Props = $props();
31
+
32
+ let canvasElement: HTMLDivElement | null = $state(null);
33
+ let isPanning = $state(false);
34
+ let panStart: Point = $state({ x: 0, y: 0 });
35
+ let offsetStart: Point = $state({ x: 0, y: 0 });
36
+ let isSpacePressed = $state(false);
37
+
38
+ // Sort assets by zIndex for proper layering
39
+ const sortedAssets = $derived(
40
+ [...scene.assets].sort((a, b) => a.zIndex - b.zIndex)
41
+ );
42
+
43
+ // Canvas transform style
44
+ const canvasTransform = $derived(
45
+ `translate(${panOffset.x}px, ${panOffset.y}px)`
46
+ );
47
+
48
+ function handleMouseDown(event: MouseEvent) {
49
+ // Middle mouse button or Space + left click for panning
50
+ const shouldPan = event.button === 1 || (isSpacePressed && event.button === 0);
51
+
52
+ if (shouldPan) {
53
+ event.preventDefault();
54
+ isPanning = true;
55
+ panStart = { x: event.clientX, y: event.clientY };
56
+ offsetStart = { ...panOffset };
57
+
58
+ if (canvasElement) {
59
+ canvasElement.style.cursor = 'grabbing';
60
+ }
61
+ } else if (event.button === 0 && !isSpacePressed) {
62
+ // Left click on empty canvas deselects
63
+ const target = event.target as HTMLElement;
64
+ if (target === canvasElement || target.closest('[data-canvas-background]')) {
65
+ onCanvasClick?.();
66
+ }
67
+ }
68
+ }
69
+
70
+ function handleMouseMove(event: MouseEvent) {
71
+ if (!isPanning) return;
72
+
73
+ const dx = event.clientX - panStart.x;
74
+ const dy = event.clientY - panStart.y;
75
+
76
+ const newOffset = {
77
+ x: offsetStart.x + dx,
78
+ y: offsetStart.y + dy
79
+ };
80
+
81
+ onPan?.(newOffset);
82
+ }
83
+
84
+ function handleMouseUp() {
85
+ if (isPanning) {
86
+ isPanning = false;
87
+ if (canvasElement) {
88
+ canvasElement.style.cursor = isSpacePressed ? 'grab' : 'default';
89
+ }
90
+ }
91
+ }
92
+
93
+ function handleKeyDown(event: KeyboardEvent) {
94
+ if (event.code === 'Space' && !isSpacePressed) {
95
+ isSpacePressed = true;
96
+ if (canvasElement && !isPanning) {
97
+ canvasElement.style.cursor = 'grab';
98
+ }
99
+ }
100
+ }
101
+
102
+ function handleKeyUp(event: KeyboardEvent) {
103
+ if (event.code === 'Space') {
104
+ isSpacePressed = false;
105
+ if (canvasElement && !isPanning) {
106
+ canvasElement.style.cursor = 'default';
107
+ }
108
+ }
109
+ }
110
+
111
+ function handleTouchStart(event: TouchEvent) {
112
+ if (event.touches.length === 2) {
113
+ // Two-finger touch for panning
114
+ event.preventDefault();
115
+ isPanning = true;
116
+ const touch = event.touches[0];
117
+ panStart = { x: touch.clientX, y: touch.clientY };
118
+ offsetStart = { ...panOffset };
119
+ }
120
+ }
121
+
122
+ function handleTouchMove(event: TouchEvent) {
123
+ if (!isPanning || event.touches.length !== 2) return;
124
+
125
+ const touch = event.touches[0];
126
+ const dx = touch.clientX - panStart.x;
127
+ const dy = touch.clientY - panStart.y;
128
+
129
+ const newOffset = {
130
+ x: offsetStart.x + dx,
131
+ y: offsetStart.y + dy
132
+ };
133
+
134
+ onPan?.(newOffset);
135
+ }
136
+
137
+ function handleTouchEnd() {
138
+ if (isPanning) {
139
+ isPanning = false;
140
+ }
141
+ }
142
+
143
+ function handleAssetSelect(assetId: string) {
144
+ onAssetSelect?.(assetId);
145
+ }
146
+
147
+ function handleAssetMove(assetId: string, position: Point) {
148
+ onAssetMove?.(assetId, position);
149
+ }
150
+
151
+ // Set up global event listeners for mouse and keyboard
152
+ $effect(() => {
153
+ const handleGlobalMouseMove = (e: MouseEvent) => handleMouseMove(e);
154
+ const handleGlobalMouseUp = () => handleMouseUp();
155
+
156
+ if (isPanning) {
157
+ window.addEventListener('mousemove', handleGlobalMouseMove);
158
+ window.addEventListener('mouseup', handleGlobalMouseUp);
159
+
160
+ return () => {
161
+ window.removeEventListener('mousemove', handleGlobalMouseMove);
162
+ window.removeEventListener('mouseup', handleGlobalMouseUp);
163
+ };
164
+ }
165
+ });
166
+
167
+ $effect(() => {
168
+ window.addEventListener('keydown', handleKeyDown);
169
+ window.addEventListener('keyup', handleKeyUp);
170
+
171
+ return () => {
172
+ window.removeEventListener('keydown', handleKeyDown);
173
+ window.removeEventListener('keyup', handleKeyUp);
174
+ };
175
+ });
176
+ </script>
177
+
178
+ <!-- svelte-ignore a11y_no_noninteractive_element_interactions a11y_no_noninteractive_tabindex -->
179
+ <div
180
+ bind:this={canvasElement}
181
+ class="relative w-full h-full overflow-hidden select-none"
182
+ role="application"
183
+ tabindex="0"
184
+ aria-label="Terrarium canvas workspace - Use arrow keys to pan, scroll to zoom"
185
+ onmousedown={handleMouseDown}
186
+ ontouchstart={handleTouchStart}
187
+ ontouchmove={handleTouchMove}
188
+ ontouchend={handleTouchEnd}
189
+ >
190
+ <!-- Canvas background -->
191
+ <div
192
+ data-canvas-background
193
+ class="absolute inset-0"
194
+ style="background: {scene.canvas.background}; width: {scene.canvas.width}px; height: {scene.canvas.height}px; transform: {canvasTransform};"
195
+ >
196
+ <!-- Grid overlay -->
197
+ {#if scene.canvas.gridEnabled}
198
+ <div
199
+ class="absolute inset-0 pointer-events-none"
200
+ style="
201
+ background-image:
202
+ linear-gradient(to right, rgba(0, 0, 0, 0.1) 1px, transparent 1px),
203
+ linear-gradient(to bottom, rgba(0, 0, 0, 0.1) 1px, transparent 1px);
204
+ background-size: {scene.canvas.gridSize}px {scene.canvas.gridSize}px;
205
+ "
206
+ />
207
+ {/if}
208
+
209
+ <!-- Placed assets -->
210
+ <div class="relative w-full h-full">
211
+ {#each sortedAssets as asset (asset.id)}
212
+ <PlacedAssetComponent
213
+ {asset}
214
+ isSelected={asset.id === selectedAssetId}
215
+ {animationsEnabled}
216
+ onSelect={() => handleAssetSelect(asset.id)}
217
+ onMove={(position) => handleAssetMove(asset.id, position)}
218
+ />
219
+ {/each}
220
+ </div>
221
+ </div>
222
+ </div>
223
+
224
+ <style>
225
+ /* Custom cursor styles */
226
+ [data-canvas-background] {
227
+ -moz-user-select: none;
228
+ user-select: none;
229
+ -webkit-user-select: none;
230
+ }
231
+ </style>
@@ -0,0 +1,14 @@
1
+ import type { TerrariumScene, Point } from './types';
2
+ interface Props {
3
+ scene: TerrariumScene;
4
+ selectedAssetId: string | null;
5
+ animationsEnabled?: boolean;
6
+ panOffset?: Point;
7
+ onAssetSelect?: (assetId: string | null) => void;
8
+ onAssetMove?: (assetId: string, position: Point) => void;
9
+ onCanvasClick?: () => void;
10
+ onPan?: (offset: Point) => void;
11
+ }
12
+ declare const Canvas: import("svelte").Component<Props, {}, "">;
13
+ type Canvas = ReturnType<typeof Canvas>;
14
+ export default Canvas;