@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,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;
|