@autumnsgrove/groveengine 0.1.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.
Files changed (219) hide show
  1. package/README.md +163 -0
  2. package/dist/auth/jwt.d.ts +14 -0
  3. package/dist/auth/jwt.js +109 -0
  4. package/dist/auth/session.d.ts +42 -0
  5. package/dist/auth/session.js +105 -0
  6. package/dist/components/admin/GutterManager.svelte +910 -0
  7. package/dist/components/admin/GutterManager.svelte.d.ts +15 -0
  8. package/dist/components/admin/MarkdownEditor.svelte +3114 -0
  9. package/dist/components/admin/MarkdownEditor.svelte.d.ts +43 -0
  10. package/dist/components/custom/CollapsibleSection.svelte +74 -0
  11. package/dist/components/custom/CollapsibleSection.svelte.d.ts +15 -0
  12. package/dist/components/custom/ContentWithGutter.svelte +646 -0
  13. package/dist/components/custom/ContentWithGutter.svelte.d.ts +19 -0
  14. package/dist/components/custom/GutterItem.svelte +201 -0
  15. package/dist/components/custom/GutterItem.svelte.d.ts +11 -0
  16. package/dist/components/custom/LeftGutter.svelte +271 -0
  17. package/dist/components/custom/LeftGutter.svelte.d.ts +17 -0
  18. package/dist/components/custom/MobileTOC.svelte +273 -0
  19. package/dist/components/custom/MobileTOC.svelte.d.ts +11 -0
  20. package/dist/components/custom/TableOfContents.svelte +163 -0
  21. package/dist/components/custom/TableOfContents.svelte.d.ts +11 -0
  22. package/dist/components/gallery/ImageGallery.svelte +681 -0
  23. package/dist/components/gallery/ImageGallery.svelte.d.ts +11 -0
  24. package/dist/components/gallery/Lightbox.svelte +107 -0
  25. package/dist/components/gallery/Lightbox.svelte.d.ts +19 -0
  26. package/dist/components/gallery/LightboxCaption.svelte +25 -0
  27. package/dist/components/gallery/LightboxCaption.svelte.d.ts +11 -0
  28. package/dist/components/gallery/ZoomableImage.svelte +163 -0
  29. package/dist/components/gallery/ZoomableImage.svelte.d.ts +17 -0
  30. package/dist/components/ui/Accordion.svelte +74 -0
  31. package/dist/components/ui/Accordion.svelte.d.ts +42 -0
  32. package/dist/components/ui/Badge.svelte +48 -0
  33. package/dist/components/ui/Badge.svelte.d.ts +26 -0
  34. package/dist/components/ui/Button.svelte +74 -0
  35. package/dist/components/ui/Button.svelte.d.ts +34 -0
  36. package/dist/components/ui/Card.svelte +102 -0
  37. package/dist/components/ui/Card.svelte.d.ts +46 -0
  38. package/dist/components/ui/Dialog.svelte +91 -0
  39. package/dist/components/ui/Dialog.svelte.d.ts +43 -0
  40. package/dist/components/ui/Input.svelte +81 -0
  41. package/dist/components/ui/Input.svelte.d.ts +35 -0
  42. package/dist/components/ui/Select.svelte +69 -0
  43. package/dist/components/ui/Select.svelte.d.ts +36 -0
  44. package/dist/components/ui/Sheet.svelte +98 -0
  45. package/dist/components/ui/Sheet.svelte.d.ts +45 -0
  46. package/dist/components/ui/Skeleton.svelte +31 -0
  47. package/dist/components/ui/Skeleton.svelte.d.ts +26 -0
  48. package/dist/components/ui/Table.svelte +59 -0
  49. package/dist/components/ui/Table.svelte.d.ts +44 -0
  50. package/dist/components/ui/Tabs.svelte +76 -0
  51. package/dist/components/ui/Tabs.svelte.d.ts +41 -0
  52. package/dist/components/ui/Textarea.svelte +81 -0
  53. package/dist/components/ui/Textarea.svelte.d.ts +35 -0
  54. package/dist/components/ui/Toast.svelte +18 -0
  55. package/dist/components/ui/Toast.svelte.d.ts +7 -0
  56. package/dist/components/ui/accordion/accordion-content.svelte +24 -0
  57. package/dist/components/ui/accordion/accordion-content.svelte.d.ts +4 -0
  58. package/dist/components/ui/accordion/accordion-item.svelte +12 -0
  59. package/dist/components/ui/accordion/accordion-item.svelte.d.ts +4 -0
  60. package/dist/components/ui/accordion/accordion-trigger.svelte +29 -0
  61. package/dist/components/ui/accordion/accordion-trigger.svelte.d.ts +7 -0
  62. package/dist/components/ui/accordion/index.d.ts +6 -0
  63. package/dist/components/ui/accordion/index.js +8 -0
  64. package/dist/components/ui/badge/badge.svelte +50 -0
  65. package/dist/components/ui/badge/badge.svelte.d.ts +60 -0
  66. package/dist/components/ui/badge/index.d.ts +2 -0
  67. package/dist/components/ui/badge/index.js +2 -0
  68. package/dist/components/ui/button/button.svelte +82 -0
  69. package/dist/components/ui/button/button.svelte.d.ts +132 -0
  70. package/dist/components/ui/button/index.d.ts +2 -0
  71. package/dist/components/ui/button/index.js +4 -0
  72. package/dist/components/ui/card/card-content.svelte +16 -0
  73. package/dist/components/ui/card/card-content.svelte.d.ts +5 -0
  74. package/dist/components/ui/card/card-description.svelte +16 -0
  75. package/dist/components/ui/card/card-description.svelte.d.ts +5 -0
  76. package/dist/components/ui/card/card-footer.svelte +16 -0
  77. package/dist/components/ui/card/card-footer.svelte.d.ts +5 -0
  78. package/dist/components/ui/card/card-header.svelte +16 -0
  79. package/dist/components/ui/card/card-header.svelte.d.ts +5 -0
  80. package/dist/components/ui/card/card-title.svelte +25 -0
  81. package/dist/components/ui/card/card-title.svelte.d.ts +8 -0
  82. package/dist/components/ui/card/card.svelte +20 -0
  83. package/dist/components/ui/card/card.svelte.d.ts +5 -0
  84. package/dist/components/ui/card/index.d.ts +7 -0
  85. package/dist/components/ui/card/index.js +9 -0
  86. package/dist/components/ui/dialog/dialog-content.svelte +38 -0
  87. package/dist/components/ui/dialog/dialog-content.svelte.d.ts +9 -0
  88. package/dist/components/ui/dialog/dialog-description.svelte +16 -0
  89. package/dist/components/ui/dialog/dialog-description.svelte.d.ts +4 -0
  90. package/dist/components/ui/dialog/dialog-footer.svelte +20 -0
  91. package/dist/components/ui/dialog/dialog-footer.svelte.d.ts +5 -0
  92. package/dist/components/ui/dialog/dialog-header.svelte +20 -0
  93. package/dist/components/ui/dialog/dialog-header.svelte.d.ts +5 -0
  94. package/dist/components/ui/dialog/dialog-overlay.svelte +19 -0
  95. package/dist/components/ui/dialog/dialog-overlay.svelte.d.ts +4 -0
  96. package/dist/components/ui/dialog/dialog-title.svelte +16 -0
  97. package/dist/components/ui/dialog/dialog-title.svelte.d.ts +4 -0
  98. package/dist/components/ui/dialog/index.d.ts +12 -0
  99. package/dist/components/ui/dialog/index.js +14 -0
  100. package/dist/components/ui/index.d.ts +26 -0
  101. package/dist/components/ui/index.js +29 -0
  102. package/dist/components/ui/input/index.d.ts +2 -0
  103. package/dist/components/ui/input/index.js +4 -0
  104. package/dist/components/ui/input/input.svelte +46 -0
  105. package/dist/components/ui/input/input.svelte.d.ts +13 -0
  106. package/dist/components/ui/select/index.d.ts +11 -0
  107. package/dist/components/ui/select/index.js +13 -0
  108. package/dist/components/ui/select/select-content.svelte +39 -0
  109. package/dist/components/ui/select/select-content.svelte.d.ts +7 -0
  110. package/dist/components/ui/select/select-group-heading.svelte +16 -0
  111. package/dist/components/ui/select/select-group-heading.svelte.d.ts +4 -0
  112. package/dist/components/ui/select/select-item.svelte +37 -0
  113. package/dist/components/ui/select/select-item.svelte.d.ts +4 -0
  114. package/dist/components/ui/select/select-scroll-down-button.svelte +19 -0
  115. package/dist/components/ui/select/select-scroll-down-button.svelte.d.ts +4 -0
  116. package/dist/components/ui/select/select-scroll-up-button.svelte +19 -0
  117. package/dist/components/ui/select/select-scroll-up-button.svelte.d.ts +4 -0
  118. package/dist/components/ui/select/select-separator.svelte +13 -0
  119. package/dist/components/ui/select/select-separator.svelte.d.ts +4 -0
  120. package/dist/components/ui/select/select-trigger.svelte +24 -0
  121. package/dist/components/ui/select/select-trigger.svelte.d.ts +4 -0
  122. package/dist/components/ui/separator/index.d.ts +2 -0
  123. package/dist/components/ui/separator/index.js +4 -0
  124. package/dist/components/ui/separator/separator.svelte +22 -0
  125. package/dist/components/ui/separator/separator.svelte.d.ts +4 -0
  126. package/dist/components/ui/sheet/index.d.ts +12 -0
  127. package/dist/components/ui/sheet/index.js +14 -0
  128. package/dist/components/ui/sheet/sheet-content.svelte +53 -0
  129. package/dist/components/ui/sheet/sheet-content.svelte.d.ts +62 -0
  130. package/dist/components/ui/sheet/sheet-description.svelte +16 -0
  131. package/dist/components/ui/sheet/sheet-description.svelte.d.ts +4 -0
  132. package/dist/components/ui/sheet/sheet-footer.svelte +20 -0
  133. package/dist/components/ui/sheet/sheet-footer.svelte.d.ts +5 -0
  134. package/dist/components/ui/sheet/sheet-header.svelte +20 -0
  135. package/dist/components/ui/sheet/sheet-header.svelte.d.ts +5 -0
  136. package/dist/components/ui/sheet/sheet-overlay.svelte +21 -0
  137. package/dist/components/ui/sheet/sheet-overlay.svelte.d.ts +6 -0
  138. package/dist/components/ui/sheet/sheet-title.svelte +16 -0
  139. package/dist/components/ui/sheet/sheet-title.svelte.d.ts +4 -0
  140. package/dist/components/ui/skeleton/index.d.ts +2 -0
  141. package/dist/components/ui/skeleton/index.js +4 -0
  142. package/dist/components/ui/skeleton/skeleton.svelte +17 -0
  143. package/dist/components/ui/skeleton/skeleton.svelte.d.ts +5 -0
  144. package/dist/components/ui/table/index.d.ts +9 -0
  145. package/dist/components/ui/table/index.js +11 -0
  146. package/dist/components/ui/table/table-body.svelte +16 -0
  147. package/dist/components/ui/table/table-body.svelte.d.ts +5 -0
  148. package/dist/components/ui/table/table-caption.svelte +16 -0
  149. package/dist/components/ui/table/table-caption.svelte.d.ts +5 -0
  150. package/dist/components/ui/table/table-cell.svelte +20 -0
  151. package/dist/components/ui/table/table-cell.svelte.d.ts +5 -0
  152. package/dist/components/ui/table/table-footer.svelte +16 -0
  153. package/dist/components/ui/table/table-footer.svelte.d.ts +5 -0
  154. package/dist/components/ui/table/table-head.svelte +23 -0
  155. package/dist/components/ui/table/table-head.svelte.d.ts +5 -0
  156. package/dist/components/ui/table/table-header.svelte +16 -0
  157. package/dist/components/ui/table/table-header.svelte.d.ts +5 -0
  158. package/dist/components/ui/table/table-row.svelte +23 -0
  159. package/dist/components/ui/table/table-row.svelte.d.ts +5 -0
  160. package/dist/components/ui/table/table.svelte +18 -0
  161. package/dist/components/ui/table/table.svelte.d.ts +5 -0
  162. package/dist/components/ui/tabs/index.d.ts +6 -0
  163. package/dist/components/ui/tabs/index.js +8 -0
  164. package/dist/components/ui/tabs/tabs-content.svelte +19 -0
  165. package/dist/components/ui/tabs/tabs-content.svelte.d.ts +4 -0
  166. package/dist/components/ui/tabs/tabs-list.svelte +19 -0
  167. package/dist/components/ui/tabs/tabs-list.svelte.d.ts +4 -0
  168. package/dist/components/ui/tabs/tabs-trigger.svelte +19 -0
  169. package/dist/components/ui/tabs/tabs-trigger.svelte.d.ts +4 -0
  170. package/dist/components/ui/textarea/index.d.ts +2 -0
  171. package/dist/components/ui/textarea/index.js +4 -0
  172. package/dist/components/ui/textarea/textarea.svelte +24 -0
  173. package/dist/components/ui/textarea/textarea.svelte.d.ts +6 -0
  174. package/dist/components/ui/toast.d.ts +86 -0
  175. package/dist/components/ui/toast.js +99 -0
  176. package/dist/db/schema.sql +238 -0
  177. package/dist/index.d.ts +14 -0
  178. package/dist/index.js +20 -0
  179. package/dist/payments/index.d.ts +33 -0
  180. package/dist/payments/index.js +47 -0
  181. package/dist/payments/shop.d.ts +165 -0
  182. package/dist/payments/shop.js +588 -0
  183. package/dist/payments/stripe/client.d.ts +231 -0
  184. package/dist/payments/stripe/client.js +198 -0
  185. package/dist/payments/stripe/index.d.ts +18 -0
  186. package/dist/payments/stripe/index.js +17 -0
  187. package/dist/payments/stripe/provider.d.ts +50 -0
  188. package/dist/payments/stripe/provider.js +530 -0
  189. package/dist/payments/types.d.ts +355 -0
  190. package/dist/payments/types.js +7 -0
  191. package/dist/server/logger.d.ts +53 -0
  192. package/dist/server/logger.js +252 -0
  193. package/dist/styles/content.css +514 -0
  194. package/dist/styles/tokens.css +175 -0
  195. package/dist/utils/api.d.ts +20 -0
  196. package/dist/utils/api.js +109 -0
  197. package/dist/utils/cn.d.ts +15 -0
  198. package/dist/utils/cn.js +18 -0
  199. package/dist/utils/csrf.d.ts +22 -0
  200. package/dist/utils/csrf.js +72 -0
  201. package/dist/utils/debounce.d.ts +7 -0
  202. package/dist/utils/debounce.js +14 -0
  203. package/dist/utils/gallery.d.ts +66 -0
  204. package/dist/utils/gallery.js +181 -0
  205. package/dist/utils/gutter.d.ts +54 -0
  206. package/dist/utils/gutter.js +169 -0
  207. package/dist/utils/imageProcessor.d.ts +58 -0
  208. package/dist/utils/imageProcessor.js +205 -0
  209. package/dist/utils/json.d.ts +17 -0
  210. package/dist/utils/json.js +26 -0
  211. package/dist/utils/markdown.d.ts +101 -0
  212. package/dist/utils/markdown.js +947 -0
  213. package/dist/utils/sanitize.d.ts +25 -0
  214. package/dist/utils/sanitize.js +127 -0
  215. package/dist/utils/validation.d.ts +46 -0
  216. package/dist/utils/validation.js +169 -0
  217. package/dist/utils.d.ts +5 -0
  218. package/dist/utils.js +5 -0
  219. package/package.json +129 -0
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Client-side API utility with automatic CSRF token injection
3
+ * Provides fetch wrapper with security headers and error handling
4
+ */
5
+
6
+ /**
7
+ * Fetch wrapper with automatic CSRF token injection
8
+ * @param {string} url - API endpoint URL
9
+ * @param {RequestInit} options - Fetch options
10
+ * @returns {Promise<any>} Response JSON
11
+ * @throws {Error} If request fails
12
+ */
13
+ export async function apiRequest(url, options = {}) {
14
+ const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content;
15
+
16
+ // Build headers - don't set Content-Type for FormData (browser sets it with boundary)
17
+ const headers = {
18
+ ...(csrfToken && { 'X-CSRF-Token': csrfToken }),
19
+ ...options.headers
20
+ };
21
+
22
+ // Only add Content-Type if not FormData
23
+ if (!(options.body instanceof FormData)) {
24
+ headers['Content-Type'] = 'application/json';
25
+ }
26
+
27
+ const response = await fetch(url, {
28
+ ...options,
29
+ headers
30
+ });
31
+
32
+ if (!response.ok) {
33
+ let errorMessage = 'Request failed';
34
+ try {
35
+ const error = await response.json();
36
+ errorMessage = error.message || errorMessage;
37
+ } catch {
38
+ errorMessage = `${response.status} ${response.statusText}`;
39
+ }
40
+ throw new Error(errorMessage);
41
+ }
42
+
43
+ // Handle empty responses (204 No Content)
44
+ if (response.status === 204) {
45
+ return null;
46
+ }
47
+
48
+ return response.json();
49
+ }
50
+
51
+ /**
52
+ * Convenience methods for common HTTP verbs
53
+ */
54
+ export const api = {
55
+ /**
56
+ * GET request
57
+ * @param {string} url - API endpoint
58
+ * @param {RequestInit} options - Additional fetch options
59
+ */
60
+ get: (url, options = {}) =>
61
+ apiRequest(url, { ...options, method: 'GET' }),
62
+
63
+ /**
64
+ * POST request
65
+ * @param {string} url - API endpoint
66
+ * @param {any} body - Request body (will be JSON stringified)
67
+ * @param {RequestInit} options - Additional fetch options
68
+ */
69
+ post: (url, body, options = {}) =>
70
+ apiRequest(url, {
71
+ ...options,
72
+ method: 'POST',
73
+ body: JSON.stringify(body)
74
+ }),
75
+
76
+ /**
77
+ * PUT request
78
+ * @param {string} url - API endpoint
79
+ * @param {any} body - Request body (will be JSON stringified)
80
+ * @param {RequestInit} options - Additional fetch options
81
+ */
82
+ put: (url, body, options = {}) =>
83
+ apiRequest(url, {
84
+ ...options,
85
+ method: 'PUT',
86
+ body: JSON.stringify(body)
87
+ }),
88
+
89
+ /**
90
+ * DELETE request
91
+ * @param {string} url - API endpoint
92
+ * @param {RequestInit} options - Additional fetch options
93
+ */
94
+ delete: (url, options = {}) =>
95
+ apiRequest(url, { ...options, method: 'DELETE' }),
96
+
97
+ /**
98
+ * PATCH request
99
+ * @param {string} url - API endpoint
100
+ * @param {any} body - Request body (will be JSON stringified)
101
+ * @param {RequestInit} options - Additional fetch options
102
+ */
103
+ patch: (url, body, options = {}) =>
104
+ apiRequest(url, {
105
+ ...options,
106
+ method: 'PATCH',
107
+ body: JSON.stringify(body)
108
+ })
109
+ };
@@ -0,0 +1,15 @@
1
+ import { type ClassValue } from "clsx";
2
+ /**
3
+ * Utility function for merging Tailwind CSS classes
4
+ *
5
+ * Combines clsx for conditional class logic and tailwind-merge to
6
+ * handle Tailwind class conflicts intelligently.
7
+ *
8
+ * @param inputs - Class values to merge (strings, objects, arrays)
9
+ * @returns Merged class string
10
+ *
11
+ * @example
12
+ * cn('px-2 py-1', className)
13
+ * cn('text-blue-500', { 'text-red-500': isError })
14
+ */
15
+ export declare function cn(...inputs: ClassValue[]): string;
@@ -0,0 +1,18 @@
1
+ import { clsx } from "clsx";
2
+ import { twMerge } from "tailwind-merge";
3
+ /**
4
+ * Utility function for merging Tailwind CSS classes
5
+ *
6
+ * Combines clsx for conditional class logic and tailwind-merge to
7
+ * handle Tailwind class conflicts intelligently.
8
+ *
9
+ * @param inputs - Class values to merge (strings, objects, arrays)
10
+ * @returns Merged class string
11
+ *
12
+ * @example
13
+ * cn('px-2 py-1', className)
14
+ * cn('text-blue-500', { 'text-red-500': isError })
15
+ */
16
+ export function cn(...inputs) {
17
+ return twMerge(clsx(inputs));
18
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * CSRF Protection Utilities
3
+ * Validates requests come from same origin
4
+ */
5
+ /**
6
+ * Generate cryptographically secure CSRF token
7
+ * @returns {string} UUID v4 token
8
+ */
9
+ export function generateCSRFToken(): string;
10
+ /**
11
+ * Validate CSRF token from request against session token
12
+ * @param {Request} request - The incoming request
13
+ * @param {string} sessionToken - The token stored in the session
14
+ * @returns {boolean}
15
+ */
16
+ export function validateCSRFToken(request: Request, sessionToken: string): boolean;
17
+ /**
18
+ * Validate CSRF token from request headers (origin-based fallback)
19
+ * @param {Request} request
20
+ * @returns {boolean}
21
+ */
22
+ export function validateCSRF(request: Request): boolean;
@@ -0,0 +1,72 @@
1
+ /**
2
+ * CSRF Protection Utilities
3
+ * Validates requests come from same origin
4
+ */
5
+
6
+ /**
7
+ * Generate cryptographically secure CSRF token
8
+ * @returns {string} UUID v4 token
9
+ */
10
+ export function generateCSRFToken() {
11
+ return crypto.randomUUID();
12
+ }
13
+
14
+ /**
15
+ * Validate CSRF token from request against session token
16
+ * @param {Request} request - The incoming request
17
+ * @param {string} sessionToken - The token stored in the session
18
+ * @returns {boolean}
19
+ */
20
+ export function validateCSRFToken(request, sessionToken) {
21
+ if (!sessionToken) return false;
22
+
23
+ const headerToken = request.headers.get('x-csrf-token');
24
+ const bodyToken = request.headers.get('csrf-token'); // fallback
25
+
26
+ if (!headerToken && !bodyToken) return false;
27
+
28
+ return (headerToken === sessionToken) || (bodyToken === sessionToken);
29
+ }
30
+
31
+ /**
32
+ * Validate CSRF token from request headers (origin-based fallback)
33
+ * @param {Request} request
34
+ * @returns {boolean}
35
+ */
36
+ export function validateCSRF(request) {
37
+ // Handle edge cases
38
+ if (!request || typeof request !== 'object') {
39
+ return false;
40
+ }
41
+
42
+ if (!request.headers || typeof request.headers.get !== 'function') {
43
+ return false;
44
+ }
45
+
46
+ const origin = request.headers.get('origin');
47
+ const host = request.headers.get('host');
48
+
49
+ // Allow same-origin requests
50
+ if (origin) {
51
+ try {
52
+ const originUrl = new URL(origin);
53
+
54
+ // Validate protocol (must be http or https)
55
+ if (!['http:', 'https:'].includes(originUrl.protocol)) {
56
+ return false;
57
+ }
58
+
59
+ const isLocalhost = originUrl.hostname === 'localhost' ||
60
+ originUrl.hostname === '127.0.0.1';
61
+ const hostMatches = host && originUrl.host === host;
62
+
63
+ if (!isLocalhost && !hostMatches) {
64
+ return false;
65
+ }
66
+ } catch {
67
+ return false;
68
+ }
69
+ }
70
+
71
+ return true;
72
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Debounce function calls
3
+ * @param {Function} fn - Function to debounce
4
+ * @param {number} delay - Delay in milliseconds
5
+ * @returns {Function} Debounced function
6
+ */
7
+ export function debounce(fn: Function, delay?: number): Function;
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Debounce function calls
3
+ * @param {Function} fn - Function to debounce
4
+ * @param {number} delay - Delay in milliseconds
5
+ * @returns {Function} Debounced function
6
+ */
7
+ export function debounce(fn, delay = 300) {
8
+ let timeoutId;
9
+
10
+ return function (...args) {
11
+ clearTimeout(timeoutId);
12
+ timeoutId = setTimeout(() => fn.apply(this, args), delay);
13
+ };
14
+ }
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Gallery utilities for filename parsing and filtering
3
+ * Extracts metadata from smart filenames like:
4
+ * - 2025-01-15_forest-walk.jpg → date: 2025-01-15, slug: forest-walk
5
+ * - minecraft/build_2024-12-01.png → category: minecraft, date: 2024-12-01
6
+ * - selfies/2024-summer.jpg → category: selfies, slug: 2024-summer
7
+ */
8
+ /**
9
+ * Parse a filename to extract metadata
10
+ * @param {string} r2Key - R2 object key (e.g., 'minecraft/build.png' or '2025-01-15_photo.jpg')
11
+ * @returns {Object} Parsed metadata: { category, date, slug, filename, extension }
12
+ */
13
+ export function parseImageFilename(r2Key: string): Object;
14
+ /**
15
+ * Get display title for an image (uses custom title or parsed slug)
16
+ * @param {Object} image - Gallery image object
17
+ * @returns {string} Human-readable title
18
+ */
19
+ export function getImageTitle(image: Object): string;
20
+ /**
21
+ * Get display date for an image (uses custom date or parsed date)
22
+ * @param {Object} image - Gallery image object
23
+ * @returns {string|null} YYYY-MM-DD date string or null
24
+ */
25
+ export function getImageDate(image: Object): string | null;
26
+ /**
27
+ * Filter images by search query (searches title, slug, filename)
28
+ * @param {Array} images - Array of gallery images
29
+ * @param {string} query - Search query
30
+ * @returns {Array} Filtered images
31
+ */
32
+ export function searchImages(images: any[], query: string): any[];
33
+ /**
34
+ * Filter images by date range
35
+ * @param {Array} images - Array of gallery images
36
+ * @param {string|null} startDate - YYYY-MM-DD start date (inclusive)
37
+ * @param {string|null} endDate - YYYY-MM-DD end date (inclusive)
38
+ * @returns {Array} Filtered images
39
+ */
40
+ export function filterImagesByDateRange(images: any[], startDate: string | null, endDate: string | null): any[];
41
+ /**
42
+ * Filter images by tags
43
+ * @param {Array} images - Array of gallery images (must include 'tags' array)
44
+ * @param {Array} tagSlugs - Array of tag slugs to filter by
45
+ * @returns {Array} Filtered images
46
+ */
47
+ export function filterImagesByTags(images: any[], tagSlugs: any[]): any[];
48
+ /**
49
+ * Filter images by category (parsed from path)
50
+ * @param {Array} images - Array of gallery images
51
+ * @param {string|null} category - Category to filter by
52
+ * @returns {Array} Filtered images
53
+ */
54
+ export function filterImagesByCategory(images: any[], category: string | null): any[];
55
+ /**
56
+ * Extract unique years from image dates
57
+ * @param {Array} images - Array of gallery images
58
+ * @returns {Array} Sorted array of years (descending)
59
+ */
60
+ export function getAvailableYears(images: any[]): any[];
61
+ /**
62
+ * Extract unique categories from images
63
+ * @param {Array} images - Array of gallery images
64
+ * @returns {Array} Sorted array of categories
65
+ */
66
+ export function getAvailableCategories(images: any[]): any[];
@@ -0,0 +1,181 @@
1
+ /**
2
+ * Gallery utilities for filename parsing and filtering
3
+ * Extracts metadata from smart filenames like:
4
+ * - 2025-01-15_forest-walk.jpg → date: 2025-01-15, slug: forest-walk
5
+ * - minecraft/build_2024-12-01.png → category: minecraft, date: 2024-12-01
6
+ * - selfies/2024-summer.jpg → category: selfies, slug: 2024-summer
7
+ */
8
+
9
+ /**
10
+ * Parse a filename to extract metadata
11
+ * @param {string} r2Key - R2 object key (e.g., 'minecraft/build.png' or '2025-01-15_photo.jpg')
12
+ * @returns {Object} Parsed metadata: { category, date, slug, filename, extension }
13
+ */
14
+ export function parseImageFilename(r2Key) {
15
+ const parts = r2Key.split('/');
16
+ const filename = parts[parts.length - 1];
17
+ const category = parts.length > 1 ? parts[0] : null;
18
+
19
+ // Extract extension
20
+ const extMatch = filename.match(/\.([^.]+)$/);
21
+ const extension = extMatch ? extMatch[1] : null;
22
+ const nameWithoutExt = filename.replace(/\.[^.]+$/, '');
23
+
24
+ // Try to extract date (YYYY-MM-DD or YYYY_MM_DD)
25
+ const dateMatch = nameWithoutExt.match(/(\d{4}[-_]\d{2}[-_]\d{2})/);
26
+ const date = dateMatch ? dateMatch[1].replace(/_/g, '-') : null;
27
+
28
+ // Extract slug (remove date prefix if present)
29
+ let slug = nameWithoutExt;
30
+ if (date) {
31
+ slug = slug.replace(date, '').replace(/^[-_]+/, '').replace(/[-_]+$/, '');
32
+ }
33
+
34
+ // Clean up slug (convert underscores to hyphens)
35
+ slug = slug.replace(/_/g, '-').toLowerCase();
36
+
37
+ return {
38
+ category,
39
+ date,
40
+ slug: slug || 'untitled',
41
+ filename,
42
+ extension,
43
+ r2Key
44
+ };
45
+ }
46
+
47
+ /**
48
+ * Get display title for an image (uses custom title or parsed slug)
49
+ * @param {Object} image - Gallery image object
50
+ * @returns {string} Human-readable title
51
+ */
52
+ export function getImageTitle(image) {
53
+ if (image.custom_title) return image.custom_title;
54
+ if (image.parsed_slug) {
55
+ // Convert slug to Title Case
56
+ return image.parsed_slug
57
+ .split('-')
58
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
59
+ .join(' ');
60
+ }
61
+ return image.r2_key?.split('/').pop() || image.key?.split('/').pop() || 'Untitled';
62
+ }
63
+
64
+ /**
65
+ * Get display date for an image (uses custom date or parsed date)
66
+ * @param {Object} image - Gallery image object
67
+ * @returns {string|null} YYYY-MM-DD date string or null
68
+ */
69
+ export function getImageDate(image) {
70
+ return image.custom_date || image.parsed_date || null;
71
+ }
72
+
73
+ /**
74
+ * Filter images by search query (searches title, slug, filename)
75
+ * @param {Array} images - Array of gallery images
76
+ * @param {string} query - Search query
77
+ * @returns {Array} Filtered images
78
+ */
79
+ export function searchImages(images, query) {
80
+ if (!query) return images;
81
+
82
+ const lowerQuery = query.toLowerCase();
83
+
84
+ return images.filter((img) => {
85
+ const title = getImageTitle(img).toLowerCase();
86
+ const slug = (img.parsed_slug || '').toLowerCase();
87
+ const filename = (img.r2_key || img.key || '').toLowerCase();
88
+ const description = (img.custom_description || '').toLowerCase();
89
+
90
+ return (
91
+ title.includes(lowerQuery) ||
92
+ slug.includes(lowerQuery) ||
93
+ filename.includes(lowerQuery) ||
94
+ description.includes(lowerQuery)
95
+ );
96
+ });
97
+ }
98
+
99
+ /**
100
+ * Filter images by date range
101
+ * @param {Array} images - Array of gallery images
102
+ * @param {string|null} startDate - YYYY-MM-DD start date (inclusive)
103
+ * @param {string|null} endDate - YYYY-MM-DD end date (inclusive)
104
+ * @returns {Array} Filtered images
105
+ */
106
+ export function filterImagesByDateRange(images, startDate, endDate) {
107
+ if (!startDate && !endDate) return images;
108
+
109
+ return images.filter((img) => {
110
+ const imgDate = getImageDate(img);
111
+ if (!imgDate) return false;
112
+
113
+ if (startDate && imgDate < startDate) return false;
114
+ if (endDate && imgDate > endDate) return false;
115
+
116
+ return true;
117
+ });
118
+ }
119
+
120
+ /**
121
+ * Filter images by tags
122
+ * @param {Array} images - Array of gallery images (must include 'tags' array)
123
+ * @param {Array} tagSlugs - Array of tag slugs to filter by
124
+ * @returns {Array} Filtered images
125
+ */
126
+ export function filterImagesByTags(images, tagSlugs) {
127
+ if (!tagSlugs || tagSlugs.length === 0) return images;
128
+
129
+ return images.filter((img) => {
130
+ if (!img.tags || img.tags.length === 0) return false;
131
+ const imgTagSlugs = img.tags.map((t) => t.slug);
132
+ return tagSlugs.every((slug) => imgTagSlugs.includes(slug));
133
+ });
134
+ }
135
+
136
+ /**
137
+ * Filter images by category (parsed from path)
138
+ * @param {Array} images - Array of gallery images
139
+ * @param {string|null} category - Category to filter by
140
+ * @returns {Array} Filtered images
141
+ */
142
+ export function filterImagesByCategory(images, category) {
143
+ if (!category) return images;
144
+ return images.filter((img) => img.parsed_category === category);
145
+ }
146
+
147
+ /**
148
+ * Extract unique years from image dates
149
+ * @param {Array} images - Array of gallery images
150
+ * @returns {Array} Sorted array of years (descending)
151
+ */
152
+ export function getAvailableYears(images) {
153
+ const years = new Set();
154
+
155
+ images.forEach((img) => {
156
+ const date = getImageDate(img);
157
+ if (date) {
158
+ const year = date.substring(0, 4);
159
+ years.add(year);
160
+ }
161
+ });
162
+
163
+ return Array.from(years).sort((a, b) => b.localeCompare(a));
164
+ }
165
+
166
+ /**
167
+ * Extract unique categories from images
168
+ * @param {Array} images - Array of gallery images
169
+ * @returns {Array} Sorted array of categories
170
+ */
171
+ export function getAvailableCategories(images) {
172
+ const categories = new Set();
173
+
174
+ images.forEach((img) => {
175
+ if (img.parsed_category) {
176
+ categories.add(img.parsed_category);
177
+ }
178
+ });
179
+
180
+ return Array.from(categories).sort();
181
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Shared Gutter Utilities
3
+ *
4
+ * This module provides common utilities for gutter content positioning
5
+ * and anchor resolution. Used by ContentWithGutter component and related
6
+ * functionality across the site.
7
+ */
8
+ /**
9
+ * Parse anchor string to determine anchor type and value
10
+ * @param {string} anchor - The anchor string from manifest
11
+ * @returns {Object} Object with type and value properties
12
+ */
13
+ export function parseAnchor(anchor: string): Object;
14
+ /**
15
+ * Generate a unique key for an anchor (used for grouping and positioning)
16
+ * @param {string} anchor - The anchor string
17
+ * @param {Array} headers - Array of header objects with id and text
18
+ * @returns {string} A unique key for the anchor
19
+ */
20
+ export function getAnchorKey(anchor: string, headers?: any[]): string;
21
+ /**
22
+ * Get all unique anchors from items (preserving order)
23
+ * @param {Array} items - Array of gutter items
24
+ * @returns {Array} Array of unique anchor strings
25
+ */
26
+ export function getUniqueAnchors(items: any[]): any[];
27
+ /**
28
+ * Get display label for an anchor (used in overflow section)
29
+ * @param {string} anchor - The anchor string
30
+ * @returns {string} Human-readable label for the anchor
31
+ */
32
+ export function getAnchorLabel(anchor: string): string;
33
+ /**
34
+ * Get items that match a specific anchor
35
+ * @param {Array} items - Array of gutter items
36
+ * @param {string} anchor - The anchor to match
37
+ * @returns {Array} Items matching the anchor
38
+ */
39
+ export function getItemsForAnchor(items: any[], anchor: string): any[];
40
+ /**
41
+ * Get items that don't have a valid anchor (orphan items shown at top)
42
+ * @param {Array} items - Array of gutter items
43
+ * @param {Array} headers - Array of header objects
44
+ * @returns {Array} Items without valid anchors
45
+ */
46
+ export function getOrphanItems(items: any[], headers?: any[]): any[];
47
+ /**
48
+ * Find the DOM element for an anchor within a content element
49
+ * @param {string} anchor - The anchor string
50
+ * @param {HTMLElement} contentEl - The content container element
51
+ * @param {Array} headers - Array of header objects
52
+ * @returns {HTMLElement|null} The DOM element or null if not found
53
+ */
54
+ export function findAnchorElement(anchor: string, contentEl: HTMLElement, headers?: any[]): HTMLElement | null;