@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 CHANGED
@@ -1,6 +1,8 @@
1
- # @grove/engine
1
+ # Lattice (@autumnsgrove/groveengine)
2
2
 
3
- Multi-tenant blog engine for the Grove Platform. Each Grove site runs as its own Cloudflare Worker, powered by this engine.
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
- GroveEngine includes self-hosted accessibility-focused fonts in `static/fonts/`. After installing the package, copy the fonts to your project's static directory:
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: 'starter' as const,
55
+ tier: 'seedling' as const,
56
56
  post_count: 0,
57
- post_limit: 250,
58
- posts_remaining: 250,
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 === 'starter' ? 'professional' :
77
- safeStatus.tier === 'professional' ? 'business' :
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 === 'professional'}
241
- Up to 2,000 posts
241
+ {#if nextTier === 'sapling'}
242
+ Up to 250 posts
242
243
  {:else}
243
244
  Unlimited posts
244
245
  {/if}
@@ -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 === 'business') {
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 === 'starter' ? 'Professional' : 'Business';
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 === 'starter' ? 'Professional' : 'Business';
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 = "starter" | "professional" | "business";
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
- * - Starter (250 posts): Entry-level tier for hobbyists, personal bloggers,
88
- * and users testing the platform. ~1 post/day for 8 months.
89
- * - Professional (2,000 posts): For active content creators, small businesses,
90
- * and dedicated bloggers. ~1 post/day for 5+ years.
91
- * - Business (unlimited): For enterprises, agencies, and power users who need
92
- * no restrictions. Includes custom domain support.
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.
@@ -10,12 +10,14 @@
10
10
  * Post limits per subscription tier.
11
11
  *
12
12
  * Business rationale:
13
- * - Starter (250 posts): Entry-level tier for hobbyists, personal bloggers,
14
- * and users testing the platform. ~1 post/day for 8 months.
15
- * - Professional (2,000 posts): For active content creators, small businesses,
16
- * and dedicated bloggers. ~1 post/day for 5+ years.
17
- * - Business (unlimited): For enterprises, agencies, and power users who need
18
- * no restrictions. Includes custom domain support.
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
- starter: 250, // For hobbyists and personal blogs
27
- professional: 2000, // For active bloggers and small businesses
28
- business: null, // Unlimited for enterprises and power users
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
- starter: "Starter",
35
- professional: "Professional",
36
- business: "Business",
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.
@@ -227,21 +227,26 @@ export interface ConnectOnboardingResult {
227
227
  onboardingUrl: string;
228
228
  expiresAt?: Date;
229
229
  }
230
- export type PlanTier = 'starter' | 'professional' | 'business';
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
- analytics?: boolean;
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 {};