@jant/core 0.3.24 → 0.3.25

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 (206) hide show
  1. package/dist/app.js +50 -25
  2. package/dist/db/schema.js +1 -1
  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/index.js +3 -9
  7. package/dist/lib/constants.js +1 -0
  8. package/dist/lib/nav-reorder.js +1 -1
  9. package/dist/lib/navigation.js +26 -1
  10. package/dist/lib/pagination.js +44 -0
  11. package/dist/lib/render.js +7 -11
  12. package/dist/lib/schemas.js +3 -3
  13. package/dist/lib/theme.js +4 -4
  14. package/dist/lib/timeline.js +24 -48
  15. package/dist/lib/view.js +2 -2
  16. package/dist/routes/api/collections.js +124 -0
  17. package/dist/routes/api/nav-items.js +104 -0
  18. package/dist/routes/api/pages.js +91 -0
  19. package/dist/routes/api/posts.js +2 -2
  20. package/dist/routes/api/search.js +2 -2
  21. package/dist/routes/api/settings.js +68 -0
  22. package/dist/routes/compose.js +48 -0
  23. package/dist/routes/dash/collections.js +2 -2
  24. package/dist/routes/dash/index.js +1 -1
  25. package/dist/routes/dash/media.js +2 -2
  26. package/dist/routes/dash/pages.js +411 -62
  27. package/dist/routes/dash/posts.js +3 -5
  28. package/dist/routes/dash/redirects.js +2 -2
  29. package/dist/routes/dash/settings.js +79 -5
  30. package/dist/routes/feed/rss.js +2 -2
  31. package/dist/routes/feed/sitemap.js +1 -1
  32. package/dist/routes/pages/archive.js +3 -6
  33. package/dist/routes/pages/collection.js +3 -6
  34. package/dist/routes/pages/collections.js +28 -0
  35. package/dist/routes/pages/featured.js +32 -0
  36. package/dist/routes/pages/home.js +9 -50
  37. package/dist/routes/pages/page.js +29 -32
  38. package/dist/routes/pages/post.js +3 -6
  39. package/dist/routes/pages/search.js +3 -6
  40. package/dist/services/page.js +5 -1
  41. package/dist/services/post.js +40 -6
  42. package/dist/services/search.js +1 -1
  43. package/dist/ui/compose/ComposeDialog.js +452 -0
  44. package/dist/ui/compose/ComposePrompt.js +55 -0
  45. package/dist/{theme/components/TypeBadge.js → ui/dash/FormatBadge.js} +1 -2
  46. package/dist/{theme/components → ui/dash}/PostForm.js +0 -27
  47. package/dist/{theme/components → ui/dash}/PostList.js +6 -6
  48. package/dist/{theme/components/VisibilityBadge.js → ui/dash/StatusBadge.js} +1 -2
  49. package/dist/{theme/components → ui/dash}/index.js +3 -6
  50. package/dist/{themes/threads/timeline → ui/feed}/LinkCard.js +6 -2
  51. package/dist/{themes/threads/timeline → ui/feed}/NoteCard.js +11 -6
  52. package/dist/{themes/threads/timeline → ui/feed}/QuoteCard.js +10 -6
  53. package/dist/{themes/threads/timeline → ui/feed}/ThreadPreview.js +7 -9
  54. package/dist/ui/feed/TimelineFeed.js +41 -0
  55. package/dist/ui/feed/TimelineItem.js +27 -0
  56. package/dist/{theme → ui}/layouts/BaseLayout.js +10 -0
  57. package/dist/{theme → ui}/layouts/DashLayout.js +0 -8
  58. package/dist/ui/layouts/SiteLayout.js +141 -0
  59. package/dist/{themes/threads → ui}/pages/ArchivePage.js +16 -14
  60. package/dist/{themes/threads → ui}/pages/CollectionPage.js +6 -1
  61. package/dist/ui/pages/CollectionsPage.js +76 -0
  62. package/dist/ui/pages/FeaturedPage.js +24 -0
  63. package/dist/ui/pages/HomePage.js +24 -0
  64. package/dist/{themes/threads → ui}/pages/PostPage.js +13 -8
  65. package/dist/{themes/threads → ui}/pages/SearchPage.js +9 -7
  66. package/dist/{themes/threads → ui}/pages/SinglePage.js +3 -2
  67. package/dist/{theme/components → ui/shared}/MediaGallery.js +1 -1
  68. package/dist/{theme/components → ui/shared}/Pagination.js +41 -2
  69. package/dist/{theme/components → ui/shared}/ThreadView.js +2 -2
  70. package/dist/ui/shared/index.js +5 -0
  71. package/package.json +1 -9
  72. package/src/__tests__/helpers/db.ts +3 -0
  73. package/src/app.tsx +57 -27
  74. package/src/db/migrations/0006_rename_slug_to_path.sql +5 -0
  75. package/src/db/migrations/meta/_journal.json +7 -0
  76. package/src/db/schema.ts +1 -1
  77. package/src/i18n/locales/en.po +332 -181
  78. package/src/i18n/locales/en.ts +1 -1
  79. package/src/i18n/locales/zh-Hans.po +332 -181
  80. package/src/i18n/locales/zh-Hans.ts +1 -1
  81. package/src/i18n/locales/zh-Hant.po +332 -181
  82. package/src/i18n/locales/zh-Hant.ts +1 -1
  83. package/src/index.ts +7 -36
  84. package/src/lib/__tests__/schemas.test.ts +60 -19
  85. package/src/lib/__tests__/timeline.test.ts +45 -81
  86. package/src/lib/__tests__/view.test.ts +13 -7
  87. package/src/lib/constants.ts +1 -0
  88. package/src/lib/nav-reorder.ts +1 -1
  89. package/src/lib/navigation.ts +40 -2
  90. package/src/lib/pagination.ts +50 -0
  91. package/src/lib/render.tsx +7 -14
  92. package/src/lib/schemas.ts +8 -6
  93. package/src/lib/theme.ts +5 -5
  94. package/src/lib/timeline.ts +28 -57
  95. package/src/lib/view.ts +2 -2
  96. package/src/preset.css +2 -1
  97. package/src/routes/__tests__/compose.test.ts +199 -0
  98. package/src/routes/api/__tests__/collections.test.ts +249 -0
  99. package/src/routes/api/__tests__/nav-items.test.ts +222 -0
  100. package/src/routes/api/__tests__/pages.test.ts +218 -0
  101. package/src/routes/api/__tests__/settings.test.ts +132 -0
  102. package/src/routes/api/collections.ts +143 -0
  103. package/src/routes/api/nav-items.ts +115 -0
  104. package/src/routes/api/pages.ts +101 -0
  105. package/src/routes/api/posts.ts +2 -2
  106. package/src/routes/api/search.ts +2 -2
  107. package/src/routes/api/settings.ts +91 -0
  108. package/src/routes/compose.ts +63 -0
  109. package/src/routes/dash/__tests__/pages.test.ts +225 -0
  110. package/src/routes/dash/collections.tsx +2 -2
  111. package/src/routes/dash/index.tsx +1 -1
  112. package/src/routes/dash/media.tsx +2 -2
  113. package/src/routes/dash/pages.tsx +443 -70
  114. package/src/routes/dash/posts.tsx +3 -7
  115. package/src/routes/dash/redirects.tsx +2 -2
  116. package/src/routes/dash/settings.tsx +83 -5
  117. package/src/routes/feed/rss.ts +2 -2
  118. package/src/routes/feed/sitemap.ts +1 -1
  119. package/src/routes/pages/__tests__/collections.test.ts +94 -0
  120. package/src/routes/pages/__tests__/featured.test.ts +94 -0
  121. package/src/routes/pages/archive.tsx +2 -6
  122. package/src/routes/pages/collection.tsx +2 -6
  123. package/src/routes/pages/collections.tsx +36 -0
  124. package/src/routes/pages/featured.tsx +38 -0
  125. package/src/routes/pages/home.tsx +9 -55
  126. package/src/routes/pages/page.tsx +28 -30
  127. package/src/routes/pages/post.tsx +2 -5
  128. package/src/routes/pages/search.tsx +2 -6
  129. package/src/services/__tests__/page.test.ts +106 -0
  130. package/src/services/__tests__/post.test.ts +114 -15
  131. package/src/services/page.ts +13 -1
  132. package/src/services/post.ts +57 -7
  133. package/src/services/search.ts +2 -2
  134. package/src/styles/tokens.css +47 -0
  135. package/src/styles/ui.css +491 -0
  136. package/src/types.ts +29 -159
  137. package/src/ui/compose/ComposeDialog.tsx +395 -0
  138. package/src/ui/compose/ComposePrompt.tsx +55 -0
  139. package/src/{theme/components/TypeBadge.tsx → ui/dash/FormatBadge.tsx} +2 -3
  140. package/src/{theme/components → ui/dash}/PostForm.tsx +0 -25
  141. package/src/{theme/components → ui/dash}/PostList.tsx +7 -7
  142. package/src/{theme/components/VisibilityBadge.tsx → ui/dash/StatusBadge.tsx} +2 -3
  143. package/src/ui/dash/index.ts +10 -0
  144. package/src/{themes/threads/timeline → ui/feed}/LinkCard.tsx +9 -4
  145. package/src/{themes/threads/timeline → ui/feed}/NoteCard.tsx +13 -8
  146. package/src/{themes/threads/timeline → ui/feed}/QuoteCard.tsx +13 -8
  147. package/src/{themes/threads/timeline → ui/feed}/ThreadPreview.tsx +7 -8
  148. package/src/ui/feed/TimelineFeed.tsx +49 -0
  149. package/src/ui/feed/TimelineItem.tsx +45 -0
  150. package/src/{theme → ui}/layouts/BaseLayout.tsx +11 -1
  151. package/src/{theme → ui}/layouts/DashLayout.tsx +0 -10
  152. package/src/ui/layouts/SiteLayout.tsx +150 -0
  153. package/src/{themes/threads → ui}/pages/ArchivePage.tsx +22 -17
  154. package/src/{themes/threads → ui}/pages/CollectionPage.tsx +14 -5
  155. package/src/ui/pages/CollectionsPage.tsx +73 -0
  156. package/src/ui/pages/FeaturedPage.tsx +31 -0
  157. package/src/{themes/threads → ui}/pages/HomePage.tsx +11 -15
  158. package/src/{themes/threads → ui}/pages/PostPage.tsx +23 -14
  159. package/src/{themes/threads → ui}/pages/SearchPage.tsx +13 -11
  160. package/src/{themes/threads → ui}/pages/SinglePage.tsx +4 -4
  161. package/src/{theme/components → ui/shared}/MediaGallery.tsx +1 -1
  162. package/src/{theme/components → ui/shared}/Pagination.tsx +67 -4
  163. package/src/{theme/components → ui/shared}/ThreadView.tsx +2 -2
  164. package/src/ui/shared/__tests__/pagination.test.ts +46 -0
  165. package/src/ui/shared/index.ts +12 -0
  166. package/bin/jant.js +0 -185
  167. package/dist/lib/theme-components.js +0 -46
  168. package/dist/routes/dash/navigation.js +0 -289
  169. package/dist/theme/index.js +0 -18
  170. package/dist/theme/layouts/index.js +0 -2
  171. package/dist/themes/threads/ThreadsSiteLayout.js +0 -172
  172. package/dist/themes/threads/index.js +0 -81
  173. package/dist/themes/threads/pages/HomePage.js +0 -25
  174. package/dist/themes/threads/timeline/TimelineFeed.js +0 -58
  175. package/dist/themes/threads/timeline/TimelineItem.js +0 -36
  176. package/dist/themes/threads/timeline/TimelineLoadMore.js +0 -23
  177. package/dist/themes/threads/timeline/groupByDate.js +0 -22
  178. package/dist/themes/threads/timeline/timelineMore.js +0 -107
  179. package/src/lib/__tests__/theme-components.test.ts +0 -105
  180. package/src/lib/theme-components.ts +0 -65
  181. package/src/routes/dash/navigation.tsx +0 -317
  182. package/src/theme/components/index.ts +0 -23
  183. package/src/theme/index.ts +0 -22
  184. package/src/theme/layouts/index.ts +0 -7
  185. package/src/themes/threads/ThreadsSiteLayout.tsx +0 -194
  186. package/src/themes/threads/index.ts +0 -100
  187. package/src/themes/threads/style.css +0 -336
  188. package/src/themes/threads/timeline/TimelineFeed.tsx +0 -62
  189. package/src/themes/threads/timeline/TimelineItem.tsx +0 -67
  190. package/src/themes/threads/timeline/TimelineLoadMore.tsx +0 -35
  191. package/src/themes/threads/timeline/groupByDate.ts +0 -30
  192. package/src/themes/threads/timeline/timelineMore.tsx +0 -130
  193. /package/dist/{theme → ui}/color-themes.js +0 -0
  194. /package/dist/{theme/components → ui/dash}/ActionButtons.js +0 -0
  195. /package/dist/{theme/components → ui/dash}/CrudPageHeader.js +0 -0
  196. /package/dist/{theme/components → ui/dash}/DangerZone.js +0 -0
  197. /package/dist/{theme/components → ui/dash}/ListItemRow.js +0 -0
  198. /package/dist/{theme/components → ui/dash}/PageForm.js +0 -0
  199. /package/dist/{theme/components → ui/shared}/EmptyState.js +0 -0
  200. /package/src/{theme → ui}/color-themes.ts +0 -0
  201. /package/src/{theme/components → ui/dash}/ActionButtons.tsx +0 -0
  202. /package/src/{theme/components → ui/dash}/CrudPageHeader.tsx +0 -0
  203. /package/src/{theme/components → ui/dash}/DangerZone.tsx +0 -0
  204. /package/src/{theme/components → ui/dash}/ListItemRow.tsx +0 -0
  205. /package/src/{theme/components → ui/dash}/PageForm.tsx +0 -0
  206. /package/src/{theme/components → ui/shared}/EmptyState.tsx +0 -0
@@ -1,98 +1,225 @@
1
1
  import { getSiteName } from "../../lib/config.js";
2
2
  /**
3
- * Dashboard Pages Routes
3
+ * Dashboard Pages & Navigation Routes
4
4
  *
5
- * Management for standalone pages (about, now, etc.)
5
+ * Unified management for pages and navigation items (pika.page style).
6
+ * Two sections: "Your site navigation" (draggable) and "Other pages".
6
7
  */
7
8
 
8
9
  import { Hono } from "hono";
9
10
  import { useLingui } from "@lingui/react/macro";
10
- import type { Bindings, Page } from "../../types.js";
11
+ import type { Bindings, Page, NavItem } from "../../types.js";
11
12
  import type { AppVariables } from "../../app.js";
12
- import { DashLayout } from "../../theme/layouts/index.js";
13
+ import { DashLayout } from "../../ui/layouts/DashLayout.js";
13
14
  import {
14
15
  PageForm,
15
- EmptyState,
16
16
  ListItemRow,
17
17
  ActionButtons,
18
18
  CrudPageHeader,
19
19
  DangerZone,
20
- } from "../../theme/components/index.js";
21
- import * as time from "../../lib/time.js";
22
- import { dsRedirect } from "../../lib/sse.js";
20
+ } from "../../ui/dash/index.js";
21
+ import { dsRedirect, dsToast } from "../../lib/sse.js";
23
22
 
24
23
  type Env = { Bindings: Bindings; Variables: AppVariables };
25
24
 
26
25
  export const pagesRoutes = new Hono<Env>();
27
26
 
28
- function PagesListContent({ pages }: { pages: Page[] }) {
27
+ // =============================================================================
28
+ // Components
29
+ // =============================================================================
30
+
31
+ function UnifiedPagesContent({
32
+ navItems,
33
+ otherPages,
34
+ }: {
35
+ navItems: NavItem[];
36
+ otherPages: Page[];
37
+ }) {
29
38
  const { t } = useLingui();
30
39
 
31
40
  return (
32
41
  <>
33
42
  <CrudPageHeader
34
- title={t({ message: "Pages", comment: "@context: Pages main heading" })}
35
- ctaLabel={t({
36
- message: "New Page",
37
- comment: "@context: Button to create new page",
43
+ title={t({
44
+ message: "Pages",
45
+ comment: "@context: Pages main heading",
38
46
  })}
39
- ctaHref="/dash/pages/new"
40
- />
41
-
42
- {pages.length === 0 ? (
43
- <EmptyState
44
- message={t({
45
- message: "No pages yet.",
46
- comment: "@context: Empty state message when no pages exist",
47
+ >
48
+ <div class="flex gap-2">
49
+ <a href="/dash/pages/links/new" class="btn-outline">
50
+ {t({
51
+ message: "Add Link",
52
+ comment: "@context: Button to add a navigation link",
53
+ })}
54
+ </a>
55
+ <a href="/dash/pages/new" class="btn">
56
+ {t({
57
+ message: "New Page",
58
+ comment: "@context: Button to create new page",
59
+ })}
60
+ </a>
61
+ </div>
62
+ </CrudPageHeader>
63
+
64
+ {/* Navigation section */}
65
+ <section class="mb-8">
66
+ <h2 class="text-lg font-medium mb-3">
67
+ {t({
68
+ message: "Your site navigation",
69
+ comment: "@context: Section heading for navigation items",
47
70
  })}
48
- ctaText={t({
49
- message: "Create your first page",
50
- comment: "@context: Button in empty state to create first page",
71
+ </h2>
72
+ {navItems.length === 0 ? (
73
+ <p class="text-sm text-muted-foreground py-4">
74
+ {t({
75
+ message:
76
+ "No navigation links yet. Add pages to navigation or create links.",
77
+ comment: "@context: Empty state for navigation section",
78
+ })}
79
+ </p>
80
+ ) : (
81
+ <div id="nav-links-list" class="flex flex-col divide-y">
82
+ {navItems.map((item) => (
83
+ <ListItemRow
84
+ key={item.id}
85
+ actions={
86
+ item.type === "page" ? (
87
+ <>
88
+ <ActionButtons
89
+ editHref={
90
+ item.pageId
91
+ ? `/dash/pages/${item.pageId}/edit`
92
+ : undefined
93
+ }
94
+ editLabel={t({
95
+ message: "Edit",
96
+ comment: "@context: Button to edit page",
97
+ })}
98
+ />
99
+ <button
100
+ type="button"
101
+ class="btn-sm-ghost"
102
+ data-on:click__prevent={`@post('/dash/pages/${item.pageId}/remove-from-nav')`}
103
+ >
104
+ {t({
105
+ message: "Un-nav",
106
+ comment:
107
+ "@context: Button to remove page from navigation",
108
+ })}
109
+ </button>
110
+ </>
111
+ ) : (
112
+ <>
113
+ <ActionButtons
114
+ editHref={`/dash/pages/links/${item.id}/edit`}
115
+ editLabel={t({
116
+ message: "Edit",
117
+ comment: "@context: Button to edit link",
118
+ })}
119
+ deleteAction={`/dash/pages/links/${item.id}/delete`}
120
+ deleteLabel={t({
121
+ message: "Delete",
122
+ comment: "@context: Button to delete link",
123
+ })}
124
+ />
125
+ </>
126
+ )
127
+ }
128
+ >
129
+ <div
130
+ class="flex items-center gap-3 cursor-grab"
131
+ data-id={item.id}
132
+ >
133
+ <span class="text-muted-foreground select-none">⠿</span>
134
+ <div class="flex items-center gap-2">
135
+ <span class="font-medium">{item.label}</span>
136
+ <code class="text-sm text-muted-foreground bg-muted px-1 rounded">
137
+ {item.url}
138
+ </code>
139
+ <span class="badge badge-sm">
140
+ {item.type === "page"
141
+ ? t({
142
+ message: "page",
143
+ comment: "@context: Nav item type badge",
144
+ })
145
+ : t({
146
+ message: "link",
147
+ comment: "@context: Nav item type badge",
148
+ })}
149
+ </span>
150
+ </div>
151
+ </div>
152
+ </ListItemRow>
153
+ ))}
154
+ </div>
155
+ )}
156
+ </section>
157
+
158
+ {/* Other pages section */}
159
+ <section>
160
+ <h2 class="text-lg font-medium mb-3">
161
+ {t({
162
+ message: "Other pages",
163
+ comment: "@context: Section heading for pages not in navigation",
51
164
  })}
52
- ctaHref="/dash/pages/new"
53
- />
54
- ) : (
55
- <div class="flex flex-col divide-y">
56
- {pages.map((page) => (
57
- <ListItemRow
58
- key={page.id}
59
- actions={
60
- <ActionButtons
61
- editHref={`/dash/pages/${page.id}/edit`}
62
- editLabel={t({
63
- message: "Edit",
64
- comment: "@context: Button to edit page",
65
- })}
66
- viewHref={
67
- page.status !== "draft" ? `/${page.slug}` : undefined
68
- }
69
- viewLabel={t({
70
- message: "View",
71
- comment: "@context: Button to view page on public site",
72
- })}
73
- />
74
- }
75
- >
76
- <div class="flex items-center gap-2 mb-1">
77
- <span class="text-xs text-muted-foreground">
78
- {time.formatDate(page.updatedAt)}
79
- </span>
80
- </div>
81
- <a
82
- href={`/dash/pages/${page.id}`}
83
- class="font-medium hover:underline"
165
+ </h2>
166
+ {otherPages.length === 0 ? (
167
+ <p class="text-sm text-muted-foreground py-4">
168
+ {t({
169
+ message: "All pages are in your navigation.",
170
+ comment: "@context: Empty state when all pages are in nav",
171
+ })}
172
+ </p>
173
+ ) : (
174
+ <div class="flex flex-col divide-y">
175
+ {otherPages.map((page) => (
176
+ <ListItemRow
177
+ key={page.id}
178
+ actions={
179
+ <>
180
+ <button
181
+ type="button"
182
+ class="btn-sm-outline"
183
+ data-on:click__prevent={`@post('/dash/pages/${page.id}/add-to-nav')`}
184
+ >
185
+ {t({
186
+ message: "Add to nav",
187
+ comment: "@context: Button to add page to navigation",
188
+ })}
189
+ </button>
190
+ <ActionButtons
191
+ editHref={`/dash/pages/${page.id}/edit`}
192
+ editLabel={t({
193
+ message: "Edit",
194
+ comment: "@context: Button to edit page",
195
+ })}
196
+ viewHref={
197
+ page.status !== "draft" ? `/${page.slug}` : undefined
198
+ }
199
+ viewLabel={t({
200
+ message: "View",
201
+ comment: "@context: Button to view page on public site",
202
+ })}
203
+ />
204
+ </>
205
+ }
84
206
  >
85
- {page.title ||
86
- t({
87
- message: "Untitled",
88
- comment: "@context: Default title for untitled page",
89
- })}
90
- </a>
91
- <p class="text-sm text-muted-foreground mt-1">/{page.slug}</p>
92
- </ListItemRow>
93
- ))}
94
- </div>
95
- )}
207
+ <a
208
+ href={`/dash/pages/${page.id}`}
209
+ class="font-medium hover:underline"
210
+ >
211
+ {page.title ||
212
+ t({
213
+ message: "Untitled",
214
+ comment: "@context: Default title for untitled page",
215
+ })}
216
+ </a>
217
+ <p class="text-sm text-muted-foreground mt-1">/{page.slug}</p>
218
+ </ListItemRow>
219
+ ))}
220
+ </div>
221
+ )}
222
+ </section>
96
223
  </>
97
224
  );
98
225
  }
@@ -174,9 +301,123 @@ function EditPageContent({ page }: { page: Page }) {
174
301
  );
175
302
  }
176
303
 
177
- // List pages
304
+ function LinkFormContent({
305
+ item,
306
+ isEdit,
307
+ }: {
308
+ item?: NavItem;
309
+ isEdit?: boolean;
310
+ }) {
311
+ const { t } = useLingui();
312
+ const title = isEdit
313
+ ? t({ message: "Edit Link", comment: "@context: Page heading" })
314
+ : t({ message: "New Link", comment: "@context: Page heading" });
315
+
316
+ const signals = JSON.stringify({
317
+ label: item?.label ?? "",
318
+ url: item?.url ?? "",
319
+ }).replace(/</g, "\\u003c");
320
+
321
+ const action = isEdit ? `/dash/pages/links/${item?.id}` : "/dash/pages/links";
322
+
323
+ return (
324
+ <>
325
+ <h1 class="text-2xl font-semibold mb-6">{title}</h1>
326
+
327
+ <form
328
+ data-signals={signals}
329
+ data-on:submit__prevent={`@post('${action}')`}
330
+ data-indicator="_loading"
331
+ class="flex flex-col gap-4 max-w-lg"
332
+ >
333
+ <div class="field">
334
+ <label class="label">
335
+ {t({
336
+ message: "Label",
337
+ comment: "@context: Navigation link form field",
338
+ })}
339
+ </label>
340
+ <input
341
+ type="text"
342
+ data-bind="label"
343
+ class="input"
344
+ placeholder="Home"
345
+ required
346
+ />
347
+ <p class="text-xs text-muted-foreground mt-1">
348
+ {t({
349
+ message: "Display text for the link",
350
+ comment: "@context: Navigation label help text",
351
+ })}
352
+ </p>
353
+ </div>
354
+
355
+ <div class="field">
356
+ <label class="label">
357
+ {t({
358
+ message: "URL",
359
+ comment: "@context: Navigation link form field",
360
+ })}
361
+ </label>
362
+ <input
363
+ type="text"
364
+ data-bind="url"
365
+ class="input"
366
+ placeholder="/archive or https://..."
367
+ required
368
+ />
369
+ <p class="text-xs text-muted-foreground mt-1">
370
+ {t({
371
+ message:
372
+ "Path (e.g. /archive) or full URL (e.g. https://example.com)",
373
+ comment: "@context: Navigation URL help text",
374
+ })}
375
+ </p>
376
+ </div>
377
+
378
+ <div class="flex gap-2">
379
+ <button type="submit" class="btn" data-attr-disabled="$_loading">
380
+ <span data-show="!$_loading">
381
+ {isEdit
382
+ ? t({
383
+ message: "Save Changes",
384
+ comment: "@context: Button to save edited navigation link",
385
+ })
386
+ : t({
387
+ message: "Create Link",
388
+ comment: "@context: Button to save new navigation link",
389
+ })}
390
+ </span>
391
+ <span data-show="$_loading">
392
+ {t({
393
+ message: "Processing...",
394
+ comment:
395
+ "@context: Loading text shown on submit button while request is in progress",
396
+ })}
397
+ </span>
398
+ </button>
399
+ <a href="/dash/pages" class="btn-outline">
400
+ {t({
401
+ message: "Cancel",
402
+ comment: "@context: Button to cancel form",
403
+ })}
404
+ </a>
405
+ </div>
406
+ </form>
407
+ </>
408
+ );
409
+ }
410
+
411
+ // =============================================================================
412
+ // Page Routes
413
+ // =============================================================================
414
+
415
+ // List pages (unified view)
178
416
  pagesRoutes.get("/", async (c) => {
179
- const pages = await c.var.services.pages.list();
417
+ const [navItems, otherPages] = await Promise.all([
418
+ c.var.services.navItems.list(),
419
+ c.var.services.pages.listNotInNav(),
420
+ ]);
180
421
  const siteName = await getSiteName(c);
181
422
 
182
423
  return c.html(
@@ -186,7 +427,7 @@ pagesRoutes.get("/", async (c) => {
186
427
  siteName={siteName}
187
428
  currentPath="/dash/pages"
188
429
  >
189
- <PagesListContent pages={pages} />
430
+ <UnifiedPagesContent navItems={navItems} otherPages={otherPages} />
190
431
  </DashLayout>,
191
432
  );
192
433
  });
@@ -207,6 +448,105 @@ pagesRoutes.get("/new", async (c) => {
207
448
  );
208
449
  });
209
450
 
451
+ // New link form
452
+ pagesRoutes.get("/links/new", async (c) => {
453
+ const siteName = await getSiteName(c);
454
+
455
+ return c.html(
456
+ <DashLayout
457
+ c={c}
458
+ title="New Link"
459
+ siteName={siteName}
460
+ currentPath="/dash/pages"
461
+ >
462
+ <LinkFormContent />
463
+ </DashLayout>,
464
+ );
465
+ });
466
+
467
+ // Create link
468
+ pagesRoutes.post("/links", async (c) => {
469
+ const body = await c.req.json<{ label: string; url: string }>();
470
+
471
+ if (!body.label || !body.url) {
472
+ return dsToast("Label and URL are required", "error");
473
+ }
474
+
475
+ await c.var.services.navItems.create({
476
+ type: "link",
477
+ label: body.label,
478
+ url: body.url,
479
+ });
480
+
481
+ return dsRedirect("/dash/pages");
482
+ });
483
+
484
+ // Reorder nav items (must be before /:id to avoid matching)
485
+ pagesRoutes.post("/reorder", async (c) => {
486
+ const body = await c.req.json<{ ids: number[] }>();
487
+
488
+ if (!Array.isArray(body.ids)) {
489
+ return dsToast("Invalid request", "error");
490
+ }
491
+
492
+ await c.var.services.navItems.reorder(body.ids);
493
+
494
+ return dsToast("Order saved");
495
+ });
496
+
497
+ // Edit link form
498
+ pagesRoutes.get("/links/:id/edit", async (c) => {
499
+ const id = parseInt(c.req.param("id"), 10);
500
+ if (isNaN(id)) return c.notFound();
501
+
502
+ const item = await c.var.services.navItems.getById(id);
503
+ if (!item) return c.notFound();
504
+
505
+ const siteName = await getSiteName(c);
506
+
507
+ return c.html(
508
+ <DashLayout
509
+ c={c}
510
+ title="Edit Link"
511
+ siteName={siteName}
512
+ currentPath="/dash/pages"
513
+ >
514
+ <LinkFormContent item={item} isEdit />
515
+ </DashLayout>,
516
+ );
517
+ });
518
+
519
+ // Update link
520
+ pagesRoutes.post("/links/:id", async (c) => {
521
+ const id = parseInt(c.req.param("id"), 10);
522
+ if (isNaN(id)) return c.notFound();
523
+
524
+ const body = await c.req.json<{ label: string; url: string }>();
525
+
526
+ if (!body.label || !body.url) {
527
+ return dsToast("Label and URL are required", "error");
528
+ }
529
+
530
+ const updated = await c.var.services.navItems.update(id, {
531
+ label: body.label,
532
+ url: body.url,
533
+ });
534
+
535
+ if (!updated) return c.notFound();
536
+
537
+ return dsRedirect("/dash/pages");
538
+ });
539
+
540
+ // Delete link
541
+ pagesRoutes.post("/links/:id/delete", async (c) => {
542
+ const id = parseInt(c.req.param("id"), 10);
543
+ if (!isNaN(id)) {
544
+ await c.var.services.navItems.delete(id);
545
+ }
546
+
547
+ return dsRedirect("/dash/pages");
548
+ });
549
+
210
550
  // Create page
211
551
  pagesRoutes.post("/", async (c) => {
212
552
  const body = await c.req.json<{
@@ -226,6 +566,39 @@ pagesRoutes.post("/", async (c) => {
226
566
  return dsRedirect(`/dash/pages/${page.id}`);
227
567
  });
228
568
 
569
+ // Add page to navigation
570
+ pagesRoutes.post("/:id/add-to-nav", async (c) => {
571
+ const id = parseInt(c.req.param("id"), 10);
572
+ if (isNaN(id)) return c.notFound();
573
+
574
+ const page = await c.var.services.pages.getById(id);
575
+ if (!page) return c.notFound();
576
+
577
+ await c.var.services.navItems.create({
578
+ type: "page",
579
+ label: page.title || page.slug,
580
+ url: `/${page.slug}`,
581
+ pageId: page.id,
582
+ });
583
+
584
+ return dsRedirect("/dash/pages");
585
+ });
586
+
587
+ // Remove page from navigation (keeps the page, deletes the nav item)
588
+ pagesRoutes.post("/:id/remove-from-nav", async (c) => {
589
+ const pageId = parseInt(c.req.param("id"), 10);
590
+ if (isNaN(pageId)) return c.notFound();
591
+
592
+ // Find nav item by pageId
593
+ const navItems = await c.var.services.navItems.list();
594
+ const navItem = navItems.find((item) => item.pageId === pageId);
595
+ if (navItem) {
596
+ await c.var.services.navItems.delete(navItem.id);
597
+ }
598
+
599
+ return dsRedirect("/dash/pages");
600
+ });
601
+
229
602
  // View single page
230
603
  pagesRoutes.get("/:id", async (c) => {
231
604
  const id = parseInt(c.req.param("id"), 10);
@@ -7,13 +7,13 @@ import { Hono } from "hono";
7
7
  import { useLingui } from "@lingui/react/macro";
8
8
  import type { Bindings, Post, Media, Collection } from "../../types.js";
9
9
  import type { AppVariables } from "../../app.js";
10
- import { DashLayout } from "../../theme/layouts/index.js";
10
+ import { DashLayout } from "../../ui/layouts/DashLayout.js";
11
11
  import {
12
12
  PostForm,
13
13
  PostList,
14
14
  CrudPageHeader,
15
15
  ActionButtons,
16
- } from "../../theme/components/index.js";
16
+ } from "../../ui/dash/index.js";
17
17
  import * as sqid from "../../lib/sqid.js";
18
18
  import { dsRedirect } from "../../lib/sse.js";
19
19
 
@@ -95,7 +95,6 @@ postsRoutes.post("/", async (c) => {
95
95
  status: string;
96
96
  featured?: boolean;
97
97
  pinned?: boolean;
98
- slug?: string;
99
98
  url?: string;
100
99
  quoteText?: string;
101
100
  rating?: number;
@@ -110,7 +109,6 @@ postsRoutes.post("/", async (c) => {
110
109
  status: body.status as Post["status"],
111
110
  featured: body.featured,
112
111
  pinned: body.pinned,
113
- slug: body.slug || undefined,
114
112
  url: body.url || undefined,
115
113
  quoteText: body.quoteText || undefined,
116
114
  rating: body.rating || undefined,
@@ -131,7 +129,7 @@ function ViewPostContent({ post }: { post: Post }) {
131
129
  message: "Post",
132
130
  comment: "@context: Default post title",
133
131
  });
134
- const permalink = post.slug ? `/${post.slug}` : `/p/${sqid.encode(post.id)}`;
132
+ const permalink = post.path ? `/${post.path}` : `/p/${sqid.encode(post.id)}`;
135
133
 
136
134
  return (
137
135
  <>
@@ -266,7 +264,6 @@ postsRoutes.post("/:id", async (c) => {
266
264
  status: string;
267
265
  featured?: boolean;
268
266
  pinned?: boolean;
269
- slug?: string;
270
267
  url?: string;
271
268
  quoteText?: string;
272
269
  rating?: number;
@@ -281,7 +278,6 @@ postsRoutes.post("/:id", async (c) => {
281
278
  status: body.status as Post["status"],
282
279
  featured: body.featured,
283
280
  pinned: body.pinned,
284
- slug: body.slug || null,
285
281
  url: body.url || null,
286
282
  quoteText: body.quoteText || null,
287
283
  rating: body.rating || null,
@@ -7,13 +7,13 @@ import { Hono } from "hono";
7
7
  import { useLingui } from "@lingui/react/macro";
8
8
  import type { Bindings, Redirect } from "../../types.js";
9
9
  import type { AppVariables } from "../../app.js";
10
- import { DashLayout } from "../../theme/layouts/index.js";
10
+ import { DashLayout } from "../../ui/layouts/DashLayout.js";
11
11
  import {
12
12
  EmptyState,
13
13
  ListItemRow,
14
14
  ActionButtons,
15
15
  CrudPageHeader,
16
- } from "../../theme/components/index.js";
16
+ } from "../../ui/dash/index.js";
17
17
  import { dsRedirect } from "../../lib/sse.js";
18
18
 
19
19
  type Env = { Bindings: Bindings; Variables: AppVariables };