@autumnsgrove/groveengine 0.6.5 → 0.8.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/LICENSE +378 -0
- package/dist/auth/index.d.ts +1 -2
- package/dist/auth/index.js +8 -4
- package/dist/auth/session.d.ts +12 -31
- package/dist/auth/session.js +5 -103
- package/dist/components/custom/ContentWithGutter.svelte +22 -25
- package/dist/ui/components/content/RoadmapPreview.svelte +91 -0
- package/dist/ui/components/content/RoadmapPreview.svelte.d.ts +36 -0
- package/dist/ui/components/content/index.d.ts +1 -0
- package/dist/ui/components/content/index.js +1 -0
- package/dist/ui/components/nature/Logo.svelte +224 -0
- package/dist/ui/components/nature/Logo.svelte.d.ts +14 -0
- package/dist/ui/components/nature/botanical/Acorn.svelte +48 -0
- package/dist/ui/components/nature/botanical/Acorn.svelte.d.ts +8 -0
- package/dist/ui/components/nature/botanical/Berry.svelte +67 -0
- package/dist/ui/components/nature/botanical/Berry.svelte.d.ts +8 -0
- package/dist/ui/components/nature/botanical/DandelionPuff.svelte +98 -0
- package/dist/ui/components/nature/botanical/DandelionPuff.svelte.d.ts +8 -0
- package/dist/ui/components/nature/botanical/FallingLeavesLayer.svelte +170 -0
- package/dist/ui/components/nature/botanical/FallingLeavesLayer.svelte.d.ts +35 -0
- package/dist/ui/components/nature/botanical/FallingPetalsLayer.svelte +174 -0
- package/dist/ui/components/nature/botanical/FallingPetalsLayer.svelte.d.ts +25 -0
- package/dist/ui/components/nature/botanical/Leaf.svelte +77 -0
- package/dist/ui/components/nature/botanical/Leaf.svelte.d.ts +10 -0
- package/dist/ui/components/nature/botanical/LeafFalling.svelte +186 -0
- package/dist/ui/components/nature/botanical/LeafFalling.svelte.d.ts +22 -0
- package/dist/ui/components/nature/botanical/PetalFalling.svelte +266 -0
- package/dist/ui/components/nature/botanical/PetalFalling.svelte.d.ts +25 -0
- package/dist/ui/components/nature/botanical/PineCone.svelte +61 -0
- package/dist/ui/components/nature/botanical/PineCone.svelte.d.ts +7 -0
- package/dist/ui/components/nature/botanical/Vine.svelte +102 -0
- package/dist/ui/components/nature/botanical/Vine.svelte.d.ts +11 -0
- package/dist/ui/components/nature/botanical/index.d.ts +10 -0
- package/dist/ui/components/nature/botanical/index.js +11 -0
- package/dist/ui/components/nature/creatures/Bee.svelte +78 -0
- package/dist/ui/components/nature/creatures/Bee.svelte.d.ts +9 -0
- package/dist/ui/components/nature/creatures/Bird.svelte +94 -0
- package/dist/ui/components/nature/creatures/Bird.svelte.d.ts +11 -0
- package/dist/ui/components/nature/creatures/BirdFlying.svelte +83 -0
- package/dist/ui/components/nature/creatures/BirdFlying.svelte.d.ts +9 -0
- package/dist/ui/components/nature/creatures/Bluebird.svelte +95 -0
- package/dist/ui/components/nature/creatures/Bluebird.svelte.d.ts +12 -0
- package/dist/ui/components/nature/creatures/Butterfly.svelte +87 -0
- package/dist/ui/components/nature/creatures/Butterfly.svelte.d.ts +9 -0
- package/dist/ui/components/nature/creatures/Cardinal.svelte +95 -0
- package/dist/ui/components/nature/creatures/Cardinal.svelte.d.ts +12 -0
- package/dist/ui/components/nature/creatures/Chickadee.svelte +97 -0
- package/dist/ui/components/nature/creatures/Chickadee.svelte.d.ts +12 -0
- package/dist/ui/components/nature/creatures/Deer.svelte +95 -0
- package/dist/ui/components/nature/creatures/Deer.svelte.d.ts +9 -0
- package/dist/ui/components/nature/creatures/Firefly.svelte +111 -0
- package/dist/ui/components/nature/creatures/Firefly.svelte.d.ts +10 -0
- package/dist/ui/components/nature/creatures/Owl.svelte +91 -0
- package/dist/ui/components/nature/creatures/Owl.svelte.d.ts +9 -0
- package/dist/ui/components/nature/creatures/Rabbit.svelte +90 -0
- package/dist/ui/components/nature/creatures/Rabbit.svelte.d.ts +9 -0
- package/dist/ui/components/nature/creatures/Robin.svelte +98 -0
- package/dist/ui/components/nature/creatures/Robin.svelte.d.ts +12 -0
- package/dist/ui/components/nature/creatures/Squirrel.svelte +97 -0
- package/dist/ui/components/nature/creatures/Squirrel.svelte.d.ts +9 -0
- package/dist/ui/components/nature/creatures/index.d.ts +13 -0
- package/dist/ui/components/nature/creatures/index.js +14 -0
- package/dist/ui/components/nature/ground/Bush.svelte +57 -0
- package/dist/ui/components/nature/ground/Bush.svelte.d.ts +10 -0
- package/dist/ui/components/nature/ground/Crocus.svelte +83 -0
- package/dist/ui/components/nature/ground/Crocus.svelte.d.ts +12 -0
- package/dist/ui/components/nature/ground/Daffodil.svelte +75 -0
- package/dist/ui/components/nature/ground/Daffodil.svelte.d.ts +11 -0
- package/dist/ui/components/nature/ground/Fern.svelte +72 -0
- package/dist/ui/components/nature/ground/Fern.svelte.d.ts +10 -0
- package/dist/ui/components/nature/ground/FlowerWild.svelte +60 -0
- package/dist/ui/components/nature/ground/FlowerWild.svelte.d.ts +10 -0
- package/dist/ui/components/nature/ground/GrassTuft.svelte +49 -0
- package/dist/ui/components/nature/ground/GrassTuft.svelte.d.ts +10 -0
- package/dist/ui/components/nature/ground/Log.svelte +42 -0
- package/dist/ui/components/nature/ground/Log.svelte.d.ts +7 -0
- package/dist/ui/components/nature/ground/Mushroom.svelte +48 -0
- package/dist/ui/components/nature/ground/Mushroom.svelte.d.ts +9 -0
- package/dist/ui/components/nature/ground/MushroomCluster.svelte +41 -0
- package/dist/ui/components/nature/ground/MushroomCluster.svelte.d.ts +8 -0
- package/dist/ui/components/nature/ground/Rock.svelte +59 -0
- package/dist/ui/components/nature/ground/Rock.svelte.d.ts +8 -0
- package/dist/ui/components/nature/ground/Stump.svelte +44 -0
- package/dist/ui/components/nature/ground/Stump.svelte.d.ts +8 -0
- package/dist/ui/components/nature/ground/Tulip.svelte +79 -0
- package/dist/ui/components/nature/ground/Tulip.svelte.d.ts +11 -0
- package/dist/ui/components/nature/ground/index.d.ts +12 -0
- package/dist/ui/components/nature/ground/index.js +13 -0
- package/dist/ui/components/nature/index.d.ts +28 -0
- package/dist/ui/components/nature/index.js +38 -0
- package/dist/ui/components/nature/palette.d.ts +491 -0
- package/dist/ui/components/nature/palette.js +384 -0
- package/dist/ui/components/nature/sky/Cloud.svelte +122 -0
- package/dist/ui/components/nature/sky/Cloud.svelte.d.ts +11 -0
- package/dist/ui/components/nature/sky/CloudWispy.svelte +79 -0
- package/dist/ui/components/nature/sky/CloudWispy.svelte.d.ts +9 -0
- package/dist/ui/components/nature/sky/Moon.svelte +60 -0
- package/dist/ui/components/nature/sky/Moon.svelte.d.ts +9 -0
- package/dist/ui/components/nature/sky/Rainbow.svelte +101 -0
- package/dist/ui/components/nature/sky/Rainbow.svelte.d.ts +8 -0
- package/dist/ui/components/nature/sky/Star.svelte +84 -0
- package/dist/ui/components/nature/sky/Star.svelte.d.ts +10 -0
- package/dist/ui/components/nature/sky/StarCluster.svelte +85 -0
- package/dist/ui/components/nature/sky/StarCluster.svelte.d.ts +9 -0
- package/dist/ui/components/nature/sky/StarShooting.svelte +90 -0
- package/dist/ui/components/nature/sky/StarShooting.svelte.d.ts +9 -0
- package/dist/ui/components/nature/sky/Sun.svelte +70 -0
- package/dist/ui/components/nature/sky/Sun.svelte.d.ts +9 -0
- package/dist/ui/components/nature/sky/index.d.ts +8 -0
- package/dist/ui/components/nature/sky/index.js +9 -0
- package/dist/ui/components/nature/structural/Birdhouse.svelte +53 -0
- package/dist/ui/components/nature/structural/Birdhouse.svelte.d.ts +8 -0
- package/dist/ui/components/nature/structural/Bridge.svelte +65 -0
- package/dist/ui/components/nature/structural/Bridge.svelte.d.ts +7 -0
- package/dist/ui/components/nature/structural/FencePost.svelte +54 -0
- package/dist/ui/components/nature/structural/FencePost.svelte.d.ts +8 -0
- package/dist/ui/components/nature/structural/GardenGate.svelte +70 -0
- package/dist/ui/components/nature/structural/GardenGate.svelte.d.ts +8 -0
- package/dist/ui/components/nature/structural/Lantern.svelte +113 -0
- package/dist/ui/components/nature/structural/Lantern.svelte.d.ts +10 -0
- package/dist/ui/components/nature/structural/Lattice.svelte +89 -0
- package/dist/ui/components/nature/structural/Lattice.svelte.d.ts +8 -0
- package/dist/ui/components/nature/structural/LatticeWithVine.svelte +89 -0
- package/dist/ui/components/nature/structural/LatticeWithVine.svelte.d.ts +11 -0
- package/dist/ui/components/nature/structural/StonePath.svelte +48 -0
- package/dist/ui/components/nature/structural/StonePath.svelte.d.ts +7 -0
- package/dist/ui/components/nature/structural/index.d.ts +8 -0
- package/dist/ui/components/nature/structural/index.js +9 -0
- package/dist/ui/components/nature/trees/TreeAspen.svelte +163 -0
- package/dist/ui/components/nature/trees/TreeAspen.svelte.d.ts +11 -0
- package/dist/ui/components/nature/trees/TreeBirch.svelte +186 -0
- package/dist/ui/components/nature/trees/TreeBirch.svelte.d.ts +11 -0
- package/dist/ui/components/nature/trees/TreeCherry.svelte +108 -0
- package/dist/ui/components/nature/trees/TreeCherry.svelte.d.ts +11 -0
- package/dist/ui/components/nature/trees/TreePine.svelte +79 -0
- package/dist/ui/components/nature/trees/TreePine.svelte.d.ts +11 -0
- package/dist/ui/components/nature/trees/index.d.ts +4 -0
- package/dist/ui/components/nature/trees/index.js +5 -0
- package/dist/ui/components/nature/water/LilyPad.svelte +99 -0
- package/dist/ui/components/nature/water/LilyPad.svelte.d.ts +10 -0
- package/dist/ui/components/nature/water/Pond.svelte +104 -0
- package/dist/ui/components/nature/water/Pond.svelte.d.ts +8 -0
- package/dist/ui/components/nature/water/Reeds.svelte +85 -0
- package/dist/ui/components/nature/water/Reeds.svelte.d.ts +11 -0
- package/dist/ui/components/nature/water/Stream.svelte +98 -0
- package/dist/ui/components/nature/water/Stream.svelte.d.ts +8 -0
- package/dist/ui/components/nature/water/index.d.ts +4 -0
- package/dist/ui/components/nature/water/index.js +5 -0
- package/dist/ui/components/nature/weather/SnowfallLayer.svelte +175 -0
- package/dist/ui/components/nature/weather/SnowfallLayer.svelte.d.ts +25 -0
- package/dist/ui/components/nature/weather/Snowflake.svelte +99 -0
- package/dist/ui/components/nature/weather/Snowflake.svelte.d.ts +11 -0
- package/dist/ui/components/nature/weather/SnowflakeFalling.svelte +162 -0
- package/dist/ui/components/nature/weather/SnowflakeFalling.svelte.d.ts +23 -0
- package/dist/ui/components/nature/weather/index.d.ts +3 -0
- package/dist/ui/components/nature/weather/index.js +4 -0
- package/dist/ui/components/ui/Glass.svelte +158 -0
- package/dist/ui/components/ui/Glass.svelte.d.ts +52 -0
- package/dist/ui/components/ui/GlassButton.svelte +157 -0
- package/dist/ui/components/ui/GlassButton.svelte.d.ts +39 -0
- package/dist/ui/components/ui/GlassCard.svelte +160 -0
- package/dist/ui/components/ui/GlassCard.svelte.d.ts +39 -0
- package/dist/ui/components/ui/GlassConfirmDialog.svelte +208 -0
- package/dist/ui/components/ui/GlassConfirmDialog.svelte.d.ts +52 -0
- package/dist/ui/components/ui/GlassLogo.svelte +422 -0
- package/dist/ui/components/ui/GlassLogo.svelte.d.ts +23 -0
- package/dist/ui/components/ui/GlassNavbar.svelte +120 -0
- package/dist/ui/components/ui/GlassNavbar.svelte.d.ts +42 -0
- package/dist/ui/components/ui/GlassOverlay.svelte +93 -0
- package/dist/ui/components/ui/GlassOverlay.svelte.d.ts +33 -0
- package/dist/ui/components/ui/Logo.svelte +47 -52
- package/dist/ui/components/ui/Logo.svelte.d.ts +4 -3
- package/dist/ui/components/ui/index.d.ts +7 -0
- package/dist/ui/components/ui/index.js +8 -0
- package/dist/ui/styles/grove.css +151 -1
- package/dist/utils/gutter.d.ts +2 -8
- package/dist/utils/markdown.d.ts +1 -0
- package/dist/utils/markdown.js +32 -11
- package/package.json +31 -22
- package/static/fonts/alagard.ttf +0 -0
- package/static/robots.txt +34 -1
- package/dist/auth/jwt.d.ts +0 -20
- package/dist/auth/jwt.js +0 -123
|
@@ -64,7 +64,7 @@
|
|
|
64
64
|
* Helper to get anchor key with headers context
|
|
65
65
|
* @param {string} anchor
|
|
66
66
|
*/
|
|
67
|
-
function getKey(anchor) {
|
|
67
|
+
function getKey(anchor: string) {
|
|
68
68
|
return getAnchorKey(anchor, headers);
|
|
69
69
|
}
|
|
70
70
|
|
|
@@ -72,16 +72,16 @@
|
|
|
72
72
|
* Get items for a specific anchor
|
|
73
73
|
* @param {string} anchor
|
|
74
74
|
*/
|
|
75
|
-
function getItems(anchor) {
|
|
75
|
+
function getItems(anchor: string) {
|
|
76
76
|
return getItemsForAnchor(gutterContent, anchor);
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
/**
|
|
80
80
|
* Generate unique key for a gutter item
|
|
81
|
-
* @param {
|
|
81
|
+
* @param {GutterItemType} item
|
|
82
82
|
* @param {number} index
|
|
83
83
|
*/
|
|
84
|
-
function getItemKey(item, index) {
|
|
84
|
+
function getItemKey(item: GutterItemType, index: number): string {
|
|
85
85
|
// Combine item properties to create a unique identifier
|
|
86
86
|
const parts = [
|
|
87
87
|
item.type || 'unknown',
|
|
@@ -103,16 +103,15 @@
|
|
|
103
103
|
|
|
104
104
|
// Use getBoundingClientRect for accurate relative positioning
|
|
105
105
|
// This works regardless of offset parent chains and CSS transforms
|
|
106
|
-
const gutterRect = gutterElement.getBoundingClientRect();
|
|
106
|
+
const gutterRect = (gutterElement as HTMLElement).getBoundingClientRect();
|
|
107
107
|
|
|
108
108
|
let lastBottom = 0; // Track the bottom edge of the last positioned item
|
|
109
|
-
|
|
110
|
-
const newOverflowingAnchors = [];
|
|
109
|
+
const newOverflowingAnchors: string[] = [];
|
|
111
110
|
const newPositions = { ...itemPositions };
|
|
112
111
|
|
|
113
112
|
// Sort anchors by their position in the document
|
|
114
113
|
const anchorPositions = uniqueAnchors.map(anchor => {
|
|
115
|
-
const el = findAnchorElement(anchor, contentBodyElement ?? null, headers);
|
|
114
|
+
const el = findAnchorElement(anchor, (contentBodyElement ?? null) as HTMLElement | null, headers);
|
|
116
115
|
if (!el && import.meta.env.DEV) {
|
|
117
116
|
console.warn(`Anchor element not found for: ${anchor}`);
|
|
118
117
|
}
|
|
@@ -168,8 +167,7 @@
|
|
|
168
167
|
|
|
169
168
|
// Setup resize listener on mount with proper cleanup
|
|
170
169
|
onMount(() => {
|
|
171
|
-
|
|
172
|
-
let resizeTimeoutId;
|
|
170
|
+
let resizeTimeoutId: ReturnType<typeof setTimeout> | undefined;
|
|
173
171
|
const handleResize = () => {
|
|
174
172
|
clearTimeout(resizeTimeoutId);
|
|
175
173
|
resizeTimeoutId = setTimeout(() => {
|
|
@@ -186,9 +184,8 @@
|
|
|
186
184
|
|
|
187
185
|
// Setup copy button functionality for code blocks
|
|
188
186
|
onMount(() => {
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
const button = /** @type {HTMLElement} */ (event.currentTarget);
|
|
187
|
+
const handleCopyClick = async (event: Event) => {
|
|
188
|
+
const button = event.currentTarget as HTMLElement;
|
|
192
189
|
const codeText = button.getAttribute('data-code');
|
|
193
190
|
|
|
194
191
|
if (!codeText) return;
|
|
@@ -251,20 +248,19 @@
|
|
|
251
248
|
// Add IDs to headers and position mobile gutter items
|
|
252
249
|
$effect(() => {
|
|
253
250
|
// Track moved elements for cleanup
|
|
254
|
-
|
|
255
|
-
const movedElements = [];
|
|
251
|
+
const movedElements: Array<{ element: HTMLElement; originalParent: HTMLElement | null; originalNextSibling: Node | null }> = [];
|
|
256
252
|
|
|
257
253
|
untrack(() => {
|
|
258
254
|
if (!contentBodyElement) return;
|
|
259
255
|
|
|
260
256
|
// First, add IDs to headers
|
|
261
257
|
if (headers && headers.length > 0) {
|
|
262
|
-
const headerElements = contentBodyElement.querySelectorAll('h1, h2, h3, h4, h5, h6');
|
|
263
|
-
headerElements.forEach((el) => {
|
|
264
|
-
const text = el.textContent?.trim() || '';
|
|
265
|
-
const matchingHeader = headers.find(
|
|
258
|
+
const headerElements = (contentBodyElement as HTMLElement).querySelectorAll('h1, h2, h3, h4, h5, h6');
|
|
259
|
+
headerElements.forEach((el: Element) => {
|
|
260
|
+
const text = (el as HTMLElement).textContent?.trim() || '';
|
|
261
|
+
const matchingHeader = headers.find((h: Header) => h.text === text);
|
|
266
262
|
if (matchingHeader) {
|
|
267
|
-
el.id = matchingHeader.id;
|
|
263
|
+
(el as HTMLElement).id = matchingHeader.id;
|
|
268
264
|
}
|
|
269
265
|
});
|
|
270
266
|
}
|
|
@@ -279,7 +275,7 @@
|
|
|
279
275
|
const originalParent = mobileGutterEl.parentElement;
|
|
280
276
|
const originalNextSibling = mobileGutterEl.nextSibling;
|
|
281
277
|
|
|
282
|
-
const targetEl = findAnchorElement(anchor, contentBodyElement, headers);
|
|
278
|
+
const targetEl = findAnchorElement(anchor, contentBodyElement as HTMLElement, headers);
|
|
283
279
|
|
|
284
280
|
if (targetEl) {
|
|
285
281
|
targetEl.insertAdjacentElement('afterend', mobileGutterEl);
|
|
@@ -308,12 +304,13 @@
|
|
|
308
304
|
const updateHeight = () => {
|
|
309
305
|
if (!contentBodyElement) return;
|
|
310
306
|
// Get the bottom of content-body relative to the article
|
|
311
|
-
const rect = contentBodyElement.getBoundingClientRect();
|
|
312
|
-
const
|
|
307
|
+
const rect = (contentBodyElement as HTMLElement).getBoundingClientRect();
|
|
308
|
+
const articleEl = (contentBodyElement as HTMLElement).closest('.content-article');
|
|
309
|
+
const articleRect = articleEl?.getBoundingClientRect();
|
|
313
310
|
if (articleRect) {
|
|
314
311
|
contentHeight = rect.bottom - articleRect.top;
|
|
315
312
|
} else {
|
|
316
|
-
contentHeight = contentBodyElement.offsetTop + contentBodyElement.offsetHeight;
|
|
313
|
+
contentHeight = (contentBodyElement as HTMLElement).offsetTop + (contentBodyElement as HTMLElement).offsetHeight;
|
|
317
314
|
}
|
|
318
315
|
};
|
|
319
316
|
updateHeight();
|
|
@@ -348,7 +345,7 @@
|
|
|
348
345
|
* @param {string} html
|
|
349
346
|
* @param {string[]} overflowKeys
|
|
350
347
|
*/
|
|
351
|
-
function injectReferenceMarkers(html, overflowKeys) {
|
|
348
|
+
function injectReferenceMarkers(html: string, overflowKeys: string[]): string {
|
|
352
349
|
if (!overflowKeys || overflowKeys.length === 0 || typeof window === 'undefined') {
|
|
353
350
|
return html;
|
|
354
351
|
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from "svelte";
|
|
3
|
+
import { cn } from "../../utils";
|
|
4
|
+
import { MapPin, ArrowRight } from "lucide-svelte";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* RoadmapPreview - A glass card showing current development phase
|
|
8
|
+
*
|
|
9
|
+
* Displays the current phase of Grove's development with a progress bar
|
|
10
|
+
* and links to the full roadmap. Used on landing page and plant signup.
|
|
11
|
+
*
|
|
12
|
+
* @example Basic usage
|
|
13
|
+
* ```svelte
|
|
14
|
+
* <RoadmapPreview
|
|
15
|
+
* phase="Thaw"
|
|
16
|
+
* subtitle="The ice begins to crack"
|
|
17
|
+
* description="Grove opens its doors. The first trees take root."
|
|
18
|
+
* progress={33}
|
|
19
|
+
* href="/roadmap"
|
|
20
|
+
* />
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
interface Props {
|
|
25
|
+
/** Current phase name */
|
|
26
|
+
phase?: string;
|
|
27
|
+
/** Phase subtitle/tagline */
|
|
28
|
+
subtitle?: string;
|
|
29
|
+
/** Brief description of what's happening */
|
|
30
|
+
description?: string;
|
|
31
|
+
/** Progress percentage (0-100) */
|
|
32
|
+
progress?: number;
|
|
33
|
+
/** Link to full roadmap */
|
|
34
|
+
href?: string;
|
|
35
|
+
/** Whether to open link in new tab */
|
|
36
|
+
external?: boolean;
|
|
37
|
+
/** Additional CSS classes */
|
|
38
|
+
class?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
let {
|
|
42
|
+
phase = "Thaw",
|
|
43
|
+
subtitle = "The ice begins to crack",
|
|
44
|
+
description = "Grove opens its doors. The first trees take root.",
|
|
45
|
+
progress = 33,
|
|
46
|
+
href = "/roadmap",
|
|
47
|
+
external = false,
|
|
48
|
+
class: className
|
|
49
|
+
}: Props = $props();
|
|
50
|
+
|
|
51
|
+
const linkTarget = $derived(external ? "_blank" : undefined);
|
|
52
|
+
const linkRel = $derived(external ? "noopener noreferrer" : undefined);
|
|
53
|
+
</script>
|
|
54
|
+
|
|
55
|
+
<a
|
|
56
|
+
{href}
|
|
57
|
+
target={linkTarget}
|
|
58
|
+
rel={linkRel}
|
|
59
|
+
class={cn(
|
|
60
|
+
"block rounded-2xl p-6 transition-transform hover:scale-[1.02] group",
|
|
61
|
+
"bg-white/60 dark:bg-emerald-950/25 backdrop-blur-md",
|
|
62
|
+
"border border-white/40 dark:border-emerald-800/25",
|
|
63
|
+
className
|
|
64
|
+
)}
|
|
65
|
+
>
|
|
66
|
+
<div class="flex items-start justify-between mb-4">
|
|
67
|
+
<div>
|
|
68
|
+
<div class="flex items-center gap-2 mb-1">
|
|
69
|
+
<MapPin class="w-4 h-4 text-primary" />
|
|
70
|
+
<span class="text-xs text-foreground-subtle uppercase tracking-wide">Currently</span>
|
|
71
|
+
</div>
|
|
72
|
+
<h3 class="text-xl font-serif text-foreground">{phase}</h3>
|
|
73
|
+
<p class="text-sm text-foreground-muted italic">{subtitle}</p>
|
|
74
|
+
</div>
|
|
75
|
+
<ArrowRight class="w-5 h-5 text-foreground-subtle group-hover:text-primary group-hover:translate-x-1 transition-all" />
|
|
76
|
+
</div>
|
|
77
|
+
|
|
78
|
+
<!-- Progress bar -->
|
|
79
|
+
<div class="mb-3">
|
|
80
|
+
<div class="h-2 bg-surface rounded-full overflow-hidden">
|
|
81
|
+
<div
|
|
82
|
+
class="h-full bg-primary rounded-full transition-all duration-500"
|
|
83
|
+
style="width: {progress}%"
|
|
84
|
+
></div>
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
|
|
88
|
+
<p class="text-sm text-foreground-subtle">
|
|
89
|
+
{description}
|
|
90
|
+
</p>
|
|
91
|
+
</a>
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RoadmapPreview - A glass card showing current development phase
|
|
3
|
+
*
|
|
4
|
+
* Displays the current phase of Grove's development with a progress bar
|
|
5
|
+
* and links to the full roadmap. Used on landing page and plant signup.
|
|
6
|
+
*
|
|
7
|
+
* @example Basic usage
|
|
8
|
+
* ```svelte
|
|
9
|
+
* <RoadmapPreview
|
|
10
|
+
* phase="Thaw"
|
|
11
|
+
* subtitle="The ice begins to crack"
|
|
12
|
+
* description="Grove opens its doors. The first trees take root."
|
|
13
|
+
* progress={33}
|
|
14
|
+
* href="/roadmap"
|
|
15
|
+
* />
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
interface Props {
|
|
19
|
+
/** Current phase name */
|
|
20
|
+
phase?: string;
|
|
21
|
+
/** Phase subtitle/tagline */
|
|
22
|
+
subtitle?: string;
|
|
23
|
+
/** Brief description of what's happening */
|
|
24
|
+
description?: string;
|
|
25
|
+
/** Progress percentage (0-100) */
|
|
26
|
+
progress?: number;
|
|
27
|
+
/** Link to full roadmap */
|
|
28
|
+
href?: string;
|
|
29
|
+
/** Whether to open link in new tab */
|
|
30
|
+
external?: boolean;
|
|
31
|
+
/** Additional CSS classes */
|
|
32
|
+
class?: string;
|
|
33
|
+
}
|
|
34
|
+
declare const RoadmapPreview: import("svelte").Component<Props, {}, "">;
|
|
35
|
+
type RoadmapPreview = ReturnType<typeof RoadmapPreview>;
|
|
36
|
+
export default RoadmapPreview;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { default as ProductCard } from './ProductCard.svelte';
|
|
2
2
|
export { default as SearchCard } from './SearchCard.svelte';
|
|
3
3
|
export { default as PlanCard } from './PlanCard.svelte';
|
|
4
|
+
export { default as RoadmapPreview } from './RoadmapPreview.svelte';
|
|
4
5
|
export declare const CONTENT_VERSION = "0.2.0";
|
|
@@ -7,4 +7,5 @@
|
|
|
7
7
|
export { default as ProductCard } from './ProductCard.svelte';
|
|
8
8
|
export { default as SearchCard } from './SearchCard.svelte';
|
|
9
9
|
export { default as PlanCard } from './PlanCard.svelte';
|
|
10
|
+
export { default as RoadmapPreview } from './RoadmapPreview.svelte';
|
|
10
11
|
export const CONTENT_VERSION = '0.2.0';
|
|
@@ -0,0 +1,224 @@
|
|
|
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 { Season } from './palette';
|
|
8
|
+
import { autumn, winter, greens, bark, springBlossoms } from './palette';
|
|
9
|
+
import { onMount } from 'svelte';
|
|
10
|
+
|
|
11
|
+
interface Props {
|
|
12
|
+
class?: string;
|
|
13
|
+
color?: string;
|
|
14
|
+
trunkColor?: string;
|
|
15
|
+
season?: Season;
|
|
16
|
+
animate?: boolean;
|
|
17
|
+
animateEntrance?: boolean;
|
|
18
|
+
/** Add breathing animation (subtle pulse for loading states) */
|
|
19
|
+
breathing?: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let {
|
|
23
|
+
class: className = 'w-6 h-6',
|
|
24
|
+
color,
|
|
25
|
+
trunkColor,
|
|
26
|
+
season = 'autumn', // Default to autumn (Grove's signature season)
|
|
27
|
+
animate = false,
|
|
28
|
+
animateEntrance = false,
|
|
29
|
+
breathing = false
|
|
30
|
+
}: Props = $props();
|
|
31
|
+
|
|
32
|
+
// Check if winter for snow accents (Logo keeps foliage, just gets snow-dusted)
|
|
33
|
+
const isWinter = $derived(season === 'winter');
|
|
34
|
+
|
|
35
|
+
// Build animation class - breathing takes precedence over sway
|
|
36
|
+
const animationClass = $derived(breathing ? 'breathing' : (animate ? 'sway' : ''));
|
|
37
|
+
|
|
38
|
+
// Seasonal color mapping for the logo
|
|
39
|
+
// - Spring: Blossom pink - celebrating cherry blossom season!
|
|
40
|
+
// - Summer: Grove brand green
|
|
41
|
+
// - Autumn: Warm orange tones matching the forest palette
|
|
42
|
+
// - Winter: Frosted muted green (Logo stays green year-round like an evergreen)
|
|
43
|
+
const defaultColor = $derived(
|
|
44
|
+
season === 'spring' ? springBlossoms.pink : // Blossom pink for spring!
|
|
45
|
+
season === 'autumn' ? autumn.pumpkin : // Orange matching autumn forest palette
|
|
46
|
+
season === 'winter' ? winter.winterGreen :
|
|
47
|
+
greens.grove // Summer uses Grove brand green
|
|
48
|
+
);
|
|
49
|
+
const foliageColor = $derived(color ?? defaultColor);
|
|
50
|
+
// Trunk should always be brown (like real tree bark), not match the foliage
|
|
51
|
+
const actualTrunkColor = $derived(trunkColor ?? bark.bark);
|
|
52
|
+
|
|
53
|
+
// Animation state for entrance animation
|
|
54
|
+
let mounted = $state(false);
|
|
55
|
+
|
|
56
|
+
onMount(() => {
|
|
57
|
+
if (animateEntrance) {
|
|
58
|
+
// Small delay to ensure CSS transition triggers
|
|
59
|
+
requestAnimationFrame(() => {
|
|
60
|
+
mounted = true;
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// Calculate if arrows should be in final position
|
|
66
|
+
const inPosition = $derived(animateEntrance ? mounted : true);
|
|
67
|
+
</script>
|
|
68
|
+
|
|
69
|
+
{#if animateEntrance}
|
|
70
|
+
<!-- Animated version with 4 separate arrows -->
|
|
71
|
+
<svg
|
|
72
|
+
class="{className} {animationClass}"
|
|
73
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
74
|
+
viewBox="0 0 417 512.238"
|
|
75
|
+
>
|
|
76
|
+
<defs>
|
|
77
|
+
<!-- Clip paths to isolate each arrow direction -->
|
|
78
|
+
<clipPath id="clip-top">
|
|
79
|
+
<polygon points="0,0 417,0 417,208.5 208.5,208.5 0,208.5" />
|
|
80
|
+
</clipPath>
|
|
81
|
+
<clipPath id="clip-right">
|
|
82
|
+
<polygon points="208.5,0 417,0 417,400 208.5,400 208.5,208.5" />
|
|
83
|
+
</clipPath>
|
|
84
|
+
<clipPath id="clip-bottom">
|
|
85
|
+
<polygon points="0,208.5 208.5,208.5 417,208.5 417,400 0,400" />
|
|
86
|
+
</clipPath>
|
|
87
|
+
<clipPath id="clip-left">
|
|
88
|
+
<polygon points="0,0 208.5,0 208.5,208.5 208.5,400 0,400" />
|
|
89
|
+
</clipPath>
|
|
90
|
+
</defs>
|
|
91
|
+
|
|
92
|
+
<!-- Trunk (static) -->
|
|
93
|
+
<path fill={actualTrunkColor} d="M171.274 344.942h74.09v167.296h-74.09V344.942z"/>
|
|
94
|
+
|
|
95
|
+
<!-- Top Arrow -->
|
|
96
|
+
<g
|
|
97
|
+
class="arrow arrow-top"
|
|
98
|
+
class:in-position={inPosition}
|
|
99
|
+
clip-path="url(#clip-top)"
|
|
100
|
+
>
|
|
101
|
+
<path fill={foliageColor} d="M0 173.468h126.068l-89.622-85.44 49.591-50.985 85.439 87.829V0h74.086v124.872L331 37.243l49.552 50.785-89.58 85.24H417v70.502H290.252l90.183 87.629L331 381.192 208.519 258.11 86.037 381.192l-49.591-49.591 90.218-87.631H0v-70.502z"/>
|
|
102
|
+
</g>
|
|
103
|
+
|
|
104
|
+
<!-- Right Arrow -->
|
|
105
|
+
<g
|
|
106
|
+
class="arrow arrow-right"
|
|
107
|
+
class:in-position={inPosition}
|
|
108
|
+
clip-path="url(#clip-right)"
|
|
109
|
+
>
|
|
110
|
+
<path fill={foliageColor} d="M0 173.468h126.068l-89.622-85.44 49.591-50.985 85.439 87.829V0h74.086v124.872L331 37.243l49.552 50.785-89.58 85.24H417v70.502H290.252l90.183 87.629L331 381.192 208.519 258.11 86.037 381.192l-49.591-49.591 90.218-87.631H0v-70.502z"/>
|
|
111
|
+
</g>
|
|
112
|
+
|
|
113
|
+
<!-- Bottom Arrow -->
|
|
114
|
+
<g
|
|
115
|
+
class="arrow arrow-bottom"
|
|
116
|
+
class:in-position={inPosition}
|
|
117
|
+
clip-path="url(#clip-bottom)"
|
|
118
|
+
>
|
|
119
|
+
<path fill={foliageColor} d="M0 173.468h126.068l-89.622-85.44 49.591-50.985 85.439 87.829V0h74.086v124.872L331 37.243l49.552 50.785-89.58 85.24H417v70.502H290.252l90.183 87.629L331 381.192 208.519 258.11 86.037 381.192l-49.591-49.591 90.218-87.631H0v-70.502z"/>
|
|
120
|
+
</g>
|
|
121
|
+
|
|
122
|
+
<!-- Left Arrow -->
|
|
123
|
+
<g
|
|
124
|
+
class="arrow arrow-left"
|
|
125
|
+
class:in-position={inPosition}
|
|
126
|
+
clip-path="url(#clip-left)"
|
|
127
|
+
>
|
|
128
|
+
<path fill={foliageColor} d="M0 173.468h126.068l-89.622-85.44 49.591-50.985 85.439 87.829V0h74.086v124.872L331 37.243l49.552 50.785-89.58 85.24H417v70.502H290.252l90.183 87.629L331 381.192 208.519 258.11 86.037 381.192l-49.591-49.591 90.218-87.631H0v-70.502z"/>
|
|
129
|
+
</g>
|
|
130
|
+
</svg>
|
|
131
|
+
{:else}
|
|
132
|
+
<!-- Static version -->
|
|
133
|
+
<svg
|
|
134
|
+
class="{className} {animationClass}"
|
|
135
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
136
|
+
viewBox="0 0 417 512.238"
|
|
137
|
+
>
|
|
138
|
+
<!-- Trunk -->
|
|
139
|
+
<path fill={actualTrunkColor} d="M171.274 344.942h74.09v167.296h-74.09V344.942z"/>
|
|
140
|
+
<!-- Foliage -->
|
|
141
|
+
<path fill={foliageColor} d="M0 173.468h126.068l-89.622-85.44 49.591-50.985 85.439 87.829V0h74.086v124.872L331 37.243l49.552 50.785-89.58 85.24H417v70.502H290.252l90.183 87.629L331 381.192 208.519 258.11 86.037 381.192l-49.591-49.591 90.218-87.631H0v-70.502z"/>
|
|
142
|
+
<!-- Snow accents in winter -->
|
|
143
|
+
{#if isWinter}
|
|
144
|
+
<!-- Top point snow cap -->
|
|
145
|
+
<ellipse fill={winter.snow} cx="208" cy="8" rx="32" ry="10" opacity="0.85" />
|
|
146
|
+
|
|
147
|
+
<!-- Upper diagonal arm tips (the angled parts pointing up-left and up-right) -->
|
|
148
|
+
<ellipse fill={winter.snow} cx="52" cy="60" rx="18" ry="6" opacity="0.7" transform="rotate(-25 52 60)" />
|
|
149
|
+
<ellipse fill={winter.snow} cx="365" cy="60" rx="18" ry="6" opacity="0.7" transform="rotate(25 365 60)" />
|
|
150
|
+
|
|
151
|
+
<!-- Horizontal arm snow (left and right extending arms) -->
|
|
152
|
+
<ellipse fill={winter.snow} cx="45" cy="175" rx="28" ry="7" opacity="0.75" />
|
|
153
|
+
<ellipse fill={winter.snow} cx="372" cy="175" rx="28" ry="7" opacity="0.75" />
|
|
154
|
+
|
|
155
|
+
<!-- Center intersection snow pile -->
|
|
156
|
+
<ellipse fill={winter.snow} cx="208" cy="175" rx="25" ry="8" opacity="0.6" />
|
|
157
|
+
|
|
158
|
+
<!-- Lower diagonal arm tips -->
|
|
159
|
+
<ellipse fill={winter.snow} cx="95" cy="320" rx="16" ry="5" opacity="0.55" transform="rotate(25 95 320)" />
|
|
160
|
+
<ellipse fill={winter.snow} cx="322" cy="320" rx="16" ry="5" opacity="0.55" transform="rotate(-25 322 320)" />
|
|
161
|
+
{/if}
|
|
162
|
+
</svg>
|
|
163
|
+
{/if}
|
|
164
|
+
|
|
165
|
+
<style>
|
|
166
|
+
@keyframes sway {
|
|
167
|
+
0%, 100% { transform: rotate(0deg); }
|
|
168
|
+
50% { transform: rotate(1deg); }
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
@keyframes breathe {
|
|
172
|
+
0%, 100% {
|
|
173
|
+
transform: scale(1);
|
|
174
|
+
opacity: 0.7;
|
|
175
|
+
}
|
|
176
|
+
50% {
|
|
177
|
+
transform: scale(1.05);
|
|
178
|
+
opacity: 1;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
.sway {
|
|
183
|
+
transform-origin: center bottom;
|
|
184
|
+
animation: sway 4s ease-in-out infinite;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
.breathing {
|
|
188
|
+
transform-origin: center center;
|
|
189
|
+
animation: breathe 2s ease-in-out infinite;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/* Arrow entrance animations */
|
|
193
|
+
.arrow {
|
|
194
|
+
transition: transform 0.5s cubic-bezier(0.34, 1.56, 0.64, 1); /* Bounce easing */
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/* Starting positions (offset by ~20%) */
|
|
198
|
+
.arrow-top {
|
|
199
|
+
transform: translateY(-40px);
|
|
200
|
+
}
|
|
201
|
+
.arrow-right {
|
|
202
|
+
transform: translateX(40px);
|
|
203
|
+
}
|
|
204
|
+
.arrow-bottom {
|
|
205
|
+
transform: translateY(40px);
|
|
206
|
+
}
|
|
207
|
+
.arrow-left {
|
|
208
|
+
transform: translateX(-40px);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/* Final positions */
|
|
212
|
+
.arrow-top.in-position {
|
|
213
|
+
transform: translateY(0);
|
|
214
|
+
}
|
|
215
|
+
.arrow-right.in-position {
|
|
216
|
+
transform: translateX(0);
|
|
217
|
+
}
|
|
218
|
+
.arrow-bottom.in-position {
|
|
219
|
+
transform: translateY(0);
|
|
220
|
+
}
|
|
221
|
+
.arrow-left.in-position {
|
|
222
|
+
transform: translateX(0);
|
|
223
|
+
}
|
|
224
|
+
</style>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Season } from './palette';
|
|
2
|
+
interface Props {
|
|
3
|
+
class?: string;
|
|
4
|
+
color?: string;
|
|
5
|
+
trunkColor?: string;
|
|
6
|
+
season?: Season;
|
|
7
|
+
animate?: boolean;
|
|
8
|
+
animateEntrance?: boolean;
|
|
9
|
+
/** Add breathing animation (subtle pulse for loading states) */
|
|
10
|
+
breathing?: boolean;
|
|
11
|
+
}
|
|
12
|
+
declare const Logo: import("svelte").Component<Props, {}, "">;
|
|
13
|
+
type Logo = ReturnType<typeof Logo>;
|
|
14
|
+
export default Logo;
|
|
@@ -0,0 +1,48 @@
|
|
|
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 { bark, earth } from '../palette';
|
|
8
|
+
|
|
9
|
+
interface Props {
|
|
10
|
+
class?: string;
|
|
11
|
+
capColor?: string;
|
|
12
|
+
nutColor?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
let {
|
|
16
|
+
class: className = 'w-4 h-5',
|
|
17
|
+
capColor,
|
|
18
|
+
nutColor
|
|
19
|
+
}: Props = $props();
|
|
20
|
+
|
|
21
|
+
const cap = $derived(capColor ?? bark.bark);
|
|
22
|
+
const nut = $derived(nutColor ?? earth.clay);
|
|
23
|
+
const capDetail = bark.darkBark;
|
|
24
|
+
</script>
|
|
25
|
+
|
|
26
|
+
<!-- Acorn -->
|
|
27
|
+
<svg class={className} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30 40">
|
|
28
|
+
<!-- Stem -->
|
|
29
|
+
<path fill={capDetail} d="M15 0 Q18 0 18 4 L15 6 L12 4 Q12 0 15 0" />
|
|
30
|
+
|
|
31
|
+
<!-- Cap -->
|
|
32
|
+
<ellipse fill={cap} cx="15" cy="12" rx="13" ry="8" />
|
|
33
|
+
<!-- Cap texture (crosshatch pattern suggestion) -->
|
|
34
|
+
<path fill="none" stroke={capDetail} stroke-width="0.5" d="M5 10 Q15 14 25 10" opacity="0.4" />
|
|
35
|
+
<path fill="none" stroke={capDetail} stroke-width="0.5" d="M4 13 Q15 17 26 13" opacity="0.3" />
|
|
36
|
+
<path fill="none" stroke={capDetail} stroke-width="0.5" d="M8 8 L8 16" opacity="0.2" />
|
|
37
|
+
<path fill="none" stroke={capDetail} stroke-width="0.5" d="M15 6 L15 16" opacity="0.2" />
|
|
38
|
+
<path fill="none" stroke={capDetail} stroke-width="0.5" d="M22 8 L22 16" opacity="0.2" />
|
|
39
|
+
|
|
40
|
+
<!-- Nut body -->
|
|
41
|
+
<ellipse fill={nut} cx="15" cy="26" rx="11" ry="14" />
|
|
42
|
+
|
|
43
|
+
<!-- Nut highlight -->
|
|
44
|
+
<ellipse fill="white" cx="12" cy="22" rx="4" ry="5" opacity="0.15" />
|
|
45
|
+
|
|
46
|
+
<!-- Bottom point -->
|
|
47
|
+
<path fill={nut} d="M10 38 Q15 42 20 38 L15 40 Z" />
|
|
48
|
+
</svg>
|
|
@@ -0,0 +1,67 @@
|
|
|
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 { accents, greens } from '../palette';
|
|
8
|
+
|
|
9
|
+
interface Props {
|
|
10
|
+
class?: string;
|
|
11
|
+
berryColor?: string;
|
|
12
|
+
variant?: 'cluster' | 'single' | 'branch';
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
let {
|
|
16
|
+
class: className = 'w-6 h-6',
|
|
17
|
+
berryColor,
|
|
18
|
+
variant = 'cluster'
|
|
19
|
+
}: Props = $props();
|
|
20
|
+
|
|
21
|
+
const berry = $derived(berryColor ?? accents.berry.ripe);
|
|
22
|
+
const stem = greens.deepGreen;
|
|
23
|
+
const highlight = 'white';
|
|
24
|
+
</script>
|
|
25
|
+
|
|
26
|
+
<!-- Berries -->
|
|
27
|
+
<svg class={className} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50 50">
|
|
28
|
+
{#if variant === 'single'}
|
|
29
|
+
<!-- Single berry -->
|
|
30
|
+
<circle fill={berry} cx="25" cy="28" r="15" />
|
|
31
|
+
<circle fill={highlight} cx="20" cy="23" r="4" opacity="0.3" />
|
|
32
|
+
<path fill="none" stroke={stem} stroke-width="2" d="M25 13 Q25 5 20 2" />
|
|
33
|
+
<!-- Calyx -->
|
|
34
|
+
<path fill={stem} d="M20 14 L25 18 L30 14 L28 16 L25 14 L22 16 Z" />
|
|
35
|
+
{:else if variant === 'cluster'}
|
|
36
|
+
<!-- Berry cluster -->
|
|
37
|
+
<path fill="none" stroke={stem} stroke-width="1.5" d="M25 5 L25 15 M25 15 L18 22 M25 15 L32 22 M25 15 L25 25" />
|
|
38
|
+
|
|
39
|
+
<circle fill={berry} cx="18" cy="28" r="10" />
|
|
40
|
+
<circle fill={highlight} cx="14" cy="24" r="2.5" opacity="0.3" />
|
|
41
|
+
|
|
42
|
+
<circle fill={berry} cx="32" cy="28" r="10" />
|
|
43
|
+
<circle fill={highlight} cx="28" cy="24" r="2.5" opacity="0.3" />
|
|
44
|
+
|
|
45
|
+
<circle fill={berry} cx="25" cy="38" r="10" />
|
|
46
|
+
<circle fill={highlight} cx="21" cy="34" r="2.5" opacity="0.3" />
|
|
47
|
+
{:else}
|
|
48
|
+
<!-- Berry branch -->
|
|
49
|
+
<path fill="none" stroke={stem} stroke-width="2" d="M5 25 Q15 20 25 22 Q35 24 45 20" />
|
|
50
|
+
<!-- Leaves -->
|
|
51
|
+
<ellipse fill={stem} cx="12" cy="22" rx="5" ry="3" transform="rotate(-20 12 22)" />
|
|
52
|
+
<ellipse fill={stem} cx="38" cy="20" rx="5" ry="3" transform="rotate(15 38 20)" />
|
|
53
|
+
|
|
54
|
+
<!-- Berries along branch -->
|
|
55
|
+
<circle fill={berry} cx="18" cy="28" r="6" />
|
|
56
|
+
<circle fill={highlight} cx="16" cy="26" r="1.5" opacity="0.3" />
|
|
57
|
+
|
|
58
|
+
<circle fill={berry} cx="28" cy="30" r="6" />
|
|
59
|
+
<circle fill={highlight} cx="26" cy="28" r="1.5" opacity="0.3" />
|
|
60
|
+
|
|
61
|
+
<circle fill={berry} cx="22" cy="36" r="5" />
|
|
62
|
+
<circle fill={highlight} cx="20" cy="34" r="1.2" opacity="0.3" />
|
|
63
|
+
|
|
64
|
+
<circle fill={berry} cx="32" cy="38" r="5" />
|
|
65
|
+
<circle fill={highlight} cx="30" cy="36" r="1.2" opacity="0.3" />
|
|
66
|
+
{/if}
|
|
67
|
+
</svg>
|