@autumnsgrove/groveengine 0.6.4 → 0.6.5

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 (38) hide show
  1. package/dist/auth/session.d.ts +2 -2
  2. package/dist/components/admin/FloatingToolbar.svelte +373 -0
  3. package/dist/components/admin/FloatingToolbar.svelte.d.ts +17 -0
  4. package/dist/components/admin/MarkdownEditor.svelte +26 -347
  5. package/dist/components/admin/MarkdownEditor.svelte.d.ts +1 -1
  6. package/dist/components/admin/composables/index.d.ts +0 -2
  7. package/dist/components/admin/composables/index.js +0 -2
  8. package/dist/components/custom/MobileTOC.svelte +20 -13
  9. package/dist/components/quota/UpgradePrompt.svelte +1 -1
  10. package/dist/server/services/database.d.ts +138 -0
  11. package/dist/server/services/database.js +234 -0
  12. package/dist/server/services/index.d.ts +5 -1
  13. package/dist/server/services/index.js +24 -2
  14. package/dist/server/services/turnstile.d.ts +66 -0
  15. package/dist/server/services/turnstile.js +131 -0
  16. package/dist/server/services/users.d.ts +104 -0
  17. package/dist/server/services/users.js +158 -0
  18. package/dist/styles/README.md +50 -0
  19. package/dist/styles/vine-pattern.css +24 -0
  20. package/dist/types/turnstile.d.ts +42 -0
  21. package/dist/ui/components/forms/TurnstileWidget.svelte +111 -0
  22. package/dist/ui/components/forms/TurnstileWidget.svelte.d.ts +14 -0
  23. package/dist/ui/components/primitives/dialog/dialog-overlay.svelte +1 -1
  24. package/dist/ui/components/primitives/sheet/sheet-overlay.svelte +1 -1
  25. package/dist/ui/components/ui/Logo.svelte +161 -23
  26. package/dist/ui/components/ui/Logo.svelte.d.ts +4 -10
  27. package/dist/ui/tokens/fonts.d.ts +69 -0
  28. package/dist/ui/tokens/fonts.js +341 -0
  29. package/dist/ui/tokens/index.d.ts +6 -5
  30. package/dist/ui/tokens/index.js +7 -6
  31. package/package.json +22 -21
  32. package/static/fonts/alagard.ttf +0 -0
  33. package/static/robots.txt +487 -0
  34. package/LICENSE +0 -378
  35. package/dist/components/admin/composables/useCommandPalette.svelte.d.ts +0 -87
  36. package/dist/components/admin/composables/useCommandPalette.svelte.js +0 -158
  37. package/dist/components/admin/composables/useSlashCommands.svelte.d.ts +0 -104
  38. package/dist/components/admin/composables/useSlashCommands.svelte.js +0 -215
@@ -0,0 +1,104 @@
1
+ /**
2
+ * User Service - D1 User Operations
3
+ *
4
+ * Provides utilities for managing authenticated users in D1.
5
+ * Users are created/updated during auth callback from GroveAuth.
6
+ *
7
+ * @example
8
+ * ```typescript
9
+ * import { getUserByGroveAuthId, getUserFromSession } from '@autumnsgrove/groveengine/services';
10
+ *
11
+ * // Get user by GroveAuth ID (from token)
12
+ * const user = await getUserByGroveAuthId(db, groveAuthId);
13
+ *
14
+ * // Get user from session (validates token first)
15
+ * const user = await getUserFromSession(db, accessToken, authBaseUrl);
16
+ * ```
17
+ */
18
+ /**
19
+ * User record from the users table
20
+ */
21
+ export interface User {
22
+ id: string;
23
+ groveauth_id: string;
24
+ email: string;
25
+ display_name: string | null;
26
+ avatar_url: string | null;
27
+ tenant_id: string | null;
28
+ last_login_at: number | null;
29
+ login_count: number;
30
+ is_active: number;
31
+ created_at: number;
32
+ updated_at: number;
33
+ }
34
+ /**
35
+ * Get a user by their GroveAuth ID
36
+ *
37
+ * @example
38
+ * ```typescript
39
+ * const user = await getUserByGroveAuthId(db, 'groveauth-user-id');
40
+ * if (user) {
41
+ * console.log('User:', user.email);
42
+ * }
43
+ * ```
44
+ */
45
+ export declare function getUserByGroveAuthId(db: D1Database, groveAuthId: string): Promise<User | null>;
46
+ /**
47
+ * Get a user by their internal ID
48
+ */
49
+ export declare function getUserById(db: D1Database, id: string): Promise<User | null>;
50
+ /**
51
+ * Get a user by email address
52
+ */
53
+ export declare function getUserByEmail(db: D1Database, email: string): Promise<User | null>;
54
+ /**
55
+ * Get user who owns a specific tenant
56
+ */
57
+ export declare function getUserByTenantId(db: D1Database, tenantId: string): Promise<User | null>;
58
+ /**
59
+ * Get user from an access token by validating with GroveAuth
60
+ *
61
+ * This function:
62
+ * 1. Calls GroveAuth /userinfo to validate the token and get user info
63
+ * 2. Looks up the user in D1 by their groveauth_id
64
+ * 3. Returns the user if found and active
65
+ *
66
+ * @example
67
+ * ```typescript
68
+ * const user = await getUserFromSession(db, accessToken, 'https://auth-api.grove.place');
69
+ * if (!user) {
70
+ * return redirect(302, '/auth/login');
71
+ * }
72
+ * ```
73
+ */
74
+ export declare function getUserFromSession(db: D1Database, accessToken: string, authBaseUrl?: string): Promise<User | null>;
75
+ /**
76
+ * Get user from session without external validation
77
+ *
78
+ * Use this when you've already validated the token (e.g., in hooks.server.ts)
79
+ * and have the GroveAuth user ID (sub claim).
80
+ *
81
+ * @example
82
+ * ```typescript
83
+ * // In hooks.server.ts after token validation:
84
+ * const user = await getUserFromValidatedSession(db, tokenInfo.sub);
85
+ * event.locals.user = user;
86
+ * ```
87
+ */
88
+ export declare function getUserFromValidatedSession(db: D1Database, groveAuthId: string): Promise<User | null>;
89
+ /**
90
+ * Link a user to a tenant (set ownership)
91
+ */
92
+ export declare function linkUserToTenant(db: D1Database, userId: string, tenantId: string): Promise<number>;
93
+ /**
94
+ * Update user's display name
95
+ */
96
+ export declare function updateUserDisplayName(db: D1Database, userId: string, displayName: string): Promise<number>;
97
+ /**
98
+ * Deactivate a user (soft delete)
99
+ */
100
+ export declare function deactivateUser(db: D1Database, userId: string): Promise<number>;
101
+ /**
102
+ * Reactivate a user
103
+ */
104
+ export declare function reactivateUser(db: D1Database, userId: string): Promise<number>;
@@ -0,0 +1,158 @@
1
+ /**
2
+ * User Service - D1 User Operations
3
+ *
4
+ * Provides utilities for managing authenticated users in D1.
5
+ * Users are created/updated during auth callback from GroveAuth.
6
+ *
7
+ * @example
8
+ * ```typescript
9
+ * import { getUserByGroveAuthId, getUserFromSession } from '@autumnsgrove/groveengine/services';
10
+ *
11
+ * // Get user by GroveAuth ID (from token)
12
+ * const user = await getUserByGroveAuthId(db, groveAuthId);
13
+ *
14
+ * // Get user from session (validates token first)
15
+ * const user = await getUserFromSession(db, accessToken, authBaseUrl);
16
+ * ```
17
+ */
18
+ import { queryOne, update } from './database.js';
19
+ // ============================================================================
20
+ // Query Functions
21
+ // ============================================================================
22
+ /**
23
+ * Get a user by their GroveAuth ID
24
+ *
25
+ * @example
26
+ * ```typescript
27
+ * const user = await getUserByGroveAuthId(db, 'groveauth-user-id');
28
+ * if (user) {
29
+ * console.log('User:', user.email);
30
+ * }
31
+ * ```
32
+ */
33
+ export async function getUserByGroveAuthId(db, groveAuthId) {
34
+ return queryOne(db, 'SELECT * FROM users WHERE groveauth_id = ?', [groveAuthId]);
35
+ }
36
+ /**
37
+ * Get a user by their internal ID
38
+ */
39
+ export async function getUserById(db, id) {
40
+ return queryOne(db, 'SELECT * FROM users WHERE id = ?', [id]);
41
+ }
42
+ /**
43
+ * Get a user by email address
44
+ */
45
+ export async function getUserByEmail(db, email) {
46
+ return queryOne(db, 'SELECT * FROM users WHERE email = ?', [email]);
47
+ }
48
+ /**
49
+ * Get user who owns a specific tenant
50
+ */
51
+ export async function getUserByTenantId(db, tenantId) {
52
+ return queryOne(db, 'SELECT * FROM users WHERE tenant_id = ?', [tenantId]);
53
+ }
54
+ // ============================================================================
55
+ // Session Functions
56
+ // ============================================================================
57
+ /**
58
+ * Get user from an access token by validating with GroveAuth
59
+ *
60
+ * This function:
61
+ * 1. Calls GroveAuth /userinfo to validate the token and get user info
62
+ * 2. Looks up the user in D1 by their groveauth_id
63
+ * 3. Returns the user if found and active
64
+ *
65
+ * @example
66
+ * ```typescript
67
+ * const user = await getUserFromSession(db, accessToken, 'https://auth-api.grove.place');
68
+ * if (!user) {
69
+ * return redirect(302, '/auth/login');
70
+ * }
71
+ * ```
72
+ */
73
+ export async function getUserFromSession(db, accessToken, authBaseUrl = 'https://auth-api.grove.place') {
74
+ if (!accessToken) {
75
+ return null;
76
+ }
77
+ try {
78
+ // Validate token with GroveAuth and get user info
79
+ const response = await fetch(`${authBaseUrl}/userinfo`, {
80
+ headers: {
81
+ Authorization: `Bearer ${accessToken}`
82
+ }
83
+ });
84
+ if (!response.ok) {
85
+ // Don't log 401s as errors - they're expected for expired tokens
86
+ if (response.status !== 401) {
87
+ console.warn('[User Service] GroveAuth returned non-OK status:', {
88
+ status: response.status,
89
+ authBaseUrl
90
+ });
91
+ }
92
+ return null;
93
+ }
94
+ const userInfo = (await response.json());
95
+ // Look up user in D1
96
+ const user = await getUserByGroveAuthId(db, userInfo.sub);
97
+ // Return null if user doesn't exist or is inactive
98
+ if (!user || !user.is_active) {
99
+ return null;
100
+ }
101
+ return user;
102
+ }
103
+ catch (error) {
104
+ // Log auth failures for security monitoring and debugging
105
+ console.error('[User Service] Session validation failed:', {
106
+ error: error instanceof Error ? error.message : 'Unknown error',
107
+ authBaseUrl
108
+ });
109
+ return null;
110
+ }
111
+ }
112
+ /**
113
+ * Get user from session without external validation
114
+ *
115
+ * Use this when you've already validated the token (e.g., in hooks.server.ts)
116
+ * and have the GroveAuth user ID (sub claim).
117
+ *
118
+ * @example
119
+ * ```typescript
120
+ * // In hooks.server.ts after token validation:
121
+ * const user = await getUserFromValidatedSession(db, tokenInfo.sub);
122
+ * event.locals.user = user;
123
+ * ```
124
+ */
125
+ export async function getUserFromValidatedSession(db, groveAuthId) {
126
+ const user = await getUserByGroveAuthId(db, groveAuthId);
127
+ if (!user || !user.is_active) {
128
+ return null;
129
+ }
130
+ return user;
131
+ }
132
+ // ============================================================================
133
+ // Update Functions
134
+ // ============================================================================
135
+ /**
136
+ * Link a user to a tenant (set ownership)
137
+ */
138
+ export async function linkUserToTenant(db, userId, tenantId) {
139
+ return update(db, 'users', { tenant_id: tenantId }, 'id = ?', [userId]);
140
+ }
141
+ /**
142
+ * Update user's display name
143
+ */
144
+ export async function updateUserDisplayName(db, userId, displayName) {
145
+ return update(db, 'users', { display_name: displayName }, 'id = ?', [userId]);
146
+ }
147
+ /**
148
+ * Deactivate a user (soft delete)
149
+ */
150
+ export async function deactivateUser(db, userId) {
151
+ return update(db, 'users', { is_active: 0 }, 'id = ?', [userId]);
152
+ }
153
+ /**
154
+ * Reactivate a user
155
+ */
156
+ export async function reactivateUser(db, userId) {
157
+ return update(db, 'users', { is_active: 1 }, 'id = ?', [userId]);
158
+ }
@@ -0,0 +1,50 @@
1
+ # Shared Styles
2
+
3
+ This directory contains CSS styles that are shared across all Grove sites to maintain visual consistency.
4
+
5
+ ## vine-pattern.css
6
+
7
+ The `.leaf-pattern` class provides the signature Grove vine background pattern used across the platform.
8
+
9
+ ### Features
10
+
11
+ - **450x450px repeating SVG pattern** with:
12
+ - 5 flowing organic vine paths
13
+ - 8 spiral tendrils
14
+ - Multiple leaf varieties (ivy, round, slender, fern)
15
+ - Botanical details (seeds, spores)
16
+ - **Grove green (#22c55e)** from the design system
17
+ - **Automatic dark mode support** with adjusted opacity
18
+ - **Performance-optimized** inline SVG data URI (no HTTP requests)
19
+
20
+ ### Usage
21
+
22
+ Import in your site's `app.css`:
23
+
24
+ ```css
25
+ @import '@autumnsgrove/groveengine/lib/styles/vine-pattern.css';
26
+ ```
27
+
28
+ Or copy the `.leaf-pattern` class definition directly into your app's CSS.
29
+
30
+ Then apply the class to your layout:
31
+
32
+ ```svelte
33
+ <div class="min-h-screen leaf-pattern">
34
+ <!-- Your content -->
35
+ </div>
36
+ ```
37
+
38
+ ### Current Sites Using This Pattern
39
+
40
+ - **landing** - Applied globally via `routes/+layout.svelte`
41
+ - **plant** - Applied globally via `routes/+layout.svelte`
42
+
43
+ ### Customization
44
+
45
+ To adjust the pattern's appearance, you can modify:
46
+ - **Opacity values** in the SVG (currently 0.04-0.14)
47
+ - **Stroke widths** for vines (currently 0.5-1.5)
48
+ - **Color** (currently #22c55e, Grove green)
49
+
50
+ Any changes should be synchronized across all sites for consistency.
@@ -0,0 +1,24 @@
1
+ /* ═══════════════════════════════════════════════════════════════
2
+ GROVE VINE PATTERN BACKGROUND
3
+ Shared across all Grove sites for visual consistency
4
+ ═══════════════════════════════════════════════════════════════ */
5
+
6
+ /* Subtle vine pattern background - organic, forest-like
7
+
8
+ Features:
9
+ - 5 flowing vines with organic S-curves
10
+ - 8 spiral tendrils scattered throughout
11
+ - Multiple types of leaves (ivy, round, slender, fern)
12
+ - Floating seeds/spores for botanical authenticity
13
+ - 450x450px repeating pattern
14
+ - Uses Grove green (#22c55e) from the design system
15
+ - Auto-adjusts opacity for light/dark modes
16
+ */
17
+
18
+ .leaf-pattern {
19
+ background-image: url("data:image/svg+xml,%3Csvg width='450' height='450' viewBox='0 0 450 450' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' stroke='%2322c55e' stroke-linecap='round'%3E%3C!-- Long flowing vine 1 - dramatic S-curve --%3E%3Cpath d='M-30 420 C50 380 30 320 80 260 S150 180 120 120 S180 40 140 -30' stroke-width='1.5' opacity='0.12'/%3E%3C!-- Long flowing vine 2 - opposite sweep --%3E%3Cpath d='M480 380 C400 350 420 280 360 230 S280 160 310 100 S250 30 290 -20' stroke-width='1.4' opacity='0.10'/%3E%3C!-- Diagonal vine crossing --%3E%3Cpath d='M-20 280 C60 250 100 220 160 200 S250 160 320 170 S400 130 470 100' stroke-width='1.2' opacity='0.09'/%3E%3C!-- Vertical accent vine --%3E%3Cpath d='M200 480 C180 420 210 360 190 300 S220 220 200 160 S230 80 210 20' stroke-width='1.1' opacity='0.08'/%3E%3C!-- Wandering vine --%3E%3Cpath d='M350 480 C330 430 360 380 340 320 S380 260 350 200' stroke-width='1' opacity='0.08'/%3E%3C!-- Spiral tendrils scattered --%3E%3Cpath d='M80 260 Q100 250 108 238 Q115 225 105 215 Q95 208 85 215' stroke-width='0.8' opacity='0.10'/%3E%3Cpath d='M120 120 Q140 130 150 120 Q158 108 145 98 Q132 92 125 102' stroke-width='0.7' opacity='0.09'/%3E%3Cpath d='M360 230 Q340 222 330 232 Q322 245 335 255 Q348 262 358 250' stroke-width='0.7' opacity='0.09'/%3E%3Cpath d='M310 100 Q290 92 280 102 Q272 115 288 125' stroke-width='0.6' opacity='0.08'/%3E%3Cpath d='M160 200 Q180 192 188 202 Q195 215 182 225' stroke-width='0.6' opacity='0.08'/%3E%3Cpath d='M190 300 Q210 292 218 305 Q224 320 210 328' stroke-width='0.6' opacity='0.07'/%3E%3Cpath d='M340 320 Q360 328 365 342 Q368 358 352 362' stroke-width='0.6' opacity='0.07'/%3E%3Cpath d='M55 380 Q75 372 82 385 Q88 400 72 408' stroke-width='0.5' opacity='0.06'/%3E%3C/g%3E%3Cg fill='%2322c55e'%3E%3C!-- Pointed ivy leaves --%3E%3Cpath d='M70 300 Q85 280 78 265 Q70 280 55 288 Q70 295 70 300Z' opacity='0.11'/%3E%3Cpath d='M135 145 Q150 128 143 115 Q135 128 120 135 Q135 142 135 145Z' opacity='0.10'/%3E%3Cpath d='M375 255 Q360 240 365 225 Q375 240 390 245 Q375 252 375 255Z' opacity='0.09'/%3E%3Cpath d='M220 180 Q235 165 228 152 Q220 165 205 172 Q220 178 220 180Z' opacity='0.08'/%3E%3C!-- Round soft leaves --%3E%3Cellipse cx='95' cy='210' rx='9' ry='14' transform='rotate(-35 95 210)' opacity='0.10'/%3E%3Cellipse cx='335' cy='145' rx='8' ry='12' transform='rotate(28 335 145)' opacity='0.09'/%3E%3Cellipse cx='175' cy='255' rx='7' ry='11' transform='rotate(-18 175 255)' opacity='0.08'/%3E%3Cellipse cx='280' cy='195' rx='6' ry='10' transform='rotate(40 280 195)' opacity='0.08'/%3E%3Cellipse cx='405' cy='120' rx='7' ry='10' transform='rotate(-25 405 120)' opacity='0.07'/%3E%3C!-- Long slender leaves --%3E%3Cpath d='M145 175 Q152 155 145 135 Q138 155 145 175Z' opacity='0.09'/%3E%3Cpath d='M295 130 Q305 115 298 98 Q288 115 295 130Z' opacity='0.08'/%3E%3Cpath d='M210 275 Q200 258 205 240 Q218 258 210 275Z' opacity='0.08'/%3E%3Cpath d='M365 195 Q375 180 368 162 Q358 180 365 195Z' opacity='0.07'/%3E%3Cpath d='M115 365 Q125 348 118 330 Q108 348 115 365Z' opacity='0.07'/%3E%3C!-- Fern fronds scattered asymmetrically --%3E%3Cg transform='translate(45 365) rotate(-45)' opacity='0.10'%3E%3Cpath d='M0,0 L0,-30' stroke='%2322c55e' stroke-width='0.6' fill='none'/%3E%3Cellipse cx='-5' cy='-7' rx='3.5' ry='6' transform='rotate(-30 -5 -7)'/%3E%3Cellipse cx='5' cy='-12' rx='3.5' ry='6' transform='rotate(30 5 -12)'/%3E%3Cellipse cx='-4' cy='-17' rx='3' ry='5' transform='rotate(-30 -4 -17)'/%3E%3Cellipse cx='4' cy='-22' rx='3' ry='5' transform='rotate(30 4 -22)'/%3E%3Cellipse cx='0' cy='-28' rx='2.5' ry='4'/%3E%3C/g%3E%3Cg transform='translate(390 185) rotate(40)' opacity='0.09'%3E%3Cpath d='M0,0 L0,-28' stroke='%2322c55e' stroke-width='0.6' fill='none'/%3E%3Cellipse cx='-4' cy='-6' rx='3' ry='5' transform='rotate(-25 -4 -6)'/%3E%3Cellipse cx='4' cy='-11' rx='3' ry='5' transform='rotate(25 4 -11)'/%3E%3Cellipse cx='-3' cy='-16' rx='2.5' ry='4' transform='rotate(-25 -3 -16)'/%3E%3Cellipse cx='3' cy='-21' rx='2.5' ry='4' transform='rotate(25 3 -21)'/%3E%3Cellipse cx='0' cy='-26' rx='2' ry='3'/%3E%3C/g%3E%3Cg transform='translate(255 65) rotate(-25)' opacity='0.08'%3E%3Cpath d='M0,0 L0,-25' stroke='%2322c55e' stroke-width='0.5' fill='none'/%3E%3Cellipse cx='-4' cy='-6' rx='2.5' ry='4' transform='rotate(-30 -4 -6)'/%3E%3Cellipse cx='4' cy='-10' rx='2.5' ry='4' transform='rotate(30 4 -10)'/%3E%3Cellipse cx='-3' cy='-15' rx='2' ry='3.5' transform='rotate(-30 -3 -15)'/%3E%3Cellipse cx='3' cy='-20' rx='2' ry='3' transform='rotate(30 3 -20)'/%3E%3C/g%3E%3Cg transform='translate(140 420) rotate(55)' opacity='0.08'%3E%3Cpath d='M0,0 L0,-22' stroke='%2322c55e' stroke-width='0.5' fill='none'/%3E%3Cellipse cx='-3' cy='-5' rx='2' ry='3.5' transform='rotate(-25 -3 -5)'/%3E%3Cellipse cx='3' cy='-9' rx='2' ry='3.5' transform='rotate(25 3 -9)'/%3E%3Cellipse cx='-2' cy='-14' rx='1.5' ry='3' transform='rotate(-25 -2 -14)'/%3E%3Cellipse cx='2' cy='-18' rx='1.5' ry='2.5' transform='rotate(25 2 -18)'/%3E%3C/g%3E%3C!-- Tiny floating seeds/spores --%3E%3Ccircle cx='180' cy='90' r='2' opacity='0.06'/%3E%3Ccircle cx='85' cy='175' r='1.5' opacity='0.05'/%3E%3Ccircle cx='345' cy='290' r='2' opacity='0.06'/%3E%3Ccircle cx='420' cy='200' r='1.5' opacity='0.05'/%3E%3Ccircle cx='260' cy='350' r='2' opacity='0.05'/%3E%3Ccircle cx='40' cy='120' r='1.5' opacity='0.04'/%3E%3Ccircle cx='310' cy='400' r='1.5' opacity='0.04'/%3E%3C/g%3E%3C/svg%3E");
20
+ }
21
+
22
+ .dark .leaf-pattern {
23
+ background-image: url("data:image/svg+xml,%3Csvg width='450' height='450' viewBox='0 0 450 450' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' stroke='%2322c55e' stroke-linecap='round'%3E%3C!-- Long flowing vine 1 - dramatic S-curve --%3E%3Cpath d='M-30 420 C50 380 30 320 80 260 S150 180 120 120 S180 40 140 -30' stroke-width='1.5' opacity='0.14'/%3E%3C!-- Long flowing vine 2 - opposite sweep --%3E%3Cpath d='M480 380 C400 350 420 280 360 230 S280 160 310 100 S250 30 290 -20' stroke-width='1.4' opacity='0.12'/%3E%3C!-- Diagonal vine crossing --%3E%3Cpath d='M-20 280 C60 250 100 220 160 200 S250 160 320 170 S400 130 470 100' stroke-width='1.2' opacity='0.11'/%3E%3C!-- Vertical accent vine --%3E%3Cpath d='M200 480 C180 420 210 360 190 300 S220 220 200 160 S230 80 210 20' stroke-width='1.1' opacity='0.10'/%3E%3C!-- Wandering vine --%3E%3Cpath d='M350 480 C330 430 360 380 340 320 S380 260 350 200' stroke-width='1' opacity='0.10'/%3E%3C!-- Spiral tendrils scattered --%3E%3Cpath d='M80 260 Q100 250 108 238 Q115 225 105 215 Q95 208 85 215' stroke-width='0.8' opacity='0.12'/%3E%3Cpath d='M120 120 Q140 130 150 120 Q158 108 145 98 Q132 92 125 102' stroke-width='0.7' opacity='0.11'/%3E%3Cpath d='M360 230 Q340 222 330 232 Q322 245 335 255 Q348 262 358 250' stroke-width='0.7' opacity='0.11'/%3E%3Cpath d='M310 100 Q290 92 280 102 Q272 115 288 125' stroke-width='0.6' opacity='0.10'/%3E%3Cpath d='M160 200 Q180 192 188 202 Q195 215 182 225' stroke-width='0.6' opacity='0.10'/%3E%3Cpath d='M190 300 Q210 292 218 305 Q224 320 210 328' stroke-width='0.6' opacity='0.09'/%3E%3Cpath d='M340 320 Q360 328 365 342 Q368 358 352 362' stroke-width='0.6' opacity='0.09'/%3E%3Cpath d='M55 380 Q75 372 82 385 Q88 400 72 408' stroke-width='0.5' opacity='0.08'/%3E%3C/g%3E%3Cg fill='%2322c55e'%3E%3C!-- Pointed ivy leaves --%3E%3Cpath d='M70 300 Q85 280 78 265 Q70 280 55 288 Q70 295 70 300Z' opacity='0.13'/%3E%3Cpath d='M135 145 Q150 128 143 115 Q135 128 120 135 Q135 142 135 145Z' opacity='0.12'/%3E%3Cpath d='M375 255 Q360 240 365 225 Q375 240 390 245 Q375 252 375 255Z' opacity='0.11'/%3E%3Cpath d='M220 180 Q235 165 228 152 Q220 165 205 172 Q220 178 220 180Z' opacity='0.10'/%3E%3C!-- Round soft leaves --%3E%3Cellipse cx='95' cy='210' rx='9' ry='14' transform='rotate(-35 95 210)' opacity='0.12'/%3E%3Cellipse cx='335' cy='145' rx='8' ry='12' transform='rotate(28 335 145)' opacity='0.11'/%3E%3Cellipse cx='175' cy='255' rx='7' ry='11' transform='rotate(-18 175 255)' opacity='0.10'/%3E%3Cellipse cx='280' cy='195' rx='6' ry='10' transform='rotate(40 280 195)' opacity='0.10'/%3E%3Cellipse cx='405' cy='120' rx='7' ry='10' transform='rotate(-25 405 120)' opacity='0.09'/%3E%3C!-- Long slender leaves --%3E%3Cpath d='M145 175 Q152 155 145 135 Q138 155 145 175Z' opacity='0.11'/%3E%3Cpath d='M295 130 Q305 115 298 98 Q288 115 295 130Z' opacity='0.10'/%3E%3Cpath d='M210 275 Q200 258 205 240 Q218 258 210 275Z' opacity='0.10'/%3E%3Cpath d='M365 195 Q375 180 368 162 Q358 180 365 195Z' opacity='0.09'/%3E%3Cpath d='M115 365 Q125 348 118 330 Q108 348 115 365Z' opacity='0.09'/%3E%3C!-- Fern fronds scattered asymmetrically --%3E%3Cg transform='translate(45 365) rotate(-45)' opacity='0.12'%3E%3Cpath d='M0,0 L0,-30' stroke='%2322c55e' stroke-width='0.6' fill='none'/%3E%3Cellipse cx='-5' cy='-7' rx='3.5' ry='6' transform='rotate(-30 -5 -7)'/%3E%3Cellipse cx='5' cy='-12' rx='3.5' ry='6' transform='rotate(30 5 -12)'/%3E%3Cellipse cx='-4' cy='-17' rx='3' ry='5' transform='rotate(-30 -4 -17)'/%3E%3Cellipse cx='4' cy='-22' rx='3' ry='5' transform='rotate(30 4 -22)'/%3E%3Cellipse cx='0' cy='-28' rx='2.5' ry='4'/%3E%3C/g%3E%3Cg transform='translate(390 185) rotate(40)' opacity='0.11'%3E%3Cpath d='M0,0 L0,-28' stroke='%2322c55e' stroke-width='0.6' fill='none'/%3E%3Cellipse cx='-4' cy='-6' rx='3' ry='5' transform='rotate(-25 -4 -6)'/%3E%3Cellipse cx='4' cy='-11' rx='3' ry='5' transform='rotate(25 4 -11)'/%3E%3Cellipse cx='-3' cy='-16' rx='2.5' ry='4' transform='rotate(-25 -3 -16)'/%3E%3Cellipse cx='3' cy='-21' rx='2.5' ry='4' transform='rotate(25 3 -21)'/%3E%3Cellipse cx='0' cy='-26' rx='2' ry='3'/%3E%3C/g%3E%3Cg transform='translate(255 65) rotate(-25)' opacity='0.10'%3E%3Cpath d='M0,0 L0,-25' stroke='%2322c55e' stroke-width='0.5' fill='none'/%3E%3Cellipse cx='-4' cy='-6' rx='2.5' ry='4' transform='rotate(-30 -4 -6)'/%3E%3Cellipse cx='4' cy='-10' rx='2.5' ry='4' transform='rotate(30 4 -10)'/%3E%3Cellipse cx='-3' cy='-15' rx='2' ry='3.5' transform='rotate(-30 -3 -15)'/%3E%3Cellipse cx='3' cy='-20' rx='2' ry='3' transform='rotate(30 3 -20)'/%3E%3C/g%3E%3Cg transform='translate(140 420) rotate(55)' opacity='0.10'%3E%3Cpath d='M0,0 L0,-22' stroke='%2322c55e' stroke-width='0.5' fill='none'/%3E%3Cellipse cx='-3' cy='-5' rx='2' ry='3.5' transform='rotate(-25 -3 -5)'/%3E%3Cellipse cx='3' cy='-9' rx='2' ry='3.5' transform='rotate(25 3 -9)'/%3E%3Cellipse cx='-2' cy='-14' rx='1.5' ry='3' transform='rotate(-25 -2 -14)'/%3E%3Cellipse cx='2' cy='-18' rx='1.5' ry='2.5' transform='rotate(25 2 -18)'/%3E%3C/g%3E%3C!-- Tiny floating seeds/spores --%3E%3Ccircle cx='180' cy='90' r='2' opacity='0.08'/%3E%3Ccircle cx='85' cy='175' r='1.5' opacity='0.07'/%3E%3Ccircle cx='345' cy='290' r='2' opacity='0.08'/%3E%3Ccircle cx='420' cy='200' r='1.5' opacity='0.07'/%3E%3Ccircle cx='260' cy='350' r='2' opacity='0.07'/%3E%3Ccircle cx='40' cy='120' r='1.5' opacity='0.06'/%3E%3Ccircle cx='310' cy='400' r='1.5' opacity='0.06'/%3E%3C/g%3E%3C/svg%3E");
24
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Cloudflare Turnstile Type Declarations
3
+ *
4
+ * These types define the Turnstile widget API loaded from
5
+ * https://challenges.cloudflare.com/turnstile/v0/api.js
6
+ */
7
+
8
+ interface TurnstileRenderOptions {
9
+ sitekey: string;
10
+ callback?: (token: string) => void;
11
+ 'error-callback'?: (error: string) => void;
12
+ 'expired-callback'?: () => void;
13
+ theme?: 'light' | 'dark' | 'auto';
14
+ size?: 'normal' | 'compact';
15
+ tabindex?: number;
16
+ action?: string;
17
+ cData?: string;
18
+ 'response-field'?: boolean;
19
+ 'response-field-name'?: string;
20
+ 'refresh-expired'?: 'auto' | 'manual' | 'never';
21
+ language?: string;
22
+ appearance?: 'always' | 'execute' | 'interaction-only';
23
+ retry?: 'auto' | 'never';
24
+ 'retry-interval'?: number;
25
+ }
26
+
27
+ interface TurnstileWidget {
28
+ render(container: HTMLElement | string, options: TurnstileRenderOptions): string;
29
+ reset(widgetId?: string): void;
30
+ remove(widgetId: string): void;
31
+ getResponse(widgetId?: string): string | undefined;
32
+ isExpired(widgetId?: string): boolean;
33
+ execute(container?: HTMLElement | string, options?: TurnstileRenderOptions): void;
34
+ }
35
+
36
+ declare global {
37
+ interface Window {
38
+ turnstile?: TurnstileWidget;
39
+ }
40
+ }
41
+
42
+ export {};
@@ -0,0 +1,111 @@
1
+ <script lang="ts">
2
+ /**
3
+ * TurnstileWidget - Cloudflare Turnstile human verification (Shade)
4
+ *
5
+ * Usage:
6
+ * <TurnstileWidget siteKey={TURNSTILE_SITE_KEY} onverify={(token) => handleToken(token)} />
7
+ *
8
+ * The widget is invisible in "managed" mode - users only see it when suspicious.
9
+ */
10
+
11
+ import { onMount, onDestroy } from 'svelte';
12
+
13
+ interface Props {
14
+ siteKey: string;
15
+ theme?: 'light' | 'dark' | 'auto';
16
+ size?: 'normal' | 'compact';
17
+ onverify?: (token: string) => void;
18
+ onerror?: (error: string) => void;
19
+ onexpire?: () => void;
20
+ }
21
+
22
+ let {
23
+ siteKey,
24
+ theme = 'auto',
25
+ size = 'normal',
26
+ onverify,
27
+ onerror,
28
+ onexpire
29
+ }: Props = $props();
30
+
31
+ let container: HTMLDivElement;
32
+ let widgetId: string | null = null;
33
+
34
+ // Load the Turnstile script if not already loaded
35
+ function loadScript(): Promise<void> {
36
+ return new Promise((resolve, reject) => {
37
+ if (window.turnstile) {
38
+ resolve();
39
+ return;
40
+ }
41
+
42
+ const script = document.createElement('script');
43
+ script.src = 'https://challenges.cloudflare.com/turnstile/v0/api.js';
44
+ script.async = true;
45
+ script.defer = true;
46
+ script.onload = () => resolve();
47
+ script.onerror = () => reject(new Error('Failed to load Turnstile script'));
48
+ document.head.appendChild(script);
49
+ });
50
+ }
51
+
52
+ function renderWidget() {
53
+ if (!window.turnstile || !container) return;
54
+
55
+ widgetId = window.turnstile.render(container, {
56
+ sitekey: siteKey,
57
+ theme,
58
+ size,
59
+ callback: (token: string) => {
60
+ onverify?.(token);
61
+ },
62
+ 'error-callback': (error: string) => {
63
+ onerror?.(error);
64
+ },
65
+ 'expired-callback': () => {
66
+ onexpire?.();
67
+ }
68
+ });
69
+ }
70
+
71
+ onMount(async () => {
72
+ try {
73
+ await loadScript();
74
+ renderWidget();
75
+ } catch (err) {
76
+ console.error('Turnstile load error:', err);
77
+ onerror?.(err instanceof Error ? err.message : 'Failed to load Turnstile');
78
+ }
79
+ });
80
+
81
+ onDestroy(() => {
82
+ if (widgetId && window.turnstile) {
83
+ window.turnstile.remove(widgetId);
84
+ }
85
+ });
86
+
87
+ // Reset the widget (useful after form submission)
88
+ export function reset() {
89
+ if (widgetId && window.turnstile) {
90
+ window.turnstile.reset(widgetId);
91
+ }
92
+ }
93
+
94
+ // Get the current token
95
+ export function getToken(): string | undefined {
96
+ if (widgetId && window.turnstile) {
97
+ return window.turnstile.getResponse(widgetId);
98
+ }
99
+ return undefined;
100
+ }
101
+ </script>
102
+
103
+ <div bind:this={container} class="turnstile-container"></div>
104
+
105
+ <style>
106
+ .turnstile-container {
107
+ display: flex;
108
+ justify-content: center;
109
+ min-height: 65px;
110
+ }
111
+ </style>
@@ -0,0 +1,14 @@
1
+ interface Props {
2
+ siteKey: string;
3
+ theme?: 'light' | 'dark' | 'auto';
4
+ size?: 'normal' | 'compact';
5
+ onverify?: (token: string) => void;
6
+ onerror?: (error: string) => void;
7
+ onexpire?: () => void;
8
+ }
9
+ declare const TurnstileWidget: import("svelte").Component<Props, {
10
+ reset: () => void;
11
+ getToken: () => string | undefined;
12
+ }, "">;
13
+ type TurnstileWidget = ReturnType<typeof TurnstileWidget>;
14
+ export default TurnstileWidget;
@@ -12,7 +12,7 @@
12
12
  <DialogPrimitive.Overlay
13
13
  bind:ref
14
14
  class={cn(
15
- "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80",
15
+ "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50 backdrop-blur-sm",
16
16
  className
17
17
  )}
18
18
  {...restProps}
@@ -14,7 +14,7 @@
14
14
  <SheetPrimitive.Overlay
15
15
  bind:ref
16
16
  class={cn(
17
- "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80",
17
+ "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50 backdrop-blur-sm",
18
18
  className
19
19
  )}
20
20
  {...restProps}