@jant/core 0.3.32 → 0.3.34

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 (97) hide show
  1. package/dist/client/client.css +1 -1
  2. package/dist/client/client.js +1442 -989
  3. package/dist/index.js +1431 -1057
  4. package/package.json +1 -1
  5. package/src/__tests__/helpers/app.ts +6 -3
  6. package/src/__tests__/helpers/db.ts +3 -0
  7. package/src/app.tsx +1 -1
  8. package/src/client.ts +2 -1
  9. package/src/db/migrations/0011_add_path_registry.sql +23 -0
  10. package/src/db/schema.ts +12 -1
  11. package/src/i18n/locales/en.po +225 -91
  12. package/src/i18n/locales/en.ts +1 -1
  13. package/src/i18n/locales/zh-Hans.po +201 -152
  14. package/src/i18n/locales/zh-Hans.ts +1 -1
  15. package/src/i18n/locales/zh-Hant.po +201 -152
  16. package/src/i18n/locales/zh-Hant.ts +1 -1
  17. package/src/lib/__tests__/excerpt.test.ts +25 -0
  18. package/src/lib/__tests__/resolve-config.test.ts +26 -2
  19. package/src/lib/__tests__/timeline.test.ts +2 -1
  20. package/src/lib/compose-bridge.ts +30 -1
  21. package/src/lib/excerpt.ts +16 -7
  22. package/src/lib/nav-manager-bridge.ts +54 -0
  23. package/src/lib/navigation.ts +7 -4
  24. package/src/lib/render.tsx +5 -2
  25. package/src/lib/resolve-config.ts +7 -0
  26. package/src/lib/view.ts +42 -10
  27. package/src/middleware/error-handler.ts +16 -0
  28. package/src/routes/api/__tests__/posts.test.ts +80 -0
  29. package/src/routes/api/__tests__/settings.test.ts +1 -1
  30. package/src/routes/api/posts.ts +6 -29
  31. package/src/routes/api/upload.ts +2 -14
  32. package/src/routes/auth/__tests__/setup.test.ts +3 -2
  33. package/src/routes/auth/setup.tsx +1 -1
  34. package/src/routes/compose.tsx +13 -5
  35. package/src/routes/dash/__tests__/pages.test.ts +2 -1
  36. package/src/routes/dash/__tests__/settings-avatar.test.ts +151 -33
  37. package/src/routes/dash/appearance.tsx +71 -4
  38. package/src/routes/dash/collections.tsx +15 -21
  39. package/src/routes/dash/media.tsx +1 -13
  40. package/src/routes/dash/pages.tsx +5 -150
  41. package/src/routes/dash/posts.tsx +25 -32
  42. package/src/routes/dash/redirects.tsx +9 -11
  43. package/src/routes/dash/settings.tsx +29 -111
  44. package/src/routes/feed/__tests__/rss.test.ts +5 -1
  45. package/src/routes/pages/__tests__/collections.test.ts +2 -1
  46. package/src/routes/pages/__tests__/featured.test.ts +2 -1
  47. package/src/routes/pages/page.tsx +20 -25
  48. package/src/services/__tests__/collection.test.ts +2 -1
  49. package/src/services/__tests__/media.test.ts +78 -1
  50. package/src/services/__tests__/navigation.test.ts +2 -1
  51. package/src/services/__tests__/page.test.ts +78 -1
  52. package/src/services/__tests__/path-registry.test.ts +165 -0
  53. package/src/services/__tests__/post-timeline.test.ts +2 -1
  54. package/src/services/__tests__/post.test.ts +103 -1
  55. package/src/services/__tests__/redirect.test.ts +53 -4
  56. package/src/services/__tests__/search.test.ts +2 -1
  57. package/src/services/__tests__/settings.test.ts +153 -0
  58. package/src/services/index.ts +12 -4
  59. package/src/services/media.ts +72 -4
  60. package/src/services/page.ts +64 -17
  61. package/src/services/path-registry.ts +160 -0
  62. package/src/services/post.ts +119 -24
  63. package/src/services/redirect.ts +23 -3
  64. package/src/services/settings.ts +181 -0
  65. package/src/styles/components.css +135 -0
  66. package/src/styles/tokens.css +6 -1
  67. package/src/styles/ui.css +70 -26
  68. package/src/types/bindings.ts +1 -0
  69. package/src/types/config.ts +7 -2
  70. package/src/types/constants.ts +9 -1
  71. package/src/types/sortablejs.d.ts +8 -2
  72. package/src/types/views.ts +1 -1
  73. package/src/ui/color-themes.ts +31 -31
  74. package/src/ui/components/__tests__/jant-settings-avatar.test.ts +0 -3
  75. package/src/ui/components/__tests__/jant-settings-general.test.ts +2 -6
  76. package/src/ui/components/jant-compose-dialog.ts +3 -2
  77. package/src/ui/components/jant-compose-editor.ts +17 -2
  78. package/src/ui/components/jant-nav-manager.ts +1067 -0
  79. package/src/ui/components/jant-settings-general.ts +2 -35
  80. package/src/ui/components/nav-manager-types.ts +72 -0
  81. package/src/ui/components/settings-types.ts +0 -3
  82. package/src/ui/compose/ComposePrompt.tsx +3 -11
  83. package/src/ui/dash/appearance/AdvancedContent.tsx +0 -3
  84. package/src/ui/dash/appearance/AppearanceNav.tsx +12 -8
  85. package/src/ui/dash/appearance/ColorThemeContent.tsx +1 -4
  86. package/src/ui/dash/appearance/FontThemeContent.tsx +0 -3
  87. package/src/ui/dash/appearance/NavigationContent.tsx +302 -0
  88. package/src/ui/dash/pages/PagesContent.tsx +74 -0
  89. package/src/ui/dash/settings/AccountContent.tsx +0 -3
  90. package/src/ui/dash/settings/GeneralContent.tsx +1 -19
  91. package/src/ui/dash/settings/SettingsNav.tsx +2 -6
  92. package/src/ui/feed/NoteCard.tsx +2 -2
  93. package/src/ui/layouts/DashLayout.tsx +83 -86
  94. package/src/ui/layouts/SiteLayout.tsx +82 -21
  95. package/src/lib/nav-reorder.ts +0 -26
  96. package/src/ui/dash/pages/LinkFormContent.tsx +0 -119
  97. package/src/ui/dash/pages/UnifiedPagesContent.tsx +0 -203
@@ -35,7 +35,6 @@ export class JantSettingsGeneral extends LitElement {
35
35
  _siteDescription: { state: true },
36
36
  _siteFooter: { state: true },
37
37
  _siteLanguage: { state: true },
38
- _homeDefaultView: { state: true },
39
38
  _timeZone: { state: true },
40
39
  _origGeneral: { state: true },
41
40
  _generalDirty: { state: true },
@@ -59,7 +58,6 @@ export class JantSettingsGeneral extends LitElement {
59
58
  declare _siteDescription: string;
60
59
  declare _siteFooter: string;
61
60
  declare _siteLanguage: string;
62
- declare _homeDefaultView: string;
63
61
  declare _timeZone: string;
64
62
  declare _origGeneral: Record<string, string>;
65
63
  declare _generalDirty: boolean;
@@ -88,7 +86,6 @@ export class JantSettingsGeneral extends LitElement {
88
86
  this._siteDescription = "";
89
87
  this._siteFooter = "";
90
88
  this._siteLanguage = "en";
91
- this._homeDefaultView = "latest";
92
89
  this._timeZone = "UTC";
93
90
  this._origGeneral = {};
94
91
  this._generalDirty = false;
@@ -105,7 +102,6 @@ export class JantSettingsGeneral extends LitElement {
105
102
  siteName: string;
106
103
  siteDescription: string;
107
104
  siteLanguage: string;
108
- homeDefaultView: string;
109
105
  timeZone: string;
110
106
  siteFooter: string;
111
107
  noindex: boolean;
@@ -114,14 +110,12 @@ export class JantSettingsGeneral extends LitElement {
114
110
  this._siteDescription = data.siteDescription;
115
111
  this._siteFooter = data.siteFooter;
116
112
  this._siteLanguage = data.siteLanguage;
117
- this._homeDefaultView = data.homeDefaultView;
118
113
  this._timeZone = data.timeZone;
119
114
  this._origGeneral = {
120
115
  siteName: data.siteName,
121
116
  siteDescription: data.siteDescription,
122
117
  siteFooter: data.siteFooter,
123
118
  siteLanguage: data.siteLanguage,
124
- homeDefaultView: data.homeDefaultView,
125
119
  timeZone: data.timeZone,
126
120
  };
127
121
 
@@ -137,7 +131,6 @@ export class JantSettingsGeneral extends LitElement {
137
131
  siteDescription: this._siteDescription,
138
132
  siteFooter: this._siteFooter,
139
133
  siteLanguage: this._siteLanguage,
140
- homeDefaultView: this._homeDefaultView,
141
134
  timeZone: this._timeZone,
142
135
  };
143
136
  this._generalDirty = false;
@@ -166,7 +159,6 @@ export class JantSettingsGeneral extends LitElement {
166
159
  this._siteDescription = this._origGeneral.siteDescription ?? "";
167
160
  this._siteFooter = this._origGeneral.siteFooter ?? "";
168
161
  this._siteLanguage = this._origGeneral.siteLanguage ?? "en";
169
- this._homeDefaultView = this._origGeneral.homeDefaultView ?? "latest";
170
162
  this._timeZone = this._origGeneral.timeZone ?? "UTC";
171
163
  this._generalDirty = false;
172
164
  }
@@ -184,7 +176,6 @@ export class JantSettingsGeneral extends LitElement {
184
176
  siteDescription: this._siteDescription,
185
177
  siteFooter: this._siteFooter,
186
178
  siteLanguage: this._siteLanguage,
187
- homeDefaultView: this._homeDefaultView,
188
179
  timeZone: this._timeZone,
189
180
  },
190
181
  section: "general",
@@ -288,9 +279,9 @@ export class JantSettingsGeneral extends LitElement {
288
279
  <label class="label">${this.labels.aboutBlog}</label>
289
280
  <textarea
290
281
  class="textarea"
291
- rows="3"
282
+ rows="2"
292
283
  .value=${this._siteDescription}
293
- placeholder=${this.labels.markdownSupported}
284
+ placeholder=${this.siteDescriptionFallback}
294
285
  @input=${(e: Event) => {
295
286
  this._siteDescription = (e.target as HTMLTextAreaElement).value;
296
287
  this._markGeneralDirty();
@@ -340,30 +331,6 @@ export class JantSettingsGeneral extends LitElement {
340
331
  </select>
341
332
  </div>
342
333
 
343
- <div class="field">
344
- <label class="label">${this.labels.defaultHomepageView}</label>
345
- <select
346
- class="select"
347
- @change=${(e: Event) => {
348
- this._homeDefaultView = (e.target as HTMLSelectElement).value;
349
- this._markGeneralDirty();
350
- }}
351
- >
352
- <option
353
- value="latest"
354
- ?selected=${this._homeDefaultView === "latest"}
355
- >
356
- ${this.labels.latest}
357
- </option>
358
- <option
359
- value="featured"
360
- ?selected=${this._homeDefaultView === "featured"}
361
- >
362
- ${this.labels.featured}
363
- </option>
364
- </select>
365
- </div>
366
-
367
334
  <div class="field">
368
335
  <label class="label">${this.labels.timeZone}</label>
369
336
  <select
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Shared type definitions for the nav manager Lit component.
3
+ */
4
+
5
+ export interface NavManagerItem {
6
+ id: number;
7
+ type: "page" | "link" | "system";
8
+ label: string;
9
+ url: string;
10
+ pageId: number | null;
11
+ }
12
+
13
+ export interface SystemNavConfig {
14
+ key: string;
15
+ defaultLabel: string;
16
+ url: string;
17
+ description: string;
18
+ }
19
+
20
+ export interface AvailablePage {
21
+ id: number;
22
+ title: string;
23
+ slug: string;
24
+ }
25
+
26
+ export interface NavManagerLabels {
27
+ preview: string;
28
+ navigationItems: string;
29
+ emptyState: string;
30
+ page: string;
31
+ link: string;
32
+ system: string;
33
+ toggleEdit: string;
34
+ label: string;
35
+ url: string;
36
+ save: string;
37
+ delete: string;
38
+ editPage: string;
39
+ remove: string;
40
+ orderSaved: string;
41
+ labelRequired: string;
42
+ saveFailed: string;
43
+ deleteFailed: string;
44
+ systemLinks: string;
45
+ systemLinksDescription: string;
46
+ addPageToNavigation: string;
47
+ addCustomLinkToNavigation: string;
48
+ choosePage: string;
49
+ searchPages: string;
50
+ noPagesFound: string;
51
+ addLink: string;
52
+ addLinkDescription: string;
53
+ allPagesInNav: string;
54
+ urlPlaceholder: string;
55
+ labelAndUrlRequired: string;
56
+ maxVisibleLinks: string;
57
+ maxVisibleSaved: string;
58
+ useFeaturedAsDefault: string;
59
+ homeViewSaved: string;
60
+ latest: string;
61
+ featured: string;
62
+ }
63
+
64
+ export interface NavManagerUpdateDetail {
65
+ id: number;
66
+ label: string;
67
+ url?: string;
68
+ }
69
+
70
+ export interface NavManagerDeleteDetail {
71
+ id: number;
72
+ }
@@ -23,9 +23,6 @@ export interface SettingsLabels {
23
23
  footerHelp: string;
24
24
  markdownSupported: string;
25
25
  language: string;
26
- defaultHomepageView: string;
27
- latest: string;
28
- featured: string;
29
26
  timeZone: string;
30
27
 
31
28
  // SEO
@@ -30,7 +30,9 @@ export const ComposePrompt: FC = () => {
30
30
  stroke-linecap="round"
31
31
  stroke-linejoin="round"
32
32
  >
33
- <path d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 1 1 3.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z" />
33
+ <path d="M20.24 12.24a6 6 0 0 0-8.49-8.49L5 10.5V19h8.5z" />
34
+ <line x1="16" y1="8" x2="2" y2="22" />
35
+ <line x1="17.5" y1="15" x2="9" y2="15" />
34
36
  </svg>
35
37
  </span>
36
38
  <span class="compose-prompt-text">
@@ -40,16 +42,6 @@ export const ComposePrompt: FC = () => {
40
42
  })}
41
43
  </span>
42
44
  </button>
43
- <button
44
- type="button"
45
- class="compose-prompt-post-btn"
46
- onclick="const d=document.getElementById('compose-dialog');d.showModal();d.querySelector('jant-compose-editor')?.focusInput()"
47
- >
48
- {t({
49
- message: "Post",
50
- comment: "@context: Compose prompt post button",
51
- })}
52
- </button>
53
45
  </div>
54
46
  );
55
47
  };
@@ -12,9 +12,6 @@ export function AdvancedContent({ customCSS }: { customCSS: string }) {
12
12
 
13
13
  return (
14
14
  <>
15
- <h1 class="text-2xl font-semibold mb-2">
16
- {t({ message: "Appearance", comment: "@context: Dashboard heading" })}
17
- </h1>
18
15
  <AppearanceNav currentTab="advanced" />
19
16
 
20
17
  <form
@@ -4,19 +4,27 @@
4
4
 
5
5
  import { useLingui } from "@lingui/react/macro";
6
6
 
7
- export type AppearanceTab = "color" | "fonts" | "advanced";
7
+ export type AppearanceTab = "navigation" | "color" | "fonts" | "advanced";
8
8
 
9
9
  export function AppearanceNav({ currentTab }: { currentTab: AppearanceTab }) {
10
10
  const { t } = useLingui();
11
11
 
12
12
  const tabs: { id: AppearanceTab; label: string; href: string }[] = [
13
+ {
14
+ id: "navigation",
15
+ label: t({
16
+ message: "Navigation",
17
+ comment: "@context: Appearance sub-navigation tab",
18
+ }),
19
+ href: "/dash/appearance",
20
+ },
13
21
  {
14
22
  id: "color",
15
23
  label: t({
16
24
  message: "Color Theme",
17
25
  comment: "@context: Appearance sub-navigation tab",
18
26
  }),
19
- href: "/dash/appearance",
27
+ href: "/dash/appearance/color",
20
28
  },
21
29
  {
22
30
  id: "fonts",
@@ -37,16 +45,12 @@ export function AppearanceNav({ currentTab }: { currentTab: AppearanceTab }) {
37
45
  ];
38
46
 
39
47
  return (
40
- <nav class="flex gap-1 mb-6">
48
+ <nav class="dash-subnav">
41
49
  {tabs.map((tab) => (
42
50
  <a
43
51
  key={tab.id}
44
52
  href={tab.href}
45
- class={`px-3 py-2 text-sm rounded-md ${
46
- tab.id === currentTab
47
- ? "bg-accent text-accent-foreground font-medium"
48
- : "text-muted-foreground hover:bg-accent hover:text-accent-foreground"
49
- }`}
53
+ class={tab.id === currentTab ? "active" : ""}
50
54
  >
51
55
  {tab.label}
52
56
  </a>
@@ -88,14 +88,11 @@ export function ColorThemeContent({
88
88
 
89
89
  return (
90
90
  <>
91
- <h1 class="text-2xl font-semibold mb-2">
92
- {t({ message: "Appearance", comment: "@context: Dashboard heading" })}
93
- </h1>
94
91
  <AppearanceNav currentTab="color" />
95
92
 
96
93
  <div
97
94
  data-signals={themeSignals}
98
- data-on:change="@post('/dash/appearance')"
95
+ data-on:change="@post('/dash/appearance/color')"
99
96
  class="max-w-3xl"
100
97
  >
101
98
  <fieldset>
@@ -17,9 +17,6 @@ export function FontThemeContent({
17
17
 
18
18
  return (
19
19
  <>
20
- <h1 class="text-2xl font-semibold mb-2">
21
- {t({ message: "Appearance", comment: "@context: Dashboard heading" })}
22
- </h1>
23
20
  <AppearanceNav currentTab="fonts" />
24
21
 
25
22
  <div
@@ -0,0 +1,302 @@
1
+ /**
2
+ * Navigation management: Lit-powered reorderable nav items, add area, system toggles
3
+ */
4
+
5
+ import { useLingui } from "@lingui/react/macro";
6
+ import type { NavItem, Page, SystemNavKey } from "../../../types.js";
7
+ import { SYSTEM_NAV_KEYS } from "../../../types.js";
8
+ import type {
9
+ NavManagerLabels,
10
+ SystemNavConfig,
11
+ } from "../../components/nav-manager-types.js";
12
+ import { AppearanceNav } from "./AppearanceNav.js";
13
+
14
+ // =============================================================================
15
+ // System descriptions (used to build the config passed to the Lit component)
16
+ // =============================================================================
17
+
18
+ const SYSTEM_DESCRIPTIONS: Record<SystemNavKey, string> = {
19
+ rss: "Add a link to your RSS feed",
20
+ dashboard: "Shows 'Dashboard' when logged in, 'Sign in' when logged out",
21
+ collections: "Link to your collections page",
22
+ archive: "Link to the post archive",
23
+ };
24
+
25
+ // =============================================================================
26
+ // Main component
27
+ // =============================================================================
28
+
29
+ export function NavigationContent({
30
+ navItems,
31
+ availablePages,
32
+ headerNavMaxVisible,
33
+ homeDefaultView,
34
+ siteName,
35
+ }: {
36
+ navItems: NavItem[];
37
+ availablePages: Page[];
38
+ headerNavMaxVisible: number;
39
+ homeDefaultView: string;
40
+ siteName: string;
41
+ }) {
42
+ const { t } = useLingui();
43
+
44
+ // Serialize nav items for the Lit component
45
+ const itemsData = navItems.map((item) => ({
46
+ id: item.id,
47
+ type: item.type,
48
+ label: item.label,
49
+ url: item.url,
50
+ pageId: item.pageId,
51
+ }));
52
+
53
+ // Build system nav config array for the Lit component
54
+ const systemNavData: SystemNavConfig[] = (
55
+ Object.keys(SYSTEM_NAV_KEYS) as SystemNavKey[]
56
+ ).map((key) => ({
57
+ key,
58
+ defaultLabel: SYSTEM_NAV_KEYS[key].defaultLabel,
59
+ url: SYSTEM_NAV_KEYS[key].url,
60
+ description: SYSTEM_DESCRIPTIONS[key],
61
+ }));
62
+
63
+ // Serialize available pages for the Lit component
64
+ const pagesData = availablePages.map((page) => ({
65
+ id: page.id,
66
+ title: page.title,
67
+ slug: page.slug,
68
+ }));
69
+
70
+ const labels: NavManagerLabels = {
71
+ preview: t({
72
+ message: "Preview",
73
+ comment: "@context: Label for nav preview section",
74
+ }),
75
+ navigationItems: t({
76
+ message: "Navigation items",
77
+ comment: "@context: Section heading for nav items",
78
+ }),
79
+ emptyState: t({
80
+ message:
81
+ "No navigation items yet. Add pages, links, or enable system items below.",
82
+ comment: "@context: Empty state for navigation items",
83
+ }),
84
+ page: t({ message: "page", comment: "@context: Nav item type badge" }),
85
+ link: t({ message: "link", comment: "@context: Nav item type badge" }),
86
+ system: t({
87
+ message: "system",
88
+ comment: "@context: Nav item type badge",
89
+ }),
90
+ toggleEdit: t({
91
+ message: "Toggle edit panel",
92
+ comment: "@context: Button to expand/collapse nav item edit",
93
+ }),
94
+ label: t({
95
+ message: "Label",
96
+ comment: "@context: Nav item label field",
97
+ }),
98
+ url: t({ message: "URL", comment: "@context: Nav item URL field" }),
99
+ save: t({
100
+ message: "Save",
101
+ comment: "@context: Save nav item changes",
102
+ }),
103
+ delete: t({
104
+ message: "Delete",
105
+ comment: "@context: Delete nav item",
106
+ }),
107
+ editPage: t({
108
+ message: "Edit Page",
109
+ comment: "@context: Link to edit the page",
110
+ }),
111
+ remove: t({
112
+ message: "Remove",
113
+ comment: "@context: Remove page from navigation",
114
+ }),
115
+ orderSaved: t({
116
+ message: "Order saved",
117
+ comment: "@context: Toast after saving navigation item order",
118
+ }),
119
+ labelRequired: t({
120
+ message: "Label is required",
121
+ comment: "@context: Error toast when nav label is empty",
122
+ }),
123
+ saveFailed: t({
124
+ message: "Failed to save. Please try again.",
125
+ comment: "@context: Error toast when nav save fails",
126
+ }),
127
+ deleteFailed: t({
128
+ message: "Failed to delete. Please try again.",
129
+ comment: "@context: Error toast when nav delete fails",
130
+ }),
131
+ systemLinks: t({
132
+ message: "System links",
133
+ comment: "@context: Section heading for system nav items",
134
+ }),
135
+ systemLinksDescription: t({
136
+ message:
137
+ "Toggle built-in navigation items. Enabled items appear in your navigation alongside pages and links.",
138
+ comment: "@context: Description for system nav toggles",
139
+ }),
140
+ addPageToNavigation: t({
141
+ message: "Add page to navigation",
142
+ comment: "@context: Section heading for adding page to nav",
143
+ }),
144
+ addCustomLinkToNavigation: t({
145
+ message: "Add custom link to navigation",
146
+ comment: "@context: Section heading for adding custom link to nav",
147
+ }),
148
+ choosePage: t({
149
+ message: "Choose a page…",
150
+ comment: "@context: Placeholder for page select combobox trigger",
151
+ }),
152
+ searchPages: t({
153
+ message: "Search pages…",
154
+ comment: "@context: Placeholder for page search input in combobox",
155
+ }),
156
+ noPagesFound: t({
157
+ message: "No pages found.",
158
+ comment: "@context: Empty state when page search has no results",
159
+ }),
160
+ addLink: t({
161
+ message: "Add Link",
162
+ comment: "@context: Button and heading for adding custom link",
163
+ }),
164
+ addLinkDescription: t({
165
+ message: "Add a custom link to any URL",
166
+ comment: "@context: Description in link popover form",
167
+ }),
168
+ allPagesInNav: t({
169
+ message: "All pages are already in navigation.",
170
+ comment: "@context: Message when no pages available to add",
171
+ }),
172
+ urlPlaceholder: "/archive or https://...",
173
+ maxVisibleLinks: t({
174
+ message: "Max visible links",
175
+ comment: "@context: Label for max visible nav links number input",
176
+ }),
177
+ maxVisibleSaved: t({
178
+ message: "Max visible links saved",
179
+ comment: "@context: Toast after saving max visible nav links setting",
180
+ }),
181
+ useFeaturedAsDefault: t({
182
+ message: "Use Featured as default home view",
183
+ comment:
184
+ "@context: Switch label for setting featured posts as default homepage",
185
+ }),
186
+ homeViewSaved: t({
187
+ message: "Home view saved",
188
+ comment: "@context: Toast after saving home default view setting",
189
+ }),
190
+ latest: t({
191
+ message: "Latest",
192
+ comment: "@context: Browse filter label for latest posts",
193
+ }),
194
+ featured: t({
195
+ message: "Featured",
196
+ comment: "@context: Browse filter label for featured posts",
197
+ }),
198
+ labelAndUrlRequired: t({
199
+ message: "Label and URL are required",
200
+ comment: "@context: Error toast when nav link fields are empty",
201
+ }),
202
+ };
203
+
204
+ const escapeJson = (data: unknown) =>
205
+ JSON.stringify(data).replace(/</g, "\\u003c");
206
+
207
+ return (
208
+ <>
209
+ <AppearanceNav currentTab="navigation" />
210
+
211
+ <div class="max-w-3xl flex flex-col gap-8">
212
+ <jant-nav-manager
213
+ items={escapeJson(itemsData)}
214
+ labels={escapeJson(labels)}
215
+ system-nav-items={escapeJson(systemNavData)}
216
+ available-pages={escapeJson(pagesData)}
217
+ site-name={siteName}
218
+ max-visible={headerNavMaxVisible}
219
+ home-default-view={homeDefaultView}
220
+ >
221
+ {/* SSR fallback: static preview until JS hydrates */}
222
+ <div class="border rounded-lg">
223
+ <p class="text-xs text-muted-foreground px-4 pt-3">
224
+ {t({
225
+ message: "Preview",
226
+ comment: "@context: Label for nav preview section",
227
+ })}
228
+ </p>
229
+ <div class="px-5 py-3">
230
+ <div class="site-header-top">
231
+ <a href="/" class="site-logo">
232
+ {siteName}
233
+ </a>
234
+ <div class="site-header-right">
235
+ {navItems.length > 0 && (
236
+ <nav class="site-header-nav">
237
+ {navItems.slice(0, headerNavMaxVisible).map((item) => (
238
+ <a
239
+ key={item.id}
240
+ href={item.url}
241
+ class="site-header-link"
242
+ >
243
+ {item.label}
244
+ </a>
245
+ ))}
246
+ {navItems.length > headerNavMaxVisible && (
247
+ <span class="text-muted-foreground">…</span>
248
+ )}
249
+ </nav>
250
+ )}
251
+ <span class="site-header-search" aria-hidden="true">
252
+ <svg
253
+ xmlns="http://www.w3.org/2000/svg"
254
+ width="16"
255
+ height="16"
256
+ viewBox="0 0 24 24"
257
+ fill="none"
258
+ stroke="currentColor"
259
+ stroke-width="2"
260
+ stroke-linecap="round"
261
+ stroke-linejoin="round"
262
+ >
263
+ <circle cx="11" cy="11" r="8" />
264
+ <path d="m21 21-4.35-4.35" />
265
+ </svg>
266
+ </span>
267
+ </div>
268
+ </div>
269
+ <nav class="site-browse-nav">
270
+ <span class="site-browse-link site-browse-link-active">
271
+ {homeDefaultView === "featured"
272
+ ? t({
273
+ message: "Featured",
274
+ comment: "@context: Browse filter label",
275
+ })
276
+ : t({
277
+ message: "Latest",
278
+ comment: "@context: Browse filter label",
279
+ })}
280
+ </span>
281
+ <span class="site-browse-sep" aria-hidden="true">
282
+ /
283
+ </span>
284
+ <span class="site-browse-link">
285
+ {homeDefaultView === "featured"
286
+ ? t({
287
+ message: "Latest",
288
+ comment: "@context: Browse filter label",
289
+ })
290
+ : t({
291
+ message: "Featured",
292
+ comment: "@context: Browse filter label",
293
+ })}
294
+ </span>
295
+ </nav>
296
+ </div>
297
+ </div>
298
+ </jant-nav-manager>
299
+ </div>
300
+ </>
301
+ );
302
+ }
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Pages list — page CRUD only
3
+ */
4
+
5
+ import { useLingui } from "@lingui/react/macro";
6
+ import type { Page } from "../../../types.js";
7
+ import { ListItemRow, ActionButtons, CrudPageHeader } from "../index.js";
8
+
9
+ export function PagesContent({ pages }: { pages: Page[] }) {
10
+ const { t } = useLingui();
11
+
12
+ return (
13
+ <>
14
+ <CrudPageHeader
15
+ title={t({
16
+ message: "Pages",
17
+ comment: "@context: Pages main heading",
18
+ })}
19
+ >
20
+ <a href="/dash/pages/new" class="btn">
21
+ {t({
22
+ message: "New Page",
23
+ comment: "@context: Button to create new page",
24
+ })}
25
+ </a>
26
+ </CrudPageHeader>
27
+
28
+ {pages.length === 0 ? (
29
+ <p class="text-sm text-muted-foreground py-4">
30
+ {t({
31
+ message: "No pages yet. Create your first page to get started.",
32
+ comment: "@context: Empty state for pages list",
33
+ })}
34
+ </p>
35
+ ) : (
36
+ <div class="flex flex-col divide-y">
37
+ {pages.map((page) => (
38
+ <ListItemRow
39
+ key={page.id}
40
+ actions={
41
+ <ActionButtons
42
+ editHref={`/dash/pages/${page.id}/edit`}
43
+ editLabel={t({
44
+ message: "Edit",
45
+ comment: "@context: Button to edit page",
46
+ })}
47
+ viewHref={
48
+ page.status !== "draft" ? `/${page.slug}` : undefined
49
+ }
50
+ viewLabel={t({
51
+ message: "View",
52
+ comment: "@context: Button to view page on public site",
53
+ })}
54
+ />
55
+ }
56
+ >
57
+ <a
58
+ href={`/dash/pages/${page.id}`}
59
+ class="font-medium hover:underline"
60
+ >
61
+ {page.title ||
62
+ t({
63
+ message: "Untitled",
64
+ comment: "@context: Default title for untitled page",
65
+ })}
66
+ </a>
67
+ <p class="text-sm text-muted-foreground mt-1">/{page.slug}</p>
68
+ </ListItemRow>
69
+ ))}
70
+ </div>
71
+ )}
72
+ </>
73
+ );
74
+ }