@jant/core 0.3.31 → 0.3.33

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 (95) hide show
  1. package/dist/client/client.css +1 -1
  2. package/dist/client/client.js +1442 -989
  3. package/dist/index.js +1429 -1055
  4. package/package.json +2 -2
  5. package/src/__tests__/helpers/app.ts +6 -3
  6. package/src/__tests__/helpers/db.ts +3 -0
  7. package/src/client.ts +2 -1
  8. package/src/db/migrations/0011_add_path_registry.sql +23 -0
  9. package/src/db/schema.ts +12 -1
  10. package/src/i18n/locales/en.po +225 -91
  11. package/src/i18n/locales/en.ts +1 -1
  12. package/src/i18n/locales/zh-Hans.po +201 -152
  13. package/src/i18n/locales/zh-Hans.ts +1 -1
  14. package/src/i18n/locales/zh-Hant.po +201 -152
  15. package/src/i18n/locales/zh-Hant.ts +1 -1
  16. package/src/lib/__tests__/excerpt.test.ts +25 -0
  17. package/src/lib/__tests__/resolve-config.test.ts +26 -2
  18. package/src/lib/__tests__/timeline.test.ts +2 -1
  19. package/src/lib/compose-bridge.ts +30 -1
  20. package/src/lib/excerpt.ts +16 -7
  21. package/src/lib/nav-manager-bridge.ts +54 -0
  22. package/src/lib/navigation.ts +7 -4
  23. package/src/lib/render.tsx +5 -2
  24. package/src/lib/resolve-config.ts +7 -0
  25. package/src/lib/view.ts +42 -10
  26. package/src/middleware/error-handler.ts +16 -0
  27. package/src/routes/api/__tests__/posts.test.ts +80 -0
  28. package/src/routes/api/__tests__/settings.test.ts +1 -1
  29. package/src/routes/api/posts.ts +6 -29
  30. package/src/routes/api/upload.ts +2 -14
  31. package/src/routes/auth/__tests__/setup.test.ts +2 -1
  32. package/src/routes/compose.tsx +13 -5
  33. package/src/routes/dash/__tests__/pages.test.ts +2 -1
  34. package/src/routes/dash/__tests__/settings-avatar.test.ts +151 -33
  35. package/src/routes/dash/appearance.tsx +71 -4
  36. package/src/routes/dash/collections.tsx +15 -21
  37. package/src/routes/dash/media.tsx +1 -13
  38. package/src/routes/dash/pages.tsx +5 -150
  39. package/src/routes/dash/posts.tsx +25 -32
  40. package/src/routes/dash/redirects.tsx +9 -11
  41. package/src/routes/dash/settings.tsx +29 -111
  42. package/src/routes/feed/__tests__/rss.test.ts +5 -1
  43. package/src/routes/pages/__tests__/collections.test.ts +2 -1
  44. package/src/routes/pages/__tests__/featured.test.ts +2 -1
  45. package/src/routes/pages/page.tsx +20 -25
  46. package/src/services/__tests__/collection.test.ts +2 -1
  47. package/src/services/__tests__/media.test.ts +78 -1
  48. package/src/services/__tests__/navigation.test.ts +2 -1
  49. package/src/services/__tests__/page.test.ts +78 -1
  50. package/src/services/__tests__/path-registry.test.ts +165 -0
  51. package/src/services/__tests__/post-timeline.test.ts +2 -1
  52. package/src/services/__tests__/post.test.ts +103 -1
  53. package/src/services/__tests__/redirect.test.ts +53 -4
  54. package/src/services/__tests__/search.test.ts +2 -1
  55. package/src/services/__tests__/settings.test.ts +153 -0
  56. package/src/services/index.ts +12 -4
  57. package/src/services/media.ts +72 -4
  58. package/src/services/page.ts +64 -17
  59. package/src/services/path-registry.ts +160 -0
  60. package/src/services/post.ts +119 -24
  61. package/src/services/redirect.ts +23 -3
  62. package/src/services/settings.ts +181 -0
  63. package/src/styles/components.css +135 -0
  64. package/src/styles/tokens.css +6 -1
  65. package/src/styles/ui.css +70 -26
  66. package/src/types/bindings.ts +1 -0
  67. package/src/types/config.ts +7 -2
  68. package/src/types/constants.ts +9 -1
  69. package/src/types/sortablejs.d.ts +8 -2
  70. package/src/types/views.ts +1 -1
  71. package/src/ui/color-themes.ts +31 -31
  72. package/src/ui/components/__tests__/jant-settings-avatar.test.ts +0 -3
  73. package/src/ui/components/__tests__/jant-settings-general.test.ts +2 -6
  74. package/src/ui/components/jant-compose-dialog.ts +3 -2
  75. package/src/ui/components/jant-compose-editor.ts +17 -2
  76. package/src/ui/components/jant-nav-manager.ts +1067 -0
  77. package/src/ui/components/jant-settings-general.ts +2 -35
  78. package/src/ui/components/nav-manager-types.ts +72 -0
  79. package/src/ui/components/settings-types.ts +0 -3
  80. package/src/ui/compose/ComposePrompt.tsx +3 -11
  81. package/src/ui/dash/appearance/AdvancedContent.tsx +0 -3
  82. package/src/ui/dash/appearance/AppearanceNav.tsx +12 -8
  83. package/src/ui/dash/appearance/ColorThemeContent.tsx +1 -4
  84. package/src/ui/dash/appearance/FontThemeContent.tsx +0 -3
  85. package/src/ui/dash/appearance/NavigationContent.tsx +302 -0
  86. package/src/ui/dash/pages/PagesContent.tsx +74 -0
  87. package/src/ui/dash/settings/AccountContent.tsx +0 -3
  88. package/src/ui/dash/settings/GeneralContent.tsx +1 -19
  89. package/src/ui/dash/settings/SettingsNav.tsx +2 -6
  90. package/src/ui/feed/NoteCard.tsx +2 -2
  91. package/src/ui/layouts/DashLayout.tsx +83 -86
  92. package/src/ui/layouts/SiteLayout.tsx +82 -21
  93. package/src/lib/nav-reorder.ts +0 -26
  94. package/src/ui/dash/pages/LinkFormContent.tsx +0 -119
  95. package/src/ui/dash/pages/UnifiedPagesContent.tsx +0 -203
@@ -12,9 +12,6 @@ export function AccountContent({ userName }: { userName: string }) {
12
12
 
13
13
  return (
14
14
  <>
15
- <h1 class="text-2xl font-semibold mb-2">
16
- {t({ message: "Settings", comment: "@context: Dashboard heading" })}
17
- </h1>
18
15
  <SettingsNav currentTab="account" />
19
16
 
20
17
  <div class="flex flex-col max-w-lg">
@@ -18,7 +18,6 @@ export function GeneralContent({
18
18
  siteName,
19
19
  siteDescription,
20
20
  siteLanguage,
21
- homeDefaultView,
22
21
  siteNameFallback,
23
22
  siteDescriptionFallback,
24
23
  siteAvatarUrl,
@@ -31,7 +30,6 @@ export function GeneralContent({
31
30
  siteName: string;
32
31
  siteDescription: string;
33
32
  siteLanguage: string;
34
- homeDefaultView: string;
35
33
  siteNameFallback: string;
36
34
  siteDescriptionFallback: string;
37
35
  siteAvatarUrl: string;
@@ -92,25 +90,13 @@ export function GeneralContent({
92
90
  }),
93
91
  aboutBlogHelp: t({
94
92
  message:
95
- "Displayed above your blog posts on the home page. Also used as the meta description. Markdown supported.",
93
+ "A short intro for search engines and feed readers. Plain text only.",
96
94
  comment: "@context: Help text for site description field",
97
95
  }),
98
96
  language: t({
99
97
  message: "Language",
100
98
  comment: "@context: Settings form field",
101
99
  }),
102
- defaultHomepageView: t({
103
- message: "Default Homepage View",
104
- comment: "@context: Settings form field",
105
- }),
106
- latest: t({
107
- message: "Latest",
108
- comment: "@context: Homepage view option - show latest posts",
109
- }),
110
- featured: t({
111
- message: "Featured",
112
- comment: "@context: Homepage view option - show featured posts",
113
- }),
114
100
  timeZone: t({
115
101
  message: "Time Zone",
116
102
  comment: "@context: Settings form field",
@@ -161,7 +147,6 @@ export function GeneralContent({
161
147
  siteName,
162
148
  siteDescription,
163
149
  siteLanguage,
164
- homeDefaultView,
165
150
  timeZone,
166
151
  siteFooter,
167
152
  noindex,
@@ -169,9 +154,6 @@ export function GeneralContent({
169
154
 
170
155
  return (
171
156
  <>
172
- <h1 class="text-2xl font-semibold mb-2">
173
- {t({ message: "Settings", comment: "@context: Dashboard heading" })}
174
- </h1>
175
157
  <SettingsNav currentTab="general" />
176
158
 
177
159
  <div class="flex flex-col max-w-lg">
@@ -37,16 +37,12 @@ export function SettingsNav({ currentTab }: { currentTab: SettingsTab }) {
37
37
  ];
38
38
 
39
39
  return (
40
- <nav class="flex gap-1 mb-6">
40
+ <nav class="dash-subnav">
41
41
  {tabs.map((tab) => (
42
42
  <a
43
43
  key={tab.id}
44
44
  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
- }`}
45
+ class={tab.id === currentTab ? "active" : ""}
50
46
  >
51
47
  {tab.label}
52
48
  </a>
@@ -42,10 +42,10 @@ export const NoteCard: FC<TimelineCardProps> = ({ post, compact }) => {
42
42
  )}
43
43
  {!compact && isArticle && post.summaryHasMore && (
44
44
  <a
45
- href={post.permalink}
45
+ href={`${post.permalink}#continue`}
46
46
  class="text-sm text-muted-foreground hover:underline mt-1 inline-block"
47
47
  >
48
- Read more
48
+ Continue
49
49
  </a>
50
50
  )}
51
51
  <footer class="mt-2" data-post-meta>
@@ -24,122 +24,119 @@ function DashLayoutContent({
24
24
  }: PropsWithChildren<Omit<DashLayoutProps, "c" | "title">>) {
25
25
  const { t } = useLingui();
26
26
 
27
- const isActive = (path: string, match?: RegExp) => {
28
- if (!currentPath) return false;
29
- if (match) return match.test(currentPath);
30
- return currentPath === path;
31
- };
32
-
33
- const navClass = (path: string, match?: RegExp) =>
34
- `justify-start px-3 py-2 text-sm rounded-md ${
35
- isActive(path, match)
36
- ? "bg-accent text-accent-foreground font-medium"
37
- : "text-muted-foreground hover:bg-accent hover:text-accent-foreground"
38
- }`;
27
+ const navClass = (match: RegExp) =>
28
+ `dash-header-link ${currentPath && match.test(currentPath) ? "dash-header-link-active" : ""}`;
39
29
 
40
30
  return (
41
31
  <div class="min-h-screen">
42
32
  {/* Header */}
43
- <header class="border-b bg-card">
44
- <div class="container-sidebar flex h-14 items-center justify-between">
45
- <a id="site-name" href="/dash" class="font-semibold">
46
- {siteName}
47
- </a>
48
- <nav class="flex items-center gap-4">
33
+ <header class="dash-header">
34
+ <div class="container dash-header-inner">
35
+ <div class="dash-header-left">
36
+ <a id="site-name" href="/dash" class="dash-header-logo">
37
+ {siteName}
38
+ </a>
49
39
  <a
50
40
  href="/"
51
- class="text-sm text-muted-foreground hover:text-foreground"
52
- >
53
- {t({
41
+ target="_blank"
42
+ rel="noopener noreferrer"
43
+ class="dash-header-site-link"
44
+ aria-label={t({
54
45
  message: "View Site",
55
46
  comment:
56
47
  "@context: Dashboard header link to view the public site",
57
48
  })}
58
- </a>
59
- <a
60
- href="/signout"
61
- class="text-sm text-muted-foreground hover:text-foreground"
62
49
  >
63
- {t({
64
- message: "Sign Out",
65
- comment: "@context: Dashboard header link to sign out",
66
- })}
50
+ <svg
51
+ xmlns="http://www.w3.org/2000/svg"
52
+ width="14"
53
+ height="14"
54
+ viewBox="0 0 24 24"
55
+ fill="none"
56
+ stroke="currentColor"
57
+ stroke-width="2"
58
+ stroke-linecap="round"
59
+ stroke-linejoin="round"
60
+ >
61
+ <path d="M15 3h6v6" />
62
+ <path d="M10 14 21 3" />
63
+ <path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" />
64
+ </svg>
67
65
  </a>
68
- </nav>
69
- </div>
70
- </header>
66
+ </div>
71
67
 
72
- {/* Sidebar + Main */}
73
- <div class="container-sidebar sidebar-layout py-8">
74
- {/* Sidebar */}
75
- <aside class="sidebar-nav">
76
- <nav class="flex flex-col gap-1">
77
- <a href="/dash" class={navClass("/dash", /^\/dash$/)}>
78
- {t({
79
- message: "Dashboard",
80
- comment: "@context: Dashboard navigation - main dashboard page",
81
- })}
82
- </a>
83
- <a
84
- href="/dash/posts"
85
- class={navClass("/dash/posts", /^\/dash\/posts/)}
86
- >
87
- {t({
88
- message: "Posts",
89
- comment: "@context: Dashboard navigation - posts management",
90
- })}
91
- </a>
92
- <a
93
- href="/dash/pages"
94
- class={navClass("/dash/pages", /^\/dash\/pages/)}
95
- >
68
+ <nav class="dash-header-nav">
69
+ <a href="/dash/pages" class={navClass(/^\/dash\/pages/)}>
96
70
  {t({
97
71
  message: "Pages",
98
72
  comment: "@context: Dashboard navigation - pages management",
99
73
  })}
100
74
  </a>
101
- <a
102
- href="/dash/media"
103
- class={navClass("/dash/media", /^\/dash\/media/)}
104
- >
105
- {t({
106
- message: "Media",
107
- comment: "@context: Dashboard navigation - media library",
108
- })}
109
- </a>
110
- <a
111
- href="/dash/collections"
112
- class={navClass("/dash/collections", /^\/dash\/collections/)}
113
- >
114
- {t({
115
- message: "Collections",
116
- comment:
117
- "@context: Dashboard navigation - collections management",
118
- })}
119
- </a>
120
- <a
121
- href="/dash/appearance"
122
- class={navClass("/dash/appearance", /^\/dash\/appearance/)}
123
- >
75
+ <a href="/dash/appearance" class={navClass(/^\/dash\/appearance/)}>
124
76
  {t({
125
77
  message: "Appearance",
126
78
  comment: "@context: Dashboard navigation - appearance settings",
127
79
  })}
128
80
  </a>
129
- <a
130
- href="/dash/settings"
131
- class={navClass("/dash/settings", /^\/dash\/settings/)}
132
- >
81
+ <a href="/dash/settings" class={navClass(/^\/dash\/settings/)}>
133
82
  {t({
134
83
  message: "Settings",
135
84
  comment: "@context: Dashboard navigation - site settings",
136
85
  })}
137
86
  </a>
138
87
  </nav>
139
- </aside>
140
88
 
141
- {/* Main content */}
142
- <main class="sidebar-main">{children}</main>
89
+ <div class="dropdown-menu">
90
+ <button
91
+ type="button"
92
+ id="dash-menu-trigger"
93
+ class="dash-header-menu-btn"
94
+ aria-haspopup="menu"
95
+ aria-controls="dash-menu"
96
+ aria-expanded="false"
97
+ aria-label={t({
98
+ message: "Menu",
99
+ comment: "@context: Dashboard header menu button",
100
+ })}
101
+ >
102
+ <svg
103
+ xmlns="http://www.w3.org/2000/svg"
104
+ width="16"
105
+ height="16"
106
+ viewBox="0 0 24 24"
107
+ fill="currentColor"
108
+ >
109
+ <circle cx="5" cy="12" r="2" />
110
+ <circle cx="12" cy="12" r="2" />
111
+ <circle cx="19" cy="12" r="2" />
112
+ </svg>
113
+ </button>
114
+ <div
115
+ id="dash-menu-popover"
116
+ data-popover
117
+ data-align="end"
118
+ aria-hidden="true"
119
+ >
120
+ <div
121
+ role="menu"
122
+ id="dash-menu"
123
+ aria-labelledby="dash-menu-trigger"
124
+ >
125
+ <a href="/signout" role="menuitem">
126
+ {t({
127
+ message: "Sign Out",
128
+ comment: "@context: Dashboard menu item to sign out",
129
+ })}
130
+ </a>
131
+ </div>
132
+ </div>
133
+ </div>
134
+ </div>
135
+ </header>
136
+
137
+ {/* Main */}
138
+ <div class="container py-8">
139
+ <main>{children}</main>
143
140
  </div>
144
141
  </div>
145
142
  );
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Site Layout
3
3
  *
4
- * Vertical header: site name on top, custom nav links below, description under nav.
4
+ * Vertical header: site name on top, custom nav links below.
5
5
  * Content area with browse filter tabs and compose prompt/dialog for authenticated users.
6
6
  */
7
7
 
@@ -27,12 +27,12 @@ function HeaderLink({ link }: { link: NavItemView }) {
27
27
 
28
28
  export const SiteLayout: FC<PropsWithChildren<SiteLayoutProps>> = ({
29
29
  siteName,
30
- siteDescription,
31
30
  links,
32
31
  currentPath,
33
32
  isAuthenticated,
34
33
  collections,
35
34
  homeDefaultView,
35
+ headerNavMaxVisible,
36
36
  siteAvatarUrl,
37
37
  showHeaderAvatar,
38
38
  siteFooterHtml,
@@ -40,6 +40,7 @@ export const SiteLayout: FC<PropsWithChildren<SiteLayoutProps>> = ({
40
40
  children,
41
41
  }) => {
42
42
  const { t } = useLingui();
43
+ const maxVisible = headerNavMaxVisible ?? 3;
43
44
 
44
45
  const latestHref = homeDefaultView === "featured" ? "/latest" : "/";
45
46
  const featuredHref = homeDefaultView === "featured" ? "/" : "/featured";
@@ -89,9 +90,66 @@ export const SiteLayout: FC<PropsWithChildren<SiteLayoutProps>> = ({
89
90
  <div class="site-header-right">
90
91
  {links.length > 0 && (
91
92
  <nav class="site-header-nav">
92
- {links.map((link) => (
93
+ {links.slice(0, maxVisible).map((link) => (
93
94
  <HeaderLink key={link.id} link={link} />
94
95
  ))}
96
+ {links.length > maxVisible && (
97
+ <div class="dropdown-menu site-header-more">
98
+ <button
99
+ type="button"
100
+ id="site-nav-more-trigger"
101
+ class="site-header-more-btn"
102
+ aria-haspopup="menu"
103
+ aria-controls="site-nav-more-menu"
104
+ aria-expanded="false"
105
+ aria-label={t({
106
+ message: "More links",
107
+ comment:
108
+ "@context: Button to show overflow nav links",
109
+ })}
110
+ >
111
+ <svg
112
+ xmlns="http://www.w3.org/2000/svg"
113
+ width="16"
114
+ height="16"
115
+ viewBox="0 0 24 24"
116
+ fill="currentColor"
117
+ >
118
+ <circle cx="5" cy="12" r="2" />
119
+ <circle cx="12" cy="12" r="2" />
120
+ <circle cx="19" cy="12" r="2" />
121
+ </svg>
122
+ </button>
123
+ <div
124
+ id="site-nav-more-popover"
125
+ data-popover
126
+ data-align="end"
127
+ aria-hidden="true"
128
+ >
129
+ <div
130
+ role="menu"
131
+ id="site-nav-more-menu"
132
+ aria-labelledby="site-nav-more-trigger"
133
+ >
134
+ {links.slice(maxVisible).map((link) => (
135
+ <a
136
+ key={link.id}
137
+ href={link.url}
138
+ role="menuitem"
139
+ {...(link.isExternal
140
+ ? {
141
+ target: "_blank",
142
+ rel: "noopener noreferrer",
143
+ }
144
+ : {})}
145
+ >
146
+ {link.label}
147
+ </a>
148
+ ))}
149
+ </div>
150
+ </div>
151
+ </div>
152
+ )}
95
153
  </nav>
96
154
  )}
97
155
  <a
@@ -117,9 +175,6 @@ export const SiteLayout: FC<PropsWithChildren<SiteLayoutProps>> = ({
117
175
  </a>
118
176
  </div>
119
177
  </div>
120
- {isHomePage && siteDescription && (
121
- <p class="site-description">{siteDescription}</p>
122
- )}
123
178
  </div>
124
179
  </header>
125
180
 
@@ -137,22 +192,28 @@ export const SiteLayout: FC<PropsWithChildren<SiteLayoutProps>> = ({
137
192
  <div class="site-container">
138
193
  <div class="site-content">
139
194
  {isHomePage && (
140
- <nav class="site-browse-nav">
141
- {browseLinks.map((link, i) => (
142
- <>
143
- {i > 0 && <span class="site-browse-sep">/</span>}
144
- <a
145
- key={link.href}
146
- href={link.href}
147
- class={`site-browse-link ${currentPath === link.href ? "site-browse-link-active" : ""}`}
148
- >
149
- {link.label}
150
- </a>
151
- </>
152
- ))}
153
- </nav>
195
+ <div class="site-home-header">
196
+ {isAuthenticated && <ComposePrompt />}
197
+ <nav class="site-browse-nav">
198
+ {browseLinks.map((link, i) => (
199
+ <>
200
+ {i > 0 && (
201
+ <span class="site-browse-sep" aria-hidden="true">
202
+ /
203
+ </span>
204
+ )}
205
+ <a
206
+ key={link.href}
207
+ href={link.href}
208
+ class={`site-browse-link ${currentPath === link.href ? "site-browse-link-active" : ""}`}
209
+ >
210
+ {link.label}
211
+ </a>
212
+ </>
213
+ ))}
214
+ </nav>
215
+ </div>
154
216
  )}
155
- {isHomePage && isAuthenticated && <ComposePrompt />}
156
217
  {children}
157
218
  </div>
158
219
  </div>
@@ -1,26 +0,0 @@
1
- /**
2
- * Navigation Link Reorder
3
- *
4
- * Initializes SortableJS on the navigation links list in the dashboard.
5
- * Auto-detects the list element and only activates when present.
6
- */
7
-
8
- import Sortable from "sortablejs";
9
-
10
- const list = document.getElementById("nav-links-list");
11
- if (list) {
12
- Sortable.create(list, {
13
- animation: 150,
14
- handle: "[data-id]",
15
- onEnd() {
16
- const ids = [...list.querySelectorAll<HTMLElement>("[data-id]")].map(
17
- (el) => Number(el.dataset.id),
18
- );
19
- fetch("/dash/pages/reorder", {
20
- method: "POST",
21
- headers: { "Content-Type": "application/json" },
22
- body: JSON.stringify({ ids }),
23
- });
24
- },
25
- });
26
- }
@@ -1,119 +0,0 @@
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
- }