@jant/core 0.3.36 → 0.3.37
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.
- package/bin/commands/export.js +1 -1
- package/bin/commands/import-site.js +529 -0
- package/bin/commands/reset-password.js +3 -2
- package/dist/client/assets/heic-to-DIRPI3VF.js +1 -0
- package/dist/client/assets/url-FWFqPJPb.js +1 -0
- package/dist/client/client.css +1 -1
- package/dist/client/client.js +4012 -3276
- package/dist/index.js +10285 -5809
- package/package.json +11 -3
- package/src/__tests__/helpers/app.ts +9 -9
- package/src/__tests__/helpers/db.ts +91 -93
- package/src/app.tsx +157 -27
- package/src/auth.ts +20 -2
- package/src/client/archive-nav.js +187 -0
- package/src/client/audio-player.ts +478 -0
- package/src/client/audio-processor.ts +84 -0
- package/src/client/avatar-upload.ts +3 -2
- package/src/client/components/__tests__/jant-compose-dialog.test.ts +645 -49
- package/src/client/components/__tests__/jant-compose-editor.test.ts +208 -16
- package/src/client/components/__tests__/jant-post-form.test.ts +19 -9
- package/src/client/components/__tests__/jant-settings-avatar.test.ts +2 -2
- package/src/client/components/__tests__/jant-settings-general.test.ts +3 -3
- package/src/client/components/collection-sidebar-types.ts +7 -9
- package/src/client/components/compose-types.ts +101 -4
- package/src/client/components/jant-collection-form.ts +43 -7
- package/src/client/components/jant-collection-sidebar.ts +88 -84
- package/src/client/components/jant-compose-dialog.ts +1655 -219
- package/src/client/components/jant-compose-editor.ts +732 -168
- package/src/client/components/jant-compose-fullscreen.ts +23 -78
- package/src/client/components/jant-media-lightbox.ts +2 -0
- package/src/client/components/jant-nav-manager.ts +24 -284
- package/src/client/components/jant-post-form.ts +89 -9
- package/src/client/components/jant-post-menu.ts +1019 -0
- package/src/client/components/jant-settings-avatar.ts +3 -3
- package/src/client/components/jant-settings-general.ts +38 -4
- package/src/client/components/jant-text-preview.ts +232 -0
- package/src/client/components/nav-manager-types.ts +4 -19
- package/src/client/components/post-form-template.ts +107 -12
- package/src/client/components/post-form-types.ts +11 -4
- package/src/client/compose-bridge.ts +410 -109
- package/src/client/image-processor.ts +26 -8
- package/src/client/media-metadata.ts +247 -0
- package/src/client/multipart-upload.ts +160 -0
- package/src/client/post-form-bridge.ts +52 -1
- package/src/client/settings-bridge.ts +0 -12
- package/src/client/thread-context.ts +140 -0
- package/src/client/tiptap/create-editor.ts +46 -0
- package/src/client/tiptap/extensions.ts +5 -0
- package/src/client/tiptap/image-node.ts +2 -8
- package/src/client/tiptap/paste-image.ts +2 -13
- package/src/client/tiptap/slash-commands.ts +173 -63
- package/src/client/toast.ts +101 -3
- package/src/client/types/sortablejs.d.ts +15 -0
- package/src/client/upload-with-metadata.ts +54 -0
- package/src/client/video-processor.ts +207 -0
- package/src/client.ts +5 -2
- package/src/db/__tests__/migrations.test.ts +118 -0
- package/src/db/index.ts +52 -0
- package/src/db/migrations/0000_baseline.sql +269 -0
- package/src/db/migrations/0001_fts_setup.sql +31 -0
- package/src/db/migrations/meta/0000_snapshot.json +703 -119
- package/src/db/migrations/meta/0001_snapshot.json +1337 -0
- package/src/db/migrations/meta/_journal.json +4 -39
- package/src/db/schema.ts +409 -145
- package/src/i18n/__tests__/detect.test.ts +115 -0
- package/src/i18n/context.tsx +2 -2
- package/src/i18n/detect.ts +85 -1
- package/src/i18n/i18n.ts +1 -1
- package/src/i18n/index.ts +2 -1
- package/src/i18n/locales/en.po +487 -1217
- package/src/i18n/locales/en.ts +1 -1
- package/src/i18n/locales/zh-Hans.po +613 -996
- package/src/i18n/locales/zh-Hans.ts +1 -1
- package/src/i18n/locales/zh-Hant.po +624 -1007
- package/src/i18n/locales/zh-Hant.ts +1 -1
- package/src/i18n/middleware.ts +6 -0
- package/src/index.ts +5 -7
- package/src/lib/__tests__/blurhash-placeholder.test.ts +75 -0
- package/src/lib/__tests__/constants.test.ts +0 -1
- package/src/lib/__tests__/markdown-to-tiptap.test.ts +358 -0
- package/src/lib/__tests__/nanoid.test.ts +26 -0
- package/src/lib/__tests__/schemas.test.ts +181 -63
- package/src/lib/__tests__/slug.test.ts +126 -0
- package/src/lib/__tests__/sse.test.ts +6 -6
- package/src/lib/__tests__/summary.test.ts +264 -0
- package/src/lib/__tests__/theme.test.ts +1 -1
- package/src/lib/__tests__/timeline.test.ts +33 -30
- package/src/lib/__tests__/tiptap-to-markdown.test.ts +346 -0
- package/src/lib/__tests__/view.test.ts +141 -66
- package/src/lib/blurhash-placeholder.ts +102 -0
- package/src/lib/constants.ts +3 -1
- package/src/lib/emoji-catalog.ts +885 -68
- package/src/lib/errors.ts +11 -8
- package/src/lib/feed.ts +78 -32
- package/src/lib/html.ts +2 -1
- package/src/lib/icon-catalog.ts +5033 -1
- package/src/lib/icons.ts +3 -2
- package/src/lib/index.ts +0 -1
- package/src/lib/markdown-to-tiptap.ts +286 -0
- package/src/lib/media-helpers.ts +12 -3
- package/src/lib/nanoid.ts +29 -0
- package/src/lib/navigation.ts +1 -1
- package/src/lib/render.tsx +20 -2
- package/src/lib/resolve-config.ts +6 -2
- package/src/lib/schemas.ts +224 -55
- package/src/lib/search-snippet.ts +34 -0
- package/src/lib/slug.ts +96 -0
- package/src/lib/sse.ts +6 -6
- package/src/lib/storage.ts +115 -7
- package/src/lib/summary.ts +66 -0
- package/src/lib/theme.ts +11 -8
- package/src/lib/timeline.ts +74 -34
- package/src/lib/tiptap-render.ts +5 -10
- package/src/lib/tiptap-to-markdown.ts +305 -0
- package/src/lib/upload.ts +190 -29
- package/src/lib/url.ts +31 -0
- package/src/lib/view.ts +204 -37
- package/src/middleware/__tests__/auth.test.ts +191 -11
- package/src/middleware/__tests__/onboarding.test.ts +12 -10
- package/src/middleware/auth.ts +63 -9
- package/src/middleware/onboarding.ts +1 -1
- package/src/middleware/secure-headers.ts +40 -0
- package/src/preset.css +45 -2
- package/src/routes/__tests__/compose.test.ts +17 -24
- package/src/routes/api/__tests__/collections.test.ts +109 -61
- package/src/routes/api/__tests__/nav-items.test.ts +46 -29
- package/src/routes/api/__tests__/posts.test.ts +132 -68
- package/src/routes/api/__tests__/search.test.ts +15 -2
- package/src/routes/api/__tests__/upload-multipart.test.ts +534 -0
- package/src/routes/api/collections.ts +51 -42
- package/src/routes/api/custom-urls.ts +80 -0
- package/src/routes/api/export.ts +31 -0
- package/src/routes/api/nav-items.ts +23 -19
- package/src/routes/api/posts.ts +43 -39
- package/src/routes/api/search.ts +3 -4
- package/src/routes/api/upload-multipart.ts +245 -0
- package/src/routes/api/upload.ts +85 -19
- package/src/routes/auth/__tests__/setup.test.ts +20 -60
- package/src/routes/auth/setup.tsx +26 -33
- package/src/routes/auth/signin.tsx +3 -7
- package/src/routes/compose.tsx +10 -55
- package/src/routes/dash/__tests__/settings-avatar.test.ts +1 -1
- package/src/routes/dash/custom-urls.tsx +414 -0
- package/src/routes/dash/settings.tsx +304 -232
- package/src/routes/feed/__tests__/rss.test.ts +27 -28
- package/src/routes/feed/rss.ts +6 -4
- package/src/routes/feed/sitemap.ts +2 -12
- package/src/routes/pages/__tests__/collections.test.ts +5 -6
- package/src/routes/pages/__tests__/featured.test.ts +41 -22
- package/src/routes/pages/archive.tsx +175 -39
- package/src/routes/pages/collection.tsx +22 -10
- package/src/routes/pages/collections.tsx +3 -3
- package/src/routes/pages/featured.tsx +28 -4
- package/src/routes/pages/home.tsx +16 -15
- package/src/routes/pages/latest.tsx +1 -11
- package/src/routes/pages/new.tsx +39 -0
- package/src/routes/pages/page.tsx +95 -49
- package/src/routes/pages/search.tsx +1 -1
- package/src/services/__tests__/api-token.test.ts +135 -0
- package/src/services/__tests__/collection.test.ts +275 -227
- package/src/services/__tests__/custom-url.test.ts +213 -0
- package/src/services/__tests__/media.test.ts +162 -22
- package/src/services/__tests__/navigation.test.ts +109 -68
- package/src/services/__tests__/post-timeline.test.ts +205 -32
- package/src/services/__tests__/post.test.ts +713 -234
- package/src/services/__tests__/search.test.ts +67 -10
- package/src/services/api-token.ts +166 -0
- package/src/services/auth.ts +17 -2
- package/src/services/collection.ts +397 -131
- package/src/services/custom-url.ts +188 -0
- package/src/services/export.ts +802 -0
- package/src/services/index.ts +26 -19
- package/src/services/media.ts +100 -22
- package/src/services/navigation.ts +158 -47
- package/src/services/path.ts +339 -0
- package/src/services/post.ts +687 -154
- package/src/services/search.ts +160 -75
- package/src/styles/components.css +58 -7
- package/src/styles/tokens.css +84 -6
- package/src/styles/ui.css +2964 -457
- package/src/types/bindings.ts +4 -1
- package/src/types/config.ts +12 -4
- package/src/types/constants.ts +15 -3
- package/src/types/entities.ts +74 -35
- package/src/types/operations.ts +11 -24
- package/src/types/props.ts +51 -16
- package/src/types/views.ts +45 -22
- package/src/ui/color-themes.ts +133 -23
- package/src/ui/compose/ComposeDialog.tsx +239 -17
- package/src/ui/compose/ComposePrompt.tsx +1 -1
- package/src/ui/dash/CrudPageHeader.tsx +1 -1
- package/src/ui/dash/ListItemRow.tsx +1 -1
- package/src/ui/dash/StatusBadge.tsx +3 -1
- package/src/ui/dash/appearance/AdvancedContent.tsx +22 -1
- package/src/ui/dash/appearance/ColorThemeContent.tsx +22 -2
- package/src/ui/dash/appearance/FontThemeContent.tsx +1 -1
- package/src/ui/dash/appearance/NavigationContent.tsx +5 -45
- package/src/ui/dash/index.ts +0 -3
- package/src/ui/dash/settings/AccountContent.tsx +3 -57
- package/src/ui/dash/settings/AccountMenuContent.tsx +147 -0
- package/src/ui/dash/settings/ApiTokensContent.tsx +232 -0
- package/src/ui/dash/settings/AvatarContent.tsx +8 -0
- package/src/ui/dash/settings/SessionsContent.tsx +159 -0
- package/src/ui/dash/settings/SettingsRootContent.tsx +45 -15
- package/src/ui/feed/LinkCard.tsx +89 -40
- package/src/ui/feed/NoteCard.tsx +39 -25
- package/src/ui/feed/PostStatusBadges.tsx +67 -0
- package/src/ui/feed/QuoteCard.tsx +38 -23
- package/src/ui/feed/ThreadPreview.tsx +90 -26
- package/src/ui/feed/TimelineFeed.tsx +3 -2
- package/src/ui/feed/TimelineItem.tsx +15 -6
- package/src/ui/feed/__tests__/thread-preview.test.ts +107 -0
- package/src/ui/feed/thread-preview-state.ts +61 -0
- package/src/ui/font-themes.ts +2 -2
- package/src/ui/layouts/BaseLayout.tsx +2 -2
- package/src/ui/layouts/SiteLayout.tsx +105 -92
- package/src/ui/pages/ArchivePage.tsx +923 -98
- package/src/ui/pages/ComposePage.tsx +54 -0
- package/src/ui/pages/PostPage.tsx +30 -45
- package/src/ui/pages/SearchPage.tsx +181 -37
- package/src/ui/shared/AdminBreadcrumb.tsx +29 -0
- package/src/ui/shared/CollectionsSidebar.tsx +47 -37
- package/src/ui/shared/MediaGallery.tsx +445 -149
- package/src/ui/shared/PostFooter.tsx +204 -0
- package/src/ui/shared/StarRating.tsx +27 -0
- package/src/ui/shared/__tests__/format-chars.test.ts +35 -0
- package/src/ui/shared/index.ts +0 -1
- package/dist/client/assets/url-8Dj-5CLW.js +0 -1
- package/src/client/media-upload.ts +0 -161
- package/src/client/page-slug-bridge.ts +0 -42
- package/src/db/migrations/0000_square_wallflower.sql +0 -118
- package/src/db/migrations/0001_add_search_fts.sql +0 -34
- package/src/db/migrations/0002_add_media_attachments.sql +0 -3
- package/src/db/migrations/0003_add_navigation_links.sql +0 -8
- package/src/db/migrations/0004_add_storage_provider.sql +0 -3
- package/src/db/migrations/0005_v2_schema_migration.sql +0 -268
- package/src/db/migrations/0006_rename_slug_to_path.sql +0 -5
- package/src/db/migrations/0007_post_collections_m2m.sql +0 -94
- package/src/db/migrations/0008_add_collection_dividers.sql +0 -8
- package/src/db/migrations/0009_drop_collection_show_divider.sql +0 -2
- package/src/db/migrations/0010_add_performance_indexes.sql +0 -16
- package/src/db/migrations/0011_add_path_registry.sql +0 -23
- package/src/db/migrations/0012_add_tiptap_columns.sql +0 -2
- package/src/db/migrations/0013_replace_featured_with_visibility.sql +0 -8
- package/src/db/migrations/meta/0003_snapshot.json +0 -821
- package/src/lib/__tests__/sqid.test.ts +0 -65
- package/src/lib/sqid.ts +0 -79
- package/src/routes/api/__tests__/pages.test.ts +0 -218
- package/src/routes/api/pages.ts +0 -73
- package/src/routes/dash/__tests__/pages.test.ts +0 -226
- package/src/routes/dash/index.tsx +0 -109
- package/src/routes/dash/media.tsx +0 -135
- package/src/routes/dash/pages.tsx +0 -245
- package/src/routes/dash/posts.tsx +0 -338
- package/src/routes/dash/redirects.tsx +0 -263
- package/src/routes/pages/post.tsx +0 -59
- package/src/services/__tests__/page.test.ts +0 -298
- package/src/services/__tests__/path-registry.test.ts +0 -165
- package/src/services/__tests__/redirect.test.ts +0 -159
- package/src/services/page.ts +0 -216
- package/src/services/path-registry.ts +0 -160
- package/src/services/redirect.ts +0 -97
- package/src/ui/dash/PageForm.tsx +0 -187
- package/src/ui/dash/PostList.tsx +0 -95
- package/src/ui/dash/media/MediaListContent.tsx +0 -206
- package/src/ui/dash/media/ViewMediaContent.tsx +0 -208
- package/src/ui/dash/pages/PagesContent.tsx +0 -75
- package/src/ui/dash/posts/PostForm.tsx +0 -260
- package/src/ui/layouts/DashLayout.tsx +0 -247
- package/src/ui/pages/SinglePage.tsx +0 -23
- package/src/ui/shared/ThreadView.tsx +0 -136
|
@@ -88,7 +88,7 @@ export function ColorThemeContent({
|
|
|
88
88
|
return (
|
|
89
89
|
<div
|
|
90
90
|
data-signals={themeSignals}
|
|
91
|
-
data-on:change="@post('/
|
|
91
|
+
data-on:change="@post('/settings/color-theme')"
|
|
92
92
|
class="max-w-3xl"
|
|
93
93
|
>
|
|
94
94
|
<fieldset>
|
|
@@ -101,8 +101,28 @@ export function ColorThemeContent({
|
|
|
101
101
|
<p class="text-sm text-muted-foreground mb-4">
|
|
102
102
|
{t({
|
|
103
103
|
message:
|
|
104
|
-
"
|
|
104
|
+
"Applies to your entire site, including admin pages. All themes support dark mode.",
|
|
105
105
|
comment: "@context: Appearance settings description",
|
|
106
|
+
})}{" "}
|
|
107
|
+
{t({
|
|
108
|
+
message: "Want more control?",
|
|
109
|
+
comment:
|
|
110
|
+
"@context: Prefix before Custom CSS link on color theme page",
|
|
111
|
+
})}{" "}
|
|
112
|
+
<a
|
|
113
|
+
href="/settings/custom-css"
|
|
114
|
+
class="underline hover:text-foreground transition-colors"
|
|
115
|
+
>
|
|
116
|
+
{t({
|
|
117
|
+
message: "Custom CSS",
|
|
118
|
+
comment:
|
|
119
|
+
"@context: Link to Custom CSS settings from color theme page",
|
|
120
|
+
})}
|
|
121
|
+
</a>{" "}
|
|
122
|
+
{t({
|
|
123
|
+
message: "lets you override any theme variable.",
|
|
124
|
+
comment:
|
|
125
|
+
"@context: Suffix after Custom CSS link on color theme page",
|
|
106
126
|
})}
|
|
107
127
|
</p>
|
|
108
128
|
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { useLingui } from "@lingui/react/macro";
|
|
6
|
-
import type { NavItem,
|
|
6
|
+
import type { NavItem, SystemNavKey } from "../../../types.js";
|
|
7
7
|
import { SYSTEM_NAV_KEYS } from "../../../types.js";
|
|
8
8
|
import type {
|
|
9
9
|
NavManagerLabels,
|
|
@@ -15,7 +15,7 @@ import type {
|
|
|
15
15
|
|
|
16
16
|
const SYSTEM_DESCRIPTIONS: Record<SystemNavKey, string> = {
|
|
17
17
|
rss: "Add a link to your RSS feed",
|
|
18
|
-
|
|
18
|
+
settings: "Shows 'Settings' when logged in, 'Sign in' when logged out",
|
|
19
19
|
collections: "Link to your collections page",
|
|
20
20
|
archive: "Link to the post archive",
|
|
21
21
|
};
|
|
@@ -26,13 +26,11 @@ const SYSTEM_DESCRIPTIONS: Record<SystemNavKey, string> = {
|
|
|
26
26
|
|
|
27
27
|
export function NavigationContent({
|
|
28
28
|
navItems,
|
|
29
|
-
availablePages,
|
|
30
29
|
headerNavMaxVisible,
|
|
31
30
|
homeDefaultView,
|
|
32
31
|
siteName,
|
|
33
32
|
}: {
|
|
34
33
|
navItems: NavItem[];
|
|
35
|
-
availablePages: Page[];
|
|
36
34
|
headerNavMaxVisible: number;
|
|
37
35
|
homeDefaultView: string;
|
|
38
36
|
siteName: string;
|
|
@@ -45,7 +43,6 @@ export function NavigationContent({
|
|
|
45
43
|
type: item.type,
|
|
46
44
|
label: item.label,
|
|
47
45
|
url: item.url,
|
|
48
|
-
pageId: item.pageId,
|
|
49
46
|
}));
|
|
50
47
|
|
|
51
48
|
// Build system nav config array for the Lit component
|
|
@@ -58,13 +55,6 @@ export function NavigationContent({
|
|
|
58
55
|
description: SYSTEM_DESCRIPTIONS[key],
|
|
59
56
|
}));
|
|
60
57
|
|
|
61
|
-
// Serialize available pages for the Lit component
|
|
62
|
-
const pagesData = availablePages.map((page) => ({
|
|
63
|
-
id: page.id,
|
|
64
|
-
title: page.title,
|
|
65
|
-
slug: page.slug,
|
|
66
|
-
}));
|
|
67
|
-
|
|
68
58
|
const labels: NavManagerLabels = {
|
|
69
59
|
preview: t({
|
|
70
60
|
message: "Navigation Preview",
|
|
@@ -76,10 +66,9 @@ export function NavigationContent({
|
|
|
76
66
|
}),
|
|
77
67
|
emptyState: t({
|
|
78
68
|
message:
|
|
79
|
-
"No navigation items yet. Add
|
|
69
|
+
"No navigation items yet. Add links or enable system items below.",
|
|
80
70
|
comment: "@context: Empty state for navigation items",
|
|
81
71
|
}),
|
|
82
|
-
page: t({ message: "page", comment: "@context: Nav item type badge" }),
|
|
83
72
|
link: t({ message: "link", comment: "@context: Nav item type badge" }),
|
|
84
73
|
system: t({
|
|
85
74
|
message: "system",
|
|
@@ -102,13 +91,9 @@ export function NavigationContent({
|
|
|
102
91
|
message: "Delete",
|
|
103
92
|
comment: "@context: Delete nav item",
|
|
104
93
|
}),
|
|
105
|
-
editPage: t({
|
|
106
|
-
message: "Edit Page",
|
|
107
|
-
comment: "@context: Link to edit the page",
|
|
108
|
-
}),
|
|
109
94
|
remove: t({
|
|
110
95
|
message: "Remove",
|
|
111
|
-
comment: "@context: Remove
|
|
96
|
+
comment: "@context: Remove system item from navigation",
|
|
112
97
|
}),
|
|
113
98
|
orderSaved: t({
|
|
114
99
|
message: "Order saved",
|
|
@@ -132,29 +117,13 @@ export function NavigationContent({
|
|
|
132
117
|
}),
|
|
133
118
|
systemLinksDescription: t({
|
|
134
119
|
message:
|
|
135
|
-
"Toggle built-in navigation items. Enabled items appear in your navigation alongside
|
|
120
|
+
"Toggle built-in navigation items. Enabled items appear in your navigation alongside links.",
|
|
136
121
|
comment: "@context: Description for system nav toggles",
|
|
137
122
|
}),
|
|
138
|
-
addPageToNavigation: t({
|
|
139
|
-
message: "Add page to navigation",
|
|
140
|
-
comment: "@context: Section heading for adding page to nav",
|
|
141
|
-
}),
|
|
142
123
|
addCustomLinkToNavigation: t({
|
|
143
124
|
message: "Add custom link to navigation",
|
|
144
125
|
comment: "@context: Section heading for adding custom link to nav",
|
|
145
126
|
}),
|
|
146
|
-
choosePage: t({
|
|
147
|
-
message: "Choose a page…",
|
|
148
|
-
comment: "@context: Placeholder for page select combobox trigger",
|
|
149
|
-
}),
|
|
150
|
-
searchPages: t({
|
|
151
|
-
message: "Search pages…",
|
|
152
|
-
comment: "@context: Placeholder for page search input in combobox",
|
|
153
|
-
}),
|
|
154
|
-
noPagesFound: t({
|
|
155
|
-
message: "No matching pages.",
|
|
156
|
-
comment: "@context: Empty state when page search has no results",
|
|
157
|
-
}),
|
|
158
127
|
addLink: t({
|
|
159
128
|
message: "Add Link",
|
|
160
129
|
comment: "@context: Button and heading for adding custom link",
|
|
@@ -163,14 +132,6 @@ export function NavigationContent({
|
|
|
163
132
|
message: "Add a custom link to any URL",
|
|
164
133
|
comment: "@context: Description in link popover form",
|
|
165
134
|
}),
|
|
166
|
-
allPagesInNav: t({
|
|
167
|
-
message: "All pages are already in navigation.",
|
|
168
|
-
comment: "@context: Message when no pages available to add",
|
|
169
|
-
}),
|
|
170
|
-
createPage: t({
|
|
171
|
-
message: "Create page",
|
|
172
|
-
comment: "@context: Link at bottom of page combobox to create a new page",
|
|
173
|
-
}),
|
|
174
135
|
urlPlaceholder: "/archive or https://...",
|
|
175
136
|
maxVisibleLinks: t({
|
|
176
137
|
message: "Links shown in header",
|
|
@@ -222,7 +183,6 @@ export function NavigationContent({
|
|
|
222
183
|
items={escapeJson(itemsData)}
|
|
223
184
|
labels={escapeJson(labels)}
|
|
224
185
|
system-nav-items={escapeJson(systemNavData)}
|
|
225
|
-
available-pages={escapeJson(pagesData)}
|
|
226
186
|
site-name={siteName}
|
|
227
187
|
max-visible={headerNavMaxVisible}
|
|
228
188
|
home-default-view={homeDefaultView}
|
package/src/ui/dash/index.ts
CHANGED
|
@@ -4,7 +4,4 @@ export { DangerZone, type DangerZoneProps } from "./DangerZone.js";
|
|
|
4
4
|
export { EmptyState, type EmptyStateProps } from "../shared/EmptyState.js";
|
|
5
5
|
export { FormatBadge, type FormatBadgeProps } from "./FormatBadge.js";
|
|
6
6
|
export { ListItemRow, type ListItemRowProps } from "./ListItemRow.js";
|
|
7
|
-
export { PageForm, type PageFormProps } from "./PageForm.js";
|
|
8
|
-
export { PostForm, type PostFormProps } from "./posts/PostForm.js";
|
|
9
|
-
export { PostList, type PostListProps } from "./PostList.js";
|
|
10
7
|
export { StatusBadge, type StatusBadgeProps } from "./StatusBadge.js";
|
|
@@ -1,71 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Password settings: change sign-in password
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { useLingui } from "@lingui/react/macro";
|
|
6
6
|
|
|
7
|
-
export function AccountContent(
|
|
7
|
+
export function AccountContent() {
|
|
8
8
|
const { t } = useLingui();
|
|
9
9
|
|
|
10
|
-
const profileSignals = JSON.stringify({ userName }).replace(/</g, "\\u003c");
|
|
11
|
-
|
|
12
10
|
return (
|
|
13
11
|
<div class="flex flex-col max-w-lg">
|
|
14
|
-
<form
|
|
15
|
-
data-signals={profileSignals}
|
|
16
|
-
data-on:submit__prevent="@post('/dash/settings/account')"
|
|
17
|
-
data-indicator="_profileLoading"
|
|
18
|
-
>
|
|
19
|
-
<h2 class="text-lg font-semibold mb-4">
|
|
20
|
-
{t({
|
|
21
|
-
message: "Profile",
|
|
22
|
-
comment: "@context: Account settings section heading",
|
|
23
|
-
})}
|
|
24
|
-
</h2>
|
|
25
|
-
<div class="flex flex-col gap-4">
|
|
26
|
-
<div class="field">
|
|
27
|
-
<label class="label">
|
|
28
|
-
{t({
|
|
29
|
-
message: "Name",
|
|
30
|
-
comment: "@context: Account settings form field",
|
|
31
|
-
})}
|
|
32
|
-
</label>
|
|
33
|
-
<input type="text" data-bind="userName" class="input" required />
|
|
34
|
-
</div>
|
|
35
|
-
</div>
|
|
36
|
-
|
|
37
|
-
<button
|
|
38
|
-
type="submit"
|
|
39
|
-
class="btn mt-4"
|
|
40
|
-
data-attr:disabled="$_profileLoading"
|
|
41
|
-
>
|
|
42
|
-
<svg
|
|
43
|
-
data-show="$_profileLoading"
|
|
44
|
-
style="display:none"
|
|
45
|
-
class="animate-spin size-4"
|
|
46
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
47
|
-
viewBox="0 0 24 24"
|
|
48
|
-
fill="none"
|
|
49
|
-
stroke="currentColor"
|
|
50
|
-
stroke-width="2"
|
|
51
|
-
stroke-linecap="round"
|
|
52
|
-
stroke-linejoin="round"
|
|
53
|
-
role="status"
|
|
54
|
-
>
|
|
55
|
-
<path d="M21 12a9 9 0 1 1-6.219-8.56" />
|
|
56
|
-
</svg>
|
|
57
|
-
{t({
|
|
58
|
-
message: "Save Profile",
|
|
59
|
-
comment: "@context: Button to save profile",
|
|
60
|
-
})}
|
|
61
|
-
</button>
|
|
62
|
-
</form>
|
|
63
|
-
|
|
64
|
-
<hr class="my-8" />
|
|
65
|
-
|
|
66
12
|
<form
|
|
67
13
|
data-signals="{currentPassword: '', newPassword: '', confirmPassword: ''}"
|
|
68
|
-
data-on:submit__prevent="@post('/
|
|
14
|
+
data-on:submit__prevent="@post('/settings/account/password')"
|
|
69
15
|
data-indicator="_passwordLoading"
|
|
70
16
|
>
|
|
71
17
|
<h2 class="text-lg font-semibold mb-4">
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Account settings sub-menu — lists Sessions and Password options
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { useLingui } from "@lingui/react/macro";
|
|
6
|
+
|
|
7
|
+
/** Chevron right icon */
|
|
8
|
+
function ChevronRight() {
|
|
9
|
+
return (
|
|
10
|
+
<svg
|
|
11
|
+
class="settings-item-chevron"
|
|
12
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
13
|
+
width="16"
|
|
14
|
+
height="16"
|
|
15
|
+
viewBox="0 0 24 24"
|
|
16
|
+
fill="none"
|
|
17
|
+
stroke="currentColor"
|
|
18
|
+
stroke-width="2"
|
|
19
|
+
stroke-linecap="round"
|
|
20
|
+
stroke-linejoin="round"
|
|
21
|
+
>
|
|
22
|
+
<path d="m9 18 6-6-6-6" />
|
|
23
|
+
</svg>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function AccountMenuItem({
|
|
28
|
+
href,
|
|
29
|
+
icon,
|
|
30
|
+
color,
|
|
31
|
+
name,
|
|
32
|
+
description,
|
|
33
|
+
}: {
|
|
34
|
+
href: string;
|
|
35
|
+
icon: string;
|
|
36
|
+
color: string;
|
|
37
|
+
name: string;
|
|
38
|
+
description: string;
|
|
39
|
+
}) {
|
|
40
|
+
return (
|
|
41
|
+
<a href={href} class="settings-item">
|
|
42
|
+
<span class="settings-item-icon" style={`background-color:${color}`}>
|
|
43
|
+
<span dangerouslySetInnerHTML={{ __html: icon }} />
|
|
44
|
+
</span>
|
|
45
|
+
<span class="settings-item-text">
|
|
46
|
+
<span class="settings-item-name">{name}</span>
|
|
47
|
+
<span class="settings-item-desc">{description}</span>
|
|
48
|
+
</span>
|
|
49
|
+
<ChevronRight />
|
|
50
|
+
</a>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const ICONS = {
|
|
55
|
+
monitor: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="20" height="14" x="2" y="3" rx="2"/><line x1="8" x2="16" y1="21" y2="21"/><line x1="12" x2="12" y1="17" y2="21"/></svg>`,
|
|
56
|
+
lock: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="11" x="3" y="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>`,
|
|
57
|
+
download: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" x2="12" y1="15" y2="3"/></svg>`,
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const COLORS = {
|
|
61
|
+
teal: "oklch(0.55 0.15 185)",
|
|
62
|
+
gray: "oklch(0.55 0.01 250)",
|
|
63
|
+
green: "oklch(0.55 0.18 155)",
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export function AccountMenuContent() {
|
|
67
|
+
const { t } = useLingui();
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<div class="settings-root">
|
|
71
|
+
<div>
|
|
72
|
+
<div class="settings-group">
|
|
73
|
+
<AccountMenuItem
|
|
74
|
+
href="/settings/account/sessions"
|
|
75
|
+
icon={ICONS.monitor}
|
|
76
|
+
color={COLORS.teal}
|
|
77
|
+
name={t({
|
|
78
|
+
message: "Sessions",
|
|
79
|
+
comment: "@context: Settings item — session management",
|
|
80
|
+
})}
|
|
81
|
+
description={t({
|
|
82
|
+
message: "Manage active sign-in sessions",
|
|
83
|
+
comment: "@context: Settings item description for sessions",
|
|
84
|
+
})}
|
|
85
|
+
/>
|
|
86
|
+
<AccountMenuItem
|
|
87
|
+
href="/settings/account/password"
|
|
88
|
+
icon={ICONS.lock}
|
|
89
|
+
color={COLORS.gray}
|
|
90
|
+
name={t({
|
|
91
|
+
message: "Password",
|
|
92
|
+
comment: "@context: Settings item — password settings",
|
|
93
|
+
})}
|
|
94
|
+
description={t({
|
|
95
|
+
message: "Change your sign-in password",
|
|
96
|
+
comment:
|
|
97
|
+
"@context: Settings item description for password change",
|
|
98
|
+
})}
|
|
99
|
+
/>
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
|
|
103
|
+
{/* Data */}
|
|
104
|
+
<div>
|
|
105
|
+
<div class="settings-group-label">
|
|
106
|
+
{t({
|
|
107
|
+
message: "Data",
|
|
108
|
+
comment: "@context: Settings group label for data export/import",
|
|
109
|
+
})}
|
|
110
|
+
</div>
|
|
111
|
+
<div class="settings-group">
|
|
112
|
+
<form
|
|
113
|
+
method="post"
|
|
114
|
+
action="/api/export/zola"
|
|
115
|
+
class="settings-export-form"
|
|
116
|
+
>
|
|
117
|
+
<button type="submit" class="settings-item">
|
|
118
|
+
<span
|
|
119
|
+
class="settings-item-icon"
|
|
120
|
+
style={`background-color:${COLORS.green}`}
|
|
121
|
+
>
|
|
122
|
+
<span dangerouslySetInnerHTML={{ __html: ICONS.download }} />
|
|
123
|
+
</span>
|
|
124
|
+
<span class="settings-item-text">
|
|
125
|
+
<span class="settings-item-name">
|
|
126
|
+
{t({
|
|
127
|
+
message: "Export Site",
|
|
128
|
+
comment:
|
|
129
|
+
"@context: Settings item — export site as Zola ZIP",
|
|
130
|
+
})}
|
|
131
|
+
</span>
|
|
132
|
+
<span class="settings-item-desc">
|
|
133
|
+
{t({
|
|
134
|
+
message: "Download as a Zola static site (.zip)",
|
|
135
|
+
comment:
|
|
136
|
+
"@context: Settings item description for site export",
|
|
137
|
+
})}
|
|
138
|
+
</span>
|
|
139
|
+
</span>
|
|
140
|
+
<ChevronRight />
|
|
141
|
+
</button>
|
|
142
|
+
</form>
|
|
143
|
+
</div>
|
|
144
|
+
</div>
|
|
145
|
+
</div>
|
|
146
|
+
);
|
|
147
|
+
}
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API Tokens Settings Page
|
|
3
|
+
*
|
|
4
|
+
* Manage Bearer tokens for programmatic API access.
|
|
5
|
+
* Tokens are shown only once at creation — after that, only the prefix is visible.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { useLingui } from "@lingui/react/macro";
|
|
9
|
+
import type { ApiToken } from "../../../types/entities.js";
|
|
10
|
+
import { formatDate } from "../../../lib/time.js";
|
|
11
|
+
|
|
12
|
+
const API_DOCS_URL = "https://github.com/jant-me/jant/blob/main/docs/API.md";
|
|
13
|
+
|
|
14
|
+
function TokenRow({ token }: { token: ApiToken }) {
|
|
15
|
+
const { t } = useLingui();
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<div class="py-4 flex items-start gap-4 border-b border-border last:border-b-0">
|
|
19
|
+
<div class="flex-1 min-w-0">
|
|
20
|
+
<div class="font-medium">{token.name}</div>
|
|
21
|
+
<div class="text-sm text-muted-foreground mt-0.5">
|
|
22
|
+
<code class="text-xs bg-muted px-1.5 py-0.5 rounded">
|
|
23
|
+
jnt_{token.prefix}...
|
|
24
|
+
</code>
|
|
25
|
+
<span class="mx-2">·</span>
|
|
26
|
+
{t({
|
|
27
|
+
message: `Created ${formatDate(token.createdAt)}`,
|
|
28
|
+
comment: "@context: Token creation date",
|
|
29
|
+
})}
|
|
30
|
+
{token.lastUsedAt && (
|
|
31
|
+
<>
|
|
32
|
+
<span class="mx-2">·</span>
|
|
33
|
+
{t({
|
|
34
|
+
message: `Last used ${formatDate(token.lastUsedAt)}`,
|
|
35
|
+
comment: "@context: Token last used date",
|
|
36
|
+
})}
|
|
37
|
+
</>
|
|
38
|
+
)}
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
<button
|
|
42
|
+
type="button"
|
|
43
|
+
class="btn-sm-ghost text-destructive"
|
|
44
|
+
data-on:click__prevent={`confirm('${t({ message: "Revoke this token? Any scripts using it will stop working.", comment: "@context: Confirm dialog for revoking API token" })}') && @post('/settings/api-tokens/${token.id}/delete')`}
|
|
45
|
+
>
|
|
46
|
+
{t({
|
|
47
|
+
message: "Revoke",
|
|
48
|
+
comment: "@context: Button to revoke API token",
|
|
49
|
+
})}
|
|
50
|
+
</button>
|
|
51
|
+
</div>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function ApiTokensContent({
|
|
56
|
+
tokens,
|
|
57
|
+
siteUrl,
|
|
58
|
+
}: {
|
|
59
|
+
tokens: ApiToken[];
|
|
60
|
+
siteUrl: string;
|
|
61
|
+
}) {
|
|
62
|
+
const { t } = useLingui();
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<div
|
|
66
|
+
class="flex flex-col gap-8 max-w-2xl"
|
|
67
|
+
data-signals="{tokenName: '', _tokenLoading: false, _newPlaintext: '', _tokenCopied: false}"
|
|
68
|
+
>
|
|
69
|
+
{/* New token alert — shown after creation via signal patch */}
|
|
70
|
+
<div data-show="$_newPlaintext" style="display:none">
|
|
71
|
+
<div class="alert" role="alert">
|
|
72
|
+
<strong>
|
|
73
|
+
{t({
|
|
74
|
+
message: "Copy your token now — it won't be shown again.",
|
|
75
|
+
comment: "@context: Warning to copy newly created API token",
|
|
76
|
+
})}
|
|
77
|
+
</strong>
|
|
78
|
+
<section class="mt-2">
|
|
79
|
+
<div class="flex items-center gap-2">
|
|
80
|
+
<code
|
|
81
|
+
class="bg-muted px-3 py-2 rounded break-all select-all flex-1 text-sm"
|
|
82
|
+
data-text="$_newPlaintext"
|
|
83
|
+
>
|
|
84
|
+
{" "}
|
|
85
|
+
</code>
|
|
86
|
+
<button
|
|
87
|
+
type="button"
|
|
88
|
+
class="btn-sm-outline shrink-0"
|
|
89
|
+
data-on:click="navigator.clipboard.writeText($_newPlaintext); $_tokenCopied = true"
|
|
90
|
+
data-text={`$_tokenCopied ? '${t({ message: "Copied", comment: "@context: Feedback after copying API token" })}' : '${t({ message: "Copy Token", comment: "@context: Button to copy API token to clipboard" })}'`}
|
|
91
|
+
>
|
|
92
|
+
{t({
|
|
93
|
+
message: "Copy Token",
|
|
94
|
+
comment: "@context: Button to copy API token to clipboard",
|
|
95
|
+
})}
|
|
96
|
+
</button>
|
|
97
|
+
</div>
|
|
98
|
+
</section>
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
|
|
102
|
+
{/* Generate token form */}
|
|
103
|
+
<div>
|
|
104
|
+
<h2 class="text-lg font-medium mb-4">
|
|
105
|
+
{t({
|
|
106
|
+
message: "API Tokens",
|
|
107
|
+
comment: "@context: Settings section heading",
|
|
108
|
+
})}
|
|
109
|
+
</h2>
|
|
110
|
+
<p class="text-sm text-muted-foreground mb-4">
|
|
111
|
+
{t({
|
|
112
|
+
message:
|
|
113
|
+
"Tokens let you access the API from scripts, shortcuts, and other tools without signing in.",
|
|
114
|
+
comment: "@context: API tokens description",
|
|
115
|
+
})}
|
|
116
|
+
</p>
|
|
117
|
+
<form
|
|
118
|
+
data-on:submit__prevent="@post('/settings/api-tokens')"
|
|
119
|
+
data-indicator="_tokenLoading"
|
|
120
|
+
class="flex gap-2 items-end"
|
|
121
|
+
>
|
|
122
|
+
<div class="field flex-1">
|
|
123
|
+
<label class="label" for="tokenName">
|
|
124
|
+
{t({
|
|
125
|
+
message: "Token name",
|
|
126
|
+
comment: "@context: API token name field label",
|
|
127
|
+
})}
|
|
128
|
+
</label>
|
|
129
|
+
<input
|
|
130
|
+
type="text"
|
|
131
|
+
id="tokenName"
|
|
132
|
+
data-bind="tokenName"
|
|
133
|
+
class="input"
|
|
134
|
+
placeholder={t({
|
|
135
|
+
message: "e.g. iOS Shortcuts",
|
|
136
|
+
comment: "@context: Placeholder for API token name input",
|
|
137
|
+
})}
|
|
138
|
+
required
|
|
139
|
+
/>
|
|
140
|
+
</div>
|
|
141
|
+
<button
|
|
142
|
+
type="submit"
|
|
143
|
+
class="btn"
|
|
144
|
+
data-attr:disabled="$_tokenLoading || !$tokenName.trim()"
|
|
145
|
+
>
|
|
146
|
+
{t({
|
|
147
|
+
message: "Generate Token",
|
|
148
|
+
comment: "@context: Button to create new API token",
|
|
149
|
+
})}
|
|
150
|
+
</button>
|
|
151
|
+
</form>
|
|
152
|
+
</div>
|
|
153
|
+
|
|
154
|
+
{/* Token list */}
|
|
155
|
+
{tokens.length > 0 && (
|
|
156
|
+
<div>
|
|
157
|
+
<h3 class="text-sm font-medium text-muted-foreground uppercase tracking-wide mb-2">
|
|
158
|
+
{t({
|
|
159
|
+
message: "Active Tokens",
|
|
160
|
+
comment: "@context: Heading for list of active API tokens",
|
|
161
|
+
})}
|
|
162
|
+
</h3>
|
|
163
|
+
<div class="border border-border rounded-lg px-4">
|
|
164
|
+
{tokens.map((token) => (
|
|
165
|
+
<TokenRow key={token.id} token={token} />
|
|
166
|
+
))}
|
|
167
|
+
</div>
|
|
168
|
+
</div>
|
|
169
|
+
)}
|
|
170
|
+
|
|
171
|
+
{/* Usage examples */}
|
|
172
|
+
<div>
|
|
173
|
+
<h3 class="text-sm font-medium text-muted-foreground uppercase tracking-wide mb-2">
|
|
174
|
+
{t({
|
|
175
|
+
message: "Usage",
|
|
176
|
+
comment: "@context: Heading for API token usage examples",
|
|
177
|
+
})}
|
|
178
|
+
</h3>
|
|
179
|
+
<div class="flex flex-col gap-3 text-sm">
|
|
180
|
+
<div>
|
|
181
|
+
<div class="text-muted-foreground mb-1">
|
|
182
|
+
{t({
|
|
183
|
+
message: "Create a post with curl:",
|
|
184
|
+
comment: "@context: Label for curl example",
|
|
185
|
+
})}
|
|
186
|
+
</div>
|
|
187
|
+
<pre class="bg-muted px-3 py-2 rounded text-xs overflow-x-auto">
|
|
188
|
+
<code>
|
|
189
|
+
{`curl -X POST ${siteUrl}/api/posts \\
|
|
190
|
+
-H "Authorization: Bearer jnt_YOUR_TOKEN" \\
|
|
191
|
+
-H "Content-Type: application/json" \\
|
|
192
|
+
-d '{"format":"note","body":"Hello from the API"}'`}
|
|
193
|
+
</code>
|
|
194
|
+
</pre>
|
|
195
|
+
</div>
|
|
196
|
+
<div>
|
|
197
|
+
<div class="text-muted-foreground mb-1">
|
|
198
|
+
{t({
|
|
199
|
+
message: "List posts:",
|
|
200
|
+
comment: "@context: Label for list posts curl example",
|
|
201
|
+
})}
|
|
202
|
+
</div>
|
|
203
|
+
<pre class="bg-muted px-3 py-2 rounded text-xs overflow-x-auto">
|
|
204
|
+
<code>
|
|
205
|
+
{`curl ${siteUrl}/api/posts \\
|
|
206
|
+
-H "Authorization: Bearer jnt_YOUR_TOKEN"`}
|
|
207
|
+
</code>
|
|
208
|
+
</pre>
|
|
209
|
+
</div>
|
|
210
|
+
<p class="text-muted-foreground">
|
|
211
|
+
<a
|
|
212
|
+
href={API_DOCS_URL}
|
|
213
|
+
target="_blank"
|
|
214
|
+
rel="noopener noreferrer"
|
|
215
|
+
class="underline hover:text-foreground transition-colors"
|
|
216
|
+
>
|
|
217
|
+
{t({
|
|
218
|
+
message: "API reference",
|
|
219
|
+
comment: "@context: Link to API documentation",
|
|
220
|
+
})}
|
|
221
|
+
</a>
|
|
222
|
+
{" — "}
|
|
223
|
+
{t({
|
|
224
|
+
message: "all available endpoints and request formats.",
|
|
225
|
+
comment: "@context: Description after API reference link",
|
|
226
|
+
})}
|
|
227
|
+
</p>
|
|
228
|
+
</div>
|
|
229
|
+
</div>
|
|
230
|
+
</div>
|
|
231
|
+
);
|
|
232
|
+
}
|
|
@@ -50,6 +50,14 @@ export function AvatarContent({
|
|
|
50
50
|
message: "Upload failed. Please try again.",
|
|
51
51
|
comment: "@context: Error message when avatar upload fails",
|
|
52
52
|
}),
|
|
53
|
+
save: t({
|
|
54
|
+
message: "Save",
|
|
55
|
+
comment: "@context: Button to save settings changes",
|
|
56
|
+
}),
|
|
57
|
+
cancel: t({
|
|
58
|
+
message: "Cancel",
|
|
59
|
+
comment: "@context: Button to cancel settings changes",
|
|
60
|
+
}),
|
|
53
61
|
}).replace(/</g, "\\u003c");
|
|
54
62
|
|
|
55
63
|
return (
|