@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,109 @@
1
+ /**
2
+ * Terrarium Configuration
3
+ *
4
+ * Central configuration for the Terrarium creative canvas.
5
+ * All limits, constraints, and settings in one place.
6
+ */
7
+ export declare const TERRARIUM_CONFIG: {
8
+ readonly scene: {
9
+ readonly maxNameLength: 100;
10
+ readonly maxSizeBytes: 1000000;
11
+ };
12
+ readonly complexity: {
13
+ readonly maxComplexity: 200;
14
+ readonly weights: {
15
+ readonly animated: 5;
16
+ readonly scaled: 2;
17
+ readonly normal: 1;
18
+ };
19
+ readonly warningThreshold: 0.8;
20
+ };
21
+ readonly canvas: {
22
+ readonly maxWidth: 4000;
23
+ readonly maxHeight: 4000;
24
+ readonly minWidth: 200;
25
+ readonly minHeight: 200;
26
+ readonly defaultWidth: 1200;
27
+ readonly defaultHeight: 800;
28
+ readonly gridSizes: readonly [16, 32, 64];
29
+ readonly defaultGridSize: 32;
30
+ };
31
+ readonly asset: {
32
+ readonly maxScale: 5;
33
+ readonly minScale: 0.1;
34
+ readonly defaultScale: 1;
35
+ };
36
+ readonly storage: {
37
+ readonly backend: "indexeddb";
38
+ readonly dbName: "terrarium";
39
+ readonly dbVersion: 1;
40
+ readonly maxSavedScenes: {
41
+ readonly free: 0;
42
+ readonly seedling: 5;
43
+ readonly sapling: 20;
44
+ readonly oak: 100;
45
+ readonly evergreen: number;
46
+ };
47
+ readonly maxDecorationsPerZone: {
48
+ readonly free: 0;
49
+ readonly seedling: 1;
50
+ readonly sapling: 3;
51
+ readonly oak: number;
52
+ readonly evergreen: number;
53
+ };
54
+ };
55
+ readonly export: {
56
+ readonly maxWidth: 4096;
57
+ readonly maxHeight: 4096;
58
+ readonly defaultScale: 2;
59
+ readonly format: "png";
60
+ readonly expectedTimeMs: {
61
+ readonly min: 1000;
62
+ readonly typical: 5000;
63
+ readonly max: 10000;
64
+ };
65
+ };
66
+ readonly autoSave: {
67
+ readonly enabled: true;
68
+ readonly debounceMs: 2000;
69
+ readonly maxIntervalMs: 30000;
70
+ readonly showIndicator: true;
71
+ };
72
+ readonly zones: {
73
+ readonly header: {
74
+ readonly recommendedAspectRatio: [number, number];
75
+ readonly maxHeight: 200;
76
+ readonly fitBehavior: "scale";
77
+ };
78
+ readonly sidebar: {
79
+ readonly recommendedAspectRatio: [number, number];
80
+ readonly maxHeight: 400;
81
+ readonly fitBehavior: "scale";
82
+ };
83
+ readonly footer: {
84
+ readonly recommendedAspectRatio: [number, number];
85
+ readonly maxHeight: 150;
86
+ readonly fitBehavior: "scale";
87
+ };
88
+ readonly background: {
89
+ readonly recommendedAspectRatio: null;
90
+ readonly maxHeight: null;
91
+ readonly fitBehavior: "cover";
92
+ };
93
+ };
94
+ readonly performance: {
95
+ readonly targetFPS: 60;
96
+ readonly maxAnimatedAssets: 20;
97
+ readonly dragThrottleMs: 16;
98
+ };
99
+ readonly starterAssets: readonly ["TreeAspen", "TreeBirch", "Lattice", "LatticeWithVine", "Lantern", "Butterfly", "Firefly", "Mushroom", "Rock", "Vine"];
100
+ readonly ui: {
101
+ readonly duplicateOffset: 20;
102
+ readonly exportWaitMs: 150;
103
+ readonly filenameMaxLength: 100;
104
+ };
105
+ };
106
+ export type TerrariumConfig = typeof TERRARIUM_CONFIG;
107
+ export type GridSize = (typeof TERRARIUM_CONFIG.canvas.gridSizes)[number];
108
+ export type DecorationZone = keyof typeof TERRARIUM_CONFIG.zones;
109
+ export type UserTier = keyof typeof TERRARIUM_CONFIG.storage.maxSavedScenes;
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Terrarium Configuration
3
+ *
4
+ * Central configuration for the Terrarium creative canvas.
5
+ * All limits, constraints, and settings in one place.
6
+ */
7
+ export const TERRARIUM_CONFIG = {
8
+ // Scene constraints
9
+ scene: {
10
+ maxNameLength: 100,
11
+ maxSizeBytes: 1_000_000 // 1MB JSON
12
+ },
13
+ // Complexity budget system (replaces hard asset limit)
14
+ // Total complexity cannot exceed maxComplexity
15
+ complexity: {
16
+ maxComplexity: 200,
17
+ weights: {
18
+ animated: 5, // Animated assets cost 5 points
19
+ scaled: 2, // Scale > 1.5 or < 0.5 costs 2 points
20
+ normal: 1 // Standard assets cost 1 point
21
+ },
22
+ warningThreshold: 0.8 // Warn at 80% budget
23
+ },
24
+ // Canvas constraints
25
+ canvas: {
26
+ maxWidth: 4000,
27
+ maxHeight: 4000,
28
+ minWidth: 200,
29
+ minHeight: 200,
30
+ defaultWidth: 1200,
31
+ defaultHeight: 800,
32
+ gridSizes: [16, 32, 64],
33
+ defaultGridSize: 32
34
+ },
35
+ // Asset constraints
36
+ asset: {
37
+ maxScale: 5,
38
+ minScale: 0.1,
39
+ defaultScale: 1
40
+ },
41
+ // Storage limits (per tier)
42
+ storage: {
43
+ backend: 'indexeddb',
44
+ dbName: 'terrarium',
45
+ dbVersion: 1,
46
+ maxSavedScenes: {
47
+ free: 0,
48
+ seedling: 5,
49
+ sapling: 20,
50
+ oak: 100,
51
+ evergreen: Infinity
52
+ },
53
+ maxDecorationsPerZone: {
54
+ free: 0,
55
+ seedling: 1,
56
+ sapling: 3,
57
+ oak: Infinity,
58
+ evergreen: Infinity
59
+ }
60
+ },
61
+ // Export settings
62
+ export: {
63
+ maxWidth: 4096,
64
+ maxHeight: 4096,
65
+ defaultScale: 2, // 2x for retina
66
+ format: 'png',
67
+ expectedTimeMs: { min: 1000, typical: 5000, max: 10000 }
68
+ },
69
+ // Auto-save settings
70
+ autoSave: {
71
+ enabled: true,
72
+ debounceMs: 2000, // Wait 2s after last change
73
+ maxIntervalMs: 30000, // Force save every 30s during activity
74
+ showIndicator: true // Show "Saving..." indicator
75
+ },
76
+ // Zone constraints for Foliage integration
77
+ zones: {
78
+ header: {
79
+ recommendedAspectRatio: [16, 3],
80
+ maxHeight: 200,
81
+ fitBehavior: 'scale'
82
+ },
83
+ sidebar: {
84
+ recommendedAspectRatio: [1, 2],
85
+ maxHeight: 400,
86
+ fitBehavior: 'scale'
87
+ },
88
+ footer: {
89
+ recommendedAspectRatio: [16, 2],
90
+ maxHeight: 150,
91
+ fitBehavior: 'scale'
92
+ },
93
+ background: {
94
+ recommendedAspectRatio: null,
95
+ maxHeight: null,
96
+ fitBehavior: 'cover'
97
+ }
98
+ },
99
+ // Performance
100
+ performance: {
101
+ targetFPS: 60,
102
+ maxAnimatedAssets: 20, // Warning threshold
103
+ dragThrottleMs: 16 // One frame
104
+ },
105
+ // Phase 1 starter assets (10 components)
106
+ // Note: All components must exist in packages/engine/src/lib/ui/components/nature/
107
+ starterAssets: [
108
+ 'TreeAspen',
109
+ 'TreeBirch',
110
+ 'Lattice',
111
+ 'LatticeWithVine',
112
+ 'Lantern',
113
+ 'Butterfly',
114
+ 'Firefly',
115
+ 'Mushroom',
116
+ 'Rock',
117
+ 'Vine'
118
+ ],
119
+ // Magic numbers centralized for maintainability
120
+ ui: {
121
+ duplicateOffset: 20, // Pixels to offset duplicated assets
122
+ exportWaitMs: 150, // Wait time for animations to pause before export
123
+ filenameMaxLength: 100 // Max sanitized filename length
124
+ }
125
+ };
@@ -25,6 +25,56 @@
25
25
  --color-bg-secondary: hsl(var(--secondary));
26
26
  --color-border: hsl(var(--border));
27
27
 
28
+ /* Grove overlay tokens - centralized green overlays for glassmorphism */
29
+ /* Base RGB values for color mixing */
30
+ --grove-green-rgb: 34, 197, 94;
31
+ --grove-green-light-rgb: 74, 222, 128;
32
+ --grove-green-dark-rgb: 74, 124, 89;
33
+ --grove-sage-rgb: 167, 199, 183;
34
+
35
+ /* Grove overlays - light mode (use grove green) */
36
+ --grove-overlay-5: rgba(34, 197, 94, 0.05);
37
+ --grove-overlay-8: rgba(34, 197, 94, 0.08);
38
+ --grove-overlay-10: rgba(34, 197, 94, 0.1);
39
+ --grove-overlay-12: rgba(34, 197, 94, 0.12);
40
+ --grove-overlay-15: rgba(34, 197, 94, 0.15);
41
+ --grove-overlay-18: rgba(34, 197, 94, 0.18);
42
+ --grove-overlay-20: rgba(34, 197, 94, 0.2);
43
+ --grove-overlay-25: rgba(34, 197, 94, 0.25);
44
+ --grove-overlay-30: rgba(34, 197, 94, 0.3);
45
+ --grove-overlay-35: rgba(34, 197, 94, 0.35);
46
+ --grove-overlay-70: rgba(34, 197, 94, 0.7);
47
+
48
+ /* Grove borders - light mode */
49
+ --grove-border-subtle: rgba(34, 197, 94, 0.1);
50
+ --grove-border: rgba(34, 197, 94, 0.2);
51
+ --grove-border-strong: rgba(34, 197, 94, 0.35);
52
+
53
+ /* Glass backgrounds - light mode */
54
+ --glass-bg: rgba(255, 255, 255, 0.85);
55
+ --glass-bg-medium: rgba(255, 255, 255, 0.6);
56
+ --glass-bg-subtle: rgba(255, 255, 255, 0.5);
57
+ --glass-border: rgba(255, 255, 255, 0.3);
58
+ --glass-border-strong: rgba(255, 255, 255, 0.4);
59
+
60
+ /* Slate glass (for some dark backgrounds in light mode) */
61
+ --slate-glass-bg: rgba(30, 41, 59, 0.5);
62
+ --slate-glass-border: rgba(71, 85, 105, 0.3);
63
+
64
+ /* Neutral overlays */
65
+ --overlay-dark-5: rgba(0, 0, 0, 0.05);
66
+ --overlay-dark-10: rgba(0, 0, 0, 0.1);
67
+ --overlay-dark-40: rgba(0, 0, 0, 0.4);
68
+ --overlay-light-5: rgba(255, 255, 255, 0.05);
69
+ --overlay-light-10: rgba(255, 255, 255, 0.1);
70
+ --overlay-light-20: rgba(255, 255, 255, 0.2);
71
+
72
+ /* Shadow tokens */
73
+ --shadow-sm: 0 4px 12px rgba(0, 0, 0, 0.08);
74
+ --shadow-md: 0 4px 24px rgba(0, 0, 0, 0.08);
75
+ --shadow-lg: 0 8px 24px rgba(0, 0, 0, 0.1);
76
+ --shadow-grove: 0 8px 32px rgba(34, 197, 94, 0.15);
77
+
28
78
  /* Dark mode values */
29
79
  --color-text-dark: hsl(var(--foreground));
30
80
  --color-text-muted-dark: hsl(var(--muted-foreground));
@@ -118,6 +168,46 @@
118
168
  --tag-bg: hsl(270 38% 55%);
119
169
  --tag-bg-hover: hsl(270 38% 49%);
120
170
 
171
+ /* Grove overlays - dark mode (use lighter grove green) */
172
+ --grove-overlay-5: rgba(74, 222, 128, 0.05);
173
+ --grove-overlay-8: rgba(74, 222, 128, 0.08);
174
+ --grove-overlay-10: rgba(74, 222, 128, 0.1);
175
+ --grove-overlay-12: rgba(74, 222, 128, 0.12);
176
+ --grove-overlay-15: rgba(74, 222, 128, 0.15);
177
+ --grove-overlay-18: rgba(74, 222, 128, 0.18);
178
+ --grove-overlay-20: rgba(74, 222, 128, 0.2);
179
+ --grove-overlay-25: rgba(74, 222, 128, 0.25);
180
+ --grove-overlay-30: rgba(74, 222, 128, 0.3);
181
+ --grove-overlay-35: rgba(74, 222, 128, 0.35);
182
+ --grove-overlay-70: rgba(74, 222, 128, 0.7);
183
+
184
+ /* Grove borders - dark mode */
185
+ --grove-border-subtle: rgba(74, 124, 89, 0.2);
186
+ --grove-border: rgba(74, 222, 128, 0.2);
187
+ --grove-border-strong: rgba(74, 222, 128, 0.35);
188
+
189
+ /* Glass backgrounds - dark mode (dark grove tint) */
190
+ --glass-bg: rgba(20, 30, 25, 0.92);
191
+ --glass-bg-medium: rgba(30, 45, 35, 0.6);
192
+ --glass-bg-subtle: rgba(30, 41, 59, 0.4);
193
+ --glass-border: rgba(74, 124, 89, 0.25);
194
+ --glass-border-strong: rgba(74, 124, 89, 0.3);
195
+
196
+ /* Slate glass - dark mode */
197
+ --slate-glass-bg: rgba(30, 41, 59, 0.9);
198
+ --slate-glass-border: rgba(71, 85, 105, 0.3);
199
+
200
+ /* Grove sage text - dark mode muted text (WCAG AA compliant) */
201
+ --grove-text-muted: rgba(167, 199, 183, 0.75);
202
+ --grove-text-subtle: rgba(167, 199, 183, 0.6);
203
+ --grove-text-strong: rgba(167, 199, 183, 0.9);
204
+
205
+ /* Shadow tokens - dark mode */
206
+ --shadow-sm: 0 4px 12px rgba(0, 0, 0, 0.2);
207
+ --shadow-md: 0 4px 24px rgba(0, 0, 0, 0.3);
208
+ --shadow-lg: 0 8px 24px rgba(0, 0, 0, 0.3);
209
+ --shadow-grove: 0 8px 32px rgba(74, 222, 128, 0.15);
210
+
121
211
  /* Dark mode component-specific overrides */
122
212
  /* Blog header */
123
213
  --blog-header-title: #5cb85f;
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Type declarations for dom-to-image-more
3
+ *
4
+ * Grove — A place to Be
5
+ * Copyright (c) 2025 Autumn Brown
6
+ * Licensed under AGPL-3.0
7
+ */
8
+
9
+ declare module 'dom-to-image-more' {
10
+ interface Options {
11
+ /** Filter out elements */
12
+ filter?: (node: Element) => boolean;
13
+ /** Background color */
14
+ bgcolor?: string;
15
+ /** Image width */
16
+ width?: number;
17
+ /** Image height */
18
+ height?: number;
19
+ /** Style to apply to the root element */
20
+ style?: Record<string, string>;
21
+ /** Image quality (0-1) */
22
+ quality?: number;
23
+ /** Cache bust */
24
+ cacheBust?: boolean;
25
+ /** Image placeholder */
26
+ imagePlaceholder?: string;
27
+ }
28
+
29
+ interface DomToImage {
30
+ toSvg(node: Node, options?: Options): Promise<string>;
31
+ toPng(node: Node, options?: Options): Promise<string>;
32
+ toJpeg(node: Node, options?: Options): Promise<string>;
33
+ toBlob(node: Node, options?: Options): Promise<Blob>;
34
+ toPixelData(node: Node, options?: Options): Promise<Uint8ClampedArray>;
35
+ }
36
+
37
+ const domtoimage: DomToImage;
38
+ export default domtoimage;
39
+ }
@@ -0,0 +1,137 @@
1
+ <script lang="ts">
2
+ import ThemeToggle from './ThemeToggle.svelte';
3
+ import { Logo } from '../nature';
4
+ import {
5
+ Github,
6
+ ExternalLink,
7
+ BookOpen,
8
+ MapPin,
9
+ Tag,
10
+ Telescope,
11
+ Mail,
12
+ PenLine,
13
+ Hammer,
14
+ Scroll,
15
+ Grape,
16
+ Trees
17
+ } from 'lucide-svelte';
18
+ import { seasonStore } from '../../stores/season';
19
+ import type { FooterLink, MaxWidth, Season } from './types';
20
+ import { DEFAULT_RESOURCE_LINKS, DEFAULT_CONNECT_LINKS, DEFAULT_LEGAL_LINKS } from './defaults';
21
+
22
+ interface Props {
23
+ resourceLinks?: FooterLink[];
24
+ connectLinks?: FooterLink[];
25
+ legalLinks?: FooterLink[];
26
+ season?: Season;
27
+ maxWidth?: MaxWidth;
28
+ }
29
+
30
+ let {
31
+ resourceLinks,
32
+ connectLinks,
33
+ legalLinks,
34
+ season,
35
+ maxWidth = 'default'
36
+ }: Props = $props();
37
+
38
+ const maxWidthClass = {
39
+ narrow: 'max-w-2xl',
40
+ default: 'max-w-4xl',
41
+ wide: 'max-w-5xl'
42
+ };
43
+
44
+ const resources = resourceLinks || DEFAULT_RESOURCE_LINKS;
45
+ const connect = connectLinks || DEFAULT_CONNECT_LINKS;
46
+ const legal = legalLinks || DEFAULT_LEGAL_LINKS;
47
+ </script>
48
+
49
+ <footer class="py-12 border-t border-default">
50
+ <div class="{maxWidthClass[maxWidth]} mx-auto px-6">
51
+ <!-- Three Column Layout (stacked on mobile) -->
52
+ <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-8 lg:gap-12 mb-10">
53
+ <!-- Column 1: Grove Brand -->
54
+ <div class="text-center sm:text-left">
55
+ <div class="flex items-center gap-2 justify-center sm:justify-start mb-3">
56
+ <Logo class="w-6 h-6" season={season || $seasonStore} />
57
+ <span class="text-xl font-serif text-foreground">Grove</span>
58
+ </div>
59
+ <p class="text-sm font-sans text-foreground-subtle italic mb-4">
60
+ A place to Be
61
+ </p>
62
+ <p class="text-sm font-sans text-foreground-subtle leading-relaxed">
63
+ A quiet corner of the internet where your words can grow and flourish.
64
+ </p>
65
+ </div>
66
+
67
+ <!-- Column 2: Resources -->
68
+ <div class="text-center sm:text-left">
69
+ <h3 class="text-sm font-sans font-medium text-foreground uppercase tracking-wide mb-4">Resources</h3>
70
+ <ul class="space-y-2.5 text-sm font-sans">
71
+ {#each resources as link}
72
+ <li>
73
+ <a href={link.href} class="inline-flex items-center gap-1.5 text-foreground-subtle hover:text-accent-muted transition-colors">
74
+ {#if link.icon}
75
+ <svelte:component this={link.icon} class="w-4 h-4" />
76
+ {/if}
77
+ {link.label}
78
+ </a>
79
+ </li>
80
+ {/each}
81
+ </ul>
82
+ </div>
83
+
84
+ <!-- Column 3: Connect -->
85
+ <div class="text-center sm:text-left">
86
+ <h3 class="text-sm font-sans font-medium text-foreground uppercase tracking-wide mb-4">Connect</h3>
87
+ <ul class="space-y-2.5 text-sm font-sans">
88
+ {#each connect as link}
89
+ <li>
90
+ <a
91
+ href={link.href}
92
+ target={link.external ? '_blank' : undefined}
93
+ rel={link.external ? 'noopener noreferrer' : undefined}
94
+ class="inline-flex items-center gap-1.5 text-foreground-subtle hover:text-accent-muted transition-colors"
95
+ >
96
+ {#if link.icon}
97
+ <svelte:component this={link.icon} class="w-4 h-4" />
98
+ {/if}
99
+ {link.label}
100
+ {#if link.external}
101
+ <ExternalLink class="w-3 h-3" />
102
+ {/if}
103
+ </a>
104
+ </li>
105
+ {/each}
106
+ </ul>
107
+ </div>
108
+ </div>
109
+
110
+ <!-- Bottom Bar -->
111
+ <div class="pt-6 border-t border-default">
112
+ <div class="flex flex-col sm:flex-row items-center justify-between gap-4">
113
+ <!-- Copyright & Legal Links -->
114
+ <div class="flex flex-wrap items-center justify-center sm:justify-start gap-x-4 gap-y-2 text-xs font-sans text-foreground-subtle">
115
+ <span>&copy; {new Date().getFullYear()} Autumn Brown</span>
116
+ <span class="text-divider">·</span>
117
+ <span>Made with care</span>
118
+ <span class="text-divider hidden sm:inline">·</span>
119
+ <a href="/credits" class="hover:text-accent-muted transition-colors">Credits</a>
120
+ <span class="text-divider">·</span>
121
+ {#each legal as link, index}
122
+ <a href={link.href} class="hover:text-accent-muted transition-colors">{link.label}</a>
123
+ {#if index < legal.length - 1}
124
+ <span class="text-divider">·</span>
125
+ {/if}
126
+ {/each}
127
+ </div>
128
+
129
+ <!-- Theme Toggle -->
130
+ <div class="flex items-center gap-2">
131
+ <span class="text-xs text-foreground-faint font-sans">Theme</span>
132
+ <ThemeToggle />
133
+ </div>
134
+ </div>
135
+ </div>
136
+ </div>
137
+ </footer>
@@ -0,0 +1,11 @@
1
+ import type { FooterLink, MaxWidth, Season } from './types';
2
+ interface Props {
3
+ resourceLinks?: FooterLink[];
4
+ connectLinks?: FooterLink[];
5
+ legalLinks?: FooterLink[];
6
+ season?: Season;
7
+ maxWidth?: MaxWidth;
8
+ }
9
+ declare const Footer: import("svelte").Component<Props, {}, "">;
10
+ type Footer = ReturnType<typeof Footer>;
11
+ export default Footer;
@@ -0,0 +1,75 @@
1
+ <script lang="ts">
2
+ /**
3
+ * Footer - Status page footer
4
+ *
5
+ * Minimal footer with links to main Grove site and support.
6
+ */
7
+ import { cn } from '../../../utils/cn';
8
+ import { Trees, Mail, ExternalLink } from 'lucide-svelte';
9
+ import type { FooterLink, MaxWidth } from './types';
10
+
11
+ interface Props {
12
+ class?: string;
13
+ links?: FooterLink[];
14
+ tagline?: string;
15
+ maxWidth?: MaxWidth;
16
+ }
17
+
18
+ let {
19
+ class: className,
20
+ links,
21
+ tagline = 'A clearing in the forest where you can see what\'s happening.',
22
+ maxWidth = 'default'
23
+ }: Props = $props();
24
+
25
+ const maxWidthClass = {
26
+ narrow: 'max-w-2xl',
27
+ default: 'max-w-4xl',
28
+ wide: 'max-w-5xl'
29
+ };
30
+
31
+ // Default links for status page
32
+ const DEFAULT_LINKS: FooterLink[] = [
33
+ { href: 'https://grove.place', label: 'grove.place', icon: Trees, external: true },
34
+ { href: '/feed', label: 'Subscribe via RSS' },
35
+ { href: 'mailto:support@grove.place', label: 'Contact Support', icon: Mail }
36
+ ];
37
+
38
+ const items = links || DEFAULT_LINKS;
39
+ </script>
40
+
41
+ <footer
42
+ class={cn(
43
+ 'mt-auto py-8 px-6',
44
+ 'border-t border-white/20 dark:border-slate-700/30',
45
+ 'bg-white/30 dark:bg-slate-900/30',
46
+ className
47
+ )}
48
+ >
49
+ <div class="{maxWidthClass[maxWidth]} mx-auto">
50
+ <!-- Links -->
51
+ <div class="flex flex-wrap justify-center gap-6 mb-6 text-sm">
52
+ {#each items as link}
53
+ <a
54
+ href={link.href}
55
+ target={link.external ? '_blank' : undefined}
56
+ rel={link.external ? 'noopener noreferrer' : undefined}
57
+ class="inline-flex items-center gap-1.5 text-foreground-muted hover:text-foreground transition-colors"
58
+ >
59
+ {#if link.icon}
60
+ <svelte:component this={link.icon} class="w-4 h-4" />
61
+ {/if}
62
+ {link.label}
63
+ {#if link.external}
64
+ <ExternalLink class="w-3 h-3 opacity-50" />
65
+ {/if}
66
+ </a>
67
+ {/each}
68
+ </div>
69
+
70
+ <!-- Copyright -->
71
+ <p class="text-center text-sm text-foreground-subtle">
72
+ {tagline}
73
+ </p>
74
+ </div>
75
+ </footer>
@@ -0,0 +1,10 @@
1
+ import type { FooterLink, MaxWidth } from './types';
2
+ interface Props {
3
+ class?: string;
4
+ links?: FooterLink[];
5
+ tagline?: string;
6
+ maxWidth?: MaxWidth;
7
+ }
8
+ declare const FooterMinimal: import("svelte").Component<Props, {}, "">;
9
+ type FooterMinimal = ReturnType<typeof FooterMinimal>;
10
+ export default FooterMinimal;