@autumnsgrove/groveengine 0.6.4 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/auth/index.d.ts +1 -2
- package/dist/auth/index.js +8 -4
- package/dist/auth/session.d.ts +14 -33
- package/dist/auth/session.js +5 -103
- package/dist/components/admin/FloatingToolbar.svelte +373 -0
- package/dist/components/admin/FloatingToolbar.svelte.d.ts +17 -0
- package/dist/components/admin/MarkdownEditor.svelte +26 -347
- package/dist/components/admin/MarkdownEditor.svelte.d.ts +1 -1
- package/dist/components/admin/composables/index.d.ts +0 -2
- package/dist/components/admin/composables/index.js +0 -2
- package/dist/components/custom/ContentWithGutter.svelte +22 -25
- package/dist/components/custom/MobileTOC.svelte +20 -13
- package/dist/components/quota/UpgradePrompt.svelte +1 -1
- package/dist/server/services/database.d.ts +138 -0
- package/dist/server/services/database.js +234 -0
- package/dist/server/services/index.d.ts +5 -1
- package/dist/server/services/index.js +24 -2
- package/dist/server/services/turnstile.d.ts +66 -0
- package/dist/server/services/turnstile.js +131 -0
- package/dist/server/services/users.d.ts +104 -0
- package/dist/server/services/users.js +158 -0
- package/dist/styles/README.md +50 -0
- package/dist/styles/vine-pattern.css +24 -0
- package/dist/types/turnstile.d.ts +42 -0
- package/dist/ui/components/forms/TurnstileWidget.svelte +111 -0
- package/dist/ui/components/forms/TurnstileWidget.svelte.d.ts +14 -0
- package/dist/ui/components/primitives/dialog/dialog-overlay.svelte +1 -1
- package/dist/ui/components/primitives/sheet/sheet-overlay.svelte +1 -1
- 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/GlassOverlay.svelte +93 -0
- package/dist/ui/components/ui/GlassOverlay.svelte.d.ts +33 -0
- package/dist/ui/components/ui/Logo.svelte +161 -23
- package/dist/ui/components/ui/Logo.svelte.d.ts +4 -10
- package/dist/ui/components/ui/index.d.ts +5 -0
- package/dist/ui/components/ui/index.js +6 -0
- package/dist/ui/styles/grove.css +136 -0
- package/dist/ui/tokens/fonts.d.ts +69 -0
- package/dist/ui/tokens/fonts.js +341 -0
- package/dist/ui/tokens/index.d.ts +6 -5
- package/dist/ui/tokens/index.js +7 -6
- 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 +1 -1
- package/static/robots.txt +520 -0
- package/dist/auth/jwt.d.ts +0 -20
- package/dist/auth/jwt.js +0 -123
- package/dist/components/admin/composables/useCommandPalette.svelte.d.ts +0 -87
- package/dist/components/admin/composables/useCommandPalette.svelte.js +0 -158
- package/dist/components/admin/composables/useSlashCommands.svelte.d.ts +0 -104
- package/dist/components/admin/composables/useSlashCommands.svelte.js +0 -215
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from "svelte";
|
|
3
|
+
import { cn } from "../../utils";
|
|
4
|
+
import { fade, scale } from "svelte/transition";
|
|
5
|
+
import { AlertTriangle, Trash2, HelpCircle } from "lucide-svelte";
|
|
6
|
+
import Button from "./Button.svelte";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* GlassConfirmDialog - A confirmation dialog with glassmorphism styling
|
|
10
|
+
*
|
|
11
|
+
* Perfect for destructive actions like delete, or any action that needs user confirmation.
|
|
12
|
+
* Features a glass-effect card over a blurred overlay.
|
|
13
|
+
*
|
|
14
|
+
* @example Basic confirmation
|
|
15
|
+
* ```svelte
|
|
16
|
+
* <GlassConfirmDialog
|
|
17
|
+
* bind:open={showDeleteDialog}
|
|
18
|
+
* title="Delete Post"
|
|
19
|
+
* message="Are you sure you want to delete this post? This cannot be undone."
|
|
20
|
+
* confirmLabel="Delete"
|
|
21
|
+
* variant="danger"
|
|
22
|
+
* onconfirm={handleDelete}
|
|
23
|
+
* />
|
|
24
|
+
* ```
|
|
25
|
+
*
|
|
26
|
+
* @example Custom content
|
|
27
|
+
* ```svelte
|
|
28
|
+
* <GlassConfirmDialog bind:open={showDialog} title="Confirm Changes" onconfirm={save}>
|
|
29
|
+
* <p>You have unsaved changes. Would you like to save them?</p>
|
|
30
|
+
* </GlassConfirmDialog>
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
type DialogVariant = "default" | "danger" | "warning";
|
|
35
|
+
|
|
36
|
+
interface Props {
|
|
37
|
+
/** Whether the dialog is open (bindable) */
|
|
38
|
+
open?: boolean;
|
|
39
|
+
/** Dialog title */
|
|
40
|
+
title: string;
|
|
41
|
+
/** Dialog message (if not using children) */
|
|
42
|
+
message?: string;
|
|
43
|
+
/** Confirm button label */
|
|
44
|
+
confirmLabel?: string;
|
|
45
|
+
/** Cancel button label */
|
|
46
|
+
cancelLabel?: string;
|
|
47
|
+
/** Dialog variant (affects styling and icon) */
|
|
48
|
+
variant?: DialogVariant;
|
|
49
|
+
/** Whether the confirm action is loading */
|
|
50
|
+
loading?: boolean;
|
|
51
|
+
/** Called when user confirms */
|
|
52
|
+
onconfirm?: () => void | Promise<void>;
|
|
53
|
+
/** Called when user cancels or closes */
|
|
54
|
+
oncancel?: () => void;
|
|
55
|
+
/** Custom content (overrides message) */
|
|
56
|
+
children?: Snippet;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
let {
|
|
60
|
+
open = $bindable(false),
|
|
61
|
+
title,
|
|
62
|
+
message,
|
|
63
|
+
confirmLabel = "Confirm",
|
|
64
|
+
cancelLabel = "Cancel",
|
|
65
|
+
variant = "default",
|
|
66
|
+
loading = false,
|
|
67
|
+
onconfirm,
|
|
68
|
+
oncancel,
|
|
69
|
+
children
|
|
70
|
+
}: Props = $props();
|
|
71
|
+
|
|
72
|
+
// Variant-specific styling
|
|
73
|
+
const variantConfig = {
|
|
74
|
+
default: {
|
|
75
|
+
icon: HelpCircle,
|
|
76
|
+
iconClass: "text-accent-muted",
|
|
77
|
+
confirmVariant: "primary" as const
|
|
78
|
+
},
|
|
79
|
+
danger: {
|
|
80
|
+
icon: Trash2,
|
|
81
|
+
iconClass: "text-red-500 dark:text-red-400",
|
|
82
|
+
confirmVariant: "danger" as const
|
|
83
|
+
},
|
|
84
|
+
warning: {
|
|
85
|
+
icon: AlertTriangle,
|
|
86
|
+
iconClass: "text-amber-500 dark:text-amber-400",
|
|
87
|
+
confirmVariant: "primary" as const
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const config = $derived(variantConfig[variant]);
|
|
92
|
+
|
|
93
|
+
function handleCancel() {
|
|
94
|
+
open = false;
|
|
95
|
+
oncancel?.();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function handleConfirm() {
|
|
99
|
+
try {
|
|
100
|
+
await onconfirm?.();
|
|
101
|
+
open = false;
|
|
102
|
+
} catch (error) {
|
|
103
|
+
// Don't close on error - let the caller handle it
|
|
104
|
+
console.error('Confirm action failed:', error);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function handleKeydown(event: KeyboardEvent) {
|
|
109
|
+
if (event.key === "Escape") {
|
|
110
|
+
handleCancel();
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function handleBackdropClick(event: MouseEvent) {
|
|
115
|
+
// Only close if clicking the backdrop itself, not the dialog
|
|
116
|
+
if (event.target === event.currentTarget) {
|
|
117
|
+
handleCancel();
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
</script>
|
|
121
|
+
|
|
122
|
+
<svelte:window onkeydown={handleKeydown} />
|
|
123
|
+
|
|
124
|
+
{#if open}
|
|
125
|
+
<!-- Backdrop with glass effect -->
|
|
126
|
+
<div
|
|
127
|
+
class="fixed inset-0 z-50 flex items-center justify-center p-4"
|
|
128
|
+
onclick={handleBackdropClick}
|
|
129
|
+
role="dialog"
|
|
130
|
+
aria-modal="true"
|
|
131
|
+
aria-labelledby="confirm-dialog-title"
|
|
132
|
+
transition:fade={{ duration: 150 }}
|
|
133
|
+
>
|
|
134
|
+
<!-- Dark overlay with blur -->
|
|
135
|
+
<div
|
|
136
|
+
class="absolute inset-0 bg-black/50 dark:bg-black/60 backdrop-blur-sm"
|
|
137
|
+
aria-hidden="true"
|
|
138
|
+
></div>
|
|
139
|
+
|
|
140
|
+
<!-- Dialog card -->
|
|
141
|
+
<div
|
|
142
|
+
class={cn(
|
|
143
|
+
"relative z-10 w-full max-w-md",
|
|
144
|
+
"bg-white/80 dark:bg-slate-900/80",
|
|
145
|
+
"backdrop-blur-xl",
|
|
146
|
+
"border border-white/40 dark:border-slate-700/40",
|
|
147
|
+
"rounded-2xl shadow-2xl",
|
|
148
|
+
"overflow-hidden"
|
|
149
|
+
)}
|
|
150
|
+
transition:scale={{ duration: 150, start: 0.95 }}
|
|
151
|
+
>
|
|
152
|
+
<!-- Header with icon -->
|
|
153
|
+
<div class="px-6 pt-6 pb-4 flex items-start gap-4">
|
|
154
|
+
<div class={cn(
|
|
155
|
+
"flex-shrink-0 p-3 rounded-full",
|
|
156
|
+
variant === "danger" && "bg-red-100 dark:bg-red-900/30",
|
|
157
|
+
variant === "warning" && "bg-amber-100 dark:bg-amber-900/30",
|
|
158
|
+
variant === "default" && "bg-accent/10 dark:bg-accent/20"
|
|
159
|
+
)}>
|
|
160
|
+
<svelte:component this={config.icon} class={cn("w-6 h-6", config.iconClass)} />
|
|
161
|
+
</div>
|
|
162
|
+
<div class="flex-1 min-w-0">
|
|
163
|
+
<h3
|
|
164
|
+
id="confirm-dialog-title"
|
|
165
|
+
class="text-lg font-semibold text-foreground leading-tight"
|
|
166
|
+
>
|
|
167
|
+
{title}
|
|
168
|
+
</h3>
|
|
169
|
+
{#if message && !children}
|
|
170
|
+
<p class="mt-2 text-sm text-muted-foreground leading-relaxed">
|
|
171
|
+
{message}
|
|
172
|
+
</p>
|
|
173
|
+
{/if}
|
|
174
|
+
{#if children}
|
|
175
|
+
<div class="mt-2 text-sm text-muted-foreground">
|
|
176
|
+
{@render children()}
|
|
177
|
+
</div>
|
|
178
|
+
{/if}
|
|
179
|
+
</div>
|
|
180
|
+
</div>
|
|
181
|
+
|
|
182
|
+
<!-- Footer with actions -->
|
|
183
|
+
<div class="px-6 py-4 bg-slate-50/50 dark:bg-slate-800/30 border-t border-white/20 dark:border-slate-700/30 flex justify-end gap-3">
|
|
184
|
+
<Button
|
|
185
|
+
variant="ghost"
|
|
186
|
+
onclick={handleCancel}
|
|
187
|
+
disabled={loading}
|
|
188
|
+
>
|
|
189
|
+
{cancelLabel}
|
|
190
|
+
</Button>
|
|
191
|
+
<Button
|
|
192
|
+
variant={config.confirmVariant}
|
|
193
|
+
onclick={handleConfirm}
|
|
194
|
+
disabled={loading}
|
|
195
|
+
>
|
|
196
|
+
{#if loading}
|
|
197
|
+
<span class="inline-flex items-center gap-2">
|
|
198
|
+
<span class="w-4 h-4 border-2 border-current border-t-transparent rounded-full animate-spin"></span>
|
|
199
|
+
Processing...
|
|
200
|
+
</span>
|
|
201
|
+
{:else}
|
|
202
|
+
{confirmLabel}
|
|
203
|
+
{/if}
|
|
204
|
+
</Button>
|
|
205
|
+
</div>
|
|
206
|
+
</div>
|
|
207
|
+
</div>
|
|
208
|
+
{/if}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { Snippet } from "svelte";
|
|
2
|
+
/**
|
|
3
|
+
* GlassConfirmDialog - A confirmation dialog with glassmorphism styling
|
|
4
|
+
*
|
|
5
|
+
* Perfect for destructive actions like delete, or any action that needs user confirmation.
|
|
6
|
+
* Features a glass-effect card over a blurred overlay.
|
|
7
|
+
*
|
|
8
|
+
* @example Basic confirmation
|
|
9
|
+
* ```svelte
|
|
10
|
+
* <GlassConfirmDialog
|
|
11
|
+
* bind:open={showDeleteDialog}
|
|
12
|
+
* title="Delete Post"
|
|
13
|
+
* message="Are you sure you want to delete this post? This cannot be undone."
|
|
14
|
+
* confirmLabel="Delete"
|
|
15
|
+
* variant="danger"
|
|
16
|
+
* onconfirm={handleDelete}
|
|
17
|
+
* />
|
|
18
|
+
* ```
|
|
19
|
+
*
|
|
20
|
+
* @example Custom content
|
|
21
|
+
* ```svelte
|
|
22
|
+
* <GlassConfirmDialog bind:open={showDialog} title="Confirm Changes" onconfirm={save}>
|
|
23
|
+
* <p>You have unsaved changes. Would you like to save them?</p>
|
|
24
|
+
* </GlassConfirmDialog>
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
type DialogVariant = "default" | "danger" | "warning";
|
|
28
|
+
interface Props {
|
|
29
|
+
/** Whether the dialog is open (bindable) */
|
|
30
|
+
open?: boolean;
|
|
31
|
+
/** Dialog title */
|
|
32
|
+
title: string;
|
|
33
|
+
/** Dialog message (if not using children) */
|
|
34
|
+
message?: string;
|
|
35
|
+
/** Confirm button label */
|
|
36
|
+
confirmLabel?: string;
|
|
37
|
+
/** Cancel button label */
|
|
38
|
+
cancelLabel?: string;
|
|
39
|
+
/** Dialog variant (affects styling and icon) */
|
|
40
|
+
variant?: DialogVariant;
|
|
41
|
+
/** Whether the confirm action is loading */
|
|
42
|
+
loading?: boolean;
|
|
43
|
+
/** Called when user confirms */
|
|
44
|
+
onconfirm?: () => void | Promise<void>;
|
|
45
|
+
/** Called when user cancels or closes */
|
|
46
|
+
oncancel?: () => void;
|
|
47
|
+
/** Custom content (overrides message) */
|
|
48
|
+
children?: Snippet;
|
|
49
|
+
}
|
|
50
|
+
declare const GlassConfirmDialog: import("svelte").Component<Props, {}, "open">;
|
|
51
|
+
type GlassConfirmDialog = ReturnType<typeof GlassConfirmDialog>;
|
|
52
|
+
export default GlassConfirmDialog;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from "svelte";
|
|
3
|
+
import type { HTMLAttributes } from "svelte/elements";
|
|
4
|
+
import { cn } from "../../utils";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* GlassOverlay - A fullscreen overlay with glassmorphism effect
|
|
8
|
+
*
|
|
9
|
+
* Perfect for modal backdrops, loading screens, and focus overlays.
|
|
10
|
+
* Provides a dark translucent backdrop with blur effect.
|
|
11
|
+
*
|
|
12
|
+
* @example Basic overlay
|
|
13
|
+
* ```svelte
|
|
14
|
+
* {#if showModal}
|
|
15
|
+
* <GlassOverlay onclick={closeModal} />
|
|
16
|
+
* {/if}
|
|
17
|
+
* ```
|
|
18
|
+
*
|
|
19
|
+
* @example Custom intensity
|
|
20
|
+
* ```svelte
|
|
21
|
+
* <GlassOverlay variant="light" intensity="strong" />
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
type OverlayVariant =
|
|
26
|
+
| "dark" // Dark overlay (default for modals)
|
|
27
|
+
| "light" // Light overlay
|
|
28
|
+
| "accent"; // Tinted with accent color
|
|
29
|
+
|
|
30
|
+
type BlurIntensity =
|
|
31
|
+
| "none" // No blur
|
|
32
|
+
| "light" // 4px blur
|
|
33
|
+
| "medium" // 8px blur
|
|
34
|
+
| "strong"; // 12px blur
|
|
35
|
+
|
|
36
|
+
interface Props extends Omit<HTMLAttributes<HTMLDivElement>, "class"> {
|
|
37
|
+
variant?: OverlayVariant;
|
|
38
|
+
intensity?: BlurIntensity;
|
|
39
|
+
/** Whether clicking the overlay should be interactive (for close handlers) */
|
|
40
|
+
interactive?: boolean;
|
|
41
|
+
class?: string;
|
|
42
|
+
children?: Snippet;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
let {
|
|
46
|
+
variant = "dark",
|
|
47
|
+
intensity = "light",
|
|
48
|
+
interactive = true,
|
|
49
|
+
class: className,
|
|
50
|
+
children,
|
|
51
|
+
...restProps
|
|
52
|
+
}: Props = $props();
|
|
53
|
+
|
|
54
|
+
// Variant backgrounds
|
|
55
|
+
const variantClasses: Record<OverlayVariant, string> = {
|
|
56
|
+
dark: "bg-black/50 dark:bg-black/60",
|
|
57
|
+
light: "bg-white/50 dark:bg-slate-900/50",
|
|
58
|
+
accent: "bg-accent/30 dark:bg-accent/20"
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// Blur intensities
|
|
62
|
+
const intensityClasses: Record<BlurIntensity, string> = {
|
|
63
|
+
none: "",
|
|
64
|
+
light: "backdrop-blur-sm",
|
|
65
|
+
medium: "backdrop-blur",
|
|
66
|
+
strong: "backdrop-blur-md"
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const computedClass = $derived(
|
|
70
|
+
cn(
|
|
71
|
+
"fixed inset-0 z-50",
|
|
72
|
+
variantClasses[variant],
|
|
73
|
+
intensityClasses[intensity],
|
|
74
|
+
interactive && "cursor-pointer",
|
|
75
|
+
// Animation classes for when used with transitions
|
|
76
|
+
"data-[state=open]:animate-in data-[state=closed]:animate-out",
|
|
77
|
+
"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
|
78
|
+
className
|
|
79
|
+
)
|
|
80
|
+
);
|
|
81
|
+
</script>
|
|
82
|
+
|
|
83
|
+
<div
|
|
84
|
+
class={computedClass}
|
|
85
|
+
role={interactive ? "button" : "presentation"}
|
|
86
|
+
tabindex={interactive ? 0 : -1}
|
|
87
|
+
aria-label={interactive ? "Close overlay" : undefined}
|
|
88
|
+
{...restProps}
|
|
89
|
+
>
|
|
90
|
+
{#if children}
|
|
91
|
+
{@render children()}
|
|
92
|
+
{/if}
|
|
93
|
+
</div>
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { Snippet } from "svelte";
|
|
2
|
+
import type { HTMLAttributes } from "svelte/elements";
|
|
3
|
+
/**
|
|
4
|
+
* GlassOverlay - A fullscreen overlay with glassmorphism effect
|
|
5
|
+
*
|
|
6
|
+
* Perfect for modal backdrops, loading screens, and focus overlays.
|
|
7
|
+
* Provides a dark translucent backdrop with blur effect.
|
|
8
|
+
*
|
|
9
|
+
* @example Basic overlay
|
|
10
|
+
* ```svelte
|
|
11
|
+
* {#if showModal}
|
|
12
|
+
* <GlassOverlay onclick={closeModal} />
|
|
13
|
+
* {/if}
|
|
14
|
+
* ```
|
|
15
|
+
*
|
|
16
|
+
* @example Custom intensity
|
|
17
|
+
* ```svelte
|
|
18
|
+
* <GlassOverlay variant="light" intensity="strong" />
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
type OverlayVariant = "dark" | "light" | "accent";
|
|
22
|
+
type BlurIntensity = "none" | "light" | "medium" | "strong";
|
|
23
|
+
interface Props extends Omit<HTMLAttributes<HTMLDivElement>, "class"> {
|
|
24
|
+
variant?: OverlayVariant;
|
|
25
|
+
intensity?: BlurIntensity;
|
|
26
|
+
/** Whether clicking the overlay should be interactive (for close handlers) */
|
|
27
|
+
interactive?: boolean;
|
|
28
|
+
class?: string;
|
|
29
|
+
children?: Snippet;
|
|
30
|
+
}
|
|
31
|
+
declare const GlassOverlay: import("svelte").Component<Props, {}, "">;
|
|
32
|
+
type GlassOverlay = ReturnType<typeof GlassOverlay>;
|
|
33
|
+
export default GlassOverlay;
|
|
@@ -7,8 +7,21 @@
|
|
|
7
7
|
* when placed in an accent-colored context, or can be overridden.
|
|
8
8
|
*
|
|
9
9
|
* The trunk defaults to Grove's classic bark brown (#5d4037).
|
|
10
|
+
*
|
|
11
|
+
* @example Loading state (breathing animation)
|
|
12
|
+
* ```svelte
|
|
13
|
+
* <Logo breathing />
|
|
14
|
+
* <Logo breathing breathingSpeed="slow" />
|
|
15
|
+
* ```
|
|
16
|
+
* Note: breathing is intended for single loading indicators, not lists.
|
|
10
17
|
*/
|
|
11
18
|
|
|
19
|
+
import { tweened } from 'svelte/motion';
|
|
20
|
+
import { cubicInOut } from 'svelte/easing';
|
|
21
|
+
import { browser } from '$app/environment';
|
|
22
|
+
|
|
23
|
+
type BreathingSpeed = 'slow' | 'normal' | 'fast';
|
|
24
|
+
|
|
12
25
|
interface Props {
|
|
13
26
|
class?: string;
|
|
14
27
|
/** Foliage color - defaults to currentColor (inherits accent) */
|
|
@@ -19,8 +32,10 @@
|
|
|
19
32
|
monochrome?: boolean;
|
|
20
33
|
/** Add subtle sway animation */
|
|
21
34
|
animate?: boolean;
|
|
22
|
-
/** Add breathing animation (for loading states) */
|
|
35
|
+
/** Add breathing animation (for loading states, not lists) */
|
|
23
36
|
breathing?: boolean;
|
|
37
|
+
/** Breathing animation speed - 'slow' (1500ms), 'normal' (800ms), 'fast' (400ms) */
|
|
38
|
+
breathingSpeed?: BreathingSpeed;
|
|
24
39
|
}
|
|
25
40
|
|
|
26
41
|
let {
|
|
@@ -29,9 +44,28 @@
|
|
|
29
44
|
trunkColor,
|
|
30
45
|
monochrome = false,
|
|
31
46
|
animate = false,
|
|
32
|
-
breathing = false
|
|
47
|
+
breathing = false,
|
|
48
|
+
breathingSpeed = 'normal'
|
|
33
49
|
}: Props = $props();
|
|
34
50
|
|
|
51
|
+
// Breathing speed presets (duration per half-cycle in ms)
|
|
52
|
+
const BREATHING_SPEEDS = {
|
|
53
|
+
slow: 1500, // 3s full cycle - calm, meditative
|
|
54
|
+
normal: 800, // 1.6s full cycle - balanced
|
|
55
|
+
fast: 400 // 0.8s full cycle - urgent
|
|
56
|
+
} as const;
|
|
57
|
+
|
|
58
|
+
// Respect user's reduced motion preference (reactive to system changes)
|
|
59
|
+
const reducedMotionQuery = browser ? window.matchMedia('(prefers-reduced-motion: reduce)') : null;
|
|
60
|
+
let prefersReducedMotion = $state(reducedMotionQuery?.matches ?? false);
|
|
61
|
+
|
|
62
|
+
$effect(() => {
|
|
63
|
+
if (!reducedMotionQuery) return;
|
|
64
|
+
const handler = (e: MediaQueryListEvent) => { prefersReducedMotion = e.matches; };
|
|
65
|
+
reducedMotionQuery.addEventListener('change', handler);
|
|
66
|
+
return () => reducedMotionQuery.removeEventListener('change', handler);
|
|
67
|
+
});
|
|
68
|
+
|
|
35
69
|
// Classic bark brown from the nature palette
|
|
36
70
|
const BARK_BROWN = '#5d4037';
|
|
37
71
|
|
|
@@ -41,8 +75,85 @@
|
|
|
41
75
|
? foliageColor
|
|
42
76
|
: (trunkColor ?? BARK_BROWN);
|
|
43
77
|
|
|
44
|
-
//
|
|
45
|
-
const
|
|
78
|
+
// Breathing animation using tweened store (duration set dynamically in $effect)
|
|
79
|
+
const breathValue = tweened(0, { easing: cubicInOut });
|
|
80
|
+
|
|
81
|
+
// Animation loop for breathing effect
|
|
82
|
+
// Re-runs when breathing or breathingSpeed changes, ensuring reactive duration updates
|
|
83
|
+
$effect(() => {
|
|
84
|
+
const duration = BREATHING_SPEEDS[breathingSpeed];
|
|
85
|
+
|
|
86
|
+
// Disable animation if breathing is off or user prefers reduced motion
|
|
87
|
+
if (!breathing || prefersReducedMotion) {
|
|
88
|
+
// Use half the breathing duration for smoother exit transition
|
|
89
|
+
breathValue.set(0, { duration: Math.min(duration / 2, 300) });
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
let cancelled = false;
|
|
94
|
+
|
|
95
|
+
async function pulse() {
|
|
96
|
+
while (!cancelled) {
|
|
97
|
+
await breathValue.set(1, { duration });
|
|
98
|
+
if (cancelled) break;
|
|
99
|
+
await breathValue.set(0, { duration });
|
|
100
|
+
if (cancelled) break;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
pulse();
|
|
105
|
+
|
|
106
|
+
return () => {
|
|
107
|
+
cancelled = true;
|
|
108
|
+
};
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// Expansion values for breathing animation (in SVG units, tied to viewBox 417×512.238)
|
|
112
|
+
// These are absolute values within the SVG coordinate system, so they scale
|
|
113
|
+
// proportionally with the logo regardless of rendered size.
|
|
114
|
+
const expansion = $derived($breathValue * 22);
|
|
115
|
+
const diagExpansion = $derived($breathValue * 16); // ~16px at 45° angles
|
|
116
|
+
|
|
117
|
+
// Individual branch transforms
|
|
118
|
+
const leftTransform = $derived(`translate(${-expansion}, 0)`);
|
|
119
|
+
const rightTransform = $derived(`translate(${expansion}, 0)`);
|
|
120
|
+
const topTransform = $derived(`translate(0, ${-expansion})`);
|
|
121
|
+
const topLeftTransform = $derived(`translate(${-diagExpansion}, ${-diagExpansion})`);
|
|
122
|
+
const topRightTransform = $derived(`translate(${diagExpansion}, ${-diagExpansion})`);
|
|
123
|
+
const bottomLeftTransform = $derived(`translate(${-diagExpansion}, ${diagExpansion})`);
|
|
124
|
+
const bottomRightTransform = $derived(`translate(${diagExpansion}, ${diagExpansion})`);
|
|
125
|
+
|
|
126
|
+
// Build animation classes (sway only, breathing uses transforms)
|
|
127
|
+
const animationClass = $derived(animate && !breathing ? 'grove-logo-sway' : '');
|
|
128
|
+
|
|
129
|
+
// Decomposed foliage paths (8 pieces) for breathing animation
|
|
130
|
+
// Original path: "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"
|
|
131
|
+
// Decomposed by tracing path commands and isolating geometric boundaries where arms meet center.
|
|
132
|
+
// If modifying, ensure pieces align at rest (breathValue=0) to match the original silhouette.
|
|
133
|
+
|
|
134
|
+
// Center anchor - the hub where all branches connect (stays stationary)
|
|
135
|
+
const centerPath = "M126 173.468 L171.476 124.872 L171.476 173.468 L126 173.468 M245.562 124.872 L290.972 173.268 L245.562 173.268 L245.562 124.872 M126.664 243.97 L171.476 243.97 L171.476 173.468 L126 173.468 L126.664 243.97 M290.252 243.77 L245.562 243.77 L245.562 173.268 L290.972 173.268 L290.252 243.77 M171.476 243.97 L208.519 258.11 L245.562 243.77 L245.562 173.268 L171.476 173.468 L171.476 243.97";
|
|
136
|
+
|
|
137
|
+
// Left horizontal bar
|
|
138
|
+
const leftBarPath = "M0 173.468 L126 173.468 L126.664 243.97 L0 243.97 Z";
|
|
139
|
+
|
|
140
|
+
// Right horizontal bar
|
|
141
|
+
const rightBarPath = "M290.972 173.268 L417 173.268 L417 243.77 L290.252 243.77 Z";
|
|
142
|
+
|
|
143
|
+
// Top vertical bar
|
|
144
|
+
const topBarPath = "M171.476 0 L245.562 0 L245.562 124.872 L171.476 124.872 Z";
|
|
145
|
+
|
|
146
|
+
// Top-left diagonal branch (arrow shape)
|
|
147
|
+
const topLeftDiagPath = "M126.068 173.468 L36.446 88.028 L86.037 37.043 L171.476 124.872 Z";
|
|
148
|
+
|
|
149
|
+
// Top-right diagonal branch (arrow shape)
|
|
150
|
+
const topRightDiagPath = "M245.562 124.872 L331 37.243 L380.552 88.028 L290.972 173.268 Z";
|
|
151
|
+
|
|
152
|
+
// Bottom-left diagonal branch (arrow shape)
|
|
153
|
+
const bottomLeftDiagPath = "M126.664 243.97 L36.446 331.601 L86.037 381.192 L208.519 258.11 L171.476 243.97 Z";
|
|
154
|
+
|
|
155
|
+
// Bottom-right diagonal branch (arrow shape)
|
|
156
|
+
const bottomRightDiagPath = "M290.252 243.77 L380.435 331.399 L331 381.192 L208.519 258.11 L245.562 243.77 Z";
|
|
46
157
|
</script>
|
|
47
158
|
|
|
48
159
|
<svg
|
|
@@ -51,10 +162,53 @@
|
|
|
51
162
|
viewBox="0 0 417 512.238"
|
|
52
163
|
aria-label="Grove logo"
|
|
53
164
|
>
|
|
54
|
-
<!-- Trunk -->
|
|
165
|
+
<!-- Trunk (always static) -->
|
|
55
166
|
<path fill={actualTrunkColor} d="M171.274 344.942h74.09v167.296h-74.09V344.942z"/>
|
|
56
|
-
|
|
57
|
-
|
|
167
|
+
|
|
168
|
+
{#if breathing}
|
|
169
|
+
<!-- Decomposed foliage with breathing animation -->
|
|
170
|
+
|
|
171
|
+
<!-- Center anchor (stationary) -->
|
|
172
|
+
<path fill={foliageColor} d={centerPath}/>
|
|
173
|
+
|
|
174
|
+
<!-- Left horizontal bar -->
|
|
175
|
+
<g transform={leftTransform}>
|
|
176
|
+
<path fill={foliageColor} d={leftBarPath}/>
|
|
177
|
+
</g>
|
|
178
|
+
|
|
179
|
+
<!-- Right horizontal bar -->
|
|
180
|
+
<g transform={rightTransform}>
|
|
181
|
+
<path fill={foliageColor} d={rightBarPath}/>
|
|
182
|
+
</g>
|
|
183
|
+
|
|
184
|
+
<!-- Top vertical bar -->
|
|
185
|
+
<g transform={topTransform}>
|
|
186
|
+
<path fill={foliageColor} d={topBarPath}/>
|
|
187
|
+
</g>
|
|
188
|
+
|
|
189
|
+
<!-- Top-left diagonal -->
|
|
190
|
+
<g transform={topLeftTransform}>
|
|
191
|
+
<path fill={foliageColor} d={topLeftDiagPath}/>
|
|
192
|
+
</g>
|
|
193
|
+
|
|
194
|
+
<!-- Top-right diagonal -->
|
|
195
|
+
<g transform={topRightTransform}>
|
|
196
|
+
<path fill={foliageColor} d={topRightDiagPath}/>
|
|
197
|
+
</g>
|
|
198
|
+
|
|
199
|
+
<!-- Bottom-left diagonal -->
|
|
200
|
+
<g transform={bottomLeftTransform}>
|
|
201
|
+
<path fill={foliageColor} d={bottomLeftDiagPath}/>
|
|
202
|
+
</g>
|
|
203
|
+
|
|
204
|
+
<!-- Bottom-right diagonal -->
|
|
205
|
+
<g transform={bottomRightTransform}>
|
|
206
|
+
<path fill={foliageColor} d={bottomRightDiagPath}/>
|
|
207
|
+
</g>
|
|
208
|
+
{:else}
|
|
209
|
+
<!-- Original single foliage path (for non-breathing state) -->
|
|
210
|
+
<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"/>
|
|
211
|
+
{/if}
|
|
58
212
|
</svg>
|
|
59
213
|
|
|
60
214
|
<style>
|
|
@@ -63,24 +217,8 @@
|
|
|
63
217
|
50% { transform: rotate(1deg); }
|
|
64
218
|
}
|
|
65
219
|
|
|
66
|
-
@keyframes grove-logo-breathe {
|
|
67
|
-
0%, 100% {
|
|
68
|
-
transform: scale(1);
|
|
69
|
-
opacity: 0.7;
|
|
70
|
-
}
|
|
71
|
-
50% {
|
|
72
|
-
transform: scale(1.05);
|
|
73
|
-
opacity: 1;
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
220
|
.grove-logo-sway {
|
|
78
221
|
transform-origin: center bottom;
|
|
79
222
|
animation: grove-logo-sway 4s ease-in-out infinite;
|
|
80
223
|
}
|
|
81
|
-
|
|
82
|
-
.grove-logo-breathe {
|
|
83
|
-
transform-origin: center center;
|
|
84
|
-
animation: grove-logo-breathe 2s ease-in-out infinite;
|
|
85
|
-
}
|
|
86
224
|
</style>
|
|
@@ -1,12 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
* Grove Logo Component
|
|
3
|
-
*
|
|
4
|
-
* A logo that respects the user's accent color by default.
|
|
5
|
-
* The foliage uses `currentColor` which inherits from --accent-color
|
|
6
|
-
* when placed in an accent-colored context, or can be overridden.
|
|
7
|
-
*
|
|
8
|
-
* The trunk defaults to Grove's classic bark brown (#5d4037).
|
|
9
|
-
*/
|
|
1
|
+
type BreathingSpeed = 'slow' | 'normal' | 'fast';
|
|
10
2
|
interface Props {
|
|
11
3
|
class?: string;
|
|
12
4
|
/** Foliage color - defaults to currentColor (inherits accent) */
|
|
@@ -17,8 +9,10 @@ interface Props {
|
|
|
17
9
|
monochrome?: boolean;
|
|
18
10
|
/** Add subtle sway animation */
|
|
19
11
|
animate?: boolean;
|
|
20
|
-
/** Add breathing animation (for loading states) */
|
|
12
|
+
/** Add breathing animation (for loading states, not lists) */
|
|
21
13
|
breathing?: boolean;
|
|
14
|
+
/** Breathing animation speed - 'slow' (1500ms), 'normal' (800ms), 'fast' (400ms) */
|
|
15
|
+
breathingSpeed?: BreathingSpeed;
|
|
22
16
|
}
|
|
23
17
|
declare const Logo: import("svelte").Component<Props, {}, "">;
|
|
24
18
|
type Logo = ReturnType<typeof Logo>;
|
|
@@ -15,6 +15,11 @@ export { default as Table } from './Table.svelte';
|
|
|
15
15
|
export { default as CollapsibleSection } from './CollapsibleSection.svelte';
|
|
16
16
|
export { default as Logo } from './Logo.svelte';
|
|
17
17
|
export { default as LogoLoader } from './LogoLoader.svelte';
|
|
18
|
+
export { default as Glass } from './Glass.svelte';
|
|
19
|
+
export { default as GlassButton } from './GlassButton.svelte';
|
|
20
|
+
export { default as GlassCard } from './GlassCard.svelte';
|
|
21
|
+
export { default as GlassConfirmDialog } from './GlassConfirmDialog.svelte';
|
|
22
|
+
export { default as GlassOverlay } from './GlassOverlay.svelte';
|
|
18
23
|
export { TableHeader, TableBody, TableRow, TableCell, TableHead, TableFooter, TableCaption, } from '../primitives/table';
|
|
19
24
|
export * from './toast.js';
|
|
20
25
|
export declare const UI_VERSION = "0.2.0";
|
|
@@ -23,6 +23,12 @@ export { default as Table } from './Table.svelte';
|
|
|
23
23
|
export { default as CollapsibleSection } from './CollapsibleSection.svelte';
|
|
24
24
|
export { default as Logo } from './Logo.svelte';
|
|
25
25
|
export { default as LogoLoader } from './LogoLoader.svelte';
|
|
26
|
+
// Glass suite - glassmorphism components
|
|
27
|
+
export { default as Glass } from './Glass.svelte';
|
|
28
|
+
export { default as GlassButton } from './GlassButton.svelte';
|
|
29
|
+
export { default as GlassCard } from './GlassCard.svelte';
|
|
30
|
+
export { default as GlassConfirmDialog } from './GlassConfirmDialog.svelte';
|
|
31
|
+
export { default as GlassOverlay } from './GlassOverlay.svelte';
|
|
26
32
|
// Table sub-components (from primitives)
|
|
27
33
|
export { TableHeader, TableBody, TableRow, TableCell, TableHead, TableFooter, TableCaption, } from '../primitives/table';
|
|
28
34
|
// Toast utility
|