@jant/core 0.3.25 → 0.3.26

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 (131) hide show
  1. package/dist/app.js +67 -562
  2. package/dist/client.js +1 -0
  3. package/dist/i18n/locales/en.js +1 -1
  4. package/dist/i18n/locales/zh-Hans.js +1 -1
  5. package/dist/i18n/locales/zh-Hant.js +1 -1
  6. package/dist/lib/avatar-upload.js +134 -0
  7. package/dist/lib/config.js +39 -0
  8. package/dist/lib/constants.js +10 -10
  9. package/dist/lib/favicon.js +102 -0
  10. package/dist/lib/image.js +13 -17
  11. package/dist/lib/media-helpers.js +2 -2
  12. package/dist/lib/navigation.js +23 -3
  13. package/dist/lib/render.js +10 -1
  14. package/dist/lib/schemas.js +31 -0
  15. package/dist/lib/timezones.js +388 -0
  16. package/dist/lib/view.js +1 -1
  17. package/dist/routes/api/posts.js +1 -1
  18. package/dist/routes/api/upload.js +3 -3
  19. package/dist/routes/auth/reset.js +221 -0
  20. package/dist/routes/auth/setup.js +194 -0
  21. package/dist/routes/auth/signin.js +176 -0
  22. package/dist/routes/dash/collections.js +23 -415
  23. package/dist/routes/dash/media.js +12 -392
  24. package/dist/routes/dash/pages.js +7 -330
  25. package/dist/routes/dash/redirects.js +18 -12
  26. package/dist/routes/dash/settings.js +198 -577
  27. package/dist/routes/feed/rss.js +2 -1
  28. package/dist/routes/feed/sitemap.js +4 -2
  29. package/dist/routes/pages/featured.js +5 -1
  30. package/dist/routes/pages/home.js +26 -1
  31. package/dist/routes/pages/latest.js +45 -0
  32. package/dist/services/post.js +30 -50
  33. package/dist/types/bindings.js +3 -0
  34. package/dist/types/config.js +147 -0
  35. package/dist/types/constants.js +27 -0
  36. package/dist/types/entities.js +3 -0
  37. package/dist/types/operations.js +3 -0
  38. package/dist/types/props.js +3 -0
  39. package/dist/types/views.js +5 -0
  40. package/dist/types.js +8 -111
  41. package/dist/ui/color-themes.js +33 -33
  42. package/dist/ui/compose/ComposeDialog.js +36 -21
  43. package/dist/ui/dash/PageForm.js +21 -15
  44. package/dist/ui/dash/PostForm.js +22 -16
  45. package/dist/ui/dash/collections/CollectionForm.js +152 -0
  46. package/dist/ui/dash/collections/CollectionsListContent.js +68 -0
  47. package/dist/ui/dash/collections/ViewCollectionContent.js +96 -0
  48. package/dist/ui/dash/media/MediaListContent.js +166 -0
  49. package/dist/ui/dash/media/ViewMediaContent.js +212 -0
  50. package/dist/ui/dash/pages/LinkFormContent.js +130 -0
  51. package/dist/ui/dash/pages/UnifiedPagesContent.js +193 -0
  52. package/dist/ui/dash/settings/AccountContent.js +209 -0
  53. package/dist/ui/dash/settings/AppearanceContent.js +259 -0
  54. package/dist/ui/dash/settings/GeneralContent.js +536 -0
  55. package/dist/ui/dash/settings/SettingsNav.js +41 -0
  56. package/dist/ui/font-themes.js +36 -0
  57. package/dist/ui/layouts/BaseLayout.js +24 -2
  58. package/dist/ui/layouts/SiteLayout.js +47 -19
  59. package/package.json +1 -1
  60. package/src/app.tsx +93 -553
  61. package/src/client.ts +1 -0
  62. package/src/i18n/locales/en.po +240 -175
  63. package/src/i18n/locales/en.ts +1 -1
  64. package/src/i18n/locales/zh-Hans.po +240 -175
  65. package/src/i18n/locales/zh-Hans.ts +1 -1
  66. package/src/i18n/locales/zh-Hant.po +240 -175
  67. package/src/i18n/locales/zh-Hant.ts +1 -1
  68. package/src/lib/__tests__/config.test.ts +192 -0
  69. package/src/lib/__tests__/favicon.test.ts +151 -0
  70. package/src/lib/__tests__/image.test.ts +2 -6
  71. package/src/lib/__tests__/timezones.test.ts +61 -0
  72. package/src/lib/__tests__/view.test.ts +2 -2
  73. package/src/lib/avatar-upload.ts +165 -0
  74. package/src/lib/config.ts +47 -0
  75. package/src/lib/constants.ts +19 -11
  76. package/src/lib/favicon.ts +115 -0
  77. package/src/lib/image.ts +13 -21
  78. package/src/lib/media-helpers.ts +2 -2
  79. package/src/lib/navigation.ts +33 -2
  80. package/src/lib/render.tsx +15 -1
  81. package/src/lib/schemas.ts +39 -0
  82. package/src/lib/timezones.ts +325 -0
  83. package/src/lib/view.ts +1 -1
  84. package/src/routes/api/posts.ts +1 -1
  85. package/src/routes/api/upload.ts +2 -3
  86. package/src/routes/auth/reset.tsx +239 -0
  87. package/src/routes/auth/setup.tsx +189 -0
  88. package/src/routes/auth/signin.tsx +163 -0
  89. package/src/routes/dash/__tests__/settings-avatar.test.ts +89 -0
  90. package/src/routes/dash/collections.tsx +17 -366
  91. package/src/routes/dash/media.tsx +12 -414
  92. package/src/routes/dash/pages.tsx +8 -348
  93. package/src/routes/dash/redirects.tsx +20 -14
  94. package/src/routes/dash/settings.tsx +243 -534
  95. package/src/routes/feed/__tests__/rss.test.ts +141 -0
  96. package/src/routes/feed/rss.ts +3 -1
  97. package/src/routes/feed/sitemap.ts +4 -2
  98. package/src/routes/pages/featured.tsx +7 -1
  99. package/src/routes/pages/home.tsx +25 -2
  100. package/src/routes/pages/latest.tsx +59 -0
  101. package/src/services/post.ts +34 -66
  102. package/src/styles/components.css +0 -65
  103. package/src/styles/tokens.css +1 -1
  104. package/src/styles/ui.css +24 -40
  105. package/src/types/bindings.ts +30 -0
  106. package/src/types/config.ts +183 -0
  107. package/src/types/constants.ts +26 -0
  108. package/src/types/entities.ts +109 -0
  109. package/src/types/operations.ts +88 -0
  110. package/src/types/props.ts +115 -0
  111. package/src/types/views.ts +172 -0
  112. package/src/types.ts +8 -644
  113. package/src/ui/__tests__/font-themes.test.ts +34 -0
  114. package/src/ui/color-themes.ts +34 -34
  115. package/src/ui/compose/ComposeDialog.tsx +40 -21
  116. package/src/ui/dash/PageForm.tsx +25 -19
  117. package/src/ui/dash/PostForm.tsx +26 -20
  118. package/src/ui/dash/collections/CollectionForm.tsx +153 -0
  119. package/src/ui/dash/collections/CollectionsListContent.tsx +85 -0
  120. package/src/ui/dash/collections/ViewCollectionContent.tsx +92 -0
  121. package/src/ui/dash/media/MediaListContent.tsx +201 -0
  122. package/src/ui/dash/media/ViewMediaContent.tsx +208 -0
  123. package/src/ui/dash/pages/LinkFormContent.tsx +119 -0
  124. package/src/ui/dash/pages/UnifiedPagesContent.tsx +203 -0
  125. package/src/ui/dash/settings/AccountContent.tsx +176 -0
  126. package/src/ui/dash/settings/AppearanceContent.tsx +254 -0
  127. package/src/ui/dash/settings/GeneralContent.tsx +533 -0
  128. package/src/ui/dash/settings/SettingsNav.tsx +56 -0
  129. package/src/ui/font-themes.ts +54 -0
  130. package/src/ui/layouts/BaseLayout.tsx +17 -0
  131. package/src/ui/layouts/SiteLayout.tsx +45 -31
package/src/lib/config.ts CHANGED
@@ -118,3 +118,50 @@ export async function getSiteDescription(c: Context): Promise<string> {
118
118
  export async function getSiteLanguage(c: Context): Promise<string> {
119
119
  return getConfig(c, "SITE_LANGUAGE");
120
120
  }
121
+
122
+ /**
123
+ * Get home default view with fallback chain: DB > ENV > Default
124
+ *
125
+ * @param c - Hono context
126
+ * @returns Home default view ("latest" or "featured")
127
+ *
128
+ * @example
129
+ * ```typescript
130
+ * const view = await getHomeDefaultView(c);
131
+ * // Returns: (DB: HOME_DEFAULT_VIEW) ?? "latest"
132
+ * ```
133
+ */
134
+ export async function getHomeDefaultView(c: Context): Promise<string> {
135
+ return getConfig(c, "HOME_DEFAULT_VIEW");
136
+ }
137
+
138
+ /**
139
+ * Get timezone with fallback chain: DB > ENV > Default
140
+ *
141
+ * @param c - Hono context
142
+ * @returns Timezone string (e.g. "Beijing", "UTC")
143
+ */
144
+ export async function getTimeZone(c: Context): Promise<string> {
145
+ return getConfig(c, "TIME_ZONE");
146
+ }
147
+
148
+ /**
149
+ * Get site footer markdown with fallback chain: DB > ENV > Default
150
+ *
151
+ * @param c - Hono context
152
+ * @returns Footer markdown string (empty string if not set)
153
+ */
154
+ export async function getSiteFooter(c: Context): Promise<string> {
155
+ return getConfig(c, "SITE_FOOTER");
156
+ }
157
+
158
+ /**
159
+ * Check if search engine indexing is disabled
160
+ *
161
+ * @param c - Hono context
162
+ * @returns true if NOINDEX is set to "true"
163
+ */
164
+ export async function isNoIndex(c: Context): Promise<boolean> {
165
+ const value = await getConfig(c, "NOINDEX");
166
+ return value === "true";
167
+ }
@@ -7,6 +7,7 @@
7
7
  */
8
8
  export const RESERVED_PATHS = [
9
9
  "featured",
10
+ "latest",
10
11
  "collections",
11
12
  "signin",
12
13
  "signout",
@@ -42,19 +43,26 @@ export function isReservedPath(path: string): boolean {
42
43
  export const DEFAULT_PAGE_SIZE = 100;
43
44
 
44
45
  /**
45
- * Settings keys (match environment variable naming)
46
+ * Settings keys - derived from CONFIG_FIELDS (Single Source of Truth)
47
+ *
48
+ * Only non-envOnly fields and internal fields are stored in DB settings.
49
+ * Environment-only fields (SITE_URL, AUTH_SECRET, etc.) are never in the DB.
46
50
  */
47
- export const SETTINGS_KEYS = {
48
- ONBOARDING_STATUS: "ONBOARDING_STATUS",
49
- SITE_NAME: "SITE_NAME",
50
- SITE_DESCRIPTION: "SITE_DESCRIPTION",
51
- SITE_LANGUAGE: "SITE_LANGUAGE",
52
- THEME: "THEME",
53
- CUSTOM_CSS: "CUSTOM_CSS",
54
- PASSWORD_RESET_TOKEN: "PASSWORD_RESET_TOKEN",
55
- } as const;
51
+ import { CONFIG_FIELDS, type ConfigKey } from "../types.js";
52
+
53
+ type SettingsFieldKey = {
54
+ [K in ConfigKey]: (typeof CONFIG_FIELDS)[K] extends { envOnly: false }
55
+ ? K
56
+ : never;
57
+ }[ConfigKey];
58
+
59
+ export const SETTINGS_KEYS = Object.fromEntries(
60
+ Object.entries(CONFIG_FIELDS)
61
+ .filter(([, field]) => !field.envOnly || "internal" in field)
62
+ .map(([key]) => [key, key]),
63
+ ) as { [K in SettingsFieldKey]: K };
56
64
 
57
- export type SettingsKey = (typeof SETTINGS_KEYS)[keyof typeof SETTINGS_KEYS];
65
+ export type SettingsKey = SettingsFieldKey;
58
66
 
59
67
  /**
60
68
  * Onboarding status values
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Favicon Utilities
3
+ *
4
+ * Sizes and ICO encoding for generated favicon variants.
5
+ * Favicon data is stored as base64 in the settings table (not R2)
6
+ * since the files are tiny and accessed on every page load.
7
+ */
8
+
9
+ /**
10
+ * Favicon variant sizes (width x height in pixels)
11
+ */
12
+ export const FAVICON_SIZES = {
13
+ ICO_16: 16,
14
+ ICO_32: 32,
15
+ APPLE_TOUCH: 180,
16
+ } as const;
17
+
18
+ /**
19
+ * Encode PNG images into an ICO file.
20
+ *
21
+ * ICO format (with PNG payloads):
22
+ * - Header: 6 bytes (reserved=0, type=1, count=N)
23
+ * - Directory: 16 bytes per entry (width, height, colors, reserved, planes, bpp, size, offset)
24
+ * - Data: raw PNG bytes for each entry
25
+ *
26
+ * @param entries - Array of { size, png } where png is an ArrayBuffer of PNG data
27
+ * @returns ICO file as a Blob
28
+ *
29
+ * @example
30
+ * ```ts
31
+ * const ico = encodeIco([
32
+ * { size: 16, png: png16ArrayBuffer },
33
+ * { size: 32, png: png32ArrayBuffer },
34
+ * ]);
35
+ * ```
36
+ */
37
+ export function encodeIco(
38
+ entries: { size: number; png: ArrayBuffer }[],
39
+ ): Blob {
40
+ const headerSize = 6;
41
+ const dirEntrySize = 16;
42
+ const dirSize = entries.length * dirEntrySize;
43
+
44
+ let dataOffset = headerSize + dirSize;
45
+
46
+ // Build header + directory
47
+ const header = new ArrayBuffer(headerSize + dirSize);
48
+ const view = new DataView(header);
49
+
50
+ // ICO header
51
+ view.setUint16(0, 0, true); // reserved
52
+ view.setUint16(2, 1, true); // type = icon
53
+ view.setUint16(4, entries.length, true); // count
54
+
55
+ const pngBuffers: ArrayBuffer[] = [];
56
+ for (let i = 0; i < entries.length; i++) {
57
+ const entry = entries[i]!;
58
+ const offset = headerSize + i * dirEntrySize;
59
+
60
+ // Width/height: 0 means 256
61
+ view.setUint8(offset + 0, entry.size < 256 ? entry.size : 0);
62
+ view.setUint8(offset + 1, entry.size < 256 ? entry.size : 0);
63
+ view.setUint8(offset + 2, 0); // color count (0 for >256 colors)
64
+ view.setUint8(offset + 3, 0); // reserved
65
+ view.setUint16(offset + 4, 1, true); // color planes
66
+ view.setUint16(offset + 6, 32, true); // bits per pixel
67
+ view.setUint32(offset + 8, entry.png.byteLength, true); // image size
68
+ view.setUint32(offset + 12, dataOffset, true); // image offset
69
+
70
+ dataOffset += entry.png.byteLength;
71
+ pngBuffers.push(entry.png);
72
+ }
73
+
74
+ return new Blob([header, ...pngBuffers], { type: "image/x-icon" });
75
+ }
76
+
77
+ /**
78
+ * Convert an ArrayBuffer to a base64 string.
79
+ *
80
+ * @param buffer - The ArrayBuffer to encode
81
+ * @returns base64-encoded string
82
+ *
83
+ * @example
84
+ * ```ts
85
+ * const b64 = arrayBufferToBase64(await blob.arrayBuffer());
86
+ * ```
87
+ */
88
+ export function arrayBufferToBase64(buffer: ArrayBuffer): string {
89
+ const bytes = new Uint8Array(buffer);
90
+ let binary = "";
91
+ for (let i = 0; i < bytes.byteLength; i++) {
92
+ binary += String.fromCharCode(bytes[i]!);
93
+ }
94
+ return btoa(binary);
95
+ }
96
+
97
+ /**
98
+ * Convert a base64 string to a Uint8Array.
99
+ *
100
+ * @param base64 - The base64 string to decode
101
+ * @returns decoded Uint8Array
102
+ *
103
+ * @example
104
+ * ```ts
105
+ * const bytes = base64ToUint8Array(storedBase64);
106
+ * ```
107
+ */
108
+ export function base64ToUint8Array(base64: string): Uint8Array {
109
+ const binary = atob(base64);
110
+ const bytes = new Uint8Array(binary.length);
111
+ for (let i = 0; i < binary.length; i++) {
112
+ bytes[i] = binary.charCodeAt(i);
113
+ }
114
+ return bytes;
115
+ }
package/src/lib/image.ts CHANGED
@@ -102,36 +102,28 @@ export function getPublicUrlForProvider(
102
102
  }
103
103
 
104
104
  /**
105
- * Generates a media URL using UUIDv7-based paths.
105
+ * Generates a media URL from a storage key.
106
106
  *
107
- * Returns a public URL for a media file. If `publicUrl` is set, uses that directly
108
- * with the storage key. Otherwise, generates a `/media/{id}.{ext}` local proxy URL.
107
+ * Both proxy and CDN paths use the same structure only the domain differs.
108
+ * Without a public URL, returns a root-relative path for the local proxy.
109
+ * With a public URL, prefixes that domain.
109
110
  *
110
- * @param mediaId - The UUIDv7 database ID of the media
111
- * @param storageKey - The storage object key (used to build CDN path and extract extension)
111
+ * @param storageKey - The storage object key (e.g. `"media/2025/01/uuid.webp"`)
112
112
  * @param publicUrl - Optional public URL base for direct CDN access
113
113
  * @returns The public URL for the media file
114
114
  *
115
115
  * @example
116
116
  * ```ts
117
- * // Without public URL - uses local proxy with UUID and extension
118
- * getMediaUrl("01902a9f-1a2b-7c3d", "media/2025/01/01902a9f-1a2b-7c3d.webp");
119
- * // Returns: "/media/01902a9f-1a2b-7c3d.webp"
117
+ * // Without public URL - local proxy
118
+ * getMediaUrl("media/2025/01/01902a9f-1a2b-7c3d.webp");
119
+ * // Returns: "/media/2025/01/01902a9f-1a2b-7c3d.webp"
120
120
  *
121
- * // With public URL - uses direct CDN
122
- * getMediaUrl("01902a9f-1a2b-7c3d", "media/2025/01/01902a9f-1a2b-7c3d.webp", "https://cdn.example.com");
121
+ * // With public URL - CDN
122
+ * getMediaUrl("media/2025/01/01902a9f-1a2b-7c3d.webp", "https://cdn.example.com");
123
123
  * // Returns: "https://cdn.example.com/media/2025/01/01902a9f-1a2b-7c3d.webp"
124
124
  * ```
125
125
  */
126
- export function getMediaUrl(
127
- mediaId: string,
128
- storageKey: string,
129
- publicUrl?: string,
130
- ): string {
131
- if (publicUrl) {
132
- return `${publicUrl.replace(/\/+$/, "")}/${storageKey}`;
133
- }
134
- // Extract extension from storage key
135
- const ext = storageKey.split(".").pop() || "bin";
136
- return `/media/${mediaId}.${ext}`;
126
+ export function getMediaUrl(storageKey: string, publicUrl?: string): string {
127
+ const base = publicUrl ? publicUrl.replace(/\/+$/, "") : "";
128
+ return `${base}/${storageKey}`;
137
129
  }
@@ -45,9 +45,9 @@ export function buildMediaMap(
45
45
  );
46
46
  return {
47
47
  id: m.id,
48
- url: getMediaUrl(m.id, m.storageKey, publicUrl),
48
+ url: getMediaUrl(m.storageKey, publicUrl),
49
49
  previewUrl: getImageUrl(
50
- getMediaUrl(m.id, m.storageKey, publicUrl),
50
+ getMediaUrl(m.storageKey, publicUrl),
51
51
  imageTransformUrl,
52
52
  { width: 400, quality: 80, format: "auto", fit: "cover" },
53
53
  ),
@@ -5,9 +5,11 @@
5
5
  */
6
6
 
7
7
  import type { Context } from "hono";
8
- import { getSiteName } from "./config.js";
8
+ import { getSiteName, getHomeDefaultView, getSiteFooter } from "./config.js";
9
9
  import type { Collection, NavItemView } from "../types.js";
10
10
  import { toNavItemViews } from "./view.js";
11
+ import { getMediaUrl, getPublicUrlForProvider } from "./image.js";
12
+ import { render as renderMarkdown } from "./markdown.js";
11
13
 
12
14
  /**
13
15
  * Navigation data needed by SiteLayout
@@ -19,6 +21,10 @@ export interface NavigationData {
19
21
  siteDescription: string;
20
22
  isAuthenticated: boolean;
21
23
  collections: Collection[];
24
+ homeDefaultView: string;
25
+ siteAvatarUrl?: string;
26
+ showHeaderAvatar?: boolean;
27
+ siteFooterHtml?: string;
22
28
  }
23
29
 
24
30
  /**
@@ -43,7 +49,11 @@ export interface NavigationData {
43
49
  export async function getNavigationData(c: Context): Promise<NavigationData> {
44
50
  const items = await c.var.services.navItems.list();
45
51
  const currentPath = new URL(c.req.url).pathname;
46
- const siteName = await getSiteName(c);
52
+ const [siteName, homeDefaultView, siteFooter] = await Promise.all([
53
+ getSiteName(c),
54
+ getHomeDefaultView(c),
55
+ getSiteFooter(c),
56
+ ]);
47
57
 
48
58
  // Only include description if explicitly set (DB or env), not the default
49
59
  const dbDescription = await c.var.services.settings.get("SITE_DESCRIPTION");
@@ -51,6 +61,23 @@ export async function getNavigationData(c: Context): Promise<NavigationData> {
51
61
  const siteDescription =
52
62
  dbDescription || (typeof envDescription === "string" ? envDescription : "");
53
63
 
64
+ // Resolve avatar URL from storage key
65
+ const avatarKey = await c.var.services.settings.get("SITE_AVATAR");
66
+ const showHeaderAvatar =
67
+ (await c.var.services.settings.get("SHOW_HEADER_AVATAR")) === "true";
68
+ let siteAvatarUrl: string | undefined;
69
+ if (avatarKey) {
70
+ const publicUrl = getPublicUrlForProvider(
71
+ c.env.STORAGE_DRIVER || "r2",
72
+ c.env.R2_PUBLIC_URL,
73
+ c.env.S3_PUBLIC_URL,
74
+ );
75
+ siteAvatarUrl = getMediaUrl(avatarKey, publicUrl);
76
+ }
77
+
78
+ // Render footer markdown
79
+ const siteFooterHtml = siteFooter ? renderMarkdown(siteFooter) : undefined;
80
+
54
81
  const links = toNavItemViews(items, currentPath);
55
82
 
56
83
  // Check auth status for compose button
@@ -79,5 +106,9 @@ export async function getNavigationData(c: Context): Promise<NavigationData> {
79
106
  siteDescription,
80
107
  isAuthenticated,
81
108
  collections,
109
+ homeDefaultView,
110
+ siteAvatarUrl,
111
+ showHeaderAvatar: showHeaderAvatar && !!siteAvatarUrl,
112
+ siteFooterHtml,
82
113
  };
83
114
  }
@@ -50,10 +50,24 @@ export function renderPublicPage(c: Context, options: RenderPublicPageOptions) {
50
50
  currentPath: navData.currentPath,
51
51
  isAuthenticated: navData.isAuthenticated,
52
52
  collections: navData.collections,
53
+ homeDefaultView: navData.homeDefaultView,
54
+ siteAvatarUrl: navData.siteAvatarUrl,
55
+ showHeaderAvatar: navData.showHeaderAvatar,
56
+ siteFooterHtml: navData.siteFooterHtml,
53
57
  };
54
58
 
59
+ // Read favicon and noindex from context (set by theme middleware)
60
+ const faviconUrl = c.get("faviconUrl") as string | undefined;
61
+ const noindex = c.get("noindex") as boolean | undefined;
62
+
55
63
  return c.html(
56
- <BaseLayout title={title} description={description} c={c}>
64
+ <BaseLayout
65
+ title={title}
66
+ description={description}
67
+ c={c}
68
+ faviconUrl={faviconUrl}
69
+ noindex={noindex}
70
+ >
57
71
  <SiteLayout {...layoutProps}>{content}</SiteLayout>
58
72
  </BaseLayout>,
59
73
  );
@@ -153,6 +153,45 @@ export const CreateCollectionSchema = z.object({
153
153
  */
154
154
  export const UpdateCollectionSchema = CreateCollectionSchema.partial();
155
155
 
156
+ // =============================================================================
157
+ // Auth Schemas
158
+ // =============================================================================
159
+
160
+ /**
161
+ * Setup form validation schema
162
+ */
163
+ export const SetupSchema = z.object({
164
+ name: z.string().min(1, "Name is required"),
165
+ email: z.string().email("Invalid email address"),
166
+ password: z.string().min(8, "Password must be at least 8 characters"),
167
+ });
168
+
169
+ /**
170
+ * Sign-in form validation schema
171
+ */
172
+ export const SigninSchema = z.object({
173
+ email: z.string().email("Invalid email address"),
174
+ password: z.string().min(1, "Password is required"),
175
+ });
176
+
177
+ /**
178
+ * Password reset form validation schema
179
+ */
180
+ export const ResetPasswordSchema = z
181
+ .object({
182
+ password: z.string().min(8, "Password must be at least 8 characters"),
183
+ confirmPassword: z.string().min(1),
184
+ token: z.string().min(1),
185
+ })
186
+ .refine((d) => d.password === d.confirmPassword, {
187
+ message: "Passwords do not match",
188
+ path: ["confirmPassword"],
189
+ });
190
+
191
+ // =============================================================================
192
+ // Form Data Helpers
193
+ // =============================================================================
194
+
156
195
  /**
157
196
  * Form data helper: safely parse a FormData value with a schema
158
197
  *