@autumnsgrove/groveengine 0.5.0 → 0.6.2
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/README.md +5 -3
- package/dist/components/OnboardingChecklist.svelte +118 -0
- package/dist/components/OnboardingChecklist.svelte.d.ts +14 -0
- package/dist/components/quota/UpgradePrompt.svelte +8 -7
- package/dist/groveauth/limits.js +11 -3
- package/dist/groveauth/types.d.ts +9 -7
- package/dist/groveauth/types.js +16 -12
- package/dist/payments/types.d.ts +7 -2
- package/dist/server/services/__mocks__/cloudflare.d.ts +54 -0
- package/dist/server/services/__mocks__/cloudflare.js +470 -0
- package/dist/server/services/cache.d.ts +170 -0
- package/dist/server/services/cache.js +335 -0
- package/dist/server/services/database.d.ts +236 -0
- package/dist/server/services/database.js +450 -0
- package/dist/server/services/index.d.ts +34 -0
- package/dist/server/services/index.js +77 -0
- package/dist/server/services/storage.d.ts +221 -0
- package/dist/server/services/storage.js +485 -0
- package/package.json +12 -2
- package/static/fonts/Calistoga-Regular.ttf +1438 -0
- package/static/fonts/Caveat-Regular.ttf +0 -0
- package/static/fonts/EBGaramond-Regular.ttf +0 -0
- package/static/fonts/Fraunces-Regular.ttf +0 -0
- package/static/fonts/InstrumentSans-Regular.ttf +0 -0
- package/static/fonts/Lora-Regular.ttf +0 -0
- package/static/fonts/Luciole-Regular.ttf +1438 -0
- package/static/fonts/Manrope-Regular.ttf +0 -0
- package/static/fonts/Merriweather-Regular.ttf +1439 -0
- package/static/fonts/Nunito-Regular.ttf +0 -0
- package/static/fonts/PlusJakartaSans-Regular.ttf +0 -0
package/README.md
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
# @
|
|
1
|
+
# Lattice (@autumnsgrove/groveengine)
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> **Internal codename:** GroveEngine
|
|
4
|
+
|
|
5
|
+
Multi-tenant blog engine for the Grove Platform. Each Grove site runs as its own Cloudflare Worker, powered by Lattice. A lattice is the framework that supports growth—vines climb it, gardens are built around it.
|
|
4
6
|
|
|
5
7
|
## Features
|
|
6
8
|
|
|
@@ -106,7 +108,7 @@ RESEND_API_KEY=re_xxxxx
|
|
|
106
108
|
|
|
107
109
|
## Fonts
|
|
108
110
|
|
|
109
|
-
|
|
111
|
+
Lattice includes self-hosted accessibility-focused fonts in `static/fonts/`. After installing the package, copy the fonts to your project's static directory:
|
|
110
112
|
|
|
111
113
|
```bash
|
|
112
114
|
# Copy fonts from node_modules to your static folder
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { Check, Circle, X, Sparkles } from 'lucide-svelte';
|
|
3
|
+
|
|
4
|
+
interface ChecklistItem {
|
|
5
|
+
id: string;
|
|
6
|
+
label: string;
|
|
7
|
+
completed: boolean;
|
|
8
|
+
href?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface Props {
|
|
12
|
+
items: ChecklistItem[];
|
|
13
|
+
onDismiss?: () => void;
|
|
14
|
+
class?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
let { items, onDismiss, class: className = '' }: Props = $props();
|
|
18
|
+
|
|
19
|
+
const completedCount = $derived(items.filter((i) => i.completed).length);
|
|
20
|
+
const allCompleted = $derived(completedCount === items.length);
|
|
21
|
+
const progress = $derived((completedCount / items.length) * 100);
|
|
22
|
+
</script>
|
|
23
|
+
|
|
24
|
+
<div class="bg-surface-elevated border border-default rounded-lg p-4 {className}">
|
|
25
|
+
<!-- Header -->
|
|
26
|
+
<div class="flex items-center justify-between mb-4">
|
|
27
|
+
<div class="flex items-center gap-2">
|
|
28
|
+
<Sparkles size={18} class="text-primary" />
|
|
29
|
+
<h3 class="font-medium text-foreground text-sm">Getting Started</h3>
|
|
30
|
+
</div>
|
|
31
|
+
{#if onDismiss}
|
|
32
|
+
<button
|
|
33
|
+
onclick={onDismiss}
|
|
34
|
+
class="text-foreground-subtle hover:text-foreground transition-colors"
|
|
35
|
+
title="Dismiss checklist"
|
|
36
|
+
>
|
|
37
|
+
<X size={16} />
|
|
38
|
+
</button>
|
|
39
|
+
{/if}
|
|
40
|
+
</div>
|
|
41
|
+
|
|
42
|
+
<!-- Progress bar -->
|
|
43
|
+
<div class="mb-4">
|
|
44
|
+
<div class="h-1.5 bg-surface rounded-full overflow-hidden">
|
|
45
|
+
<div
|
|
46
|
+
class="h-full bg-primary transition-all duration-500"
|
|
47
|
+
style="width: {progress}%"
|
|
48
|
+
></div>
|
|
49
|
+
</div>
|
|
50
|
+
<p class="text-xs text-foreground-subtle mt-1">
|
|
51
|
+
{completedCount} of {items.length} complete
|
|
52
|
+
</p>
|
|
53
|
+
</div>
|
|
54
|
+
|
|
55
|
+
<!-- Checklist items -->
|
|
56
|
+
<ul class="space-y-2">
|
|
57
|
+
{#each items as item}
|
|
58
|
+
<li>
|
|
59
|
+
{#if item.href && !item.completed}
|
|
60
|
+
<a
|
|
61
|
+
href={item.href}
|
|
62
|
+
class="flex items-center gap-3 p-2 rounded-md hover:bg-surface transition-colors group"
|
|
63
|
+
>
|
|
64
|
+
<div
|
|
65
|
+
class="w-5 h-5 rounded-full border-2 flex items-center justify-center flex-shrink-0 transition-colors"
|
|
66
|
+
class:border-primary={item.completed}
|
|
67
|
+
class:bg-primary={item.completed}
|
|
68
|
+
class:border-default={!item.completed}
|
|
69
|
+
class:group-hover:border-primary={!item.completed}
|
|
70
|
+
>
|
|
71
|
+
{#if item.completed}
|
|
72
|
+
<Check size={12} class="text-white" />
|
|
73
|
+
{/if}
|
|
74
|
+
</div>
|
|
75
|
+
<span
|
|
76
|
+
class="text-sm transition-colors"
|
|
77
|
+
class:text-foreground-subtle={item.completed}
|
|
78
|
+
class:line-through={item.completed}
|
|
79
|
+
class:text-foreground={!item.completed}
|
|
80
|
+
>
|
|
81
|
+
{item.label}
|
|
82
|
+
</span>
|
|
83
|
+
</a>
|
|
84
|
+
{:else}
|
|
85
|
+
<div class="flex items-center gap-3 p-2">
|
|
86
|
+
<div
|
|
87
|
+
class="w-5 h-5 rounded-full border-2 flex items-center justify-center flex-shrink-0"
|
|
88
|
+
class:border-primary={item.completed}
|
|
89
|
+
class:bg-primary={item.completed}
|
|
90
|
+
class:border-default={!item.completed}
|
|
91
|
+
>
|
|
92
|
+
{#if item.completed}
|
|
93
|
+
<Check size={12} class="text-white" />
|
|
94
|
+
{/if}
|
|
95
|
+
</div>
|
|
96
|
+
<span
|
|
97
|
+
class="text-sm"
|
|
98
|
+
class:text-foreground-subtle={item.completed}
|
|
99
|
+
class:line-through={item.completed}
|
|
100
|
+
class:text-foreground={!item.completed}
|
|
101
|
+
>
|
|
102
|
+
{item.label}
|
|
103
|
+
</span>
|
|
104
|
+
</div>
|
|
105
|
+
{/if}
|
|
106
|
+
</li>
|
|
107
|
+
{/each}
|
|
108
|
+
</ul>
|
|
109
|
+
|
|
110
|
+
<!-- Completion celebration -->
|
|
111
|
+
{#if allCompleted}
|
|
112
|
+
<div class="mt-4 p-3 bg-accent rounded-md text-center">
|
|
113
|
+
<p class="text-sm text-foreground">
|
|
114
|
+
🎉 All done! You're a Grove pro now.
|
|
115
|
+
</p>
|
|
116
|
+
</div>
|
|
117
|
+
{/if}
|
|
118
|
+
</div>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
interface ChecklistItem {
|
|
2
|
+
id: string;
|
|
3
|
+
label: string;
|
|
4
|
+
completed: boolean;
|
|
5
|
+
href?: string;
|
|
6
|
+
}
|
|
7
|
+
interface Props {
|
|
8
|
+
items: ChecklistItem[];
|
|
9
|
+
onDismiss?: () => void;
|
|
10
|
+
class?: string;
|
|
11
|
+
}
|
|
12
|
+
declare const OnboardingChecklist: import("svelte").Component<Props, {}, "">;
|
|
13
|
+
type OnboardingChecklist = ReturnType<typeof OnboardingChecklist>;
|
|
14
|
+
export default OnboardingChecklist;
|
|
@@ -52,10 +52,10 @@
|
|
|
52
52
|
|
|
53
53
|
// Safe status with fallback values
|
|
54
54
|
const safeStatus = $derived(isValidStatus ? status : {
|
|
55
|
-
tier: '
|
|
55
|
+
tier: 'seedling' as const,
|
|
56
56
|
post_count: 0,
|
|
57
|
-
post_limit:
|
|
58
|
-
posts_remaining:
|
|
57
|
+
post_limit: 50,
|
|
58
|
+
posts_remaining: 50,
|
|
59
59
|
percentage_used: 0,
|
|
60
60
|
is_at_limit: false,
|
|
61
61
|
is_in_grace_period: false,
|
|
@@ -73,8 +73,9 @@
|
|
|
73
73
|
|
|
74
74
|
// Tier upgrade path
|
|
75
75
|
const nextTier = $derived(
|
|
76
|
-
safeStatus.tier === '
|
|
77
|
-
safeStatus.tier === '
|
|
76
|
+
safeStatus.tier === 'seedling' ? 'sapling' :
|
|
77
|
+
safeStatus.tier === 'sapling' ? 'oak' :
|
|
78
|
+
safeStatus.tier === 'oak' ? 'evergreen' :
|
|
78
79
|
null
|
|
79
80
|
);
|
|
80
81
|
|
|
@@ -237,8 +238,8 @@
|
|
|
237
238
|
<div>
|
|
238
239
|
<p class="font-medium text-blue-900 dark:text-blue-100">{nextTierName} Plan</p>
|
|
239
240
|
<p class="text-sm text-blue-700 dark:text-blue-300">
|
|
240
|
-
{#if nextTier === '
|
|
241
|
-
Up to
|
|
241
|
+
{#if nextTier === 'sapling'}
|
|
242
|
+
Up to 250 posts
|
|
242
243
|
{:else}
|
|
243
244
|
Unlimited posts
|
|
244
245
|
{/if}
|
package/dist/groveauth/limits.js
CHANGED
|
@@ -75,7 +75,7 @@ export function getSuggestedActions(status) {
|
|
|
75
75
|
*/
|
|
76
76
|
export function getUpgradeRecommendation(status) {
|
|
77
77
|
const tierName = TIER_NAMES[status.tier];
|
|
78
|
-
if (status.tier === '
|
|
78
|
+
if (status.tier === 'evergreen') {
|
|
79
79
|
return {
|
|
80
80
|
recommended: false,
|
|
81
81
|
fromTier: tierName,
|
|
@@ -83,8 +83,16 @@ export function getUpgradeRecommendation(status) {
|
|
|
83
83
|
reason: 'You have the highest tier with unlimited posts',
|
|
84
84
|
};
|
|
85
85
|
}
|
|
86
|
+
if (status.tier === 'oak') {
|
|
87
|
+
return {
|
|
88
|
+
recommended: false,
|
|
89
|
+
fromTier: tierName,
|
|
90
|
+
toTier: 'Evergreen',
|
|
91
|
+
reason: 'You already have unlimited posts. Evergreen adds domain search and support hours.',
|
|
92
|
+
};
|
|
93
|
+
}
|
|
86
94
|
if (status.upgrade_required || status.is_at_limit) {
|
|
87
|
-
const toTier = status.tier === '
|
|
95
|
+
const toTier = status.tier === 'seedling' ? 'Sapling' : 'Oak';
|
|
88
96
|
return {
|
|
89
97
|
recommended: true,
|
|
90
98
|
fromTier: tierName,
|
|
@@ -93,7 +101,7 @@ export function getUpgradeRecommendation(status) {
|
|
|
93
101
|
};
|
|
94
102
|
}
|
|
95
103
|
if (status.percentage_used !== null && status.percentage_used >= 80) {
|
|
96
|
-
const toTier = status.tier === '
|
|
104
|
+
const toTier = status.tier === 'seedling' ? 'Sapling' : 'Oak';
|
|
97
105
|
return {
|
|
98
106
|
recommended: true,
|
|
99
107
|
fromTier: tierName,
|
|
@@ -41,7 +41,7 @@ export interface LoginUrlResult {
|
|
|
41
41
|
state: string;
|
|
42
42
|
codeVerifier: string;
|
|
43
43
|
}
|
|
44
|
-
export type SubscriptionTier = "
|
|
44
|
+
export type SubscriptionTier = "seedling" | "sapling" | "oak" | "evergreen";
|
|
45
45
|
export interface UserSubscription {
|
|
46
46
|
id: string;
|
|
47
47
|
user_id: string;
|
|
@@ -84,12 +84,14 @@ export interface CanPostResponse {
|
|
|
84
84
|
* Post limits per subscription tier.
|
|
85
85
|
*
|
|
86
86
|
* Business rationale:
|
|
87
|
-
* -
|
|
88
|
-
*
|
|
89
|
-
* -
|
|
90
|
-
*
|
|
91
|
-
* -
|
|
92
|
-
*
|
|
87
|
+
* - Seedling (50 posts): Entry-level tier for curious newcomers testing the
|
|
88
|
+
* platform. Low commitment, creates upgrade path.
|
|
89
|
+
* - Sapling (250 posts): For hobbyists and regular bloggers who know they'll
|
|
90
|
+
* stick around. ~1 post/day for 8 months.
|
|
91
|
+
* - Oak (unlimited): For serious bloggers whose blog is part of their
|
|
92
|
+
* identity. Includes BYOD (bring your own domain) and full email.
|
|
93
|
+
* - Evergreen (unlimited): Full-service tier for professionals. Includes domain
|
|
94
|
+
* search, registration, and priority support.
|
|
93
95
|
*
|
|
94
96
|
* Grace period: When users hit their limit, they have 14 days to upgrade or
|
|
95
97
|
* delete posts before their account becomes read-only.
|
package/dist/groveauth/types.js
CHANGED
|
@@ -10,12 +10,14 @@
|
|
|
10
10
|
* Post limits per subscription tier.
|
|
11
11
|
*
|
|
12
12
|
* Business rationale:
|
|
13
|
-
* -
|
|
14
|
-
*
|
|
15
|
-
* -
|
|
16
|
-
*
|
|
17
|
-
* -
|
|
18
|
-
*
|
|
13
|
+
* - Seedling (50 posts): Entry-level tier for curious newcomers testing the
|
|
14
|
+
* platform. Low commitment, creates upgrade path.
|
|
15
|
+
* - Sapling (250 posts): For hobbyists and regular bloggers who know they'll
|
|
16
|
+
* stick around. ~1 post/day for 8 months.
|
|
17
|
+
* - Oak (unlimited): For serious bloggers whose blog is part of their
|
|
18
|
+
* identity. Includes BYOD (bring your own domain) and full email.
|
|
19
|
+
* - Evergreen (unlimited): Full-service tier for professionals. Includes domain
|
|
20
|
+
* search, registration, and priority support.
|
|
19
21
|
*
|
|
20
22
|
* Grace period: When users hit their limit, they have 14 days to upgrade or
|
|
21
23
|
* delete posts before their account becomes read-only.
|
|
@@ -23,17 +25,19 @@
|
|
|
23
25
|
* @see docs/implementing-post-limits.md for full specification
|
|
24
26
|
*/
|
|
25
27
|
export const TIER_POST_LIMITS = {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
28
|
+
seedling: 50, // For curious newcomers trying blogging
|
|
29
|
+
sapling: 250, // For hobbyists and regular bloggers
|
|
30
|
+
oak: null, // Unlimited for serious bloggers
|
|
31
|
+
evergreen: null, // Unlimited for professionals
|
|
29
32
|
};
|
|
30
33
|
/**
|
|
31
34
|
* Human-readable tier names for UI display.
|
|
32
35
|
*/
|
|
33
36
|
export const TIER_NAMES = {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
+
seedling: "Seedling",
|
|
38
|
+
sapling: "Sapling",
|
|
39
|
+
oak: "Oak",
|
|
40
|
+
evergreen: "Evergreen",
|
|
37
41
|
};
|
|
38
42
|
/**
|
|
39
43
|
* Client-side error class for GroveAuth operations.
|
package/dist/payments/types.d.ts
CHANGED
|
@@ -227,21 +227,26 @@ export interface ConnectOnboardingResult {
|
|
|
227
227
|
onboardingUrl: string;
|
|
228
228
|
expiresAt?: Date;
|
|
229
229
|
}
|
|
230
|
-
export type PlanTier = '
|
|
230
|
+
export type PlanTier = 'seedling' | 'sapling' | 'oak' | 'evergreen';
|
|
231
231
|
export interface PlatformPlan {
|
|
232
232
|
tier: PlanTier;
|
|
233
233
|
name: string;
|
|
234
234
|
price: Money;
|
|
235
|
+
yearlyPrice?: Money;
|
|
235
236
|
interval: BillingInterval;
|
|
236
237
|
features: string[];
|
|
237
238
|
limits: {
|
|
238
239
|
posts?: number;
|
|
239
240
|
storage?: number;
|
|
240
241
|
customDomain?: boolean;
|
|
241
|
-
|
|
242
|
+
byod?: boolean;
|
|
243
|
+
email?: 'none' | 'forward' | 'full';
|
|
244
|
+
analytics?: 'basic' | 'full';
|
|
245
|
+
supportHours?: number;
|
|
242
246
|
shop?: boolean;
|
|
243
247
|
};
|
|
244
248
|
providerPriceId?: string;
|
|
249
|
+
yearlyPriceId?: string;
|
|
245
250
|
}
|
|
246
251
|
export interface TenantBilling {
|
|
247
252
|
tenantId: string;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mock implementations for Cloudflare services (D1, KV, R2)
|
|
3
|
+
* Used for testing the service abstraction layer
|
|
4
|
+
*/
|
|
5
|
+
interface MockRow {
|
|
6
|
+
[key: string]: unknown;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Creates a mock D1 database with in-memory storage
|
|
10
|
+
*/
|
|
11
|
+
export declare function createMockD1(): D1Database & {
|
|
12
|
+
_tables: Map<string, MockRow[]>;
|
|
13
|
+
};
|
|
14
|
+
interface KVEntry {
|
|
15
|
+
value: string;
|
|
16
|
+
expiresAt?: number;
|
|
17
|
+
metadata?: unknown;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Creates a mock KV namespace with in-memory storage
|
|
21
|
+
*/
|
|
22
|
+
export declare function createMockKV(): KVNamespace & {
|
|
23
|
+
_store: Map<string, KVEntry>;
|
|
24
|
+
};
|
|
25
|
+
interface R2Entry {
|
|
26
|
+
body: ArrayBuffer;
|
|
27
|
+
httpMetadata?: {
|
|
28
|
+
contentType?: string;
|
|
29
|
+
cacheControl?: string;
|
|
30
|
+
};
|
|
31
|
+
customMetadata?: Record<string, string>;
|
|
32
|
+
etag: string;
|
|
33
|
+
uploaded: Date;
|
|
34
|
+
size: number;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Creates a mock R2 bucket with in-memory storage
|
|
38
|
+
*/
|
|
39
|
+
export declare function createMockR2(): R2Bucket & {
|
|
40
|
+
_objects: Map<string, R2Entry>;
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* Seed a mock D1 database with initial data
|
|
44
|
+
*/
|
|
45
|
+
export declare function seedMockD1(db: ReturnType<typeof createMockD1>, tableName: string, rows: MockRow[]): void;
|
|
46
|
+
/**
|
|
47
|
+
* Clear all data from a mock D1 database
|
|
48
|
+
*/
|
|
49
|
+
export declare function clearMockD1(db: ReturnType<typeof createMockD1>): void;
|
|
50
|
+
/**
|
|
51
|
+
* Advance time for KV expiration testing
|
|
52
|
+
*/
|
|
53
|
+
export declare function advanceKVTime(kv: ReturnType<typeof createMockKV>, ms: number): void;
|
|
54
|
+
export {};
|