@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,291 @@
1
+ /**
2
+ * Grove — A place to Be
3
+ * Copyright (c) 2025 Autumn Brown
4
+ * Licensed under AGPL-3.0
5
+ */
6
+ import { DEFAULT_SCENE } from './types';
7
+ import { TERRARIUM_CONFIG } from '../../../config/terrarium';
8
+ /**
9
+ * Calculate complexity cost of a placed asset
10
+ */
11
+ function calculateAssetComplexity(asset) {
12
+ let cost = TERRARIUM_CONFIG.complexity.weights.normal;
13
+ if (asset.animationEnabled) {
14
+ cost = TERRARIUM_CONFIG.complexity.weights.animated;
15
+ }
16
+ else if (asset.scale > 1.5 || asset.scale < 0.5) {
17
+ cost = TERRARIUM_CONFIG.complexity.weights.scaled;
18
+ }
19
+ return cost;
20
+ }
21
+ /**
22
+ * Calculate total scene complexity
23
+ */
24
+ function calculateSceneComplexity(assets) {
25
+ return assets.reduce((total, asset) => total + calculateAssetComplexity(asset), 0);
26
+ }
27
+ /**
28
+ * Create a new empty scene
29
+ */
30
+ function createEmptyScene() {
31
+ const now = new Date().toISOString();
32
+ return {
33
+ ...DEFAULT_SCENE,
34
+ id: crypto.randomUUID(),
35
+ createdAt: now,
36
+ updatedAt: now
37
+ };
38
+ }
39
+ /**
40
+ * Get the highest z-index in the scene
41
+ */
42
+ function getMaxZIndex(assets) {
43
+ if (assets.length === 0)
44
+ return 0;
45
+ return Math.max(...assets.map((a) => a.zIndex));
46
+ }
47
+ /**
48
+ * Create the Terrarium state manager
49
+ */
50
+ export function createTerrariumState() {
51
+ let scene = $state(createEmptyScene());
52
+ let selectedAssetId = $state(null);
53
+ let isDragging = $state(false);
54
+ let animationsEnabled = $state(true);
55
+ let panOffset = $state({ x: 0, y: 0 });
56
+ let toolMode = $state('select');
57
+ const selectedAsset = $derived(scene.assets.find((a) => a.id === selectedAssetId) ?? null);
58
+ const assetCount = $derived(scene.assets.length);
59
+ const complexityUsage = $derived(Math.min(calculateSceneComplexity(scene.assets) / TERRARIUM_CONFIG.complexity.maxComplexity, 1));
60
+ const canAddAsset = $derived(calculateSceneComplexity(scene.assets) < TERRARIUM_CONFIG.complexity.maxComplexity);
61
+ function addAsset(componentName, category, position) {
62
+ // Guard: enforce complexity budget
63
+ if (!canAddAsset) {
64
+ return '';
65
+ }
66
+ // Validate position is within canvas bounds
67
+ const clampedPosition = {
68
+ x: Math.max(0, Math.min(position.x, scene.canvas.width)),
69
+ y: Math.max(0, Math.min(position.y, scene.canvas.height))
70
+ };
71
+ const id = crypto.randomUUID();
72
+ const maxZ = getMaxZIndex(scene.assets);
73
+ const newAsset = {
74
+ id,
75
+ componentName,
76
+ category,
77
+ position: clampedPosition,
78
+ scale: TERRARIUM_CONFIG.asset.defaultScale,
79
+ rotation: 0,
80
+ zIndex: maxZ + 1,
81
+ props: {},
82
+ animationEnabled: animationsEnabled
83
+ };
84
+ scene.assets.push(newAsset);
85
+ scene.updatedAt = new Date().toISOString();
86
+ selectedAssetId = id;
87
+ return id;
88
+ }
89
+ function updateAsset(id, updates) {
90
+ const index = scene.assets.findIndex((a) => a.id === id);
91
+ if (index === -1)
92
+ return;
93
+ scene.assets[index] = {
94
+ ...scene.assets[index],
95
+ ...updates,
96
+ id,
97
+ position: updates.position
98
+ ? { ...updates.position }
99
+ : scene.assets[index].position,
100
+ props: updates.props ? { ...updates.props } : scene.assets[index].props
101
+ };
102
+ scene.updatedAt = new Date().toISOString();
103
+ }
104
+ function deleteAsset(id) {
105
+ const index = scene.assets.findIndex((a) => a.id === id);
106
+ if (index === -1)
107
+ return;
108
+ scene.assets.splice(index, 1);
109
+ scene.updatedAt = new Date().toISOString();
110
+ if (selectedAssetId === id) {
111
+ selectedAssetId = null;
112
+ }
113
+ }
114
+ function duplicateAsset(id) {
115
+ const asset = scene.assets.find((a) => a.id === id);
116
+ if (!asset)
117
+ return '';
118
+ const newId = crypto.randomUUID();
119
+ const maxZ = getMaxZIndex(scene.assets);
120
+ const offset = TERRARIUM_CONFIG.ui.duplicateOffset;
121
+ const duplicatedAsset = {
122
+ ...asset,
123
+ id: newId,
124
+ position: {
125
+ x: asset.position.x + offset,
126
+ y: asset.position.y + offset
127
+ },
128
+ zIndex: maxZ + 1,
129
+ props: { ...asset.props }
130
+ };
131
+ scene.assets.push(duplicatedAsset);
132
+ scene.updatedAt = new Date().toISOString();
133
+ selectedAssetId = newId;
134
+ return newId;
135
+ }
136
+ function selectAsset(id) {
137
+ selectedAssetId = id;
138
+ }
139
+ function moveLayer(id, direction) {
140
+ const index = scene.assets.findIndex((a) => a.id === id);
141
+ if (index === -1)
142
+ return;
143
+ const asset = scene.assets[index];
144
+ const currentZ = asset.zIndex;
145
+ if (direction === 'top') {
146
+ const maxZ = getMaxZIndex(scene.assets);
147
+ if (currentZ === maxZ)
148
+ return;
149
+ asset.zIndex = maxZ + 1;
150
+ }
151
+ else if (direction === 'bottom') {
152
+ const minZ = Math.min(...scene.assets.map((a) => a.zIndex));
153
+ if (currentZ === minZ)
154
+ return;
155
+ asset.zIndex = minZ - 1;
156
+ }
157
+ else if (direction === 'up') {
158
+ const higherAssets = scene.assets.filter((a) => a.zIndex > currentZ);
159
+ if (higherAssets.length === 0)
160
+ return;
161
+ const nextZ = Math.min(...higherAssets.map((a) => a.zIndex));
162
+ const swapAsset = scene.assets.find((a) => a.zIndex === nextZ);
163
+ if (swapAsset) {
164
+ swapAsset.zIndex = currentZ;
165
+ asset.zIndex = nextZ;
166
+ }
167
+ }
168
+ else if (direction === 'down') {
169
+ const lowerAssets = scene.assets.filter((a) => a.zIndex < currentZ);
170
+ if (lowerAssets.length === 0)
171
+ return;
172
+ const prevZ = Math.max(...lowerAssets.map((a) => a.zIndex));
173
+ const swapAsset = scene.assets.find((a) => a.zIndex === prevZ);
174
+ if (swapAsset) {
175
+ swapAsset.zIndex = currentZ;
176
+ asset.zIndex = prevZ;
177
+ }
178
+ }
179
+ scene.updatedAt = new Date().toISOString();
180
+ // Normalize z-indices periodically to prevent drift
181
+ normalizeZIndices();
182
+ }
183
+ /**
184
+ * Normalizes z-indices to sequential values (0, 1, 2, ...) to prevent
185
+ * accumulation of very large or negative values over time.
186
+ */
187
+ function normalizeZIndices() {
188
+ if (scene.assets.length === 0)
189
+ return;
190
+ // Sort by current z-index, then reassign sequential values
191
+ const sorted = [...scene.assets].sort((a, b) => a.zIndex - b.zIndex);
192
+ sorted.forEach((asset, index) => {
193
+ const original = scene.assets.find((a) => a.id === asset.id);
194
+ if (original) {
195
+ original.zIndex = index;
196
+ }
197
+ });
198
+ }
199
+ function setScene(newScene) {
200
+ scene = {
201
+ ...newScene,
202
+ canvas: { ...newScene.canvas },
203
+ assets: newScene.assets.map((asset) => ({
204
+ ...asset,
205
+ position: { ...asset.position },
206
+ props: { ...asset.props }
207
+ }))
208
+ };
209
+ selectedAssetId = null;
210
+ panOffset = { x: 0, y: 0 };
211
+ // Normalize z-indices when loading external data
212
+ normalizeZIndices();
213
+ }
214
+ function resetScene() {
215
+ scene = createEmptyScene();
216
+ selectedAssetId = null;
217
+ panOffset = { x: 0, y: 0 };
218
+ }
219
+ function toggleAnimations() {
220
+ animationsEnabled = !animationsEnabled;
221
+ for (const asset of scene.assets) {
222
+ asset.animationEnabled = animationsEnabled;
223
+ }
224
+ scene.updatedAt = new Date().toISOString();
225
+ }
226
+ function toggleGrid() {
227
+ scene.canvas.gridEnabled = !scene.canvas.gridEnabled;
228
+ scene.updatedAt = new Date().toISOString();
229
+ }
230
+ function setGridSize(size) {
231
+ scene.canvas.gridSize = size;
232
+ scene.updatedAt = new Date().toISOString();
233
+ }
234
+ function setPanOffset(offset) {
235
+ panOffset = { ...offset };
236
+ }
237
+ function setToolMode(mode) {
238
+ toolMode = mode;
239
+ if (mode === 'pan') {
240
+ selectedAssetId = null;
241
+ }
242
+ }
243
+ return {
244
+ get scene() {
245
+ return scene;
246
+ },
247
+ get selectedAssetId() {
248
+ return selectedAssetId;
249
+ },
250
+ get isDragging() {
251
+ return isDragging;
252
+ },
253
+ set isDragging(value) {
254
+ isDragging = value;
255
+ },
256
+ get animationsEnabled() {
257
+ return animationsEnabled;
258
+ },
259
+ get panOffset() {
260
+ return panOffset;
261
+ },
262
+ get toolMode() {
263
+ return toolMode;
264
+ },
265
+ get selectedAsset() {
266
+ return selectedAsset;
267
+ },
268
+ get assetCount() {
269
+ return assetCount;
270
+ },
271
+ get canAddAsset() {
272
+ return canAddAsset;
273
+ },
274
+ get complexityUsage() {
275
+ return complexityUsage;
276
+ },
277
+ addAsset,
278
+ updateAsset,
279
+ deleteAsset,
280
+ duplicateAsset,
281
+ selectAsset,
282
+ moveLayer,
283
+ setScene,
284
+ resetScene,
285
+ toggleAnimations,
286
+ toggleGrid,
287
+ setGridSize,
288
+ setPanOffset,
289
+ setToolMode
290
+ };
291
+ }
@@ -0,0 +1,139 @@
1
+ /**
2
+ * Terrarium Type Definitions
3
+ *
4
+ * Core types for the Terrarium creative canvas system.
5
+ */
6
+ import type { Component as SvelteComponent } from 'svelte';
7
+ export type AssetCategory = 'trees' | 'creatures' | 'botanical' | 'ground' | 'sky' | 'structural' | 'water' | 'weather';
8
+ export interface Point {
9
+ x: number;
10
+ y: number;
11
+ }
12
+ export interface Size {
13
+ width: number;
14
+ height: number;
15
+ }
16
+ export interface CanvasSettings {
17
+ width: number;
18
+ height: number;
19
+ background: string;
20
+ gridEnabled: boolean;
21
+ gridSize: 16 | 32 | 64;
22
+ }
23
+ export interface AssetMeta {
24
+ displayName: string;
25
+ category: AssetCategory;
26
+ isAnimated: boolean;
27
+ defaultSize: Size;
28
+ props: PropDefinition[];
29
+ }
30
+ export interface PropDefinition {
31
+ key: string;
32
+ label: string;
33
+ type: 'number' | 'boolean' | 'string' | 'select' | 'color';
34
+ min?: number;
35
+ max?: number;
36
+ step?: number;
37
+ options?: {
38
+ value: string;
39
+ label: string;
40
+ }[];
41
+ default: unknown;
42
+ }
43
+ export interface AssetDefinition extends AssetMeta {
44
+ name: string;
45
+ load: () => Promise<{
46
+ default: SvelteComponent;
47
+ }>;
48
+ }
49
+ export interface PlacedAsset {
50
+ id: string;
51
+ componentName: string;
52
+ category: AssetCategory;
53
+ position: Point;
54
+ scale: number;
55
+ rotation: number;
56
+ zIndex: number;
57
+ props: Record<string, unknown>;
58
+ animationEnabled: boolean;
59
+ }
60
+ export interface TerrariumScene {
61
+ id: string;
62
+ name: string;
63
+ version: 1;
64
+ canvas: CanvasSettings;
65
+ assets: PlacedAsset[];
66
+ createdAt: string;
67
+ updatedAt: string;
68
+ }
69
+ export type DecorationZone = 'header' | 'sidebar' | 'footer' | 'background';
70
+ export interface DecorationOptions {
71
+ opacity: number;
72
+ }
73
+ export interface Decoration {
74
+ id: string;
75
+ name: string;
76
+ zone: DecorationZone;
77
+ scene: TerrariumScene;
78
+ options: DecorationOptions;
79
+ thumbnail?: string;
80
+ authorId?: string;
81
+ isPublic: boolean;
82
+ createdAt: string;
83
+ }
84
+ export interface ExportOptions {
85
+ scale?: number;
86
+ backgroundColor?: string;
87
+ pauseAnimations?: boolean;
88
+ width?: number;
89
+ height?: number;
90
+ }
91
+ export interface DragState {
92
+ isDragging: boolean;
93
+ startPosition: Point;
94
+ currentPosition: Point;
95
+ assetId: string | null;
96
+ }
97
+ export interface PanState {
98
+ isPanning: boolean;
99
+ offset: Point;
100
+ startOffset: Point;
101
+ startMouse: Point;
102
+ }
103
+ export interface SelectionState {
104
+ selectedId: string | null;
105
+ isMultiSelect: boolean;
106
+ selectedIds: string[];
107
+ }
108
+ export type ToolMode = 'select' | 'pan' | 'place';
109
+ export interface ToolbarAction {
110
+ id: string;
111
+ label: string;
112
+ icon: string;
113
+ shortcut?: string;
114
+ action: () => void;
115
+ disabled?: boolean;
116
+ }
117
+ export declare const DEFAULT_SCENE: TerrariumScene;
118
+ export declare const CANVAS_BACKGROUNDS: readonly [{
119
+ readonly name: "Sky Gradient";
120
+ readonly value: "linear-gradient(to bottom, #87CEEB 0%, #E0F7FA 50%, #A8E6CF 100%)";
121
+ }, {
122
+ readonly name: "Forest Dawn";
123
+ readonly value: "linear-gradient(to bottom, #FEF3C7 0%, #FDE68A 30%, #34D399 100%)";
124
+ }, {
125
+ readonly name: "Night Sky";
126
+ readonly value: "linear-gradient(to bottom, #1E1B4B 0%, #312E81 50%, #4C1D95 100%)";
127
+ }, {
128
+ readonly name: "Sunset";
129
+ readonly value: "linear-gradient(to bottom, #FDE68A 0%, #FB923C 40%, #EC4899 100%)";
130
+ }, {
131
+ readonly name: "Transparent";
132
+ readonly value: "transparent";
133
+ }, {
134
+ readonly name: "White";
135
+ readonly value: "#FFFFFF";
136
+ }, {
137
+ readonly name: "Cream";
138
+ readonly value: "#FFFBEB";
139
+ }];
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Terrarium Type Definitions
3
+ *
4
+ * Core types for the Terrarium creative canvas system.
5
+ */
6
+ // Default scene for initialization
7
+ export const DEFAULT_SCENE = {
8
+ id: '',
9
+ name: 'Untitled Scene',
10
+ version: 1,
11
+ canvas: {
12
+ width: 1200,
13
+ height: 800,
14
+ background: 'linear-gradient(to bottom, #87CEEB 0%, #E0F7FA 50%, #A8E6CF 100%)',
15
+ gridEnabled: false,
16
+ gridSize: 32
17
+ },
18
+ assets: [],
19
+ createdAt: '',
20
+ updatedAt: ''
21
+ };
22
+ // Default canvas backgrounds
23
+ export const CANVAS_BACKGROUNDS = [
24
+ {
25
+ name: 'Sky Gradient',
26
+ value: 'linear-gradient(to bottom, #87CEEB 0%, #E0F7FA 50%, #A8E6CF 100%)'
27
+ },
28
+ {
29
+ name: 'Forest Dawn',
30
+ value: 'linear-gradient(to bottom, #FEF3C7 0%, #FDE68A 30%, #34D399 100%)'
31
+ },
32
+ {
33
+ name: 'Night Sky',
34
+ value: 'linear-gradient(to bottom, #1E1B4B 0%, #312E81 50%, #4C1D95 100%)'
35
+ },
36
+ {
37
+ name: 'Sunset',
38
+ value: 'linear-gradient(to bottom, #FDE68A 0%, #FB923C 40%, #EC4899 100%)'
39
+ },
40
+ { name: 'Transparent', value: 'transparent' },
41
+ { name: 'White', value: '#FFFFFF' },
42
+ { name: 'Cream', value: '#FFFBEB' }
43
+ ];
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Terrarium PNG Export Utilities
3
+ *
4
+ * Handles exporting Terrarium scenes as PNG images using dom-to-image-more.
5
+ * Supports pausing animations, custom scaling, and thumbnail generation.
6
+ *
7
+ * This file is part of Grove — A place to Be
8
+ * Copyright (c) 2025 Autumn Brown
9
+ *
10
+ * This program is free software: you can redistribute it and/or modify
11
+ * it under the terms of the GNU Affero General Public License as published by
12
+ * the Free Software Foundation, either version 3 of the License, or
13
+ * (at your option) any later version.
14
+ *
15
+ * This program is distributed in the hope that it will be useful,
16
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
17
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18
+ * GNU Affero General Public License for more details.
19
+ */
20
+ import type { ExportOptions } from '../types';
21
+ /**
22
+ * Sanitizes a filename by converting to lowercase and replacing
23
+ * non-alphanumeric characters with hyphens.
24
+ * Also truncates to a maximum length to prevent filesystem issues.
25
+ */
26
+ export declare function sanitizeFilename(name: string): string;
27
+ /**
28
+ * Triggers a download of a data URL with the specified filename.
29
+ */
30
+ export declare function downloadDataUrl(dataUrl: string, filename: string): void;
31
+ /**
32
+ * Generates a PNG data URL from a canvas element.
33
+ * Useful for creating thumbnails or previews.
34
+ *
35
+ * @param canvasElement - The HTML element to capture
36
+ * @param options - Export configuration options
37
+ * @returns Promise resolving to a data URL string
38
+ */
39
+ export declare function generateDataUrl(canvasElement: HTMLElement, options?: ExportOptions): Promise<string>;
40
+ /**
41
+ * Exports a Terrarium scene as a PNG file.
42
+ * Pauses animations during capture and triggers a download.
43
+ *
44
+ * @param canvasElement - The HTML element to capture
45
+ * @param sceneName - Name of the scene (used for filename)
46
+ * @param options - Export configuration options
47
+ */
48
+ export declare function exportSceneAsPNG(canvasElement: HTMLElement, sceneName: string, options?: ExportOptions): Promise<void>;
@@ -0,0 +1,148 @@
1
+ /**
2
+ * Terrarium PNG Export Utilities
3
+ *
4
+ * Handles exporting Terrarium scenes as PNG images using dom-to-image-more.
5
+ * Supports pausing animations, custom scaling, and thumbnail generation.
6
+ *
7
+ * This file is part of Grove — A place to Be
8
+ * Copyright (c) 2025 Autumn Brown
9
+ *
10
+ * This program is free software: you can redistribute it and/or modify
11
+ * it under the terms of the GNU Affero General Public License as published by
12
+ * the Free Software Foundation, either version 3 of the License, or
13
+ * (at your option) any later version.
14
+ *
15
+ * This program is distributed in the hope that it will be useful,
16
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
17
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18
+ * GNU Affero General Public License for more details.
19
+ */
20
+ import domtoimage from 'dom-to-image-more';
21
+ import { TERRARIUM_CONFIG } from '../../../../config/terrarium';
22
+ /**
23
+ * Pauses all CSS animations and transitions on an element and its children.
24
+ * Returns a cleanup function to restore animations.
25
+ */
26
+ function pauseAnimations(element) {
27
+ const originalStyles = new Map();
28
+ const allElements = [element, ...Array.from(element.querySelectorAll('*'))];
29
+ allElements.forEach((el) => {
30
+ const style = el.style.cssText;
31
+ originalStyles.set(el, style);
32
+ el.style.cssText += '; animation-play-state: paused !important; transition: none !important;';
33
+ });
34
+ return () => {
35
+ originalStyles.forEach((style, el) => {
36
+ el.style.cssText = style;
37
+ });
38
+ };
39
+ }
40
+ /**
41
+ * Waits for a specified number of milliseconds.
42
+ */
43
+ function wait(ms) {
44
+ return new Promise((resolve) => setTimeout(resolve, ms));
45
+ }
46
+ /**
47
+ * Verifies that animations are actually paused on an element and all its children.
48
+ * Uses getComputedStyle to check animation-play-state.
49
+ */
50
+ function verifyAnimationsPaused(element) {
51
+ const allElements = [element, ...Array.from(element.querySelectorAll('*'))];
52
+ return allElements.every((el) => {
53
+ const style = getComputedStyle(el);
54
+ return style.animationPlayState === 'paused' || style.animationPlayState === '';
55
+ });
56
+ }
57
+ /**
58
+ * Sanitizes a filename by converting to lowercase and replacing
59
+ * non-alphanumeric characters with hyphens.
60
+ * Also truncates to a maximum length to prevent filesystem issues.
61
+ */
62
+ export function sanitizeFilename(name) {
63
+ const maxLength = TERRARIUM_CONFIG.ui.filenameMaxLength;
64
+ const sanitized = name
65
+ .toLowerCase()
66
+ .replace(/[^a-z0-9]+/g, '-')
67
+ .replace(/-+/g, '-')
68
+ .replace(/^-|-$/g, '');
69
+ // Truncate to max length, ensuring we don't cut in the middle of a word
70
+ if (sanitized.length > maxLength) {
71
+ const truncated = sanitized.slice(0, maxLength);
72
+ // Find last hyphen to avoid cutting words
73
+ const lastHyphen = truncated.lastIndexOf('-');
74
+ return lastHyphen > maxLength / 2 ? truncated.slice(0, lastHyphen) : truncated;
75
+ }
76
+ return sanitized;
77
+ }
78
+ /**
79
+ * Triggers a download of a data URL with the specified filename.
80
+ */
81
+ export function downloadDataUrl(dataUrl, filename) {
82
+ const anchor = document.createElement('a');
83
+ anchor.href = dataUrl;
84
+ anchor.download = filename;
85
+ anchor.style.display = 'none';
86
+ document.body.appendChild(anchor);
87
+ anchor.click();
88
+ document.body.removeChild(anchor);
89
+ }
90
+ /**
91
+ * Generates a PNG data URL from a canvas element.
92
+ * Useful for creating thumbnails or previews.
93
+ *
94
+ * @param canvasElement - The HTML element to capture
95
+ * @param options - Export configuration options
96
+ * @returns Promise resolving to a data URL string
97
+ */
98
+ export async function generateDataUrl(canvasElement, options = {}) {
99
+ const { scale = TERRARIUM_CONFIG.export.defaultScale, backgroundColor, pauseAnimations: shouldPauseAnimations = true, width, height } = options;
100
+ let restoreAnimations = null;
101
+ try {
102
+ if (shouldPauseAnimations) {
103
+ restoreAnimations = pauseAnimations(canvasElement);
104
+ // Wait for animations to pause, using configurable timing
105
+ // Then verify they're actually paused before proceeding
106
+ await wait(TERRARIUM_CONFIG.ui.exportWaitMs);
107
+ if (!verifyAnimationsPaused(canvasElement)) {
108
+ // If still not paused, wait a bit longer for slower devices
109
+ await wait(TERRARIUM_CONFIG.ui.exportWaitMs);
110
+ }
111
+ }
112
+ const domToImageOptions = {
113
+ quality: 1.0
114
+ };
115
+ if (width !== undefined) {
116
+ domToImageOptions.width = width * scale;
117
+ }
118
+ if (height !== undefined) {
119
+ domToImageOptions.height = height * scale;
120
+ }
121
+ if (backgroundColor !== undefined) {
122
+ domToImageOptions.style = {
123
+ backgroundColor
124
+ };
125
+ }
126
+ const dataUrl = await domtoimage.toPng(canvasElement, domToImageOptions);
127
+ return dataUrl;
128
+ }
129
+ finally {
130
+ if (restoreAnimations) {
131
+ restoreAnimations();
132
+ }
133
+ }
134
+ }
135
+ /**
136
+ * Exports a Terrarium scene as a PNG file.
137
+ * Pauses animations during capture and triggers a download.
138
+ *
139
+ * @param canvasElement - The HTML element to capture
140
+ * @param sceneName - Name of the scene (used for filename)
141
+ * @param options - Export configuration options
142
+ */
143
+ export async function exportSceneAsPNG(canvasElement, sceneName, options = {}) {
144
+ const dataUrl = await generateDataUrl(canvasElement, options);
145
+ const sanitizedName = sanitizeFilename(sceneName);
146
+ const filename = `${sanitizedName || 'terrarium-scene'}.png`;
147
+ downloadDataUrl(dataUrl, filename);
148
+ }
@@ -2,18 +2,8 @@ export { default as FontProvider } from './FontProvider.svelte';
2
2
  export { default as Lexend } from './Lexend.svelte';
3
3
  export { default as Atkinson } from './Atkinson.svelte';
4
4
  export { default as OpenDyslexic } from './OpenDyslexic.svelte';
5
- export { default as Luciole } from './Luciole.svelte';
6
- export { default as Nunito } from './Nunito.svelte';
7
5
  export { default as Quicksand } from './Quicksand.svelte';
8
- export { default as Manrope } from './Manrope.svelte';
9
- export { default as InstrumentSans } from './InstrumentSans.svelte';
10
6
  export { default as PlusJakartaSans } from './PlusJakartaSans.svelte';
11
- export { default as Cormorant } from './Cormorant.svelte';
12
- export { default as BodoniModa } from './BodoniModa.svelte';
13
- export { default as Lora } from './Lora.svelte';
14
- export { default as EBGaramond } from './EBGaramond.svelte';
15
- export { default as Merriweather } from './Merriweather.svelte';
16
- export { default as Fraunces } from './Fraunces.svelte';
17
7
  export { default as IBMPlexMono } from './IBMPlexMono.svelte';
18
8
  export { default as Cozette } from './Cozette.svelte';
19
9
  export { default as Alagard } from './Alagard.svelte';