@jant/core 0.3.24 → 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 (277) hide show
  1. package/dist/app.js +101 -571
  2. package/dist/client.js +1 -0
  3. package/dist/db/schema.js +1 -1
  4. package/dist/i18n/locales/en.js +1 -1
  5. package/dist/i18n/locales/zh-Hans.js +1 -1
  6. package/dist/i18n/locales/zh-Hant.js +1 -1
  7. package/dist/index.js +3 -9
  8. package/dist/lib/avatar-upload.js +134 -0
  9. package/dist/lib/config.js +39 -0
  10. package/dist/lib/constants.js +10 -9
  11. package/dist/lib/favicon.js +102 -0
  12. package/dist/lib/image.js +13 -17
  13. package/dist/lib/media-helpers.js +2 -2
  14. package/dist/lib/nav-reorder.js +1 -1
  15. package/dist/lib/navigation.js +48 -3
  16. package/dist/lib/pagination.js +44 -0
  17. package/dist/lib/render.js +16 -11
  18. package/dist/lib/schemas.js +34 -3
  19. package/dist/lib/theme.js +4 -4
  20. package/dist/lib/timeline.js +24 -48
  21. package/dist/lib/timezones.js +388 -0
  22. package/dist/lib/view.js +3 -3
  23. package/dist/routes/api/collections.js +124 -0
  24. package/dist/routes/api/nav-items.js +104 -0
  25. package/dist/routes/api/pages.js +91 -0
  26. package/dist/routes/api/posts.js +3 -3
  27. package/dist/routes/api/search.js +2 -2
  28. package/dist/routes/api/settings.js +68 -0
  29. package/dist/routes/api/upload.js +3 -3
  30. package/dist/routes/auth/reset.js +221 -0
  31. package/dist/routes/auth/setup.js +194 -0
  32. package/dist/routes/auth/signin.js +176 -0
  33. package/dist/routes/compose.js +48 -0
  34. package/dist/routes/dash/collections.js +24 -416
  35. package/dist/routes/dash/index.js +1 -1
  36. package/dist/routes/dash/media.js +13 -393
  37. package/dist/routes/dash/pages.js +112 -86
  38. package/dist/routes/dash/posts.js +3 -5
  39. package/dist/routes/dash/redirects.js +20 -14
  40. package/dist/routes/dash/settings.js +213 -518
  41. package/dist/routes/feed/rss.js +4 -3
  42. package/dist/routes/feed/sitemap.js +5 -3
  43. package/dist/routes/pages/archive.js +3 -6
  44. package/dist/routes/pages/collection.js +3 -6
  45. package/dist/routes/pages/collections.js +28 -0
  46. package/dist/routes/pages/featured.js +36 -0
  47. package/dist/routes/pages/home.js +33 -49
  48. package/dist/routes/pages/latest.js +45 -0
  49. package/dist/routes/pages/page.js +29 -32
  50. package/dist/routes/pages/post.js +3 -6
  51. package/dist/routes/pages/search.js +3 -6
  52. package/dist/services/page.js +5 -1
  53. package/dist/services/post.js +45 -31
  54. package/dist/services/search.js +1 -1
  55. package/dist/types/bindings.js +3 -0
  56. package/dist/types/config.js +147 -0
  57. package/dist/types/constants.js +27 -0
  58. package/dist/types/entities.js +3 -0
  59. package/dist/types/operations.js +3 -0
  60. package/dist/types/props.js +3 -0
  61. package/dist/types/views.js +5 -0
  62. package/dist/types.js +8 -111
  63. package/dist/{theme → ui}/color-themes.js +33 -33
  64. package/dist/ui/compose/ComposeDialog.js +467 -0
  65. package/dist/ui/compose/ComposePrompt.js +55 -0
  66. package/dist/{theme/components/TypeBadge.js → ui/dash/FormatBadge.js} +1 -2
  67. package/dist/{theme/components → ui/dash}/PageForm.js +21 -15
  68. package/dist/{theme/components → ui/dash}/PostForm.js +22 -43
  69. package/dist/{theme/components → ui/dash}/PostList.js +6 -6
  70. package/dist/{theme/components/VisibilityBadge.js → ui/dash/StatusBadge.js} +1 -2
  71. package/dist/ui/dash/collections/CollectionForm.js +152 -0
  72. package/dist/ui/dash/collections/CollectionsListContent.js +68 -0
  73. package/dist/ui/dash/collections/ViewCollectionContent.js +96 -0
  74. package/dist/{theme/components → ui/dash}/index.js +3 -6
  75. package/dist/ui/dash/media/MediaListContent.js +166 -0
  76. package/dist/ui/dash/media/ViewMediaContent.js +212 -0
  77. package/dist/ui/dash/pages/LinkFormContent.js +130 -0
  78. package/dist/ui/dash/pages/UnifiedPagesContent.js +193 -0
  79. package/dist/ui/dash/settings/AccountContent.js +209 -0
  80. package/dist/ui/dash/settings/AppearanceContent.js +259 -0
  81. package/dist/ui/dash/settings/GeneralContent.js +536 -0
  82. package/dist/ui/dash/settings/SettingsNav.js +41 -0
  83. package/dist/{themes/threads/timeline → ui/feed}/LinkCard.js +6 -2
  84. package/dist/{themes/threads/timeline → ui/feed}/NoteCard.js +11 -6
  85. package/dist/{themes/threads/timeline → ui/feed}/QuoteCard.js +10 -6
  86. package/dist/{themes/threads/timeline → ui/feed}/ThreadPreview.js +7 -9
  87. package/dist/ui/feed/TimelineFeed.js +41 -0
  88. package/dist/ui/feed/TimelineItem.js +27 -0
  89. package/dist/ui/font-themes.js +36 -0
  90. package/dist/{theme → ui}/layouts/BaseLayout.js +34 -2
  91. package/dist/{theme → ui}/layouts/DashLayout.js +0 -8
  92. package/dist/ui/layouts/SiteLayout.js +169 -0
  93. package/dist/{themes/threads → ui}/pages/ArchivePage.js +16 -14
  94. package/dist/{themes/threads → ui}/pages/CollectionPage.js +6 -1
  95. package/dist/ui/pages/CollectionsPage.js +76 -0
  96. package/dist/ui/pages/FeaturedPage.js +24 -0
  97. package/dist/ui/pages/HomePage.js +24 -0
  98. package/dist/{themes/threads → ui}/pages/PostPage.js +13 -8
  99. package/dist/{themes/threads → ui}/pages/SearchPage.js +9 -7
  100. package/dist/{themes/threads → ui}/pages/SinglePage.js +3 -2
  101. package/dist/{theme/components → ui/shared}/MediaGallery.js +1 -1
  102. package/dist/{theme/components → ui/shared}/Pagination.js +41 -2
  103. package/dist/{theme/components → ui/shared}/ThreadView.js +2 -2
  104. package/dist/ui/shared/index.js +5 -0
  105. package/package.json +1 -9
  106. package/src/__tests__/helpers/db.ts +3 -0
  107. package/src/app.tsx +131 -561
  108. package/src/client.ts +1 -0
  109. package/src/db/migrations/0006_rename_slug_to_path.sql +5 -0
  110. package/src/db/migrations/meta/_journal.json +7 -0
  111. package/src/db/schema.ts +1 -1
  112. package/src/i18n/locales/en.po +477 -261
  113. package/src/i18n/locales/en.ts +1 -1
  114. package/src/i18n/locales/zh-Hans.po +477 -261
  115. package/src/i18n/locales/zh-Hans.ts +1 -1
  116. package/src/i18n/locales/zh-Hant.po +477 -261
  117. package/src/i18n/locales/zh-Hant.ts +1 -1
  118. package/src/index.ts +7 -36
  119. package/src/lib/__tests__/config.test.ts +192 -0
  120. package/src/lib/__tests__/favicon.test.ts +151 -0
  121. package/src/lib/__tests__/image.test.ts +2 -6
  122. package/src/lib/__tests__/schemas.test.ts +60 -19
  123. package/src/lib/__tests__/timeline.test.ts +45 -81
  124. package/src/lib/__tests__/timezones.test.ts +61 -0
  125. package/src/lib/__tests__/view.test.ts +15 -9
  126. package/src/lib/avatar-upload.ts +165 -0
  127. package/src/lib/config.ts +47 -0
  128. package/src/lib/constants.ts +19 -10
  129. package/src/lib/favicon.ts +115 -0
  130. package/src/lib/image.ts +13 -21
  131. package/src/lib/media-helpers.ts +2 -2
  132. package/src/lib/nav-reorder.ts +1 -1
  133. package/src/lib/navigation.ts +73 -4
  134. package/src/lib/pagination.ts +50 -0
  135. package/src/lib/render.tsx +22 -15
  136. package/src/lib/schemas.ts +47 -6
  137. package/src/lib/theme.ts +5 -5
  138. package/src/lib/timeline.ts +28 -57
  139. package/src/lib/timezones.ts +325 -0
  140. package/src/lib/view.ts +3 -3
  141. package/src/preset.css +2 -1
  142. package/src/routes/__tests__/compose.test.ts +199 -0
  143. package/src/routes/api/__tests__/collections.test.ts +249 -0
  144. package/src/routes/api/__tests__/nav-items.test.ts +222 -0
  145. package/src/routes/api/__tests__/pages.test.ts +218 -0
  146. package/src/routes/api/__tests__/settings.test.ts +132 -0
  147. package/src/routes/api/collections.ts +143 -0
  148. package/src/routes/api/nav-items.ts +115 -0
  149. package/src/routes/api/pages.ts +101 -0
  150. package/src/routes/api/posts.ts +3 -3
  151. package/src/routes/api/search.ts +2 -2
  152. package/src/routes/api/settings.ts +91 -0
  153. package/src/routes/api/upload.ts +2 -3
  154. package/src/routes/auth/reset.tsx +239 -0
  155. package/src/routes/auth/setup.tsx +189 -0
  156. package/src/routes/auth/signin.tsx +163 -0
  157. package/src/routes/compose.ts +63 -0
  158. package/src/routes/dash/__tests__/pages.test.ts +225 -0
  159. package/src/routes/dash/__tests__/settings-avatar.test.ts +89 -0
  160. package/src/routes/dash/collections.tsx +18 -367
  161. package/src/routes/dash/index.tsx +1 -1
  162. package/src/routes/dash/media.tsx +13 -415
  163. package/src/routes/dash/pages.tsx +131 -98
  164. package/src/routes/dash/posts.tsx +3 -7
  165. package/src/routes/dash/redirects.tsx +22 -16
  166. package/src/routes/dash/settings.tsx +265 -478
  167. package/src/routes/feed/__tests__/rss.test.ts +141 -0
  168. package/src/routes/feed/rss.ts +5 -3
  169. package/src/routes/feed/sitemap.ts +5 -3
  170. package/src/routes/pages/__tests__/collections.test.ts +94 -0
  171. package/src/routes/pages/__tests__/featured.test.ts +94 -0
  172. package/src/routes/pages/archive.tsx +2 -6
  173. package/src/routes/pages/collection.tsx +2 -6
  174. package/src/routes/pages/collections.tsx +36 -0
  175. package/src/routes/pages/featured.tsx +44 -0
  176. package/src/routes/pages/home.tsx +30 -53
  177. package/src/routes/pages/latest.tsx +59 -0
  178. package/src/routes/pages/page.tsx +28 -30
  179. package/src/routes/pages/post.tsx +2 -5
  180. package/src/routes/pages/search.tsx +2 -6
  181. package/src/services/__tests__/page.test.ts +106 -0
  182. package/src/services/__tests__/post.test.ts +114 -15
  183. package/src/services/page.ts +13 -1
  184. package/src/services/post.ts +58 -40
  185. package/src/services/search.ts +2 -2
  186. package/src/styles/components.css +0 -65
  187. package/src/styles/tokens.css +47 -0
  188. package/src/styles/ui.css +475 -0
  189. package/src/types/bindings.ts +30 -0
  190. package/src/types/config.ts +183 -0
  191. package/src/types/constants.ts +26 -0
  192. package/src/types/entities.ts +109 -0
  193. package/src/types/operations.ts +88 -0
  194. package/src/types/props.ts +115 -0
  195. package/src/types/views.ts +172 -0
  196. package/src/types.ts +8 -774
  197. package/src/ui/__tests__/font-themes.test.ts +34 -0
  198. package/src/{theme → ui}/color-themes.ts +34 -34
  199. package/src/ui/compose/ComposeDialog.tsx +414 -0
  200. package/src/ui/compose/ComposePrompt.tsx +55 -0
  201. package/src/{theme/components/TypeBadge.tsx → ui/dash/FormatBadge.tsx} +2 -3
  202. package/src/{theme/components → ui/dash}/PageForm.tsx +25 -19
  203. package/src/{theme/components → ui/dash}/PostForm.tsx +26 -45
  204. package/src/{theme/components → ui/dash}/PostList.tsx +7 -7
  205. package/src/{theme/components/VisibilityBadge.tsx → ui/dash/StatusBadge.tsx} +2 -3
  206. package/src/ui/dash/collections/CollectionForm.tsx +153 -0
  207. package/src/ui/dash/collections/CollectionsListContent.tsx +85 -0
  208. package/src/ui/dash/collections/ViewCollectionContent.tsx +92 -0
  209. package/src/ui/dash/index.ts +10 -0
  210. package/src/ui/dash/media/MediaListContent.tsx +201 -0
  211. package/src/ui/dash/media/ViewMediaContent.tsx +208 -0
  212. package/src/ui/dash/pages/LinkFormContent.tsx +119 -0
  213. package/src/ui/dash/pages/UnifiedPagesContent.tsx +203 -0
  214. package/src/ui/dash/settings/AccountContent.tsx +176 -0
  215. package/src/ui/dash/settings/AppearanceContent.tsx +254 -0
  216. package/src/ui/dash/settings/GeneralContent.tsx +533 -0
  217. package/src/ui/dash/settings/SettingsNav.tsx +56 -0
  218. package/src/{themes/threads/timeline → ui/feed}/LinkCard.tsx +9 -4
  219. package/src/{themes/threads/timeline → ui/feed}/NoteCard.tsx +13 -8
  220. package/src/{themes/threads/timeline → ui/feed}/QuoteCard.tsx +13 -8
  221. package/src/{themes/threads/timeline → ui/feed}/ThreadPreview.tsx +7 -8
  222. package/src/ui/feed/TimelineFeed.tsx +49 -0
  223. package/src/ui/feed/TimelineItem.tsx +45 -0
  224. package/src/ui/font-themes.ts +54 -0
  225. package/src/{theme → ui}/layouts/BaseLayout.tsx +28 -1
  226. package/src/{theme → ui}/layouts/DashLayout.tsx +0 -10
  227. package/src/ui/layouts/SiteLayout.tsx +164 -0
  228. package/src/{themes/threads → ui}/pages/ArchivePage.tsx +22 -17
  229. package/src/{themes/threads → ui}/pages/CollectionPage.tsx +14 -5
  230. package/src/ui/pages/CollectionsPage.tsx +73 -0
  231. package/src/ui/pages/FeaturedPage.tsx +31 -0
  232. package/src/{themes/threads → ui}/pages/HomePage.tsx +11 -15
  233. package/src/{themes/threads → ui}/pages/PostPage.tsx +23 -14
  234. package/src/{themes/threads → ui}/pages/SearchPage.tsx +13 -11
  235. package/src/{themes/threads → ui}/pages/SinglePage.tsx +4 -4
  236. package/src/{theme/components → ui/shared}/MediaGallery.tsx +1 -1
  237. package/src/{theme/components → ui/shared}/Pagination.tsx +67 -4
  238. package/src/{theme/components → ui/shared}/ThreadView.tsx +2 -2
  239. package/src/ui/shared/__tests__/pagination.test.ts +46 -0
  240. package/src/ui/shared/index.ts +12 -0
  241. package/bin/jant.js +0 -185
  242. package/dist/lib/theme-components.js +0 -46
  243. package/dist/routes/dash/navigation.js +0 -289
  244. package/dist/theme/index.js +0 -18
  245. package/dist/theme/layouts/index.js +0 -2
  246. package/dist/themes/threads/ThreadsSiteLayout.js +0 -172
  247. package/dist/themes/threads/index.js +0 -81
  248. package/dist/themes/threads/pages/HomePage.js +0 -25
  249. package/dist/themes/threads/timeline/TimelineFeed.js +0 -58
  250. package/dist/themes/threads/timeline/TimelineItem.js +0 -36
  251. package/dist/themes/threads/timeline/TimelineLoadMore.js +0 -23
  252. package/dist/themes/threads/timeline/groupByDate.js +0 -22
  253. package/dist/themes/threads/timeline/timelineMore.js +0 -107
  254. package/src/lib/__tests__/theme-components.test.ts +0 -105
  255. package/src/lib/theme-components.ts +0 -65
  256. package/src/routes/dash/navigation.tsx +0 -317
  257. package/src/theme/components/index.ts +0 -23
  258. package/src/theme/index.ts +0 -22
  259. package/src/theme/layouts/index.ts +0 -7
  260. package/src/themes/threads/ThreadsSiteLayout.tsx +0 -194
  261. package/src/themes/threads/index.ts +0 -100
  262. package/src/themes/threads/style.css +0 -336
  263. package/src/themes/threads/timeline/TimelineFeed.tsx +0 -62
  264. package/src/themes/threads/timeline/TimelineItem.tsx +0 -67
  265. package/src/themes/threads/timeline/TimelineLoadMore.tsx +0 -35
  266. package/src/themes/threads/timeline/groupByDate.ts +0 -30
  267. package/src/themes/threads/timeline/timelineMore.tsx +0 -130
  268. /package/dist/{theme/components → ui/dash}/ActionButtons.js +0 -0
  269. /package/dist/{theme/components → ui/dash}/CrudPageHeader.js +0 -0
  270. /package/dist/{theme/components → ui/dash}/DangerZone.js +0 -0
  271. /package/dist/{theme/components → ui/dash}/ListItemRow.js +0 -0
  272. /package/dist/{theme/components → ui/shared}/EmptyState.js +0 -0
  273. /package/src/{theme/components → ui/dash}/ActionButtons.tsx +0 -0
  274. /package/src/{theme/components → ui/dash}/CrudPageHeader.tsx +0 -0
  275. /package/src/{theme/components → ui/dash}/DangerZone.tsx +0 -0
  276. /package/src/{theme/components → ui/dash}/ListItemRow.tsx +0 -0
  277. /package/src/{theme/components → ui/shared}/EmptyState.tsx +0 -0
@@ -0,0 +1,208 @@
1
+ /**
2
+ * Single media detail view
3
+ */
4
+
5
+ import { useLingui } from "@lingui/react/macro";
6
+ import type { Media } from "../../../types.js";
7
+ import { DangerZone } from "../index.js";
8
+ import * as time from "../../../lib/time.js";
9
+ import {
10
+ getMediaUrl,
11
+ getImageUrl,
12
+ getPublicUrlForProvider,
13
+ } from "../../../lib/image.js";
14
+
15
+ function formatSize(bytes: number): string {
16
+ if (bytes < 1024) return `${bytes} B`;
17
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
18
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
19
+ }
20
+
21
+ export function ViewMediaContent({
22
+ media,
23
+ r2PublicUrl,
24
+ imageTransformUrl,
25
+ s3PublicUrl,
26
+ }: {
27
+ media: Media;
28
+ r2PublicUrl?: string;
29
+ imageTransformUrl?: string;
30
+ s3PublicUrl?: string;
31
+ }) {
32
+ const { t } = useLingui();
33
+ const publicUrl = getPublicUrlForProvider(
34
+ media.provider,
35
+ r2PublicUrl,
36
+ s3PublicUrl,
37
+ );
38
+ const url = getMediaUrl(media.storageKey, publicUrl);
39
+ const thumbnailUrl = getImageUrl(url, imageTransformUrl, {
40
+ width: 600,
41
+ quality: 85,
42
+ format: "auto",
43
+ });
44
+ const isImage = media.mimeType.startsWith("image/");
45
+
46
+ return (
47
+ <>
48
+ <div class="flex items-center justify-between mb-6">
49
+ <div>
50
+ <h1 class="text-2xl font-semibold">{media.originalName}</h1>
51
+ <p class="text-muted-foreground mt-1">
52
+ {formatSize(media.size)} · {media.mimeType} ·{" "}
53
+ {time.formatDate(media.createdAt)}
54
+ </p>
55
+ </div>
56
+ <a href="/dash/media" class="btn-outline">
57
+ {t({
58
+ message: "Back",
59
+ comment: "@context: Button to go back to media list",
60
+ })}
61
+ </a>
62
+ </div>
63
+
64
+ <div class="grid gap-6 md:grid-cols-2">
65
+ {/* Preview */}
66
+ <div class="card">
67
+ <header>
68
+ <h2>
69
+ {t({
70
+ message: "Preview",
71
+ comment: "@context: Media detail section - preview",
72
+ })}
73
+ </h2>
74
+ </header>
75
+ <section>
76
+ {isImage ? (
77
+ <>
78
+ <button
79
+ type="button"
80
+ class="cursor-pointer"
81
+ onclick={`document.getElementById('lightbox-img').src = '${url}'; document.getElementById('lightbox').showModal()`}
82
+ >
83
+ <img
84
+ src={thumbnailUrl}
85
+ alt={media.alt || media.originalName}
86
+ class="max-w-full rounded-lg hover:opacity-90 transition-opacity"
87
+ />
88
+ </button>
89
+ <p class="text-xs text-muted-foreground mt-2">
90
+ {t({
91
+ message: "Click image to view full size",
92
+ comment: "@context: Hint to click image for lightbox",
93
+ })}
94
+ </p>
95
+ </>
96
+ ) : (
97
+ <div class="aspect-video bg-muted rounded-lg flex items-center justify-center text-muted-foreground">
98
+ <span>{media.mimeType}</span>
99
+ </div>
100
+ )}
101
+ </section>
102
+ </div>
103
+
104
+ {/* Details */}
105
+ <div class="space-y-6">
106
+ <div class="card">
107
+ <header>
108
+ <h2>
109
+ {t({
110
+ message: "URL",
111
+ comment: "@context: Media detail section - URL",
112
+ })}
113
+ </h2>
114
+ </header>
115
+ <section>
116
+ <div class="flex items-center gap-2">
117
+ <input
118
+ type="text"
119
+ class="input flex-1 font-mono text-sm"
120
+ value={url}
121
+ readonly
122
+ />
123
+ <button
124
+ type="button"
125
+ class="btn-outline"
126
+ onclick={`navigator.clipboard.writeText('${url}')`}
127
+ >
128
+ {t({
129
+ message: "Copy",
130
+ comment: "@context: Button to copy URL to clipboard",
131
+ })}
132
+ </button>
133
+ </div>
134
+ <p class="text-xs text-muted-foreground mt-2">
135
+ {t({
136
+ message: "Use this URL to embed the media in your posts.",
137
+ comment: "@context: Media URL helper text",
138
+ })}
139
+ </p>
140
+ </section>
141
+ </div>
142
+
143
+ <div class="card">
144
+ <header>
145
+ <h2>
146
+ {t({
147
+ message: "Markdown",
148
+ comment: "@context: Media detail section - Markdown snippet",
149
+ })}
150
+ </h2>
151
+ </header>
152
+ <section>
153
+ <div class="flex items-center gap-2">
154
+ <input
155
+ type="text"
156
+ class="input flex-1 font-mono text-sm"
157
+ value={`![${media.alt || media.originalName}](${url})`}
158
+ readonly
159
+ />
160
+ <button
161
+ type="button"
162
+ class="btn-outline"
163
+ onclick={`navigator.clipboard.writeText('![${media.alt || media.originalName}](${url})')`}
164
+ >
165
+ {t({
166
+ message: "Copy",
167
+ comment: "@context: Button to copy Markdown to clipboard",
168
+ })}
169
+ </button>
170
+ </div>
171
+ </section>
172
+ </div>
173
+
174
+ {/* Delete */}
175
+ <DangerZone
176
+ actionLabel={t({
177
+ message: "Delete Media",
178
+ comment: "@context: Button to delete media",
179
+ })}
180
+ formAction={`/dash/media/${media.id}/delete`}
181
+ confirmMessage="Are you sure you want to delete this media?"
182
+ description={t({
183
+ message:
184
+ "Deleting this media will remove it permanently from storage.",
185
+ comment: "@context: Warning message before deleting media",
186
+ })}
187
+ />
188
+ </div>
189
+ </div>
190
+
191
+ {/* Lightbox */}
192
+ {isImage && (
193
+ <dialog
194
+ id="lightbox"
195
+ class="p-0 m-auto bg-transparent backdrop:bg-black/80"
196
+ onclick="event.target === this && this.close()"
197
+ >
198
+ <img
199
+ id="lightbox-img"
200
+ src=""
201
+ alt=""
202
+ class="max-w-[90vw] max-h-[90vh] object-contain rounded-lg"
203
+ />
204
+ </dialog>
205
+ )}
206
+ </>
207
+ );
208
+ }
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Link creation/editing form
3
+ */
4
+
5
+ import { useLingui } from "@lingui/react/macro";
6
+ import type { NavItem } from "../../../types.js";
7
+
8
+ export function LinkFormContent({
9
+ item,
10
+ isEdit,
11
+ }: {
12
+ item?: NavItem;
13
+ isEdit?: boolean;
14
+ }) {
15
+ const { t } = useLingui();
16
+ const title = isEdit
17
+ ? t({ message: "Edit Link", comment: "@context: Page heading" })
18
+ : t({ message: "New Link", comment: "@context: Page heading" });
19
+
20
+ const signals = JSON.stringify({
21
+ label: item?.label ?? "",
22
+ url: item?.url ?? "",
23
+ }).replace(/</g, "\\u003c");
24
+
25
+ const action = isEdit ? `/dash/pages/links/${item?.id}` : "/dash/pages/links";
26
+
27
+ return (
28
+ <>
29
+ <h1 class="text-2xl font-semibold mb-6">{title}</h1>
30
+
31
+ <form
32
+ data-signals={signals}
33
+ data-on:submit__prevent={`@post('${action}')`}
34
+ data-indicator="_loading"
35
+ class="flex flex-col gap-4 max-w-lg"
36
+ >
37
+ <div class="field">
38
+ <label class="label">
39
+ {t({
40
+ message: "Label",
41
+ comment: "@context: Navigation link form field",
42
+ })}
43
+ </label>
44
+ <input
45
+ type="text"
46
+ data-bind="label"
47
+ class="input"
48
+ placeholder="Home"
49
+ required
50
+ />
51
+ <p class="text-xs text-muted-foreground mt-1">
52
+ {t({
53
+ message: "Display text for the link",
54
+ comment: "@context: Navigation label help text",
55
+ })}
56
+ </p>
57
+ </div>
58
+
59
+ <div class="field">
60
+ <label class="label">
61
+ {t({
62
+ message: "URL",
63
+ comment: "@context: Navigation link form field",
64
+ })}
65
+ </label>
66
+ <input
67
+ type="text"
68
+ data-bind="url"
69
+ class="input"
70
+ placeholder="/archive or https://..."
71
+ required
72
+ />
73
+ <p class="text-xs text-muted-foreground mt-1">
74
+ {t({
75
+ message:
76
+ "Path (e.g. /archive) or full URL (e.g. https://example.com)",
77
+ comment: "@context: Navigation URL help text",
78
+ })}
79
+ </p>
80
+ </div>
81
+
82
+ <div class="flex gap-2">
83
+ <button type="submit" class="btn" data-attr:disabled="$_loading">
84
+ <svg
85
+ data-show="$_loading"
86
+ style="display:none"
87
+ class="animate-spin size-4"
88
+ xmlns="http://www.w3.org/2000/svg"
89
+ viewBox="0 0 24 24"
90
+ fill="none"
91
+ stroke="currentColor"
92
+ stroke-width="2"
93
+ stroke-linecap="round"
94
+ stroke-linejoin="round"
95
+ role="status"
96
+ >
97
+ <path d="M21 12a9 9 0 1 1-6.219-8.56" />
98
+ </svg>
99
+ {isEdit
100
+ ? t({
101
+ message: "Save Changes",
102
+ comment: "@context: Button to save edited navigation link",
103
+ })
104
+ : t({
105
+ message: "Create Link",
106
+ comment: "@context: Button to save new navigation link",
107
+ })}
108
+ </button>
109
+ <a href="/dash/pages" class="btn-outline">
110
+ {t({
111
+ message: "Cancel",
112
+ comment: "@context: Button to cancel form",
113
+ })}
114
+ </a>
115
+ </div>
116
+ </form>
117
+ </>
118
+ );
119
+ }
@@ -0,0 +1,203 @@
1
+ /**
2
+ * Unified pages list - navigation items + other pages
3
+ */
4
+
5
+ import { useLingui } from "@lingui/react/macro";
6
+ import type { Page, NavItem } from "../../../types.js";
7
+ import { ListItemRow, ActionButtons, CrudPageHeader } from "../index.js";
8
+
9
+ export function UnifiedPagesContent({
10
+ navItems,
11
+ otherPages,
12
+ }: {
13
+ navItems: NavItem[];
14
+ otherPages: Page[];
15
+ }) {
16
+ const { t } = useLingui();
17
+
18
+ return (
19
+ <>
20
+ <CrudPageHeader
21
+ title={t({
22
+ message: "Pages",
23
+ comment: "@context: Pages main heading",
24
+ })}
25
+ >
26
+ <div class="flex gap-2">
27
+ <a href="/dash/pages/links/new" class="btn-outline">
28
+ {t({
29
+ message: "Add Link",
30
+ comment: "@context: Button to add a navigation link",
31
+ })}
32
+ </a>
33
+ <a href="/dash/pages/new" class="btn">
34
+ {t({
35
+ message: "New Page",
36
+ comment: "@context: Button to create new page",
37
+ })}
38
+ </a>
39
+ </div>
40
+ </CrudPageHeader>
41
+
42
+ {/* Navigation section */}
43
+ <section class="mb-8">
44
+ <h2 class="text-lg font-medium mb-3">
45
+ {t({
46
+ message: "Your site navigation",
47
+ comment: "@context: Section heading for navigation items",
48
+ })}
49
+ </h2>
50
+ {navItems.length === 0 ? (
51
+ <p class="text-sm text-muted-foreground py-4">
52
+ {t({
53
+ message:
54
+ "No navigation links yet. Add pages to navigation or create links.",
55
+ comment: "@context: Empty state for navigation section",
56
+ })}
57
+ </p>
58
+ ) : (
59
+ <div id="nav-links-list" class="flex flex-col divide-y">
60
+ {navItems.map((item) => (
61
+ <ListItemRow
62
+ key={item.id}
63
+ actions={
64
+ item.type === "page" ? (
65
+ <>
66
+ <ActionButtons
67
+ editHref={
68
+ item.pageId
69
+ ? `/dash/pages/${item.pageId}/edit`
70
+ : undefined
71
+ }
72
+ editLabel={t({
73
+ message: "Edit",
74
+ comment: "@context: Button to edit page",
75
+ })}
76
+ />
77
+ <button
78
+ type="button"
79
+ class="btn-sm-ghost"
80
+ data-on:click__prevent={`@post('/dash/pages/${item.pageId}/remove-from-nav')`}
81
+ >
82
+ {t({
83
+ message: "Un-nav",
84
+ comment:
85
+ "@context: Button to remove page from navigation",
86
+ })}
87
+ </button>
88
+ </>
89
+ ) : (
90
+ <>
91
+ <ActionButtons
92
+ editHref={`/dash/pages/links/${item.id}/edit`}
93
+ editLabel={t({
94
+ message: "Edit",
95
+ comment: "@context: Button to edit link",
96
+ })}
97
+ deleteAction={`/dash/pages/links/${item.id}/delete`}
98
+ deleteLabel={t({
99
+ message: "Delete",
100
+ comment: "@context: Button to delete link",
101
+ })}
102
+ />
103
+ </>
104
+ )
105
+ }
106
+ >
107
+ <div
108
+ class="flex items-center gap-3 cursor-grab"
109
+ data-id={item.id}
110
+ >
111
+ <span class="text-muted-foreground select-none">⠿</span>
112
+ <div class="flex items-center gap-2">
113
+ <span class="font-medium">{item.label}</span>
114
+ <code class="text-sm text-muted-foreground bg-muted px-1 rounded">
115
+ {item.url}
116
+ </code>
117
+ <span class="badge-secondary">
118
+ {item.type === "page"
119
+ ? t({
120
+ message: "page",
121
+ comment: "@context: Nav item type badge",
122
+ })
123
+ : t({
124
+ message: "link",
125
+ comment: "@context: Nav item type badge",
126
+ })}
127
+ </span>
128
+ </div>
129
+ </div>
130
+ </ListItemRow>
131
+ ))}
132
+ </div>
133
+ )}
134
+ </section>
135
+
136
+ {/* Other pages section */}
137
+ <section>
138
+ <h2 class="text-lg font-medium mb-3">
139
+ {t({
140
+ message: "Other pages",
141
+ comment: "@context: Section heading for pages not in navigation",
142
+ })}
143
+ </h2>
144
+ {otherPages.length === 0 ? (
145
+ <p class="text-sm text-muted-foreground py-4">
146
+ {t({
147
+ message: "All pages are in your navigation.",
148
+ comment: "@context: Empty state when all pages are in nav",
149
+ })}
150
+ </p>
151
+ ) : (
152
+ <div class="flex flex-col divide-y">
153
+ {otherPages.map((page) => (
154
+ <ListItemRow
155
+ key={page.id}
156
+ actions={
157
+ <>
158
+ <button
159
+ type="button"
160
+ class="btn-sm-outline"
161
+ data-on:click__prevent={`@post('/dash/pages/${page.id}/add-to-nav')`}
162
+ >
163
+ {t({
164
+ message: "Add to nav",
165
+ comment: "@context: Button to add page to navigation",
166
+ })}
167
+ </button>
168
+ <ActionButtons
169
+ editHref={`/dash/pages/${page.id}/edit`}
170
+ editLabel={t({
171
+ message: "Edit",
172
+ comment: "@context: Button to edit page",
173
+ })}
174
+ viewHref={
175
+ page.status !== "draft" ? `/${page.slug}` : undefined
176
+ }
177
+ viewLabel={t({
178
+ message: "View",
179
+ comment: "@context: Button to view page on public site",
180
+ })}
181
+ />
182
+ </>
183
+ }
184
+ >
185
+ <a
186
+ href={`/dash/pages/${page.id}`}
187
+ class="font-medium hover:underline"
188
+ >
189
+ {page.title ||
190
+ t({
191
+ message: "Untitled",
192
+ comment: "@context: Default title for untitled page",
193
+ })}
194
+ </a>
195
+ <p class="text-sm text-muted-foreground mt-1">/{page.slug}</p>
196
+ </ListItemRow>
197
+ ))}
198
+ </div>
199
+ )}
200
+ </section>
201
+ </>
202
+ );
203
+ }