@jant/core 0.6.7 → 0.6.9
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/uploads/cleanup.js +2 -0
- package/dist/{app-L1UPUArB.js → app-C-jxWmAV.js} +12421 -12033
- package/dist/app-DqHzOwL5.js +6 -0
- package/dist/client/.vite/manifest.json +3 -3
- package/dist/client/_assets/client-CGf2m3qp.css +2 -0
- package/dist/client/_assets/{client-B0MvB2r0.js → client-DWy1LEEk.js} +2 -2
- package/dist/client/_assets/{client-auth-CwwuucF_.js → client-auth-Blg-a5Ep.js} +365 -345
- package/dist/{env-CoSe-1y4.js → env-OHRKGcMj.js} +1 -1
- package/dist/{export-DLukCOO3.js → export-C2DIB7mm.js} +34 -9
- package/dist/{github-api-UD4u_7fa.js → github-api-BgSiE71w.js} +1 -1
- package/dist/{github-app-DeX6Td1O.js → github-app-BbklkFmU.js} +1 -1
- package/dist/{github-sync-BeDecPen.js → github-sync-7XQ5ZM6z.js} +3 -3
- package/dist/{github-sync-BtHY2AST.js → github-sync-BEFCfLKK.js} +3 -3
- package/dist/index.js +5 -5
- package/dist/node.js +6 -6
- package/dist/{url-XF0GbKGO.js → url-BMYO-Zlt.js} +42 -2
- package/package.json +1 -1
- package/src/__tests__/bin/uploads-cleanup.test.ts +2 -0
- package/src/client/__tests__/compose-bridge.test.ts +105 -0
- package/src/client/__tests__/hydrate-partial.test.ts +27 -0
- package/src/client/__tests__/note-expand.test.ts +130 -0
- package/src/client/archive-nav.js +2 -1
- package/src/client/audio-player.ts +7 -3
- package/src/client/components/__tests__/compose-format-convert.test.ts +357 -0
- package/src/client/components/__tests__/jant-compose-dialog.test.ts +313 -0
- package/src/client/components/__tests__/jant-compose-editor.test.ts +2 -0
- package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +2 -0
- package/src/client/components/__tests__/jant-settings-avatar.test.ts +5 -2
- package/src/client/components/__tests__/jant-settings-general.test.ts +55 -8
- package/src/client/components/compose-format-convert.ts +255 -0
- package/src/client/components/compose-types.ts +2 -0
- package/src/client/components/jant-compose-dialog.ts +110 -44
- package/src/client/components/jant-compose-editor.ts +64 -11
- package/src/client/components/jant-settings-general.ts +56 -18
- package/src/client/components/settings-types.ts +11 -0
- package/src/client/compose-bridge.ts +17 -0
- package/src/client/feed-video-player.ts +1 -1
- package/src/client/hydrate-partial.ts +25 -0
- package/src/client/note-expand.ts +63 -0
- package/src/client/settings-bridge.ts +3 -0
- package/src/client/tiptap/__tests__/mark-exit.test.ts +99 -0
- package/src/client/tiptap/bubble-menu.ts +37 -4
- package/src/client.ts +1 -0
- package/src/db/migrations/0026_absent_rhodey.sql +14 -0
- package/src/db/migrations/meta/0026_snapshot.json +2511 -0
- package/src/db/migrations/meta/_journal.json +7 -0
- package/src/db/migrations/pg/0024_high_violations.sql +14 -0
- package/src/db/migrations/pg/meta/0024_snapshot.json +3204 -0
- package/src/db/migrations/pg/meta/_journal.json +7 -0
- package/src/db/pg/schema.ts +36 -0
- package/src/db/schema.ts +36 -0
- package/src/i18n/__tests__/middleware.test.ts +46 -0
- package/src/i18n/locales/public/en.po +41 -0
- package/src/i18n/locales/public/en.ts +1 -1
- package/src/i18n/locales/public/zh-Hans.po +41 -0
- package/src/i18n/locales/public/zh-Hans.ts +1 -1
- package/src/i18n/locales/public/zh-Hant.po +41 -0
- package/src/i18n/locales/public/zh-Hant.ts +1 -1
- package/src/i18n/locales/settings/en.po +37 -22
- package/src/i18n/locales/settings/en.ts +1 -1
- package/src/i18n/locales/settings/zh-Hans.po +37 -22
- package/src/i18n/locales/settings/zh-Hans.ts +1 -1
- package/src/i18n/locales/settings/zh-Hant.po +37 -22
- package/src/i18n/locales/settings/zh-Hant.ts +1 -1
- package/src/i18n/middleware.ts +17 -8
- package/src/i18n/supported-locales.ts +5 -4
- package/src/lib/__tests__/markdown-to-tiptap.test.ts +1 -1
- package/src/lib/__tests__/markdown.test.ts +1 -1
- package/src/lib/__tests__/summary.test.ts +87 -0
- package/src/lib/__tests__/timeline.test.ts +48 -1
- package/src/lib/__tests__/tiptap-render.test.ts +4 -4
- package/src/lib/__tests__/url.test.ts +44 -0
- package/src/lib/__tests__/view.test.ts +168 -1
- package/src/lib/ids.ts +1 -0
- package/src/lib/navigation.ts +1 -0
- package/src/lib/resolve-config.ts +3 -2
- package/src/lib/summary.ts +42 -3
- package/src/lib/tiptap-render.ts +6 -2
- package/src/lib/upload.ts +16 -2
- package/src/lib/url.ts +41 -0
- package/src/lib/view.ts +102 -40
- package/src/preset.css +7 -1
- package/src/routes/api/__tests__/settings.test.ts +1 -4
- package/src/routes/api/__tests__/upload.test.ts +2 -0
- package/src/routes/api/internal/__tests__/uploads.test.ts +86 -0
- package/src/routes/api/internal/sites.ts +44 -1
- package/src/routes/api/public/__tests__/archive.test.ts +66 -0
- package/src/routes/api/public/archive.ts +22 -6
- package/src/routes/api/settings.ts +2 -1
- package/src/routes/api/telegram.ts +2 -1
- package/src/routes/auth/__tests__/setup.test.ts +14 -0
- package/src/routes/dash/__tests__/settings-avatar.test.ts +35 -17
- package/src/routes/dash/custom-urls.tsx +1 -1
- package/src/routes/dash/settings.tsx +23 -7
- package/src/routes/pages/__tests__/archive-params.test.ts +135 -0
- package/src/routes/pages/archive.tsx +116 -20
- package/src/routes/pages/collections.tsx +1 -0
- package/src/services/__tests__/media.test.ts +274 -30
- package/src/services/__tests__/post.test.ts +81 -0
- package/src/services/__tests__/settings.test.ts +55 -0
- package/src/services/bootstrap.ts +7 -0
- package/src/services/export-theme/assets/client-site.js +1 -1
- package/src/services/export-theme/layouts/_default/baseof.html +2 -1
- package/src/services/export-theme/styles/main.css +49 -15
- package/src/services/media.ts +199 -42
- package/src/services/post.ts +22 -2
- package/src/services/search.ts +4 -4
- package/src/services/settings.ts +49 -15
- package/src/services/upload-session.ts +28 -0
- package/src/styles/tokens.css +7 -5
- package/src/styles/ui.css +163 -34
- package/src/types/bindings.ts +1 -0
- package/src/types/config.ts +14 -1
- package/src/types/props.ts +3 -0
- package/src/ui/compose/ComposeDialog.tsx +13 -0
- package/src/ui/dash/settings/AccountMenuContent.tsx +0 -39
- package/src/ui/dash/settings/GeneralContent.tsx +38 -4
- package/src/ui/dash/settings/SettingsDirectory.tsx +26 -1
- package/src/ui/dash/settings/SettingsRootContent.tsx +46 -1
- package/src/ui/dash/settings/__tests__/SettingsRootContent.test.tsx +55 -0
- package/src/ui/feed/NoteCard.tsx +54 -5
- package/src/ui/feed/__tests__/timeline-cards.test.ts +73 -0
- package/src/ui/layouts/BaseLayout.tsx +1 -0
- package/src/ui/layouts/__tests__/BaseLayout.test.tsx +13 -0
- package/src/ui/pages/ArchivePage.tsx +89 -6
- package/src/ui/pages/CollectionsPage.tsx +7 -1
- package/src/ui/pages/__tests__/ArchivePage.test.tsx +37 -0
- package/src/ui/shared/CollectionDirectory.tsx +13 -3
- package/src/ui/shared/CollectionsManager.tsx +3 -0
- package/dist/app-C1QgMNRY.js +0 -6
- package/dist/client/_assets/client-BMPMuwvV.css +0 -2
|
@@ -15,6 +15,7 @@ export function GeneralContent({
|
|
|
15
15
|
siteName,
|
|
16
16
|
siteDescription,
|
|
17
17
|
siteLanguage,
|
|
18
|
+
dashboardLanguage,
|
|
18
19
|
cjkSerifFont,
|
|
19
20
|
siteNameFallback,
|
|
20
21
|
siteDescriptionFallback,
|
|
@@ -32,6 +33,7 @@ export function GeneralContent({
|
|
|
32
33
|
siteName: string;
|
|
33
34
|
siteDescription: string;
|
|
34
35
|
siteLanguage: string;
|
|
36
|
+
dashboardLanguage: string;
|
|
35
37
|
cjkSerifFont: string;
|
|
36
38
|
siteNameFallback: string;
|
|
37
39
|
siteDescriptionFallback: string;
|
|
@@ -101,15 +103,37 @@ export function GeneralContent({
|
|
|
101
103
|
),
|
|
102
104
|
siteLanguage: i18n._(
|
|
103
105
|
msg({
|
|
104
|
-
message: "
|
|
105
|
-
comment:
|
|
106
|
+
message: "Content language",
|
|
107
|
+
comment:
|
|
108
|
+
"@context: Settings form field for the public content language",
|
|
106
109
|
}),
|
|
107
110
|
),
|
|
108
111
|
siteLanguageHelp: i18n._(
|
|
109
112
|
msg({
|
|
110
113
|
message:
|
|
111
|
-
"
|
|
112
|
-
comment: "@context: Help text under the
|
|
114
|
+
"The language your posts are written in. Announced to readers and search engines through HTML lang and your RSS feed. Any BCP 47 tag works.",
|
|
115
|
+
comment: "@context: Help text under the content language picker",
|
|
116
|
+
}),
|
|
117
|
+
),
|
|
118
|
+
contentLanguagePreview: i18n._(
|
|
119
|
+
msg({
|
|
120
|
+
message: "Readers and search engines see",
|
|
121
|
+
comment:
|
|
122
|
+
"@context: Lead text before a live <html lang> preview of the content language",
|
|
123
|
+
}),
|
|
124
|
+
),
|
|
125
|
+
dashboardLanguage: i18n._(
|
|
126
|
+
msg({
|
|
127
|
+
message: "Dashboard language",
|
|
128
|
+
comment:
|
|
129
|
+
"@context: Settings form field for the admin interface language",
|
|
130
|
+
}),
|
|
131
|
+
),
|
|
132
|
+
dashboardLanguageHelp: i18n._(
|
|
133
|
+
msg({
|
|
134
|
+
message:
|
|
135
|
+
"The language this admin dashboard shows in. Available in English, 简体中文, and 繁體中文.",
|
|
136
|
+
comment: "@context: Help text under the dashboard language picker",
|
|
113
137
|
}),
|
|
114
138
|
),
|
|
115
139
|
siteLanguageSearchPlaceholder: i18n._(
|
|
@@ -308,6 +332,14 @@ export function GeneralContent({
|
|
|
308
332
|
timezones.map((tz) => ({ value: tz.value, label: tz.label })),
|
|
309
333
|
).replace(/</g, "\\u003c");
|
|
310
334
|
|
|
335
|
+
// The 3 catalog locales Jant's dashboard is translated into. Native-script
|
|
336
|
+
// labels so each reads in its own language, like the CJK font options.
|
|
337
|
+
const dashboardLanguagesJson = JSON.stringify([
|
|
338
|
+
{ value: "en", label: "English" },
|
|
339
|
+
{ value: "zh-Hans", label: "简体中文" },
|
|
340
|
+
{ value: "zh-Hant", label: "繁體中文" },
|
|
341
|
+
]).replace(/</g, "\\u003c");
|
|
342
|
+
|
|
311
343
|
const cjkFontsJson = JSON.stringify([
|
|
312
344
|
{ value: "off", label: "None" },
|
|
313
345
|
{
|
|
@@ -326,6 +358,7 @@ export function GeneralContent({
|
|
|
326
358
|
siteName,
|
|
327
359
|
siteDescription,
|
|
328
360
|
siteLanguage,
|
|
361
|
+
dashboardLanguage,
|
|
329
362
|
cjkSerifFont,
|
|
330
363
|
mainRssFeed,
|
|
331
364
|
timeZone,
|
|
@@ -341,6 +374,7 @@ export function GeneralContent({
|
|
|
341
374
|
labels={labels}
|
|
342
375
|
timezones={timezonesJson}
|
|
343
376
|
cjk-fonts={cjkFontsJson}
|
|
377
|
+
dashboard-languages={dashboardLanguagesJson}
|
|
344
378
|
sitename-fallback={siteNameFallback}
|
|
345
379
|
sitedescription-fallback={siteDescriptionFallback}
|
|
346
380
|
main-feed-url={mainFeedUrl}
|
|
@@ -22,6 +22,28 @@ function ChevronRight() {
|
|
|
22
22
|
);
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
function ExternalLinkIndicator() {
|
|
26
|
+
return (
|
|
27
|
+
<svg
|
|
28
|
+
class="settings-directory-item-chevron"
|
|
29
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
30
|
+
width="16"
|
|
31
|
+
height="16"
|
|
32
|
+
viewBox="0 0 24 24"
|
|
33
|
+
fill="none"
|
|
34
|
+
stroke="currentColor"
|
|
35
|
+
stroke-width="2"
|
|
36
|
+
stroke-linecap="round"
|
|
37
|
+
stroke-linejoin="round"
|
|
38
|
+
aria-hidden="true"
|
|
39
|
+
>
|
|
40
|
+
<path d="M15 3h6v6" />
|
|
41
|
+
<path d="M10 14 21 3" />
|
|
42
|
+
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" />
|
|
43
|
+
</svg>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
25
47
|
export function SettingsDirectorySection({
|
|
26
48
|
title,
|
|
27
49
|
tone = "default",
|
|
@@ -45,10 +67,12 @@ export function SettingsDirectoryItemContent({
|
|
|
45
67
|
icon,
|
|
46
68
|
name,
|
|
47
69
|
description,
|
|
70
|
+
external = false,
|
|
48
71
|
}: {
|
|
49
72
|
icon: string;
|
|
50
73
|
name: string;
|
|
51
74
|
description: string;
|
|
75
|
+
external?: boolean;
|
|
52
76
|
}) {
|
|
53
77
|
return (
|
|
54
78
|
<>
|
|
@@ -59,7 +83,7 @@ export function SettingsDirectoryItemContent({
|
|
|
59
83
|
<span class="settings-directory-item-name">{name}</span>
|
|
60
84
|
<span class="settings-directory-item-desc">{description}</span>
|
|
61
85
|
</span>
|
|
62
|
-
<ChevronRight />
|
|
86
|
+
{external ? <ExternalLinkIndicator /> : <ChevronRight />}
|
|
63
87
|
</>
|
|
64
88
|
);
|
|
65
89
|
}
|
|
@@ -93,6 +117,7 @@ export function SettingsDirectoryLink({
|
|
|
93
117
|
icon={icon}
|
|
94
118
|
name={name}
|
|
95
119
|
description={description}
|
|
120
|
+
external={target === "_blank"}
|
|
96
121
|
/>
|
|
97
122
|
</a>
|
|
98
123
|
);
|
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { msg } from "@lingui/core/macro";
|
|
6
|
+
import { coalesceDisplayText } from "../../../lib/display-text.js";
|
|
6
7
|
import { useLingui } from "../../../i18n/context.js";
|
|
7
|
-
import { toPublicPath } from "../../../lib/url.js";
|
|
8
|
+
import { extractDomain, toPublicPath } from "../../../lib/url.js";
|
|
8
9
|
import {
|
|
9
10
|
SettingsDirectoryLink,
|
|
10
11
|
SettingsDirectorySection,
|
|
@@ -25,14 +26,19 @@ const ICONS = {
|
|
|
25
26
|
shield: `<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="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z"/></svg>`,
|
|
26
27
|
gitBranch: `<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"><line x1="6" x2="6" y1="3" y2="15"/><circle cx="18" cy="6" r="3"/><circle cx="6" cy="18" r="3"/><path d="M18 9a9 9 0 0 1-9 9"/></svg>`,
|
|
27
28
|
send: `<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="M14.536 21.686a.5.5 0 0 0 .937-.024l6.5-19a.496.496 0 0 0-.635-.635l-19 6.5a.5.5 0 0 0-.024.937l7.93 3.18a2 2 0 0 1 1.112 1.11z"/><path d="m21.854 2.147-10.94 10.939"/></svg>`,
|
|
29
|
+
cloud: `<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="M17.5 19H9a7 7 0 1 1 6.71-9h1.79a4.5 4.5 0 1 1 0 9Z"/></svg>`,
|
|
28
30
|
};
|
|
29
31
|
|
|
30
32
|
export function SettingsRootContent({
|
|
31
33
|
sitePathPrefix = "",
|
|
32
34
|
demoMode = false,
|
|
35
|
+
hostedControlPlaneSiteSettingsUrl,
|
|
36
|
+
hostedControlPlaneProviderLabel,
|
|
33
37
|
}: {
|
|
34
38
|
sitePathPrefix?: string;
|
|
35
39
|
demoMode?: boolean;
|
|
40
|
+
hostedControlPlaneSiteSettingsUrl?: string | null;
|
|
41
|
+
hostedControlPlaneProviderLabel?: string | null;
|
|
36
42
|
}) {
|
|
37
43
|
const { i18n } = useLingui();
|
|
38
44
|
const accountDescription = demoMode
|
|
@@ -50,6 +56,19 @@ export function SettingsRootContent({
|
|
|
50
56
|
"@context: Settings item description for account settings on the settings home page",
|
|
51
57
|
}),
|
|
52
58
|
);
|
|
59
|
+
const hostingProviderLabel = hostedControlPlaneSiteSettingsUrl
|
|
60
|
+
? (coalesceDisplayText(
|
|
61
|
+
hostedControlPlaneProviderLabel,
|
|
62
|
+
extractDomain(hostedControlPlaneSiteSettingsUrl),
|
|
63
|
+
) ??
|
|
64
|
+
i18n._(
|
|
65
|
+
msg({
|
|
66
|
+
message: "Hosted account",
|
|
67
|
+
comment:
|
|
68
|
+
"@context: Generic hosted auth provider label when no explicit provider name is configured",
|
|
69
|
+
}),
|
|
70
|
+
))
|
|
71
|
+
: null;
|
|
53
72
|
|
|
54
73
|
return (
|
|
55
74
|
<div class="settings-root">
|
|
@@ -320,6 +339,32 @@ export function SettingsRootContent({
|
|
|
320
339
|
)}
|
|
321
340
|
description={accountDescription}
|
|
322
341
|
/>
|
|
342
|
+
{hostedControlPlaneSiteSettingsUrl && hostingProviderLabel ? (
|
|
343
|
+
<SettingsDirectoryLink
|
|
344
|
+
href={hostedControlPlaneSiteSettingsUrl}
|
|
345
|
+
target="_blank"
|
|
346
|
+
rel="noopener noreferrer"
|
|
347
|
+
icon={ICONS.cloud}
|
|
348
|
+
tone="subtle"
|
|
349
|
+
name={i18n._(
|
|
350
|
+
msg({
|
|
351
|
+
message: "Manage Hosting",
|
|
352
|
+
comment:
|
|
353
|
+
"@context: Settings item label for opening this site's hosted management page (domains, plan, billing) in the connected control plane",
|
|
354
|
+
}),
|
|
355
|
+
)}
|
|
356
|
+
description={i18n._(
|
|
357
|
+
msg({
|
|
358
|
+
message: "Domains, plan, and billing in {providerLabel}",
|
|
359
|
+
comment:
|
|
360
|
+
"@context: Settings item description for the hosted site management external link",
|
|
361
|
+
}),
|
|
362
|
+
{
|
|
363
|
+
providerLabel: hostingProviderLabel,
|
|
364
|
+
},
|
|
365
|
+
)}
|
|
366
|
+
/>
|
|
367
|
+
) : null}
|
|
323
368
|
</SettingsDirectorySection>
|
|
324
369
|
|
|
325
370
|
<div class="settings-root-signout">
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { Context } from "hono";
|
|
2
|
+
import { renderToString } from "hono/jsx/dom/server";
|
|
3
|
+
import { describe, expect, it } from "vitest";
|
|
4
|
+
import { I18nProvider } from "../../../../i18n/context.js";
|
|
5
|
+
import { createI18n } from "../../../../i18n/i18n.js";
|
|
6
|
+
import { SettingsRootContent } from "../SettingsRootContent.js";
|
|
7
|
+
|
|
8
|
+
function renderSettingsRootContent(
|
|
9
|
+
props: Parameters<typeof SettingsRootContent>[0],
|
|
10
|
+
): string {
|
|
11
|
+
const i18n = createI18n("en");
|
|
12
|
+
const c = {
|
|
13
|
+
get(key: string) {
|
|
14
|
+
if (key === "i18n") return i18n;
|
|
15
|
+
return undefined;
|
|
16
|
+
},
|
|
17
|
+
} as unknown as Context;
|
|
18
|
+
|
|
19
|
+
I18nProvider({ c, children: "" });
|
|
20
|
+
return renderToString(SettingsRootContent(props));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe("SettingsRootContent", () => {
|
|
24
|
+
it("omits Manage Hosting when no hosted site settings URL is configured", () => {
|
|
25
|
+
const html = renderSettingsRootContent({});
|
|
26
|
+
|
|
27
|
+
expect(html).not.toContain("Manage Hosting");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("renders the Manage Hosting external link with the configured provider label", () => {
|
|
31
|
+
const html = renderSettingsRootContent({
|
|
32
|
+
hostedControlPlaneSiteSettingsUrl:
|
|
33
|
+
"https://cloud.example/sites/core/site_123/settings",
|
|
34
|
+
hostedControlPlaneProviderLabel: "jant.me",
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
expect(html).toContain("Manage Hosting");
|
|
38
|
+
expect(html).toContain("Domains, plan, and billing in jant.me");
|
|
39
|
+
expect(html).toContain(
|
|
40
|
+
'href="https://cloud.example/sites/core/site_123/settings"',
|
|
41
|
+
);
|
|
42
|
+
expect(html).toContain('target="_blank"');
|
|
43
|
+
expect(html).toContain('rel="noopener noreferrer"');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("falls back to the hosted URL host when the provider label is blank", () => {
|
|
47
|
+
const html = renderSettingsRootContent({
|
|
48
|
+
hostedControlPlaneSiteSettingsUrl:
|
|
49
|
+
"https://cloud.example/sites/core/site_123/settings",
|
|
50
|
+
hostedControlPlaneProviderLabel: " ",
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
expect(html).toContain("Domains, plan, and billing in cloud.example");
|
|
54
|
+
});
|
|
55
|
+
});
|
package/src/ui/feed/NoteCard.tsx
CHANGED
|
@@ -5,7 +5,9 @@
|
|
|
5
5
|
* With title: article-style rendering with summary excerpt and "Read more" link.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import { msg } from "@lingui/core/macro";
|
|
8
9
|
import type { FC } from "hono/jsx";
|
|
10
|
+
import { useLingui } from "../../i18n/context.js";
|
|
9
11
|
import type { TimelineCardProps } from "../../types.js";
|
|
10
12
|
import { MediaGallery } from "../shared/MediaGallery.js";
|
|
11
13
|
import { StarRating } from "../shared/StarRating.js";
|
|
@@ -27,6 +29,7 @@ export const NoteCard: FC<TimelineCardProps> = ({
|
|
|
27
29
|
mode = "feed",
|
|
28
30
|
display,
|
|
29
31
|
}) => {
|
|
32
|
+
const { i18n } = useLingui();
|
|
30
33
|
const isCompact = mode === "compact";
|
|
31
34
|
const isDetail = mode === "detail";
|
|
32
35
|
const isArticle = !!post.title;
|
|
@@ -35,8 +38,42 @@ export const NoteCard: FC<TimelineCardProps> = ({
|
|
|
35
38
|
const fullBodyHtml = showFullBody
|
|
36
39
|
? stripContinueAnchor(post.bodyHtml)
|
|
37
40
|
: post.bodyHtml;
|
|
41
|
+
// Untitled notes only carry summaryHtml when their body was truncated; fall
|
|
42
|
+
// back to the full body so non-truncated notes render every block.
|
|
38
43
|
const displayHtml =
|
|
39
|
-
isDetail ||
|
|
44
|
+
isDetail || showFullBody
|
|
45
|
+
? fullBodyHtml
|
|
46
|
+
: (post.summaryHtml ?? fullBodyHtml);
|
|
47
|
+
const continueLabel = i18n._(
|
|
48
|
+
msg({
|
|
49
|
+
message: "Continue →",
|
|
50
|
+
comment:
|
|
51
|
+
"@context: Feed link from a truncated article excerpt to its full page",
|
|
52
|
+
}),
|
|
53
|
+
);
|
|
54
|
+
const readMoreLabel = i18n._(
|
|
55
|
+
msg({
|
|
56
|
+
message: "Read more",
|
|
57
|
+
comment:
|
|
58
|
+
"@context: Expand the rest of a truncated untitled note in place in the feed",
|
|
59
|
+
}),
|
|
60
|
+
);
|
|
61
|
+
const readLessLabel = i18n._(
|
|
62
|
+
msg({
|
|
63
|
+
message: "Read less",
|
|
64
|
+
comment:
|
|
65
|
+
"@context: Collapse an expanded untitled note back to its preview in the feed",
|
|
66
|
+
}),
|
|
67
|
+
);
|
|
68
|
+
// Untitled notes long enough to truncate render their full body with a
|
|
69
|
+
// `data-note-break` marker; this flag tells CSS to clamp the tail until the
|
|
70
|
+
// reader expands it in place.
|
|
71
|
+
const clampNote =
|
|
72
|
+
!isArticle &&
|
|
73
|
+
!isDetail &&
|
|
74
|
+
!isCompact &&
|
|
75
|
+
!showFullBody &&
|
|
76
|
+
post.summaryHasMore === true;
|
|
40
77
|
const hasVisibleRating =
|
|
41
78
|
!!post.rating && post.rating > 0 && !display?.hideRating;
|
|
42
79
|
const showHeaderRating = isDetail && isArticle && hasVisibleRating;
|
|
@@ -88,6 +125,7 @@ export const NoteCard: FC<TimelineCardProps> = ({
|
|
|
88
125
|
<div
|
|
89
126
|
class={`e-content prose ${isCompact ? "prose-sm" : isDetail || showFullBody ? "post-detail-body" : isArticle ? "post-body-summary" : ""}`}
|
|
90
127
|
data-post-body
|
|
128
|
+
{...(clampNote ? { "data-note-clamp": "" } : {})}
|
|
91
129
|
dangerouslySetInnerHTML={{ __html: displayHtml }}
|
|
92
130
|
/>
|
|
93
131
|
)}
|
|
@@ -105,12 +143,23 @@ export const NoteCard: FC<TimelineCardProps> = ({
|
|
|
105
143
|
{!isDetail &&
|
|
106
144
|
!isCompact &&
|
|
107
145
|
!showFullBody &&
|
|
108
|
-
|
|
109
|
-
|
|
146
|
+
post.summaryHasMore &&
|
|
147
|
+
(isArticle ? (
|
|
110
148
|
<a href={getContinueHref(post)} class="feed-continue-link">
|
|
111
|
-
|
|
149
|
+
{continueLabel}
|
|
150
|
+
</a>
|
|
151
|
+
) : (
|
|
152
|
+
<a
|
|
153
|
+
href={getContinueHref(post)}
|
|
154
|
+
class="feed-continue-link"
|
|
155
|
+
data-note-expand
|
|
156
|
+
aria-expanded="false"
|
|
157
|
+
data-label-more={readMoreLabel}
|
|
158
|
+
data-label-less={readLessLabel}
|
|
159
|
+
>
|
|
160
|
+
{readMoreLabel}
|
|
112
161
|
</a>
|
|
113
|
-
)}
|
|
162
|
+
))}
|
|
114
163
|
{!isCompact && !showHeaderRating && !display?.hideRating && (
|
|
115
164
|
<StarRating rating={post.rating} />
|
|
116
165
|
)}
|
|
@@ -312,6 +312,79 @@ describe("timeline cards", () => {
|
|
|
312
312
|
|
|
313
313
|
expect(html).toContain('href="/post-1"');
|
|
314
314
|
expect(html).not.toContain("#continue");
|
|
315
|
+
// Titled articles link out; they do not expand in place.
|
|
316
|
+
expect(html).not.toContain("data-note-expand");
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
it("clamps the body and renders an expand control for truncated untitled notes", () => {
|
|
320
|
+
const post = createPostView({
|
|
321
|
+
format: "note",
|
|
322
|
+
title: undefined,
|
|
323
|
+
bodyHtml: "<p>Intro</p><span data-note-break></span><p>Rest</p>",
|
|
324
|
+
summaryHasMore: true,
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
const html = renderWithI18n(NoteCard({ post, mode: "feed" }));
|
|
328
|
+
|
|
329
|
+
expect(html).toContain("data-note-expand");
|
|
330
|
+
expect(html).toContain('href="/post-1"');
|
|
331
|
+
expect(html).toContain('aria-expanded="false"');
|
|
332
|
+
expect(html).toContain('data-label-more="Read more"');
|
|
333
|
+
expect(html).toContain('data-label-less="Read less"');
|
|
334
|
+
// The full body is rendered (the marker + CSS clamp hide the tail), and the
|
|
335
|
+
// body carries the clamp flag.
|
|
336
|
+
expect(html).toContain("data-note-clamp");
|
|
337
|
+
expect(html).toContain("data-note-break");
|
|
338
|
+
expect(html).toContain("<p>Intro</p>");
|
|
339
|
+
expect(html).toContain("<p>Rest</p>");
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
it("renders untitled notes in full without a control when not truncated", () => {
|
|
343
|
+
const post = createPostView({
|
|
344
|
+
format: "note",
|
|
345
|
+
title: undefined,
|
|
346
|
+
bodyHtml: "<p>Whole note</p>",
|
|
347
|
+
summaryHasMore: undefined,
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
const html = renderWithI18n(NoteCard({ post, mode: "feed" }));
|
|
351
|
+
|
|
352
|
+
expect(html).toContain("<p>Whole note</p>");
|
|
353
|
+
expect(html).not.toContain("data-note-expand");
|
|
354
|
+
expect(html).not.toContain("data-note-clamp");
|
|
355
|
+
expect(html).not.toContain("feed-continue-link");
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
it("shows the full untitled note body without clamping on the detail page", () => {
|
|
359
|
+
const post = createPostView({
|
|
360
|
+
format: "note",
|
|
361
|
+
title: undefined,
|
|
362
|
+
bodyHtml: "<p>Intro</p><span data-note-break></span><p>Rest</p>",
|
|
363
|
+
summaryHasMore: true,
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
const html = renderWithI18n(NoteCard({ post, mode: "detail" }));
|
|
367
|
+
|
|
368
|
+
expect(html).toContain("<p>Rest</p>");
|
|
369
|
+
expect(html).not.toContain("data-note-expand");
|
|
370
|
+
expect(html).not.toContain("data-note-clamp");
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
it("renders the full untitled body with showFullBody and no clamp", () => {
|
|
374
|
+
const post = createPostView({
|
|
375
|
+
format: "note",
|
|
376
|
+
title: undefined,
|
|
377
|
+
bodyHtml: "<p>Intro</p><span data-note-break></span><p>Rest</p>",
|
|
378
|
+
summaryHasMore: true,
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
const html = renderWithI18n(
|
|
382
|
+
NoteCard({ post, mode: "feed", display: { showFullBody: true } }),
|
|
383
|
+
);
|
|
384
|
+
|
|
385
|
+
expect(html).toContain("<p>Rest</p>");
|
|
386
|
+
expect(html).not.toContain("data-note-expand");
|
|
387
|
+
expect(html).not.toContain("data-note-clamp");
|
|
315
388
|
});
|
|
316
389
|
|
|
317
390
|
it("moves rated link detail cards into the title block without changing feed ordering", () => {
|
|
@@ -277,6 +277,7 @@ export const BaseLayout: FC<PropsWithChildren<BaseLayoutProps>> = ({
|
|
|
277
277
|
{raw("<!DOCTYPE html>")}
|
|
278
278
|
<html
|
|
279
279
|
lang={resolvedLang}
|
|
280
|
+
data-theme={appConfig?.themeId}
|
|
280
281
|
data-theme-mode={themeMode}
|
|
281
282
|
data-site-path-prefix={sitePathPrefix}
|
|
282
283
|
data-asset-base-path={assetBasePath}
|
|
@@ -320,6 +320,19 @@ describe("BaseLayout", () => {
|
|
|
320
320
|
expect(html).toContain('data-asset-base-path="/blog/_assets"');
|
|
321
321
|
});
|
|
322
322
|
|
|
323
|
+
it("exposes the active theme id on the root html element", async () => {
|
|
324
|
+
const { BaseLayout } = await loadBaseLayout();
|
|
325
|
+
const html = renderToString(
|
|
326
|
+
BaseLayout({
|
|
327
|
+
title: "Jant",
|
|
328
|
+
c: createContext("featured", { themeId: "frost" }),
|
|
329
|
+
children: "Test",
|
|
330
|
+
}),
|
|
331
|
+
);
|
|
332
|
+
|
|
333
|
+
expect(html).toContain('data-theme="frost"');
|
|
334
|
+
});
|
|
335
|
+
|
|
323
336
|
it("renders theme-color tags that follow the active theme in auto mode", async () => {
|
|
324
337
|
const { BaseLayout } = await loadBaseLayout();
|
|
325
338
|
const html = renderToString(
|
|
@@ -45,14 +45,22 @@ function buildFilterUrl(
|
|
|
45
45
|
if (merged.format) params.set("format", merged.format);
|
|
46
46
|
if (merged.mediaKinds && merged.mediaKinds.length > 0) {
|
|
47
47
|
params.set("media", merged.mediaKinds.join(","));
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
params.set("hasMedia", merged.hasMedia ? "1" : "0");
|
|
48
|
+
} else if (merged.hasMedia !== undefined) {
|
|
49
|
+
params.set("media", merged.hasMedia ? "any" : "none");
|
|
51
50
|
}
|
|
52
51
|
if (merged.hasTitle !== undefined) {
|
|
53
|
-
params.set("
|
|
52
|
+
params.set("title", merged.hasTitle ? "any" : "none");
|
|
53
|
+
}
|
|
54
|
+
if (merged.hasReplies !== undefined) {
|
|
55
|
+
params.set("replies", merged.hasReplies ? "any" : "none");
|
|
56
|
+
}
|
|
57
|
+
if (merged.visibility) {
|
|
58
|
+
// "hidden" is the URL spelling of the internal latest_hidden value
|
|
59
|
+
params.set(
|
|
60
|
+
"visibility",
|
|
61
|
+
merged.visibility === "latest_hidden" ? "hidden" : merged.visibility,
|
|
62
|
+
);
|
|
54
63
|
}
|
|
55
|
-
if (merged.visibility) params.set("visibility", merged.visibility);
|
|
56
64
|
if (merged.view && merged.view !== "grid") params.set("view", merged.view);
|
|
57
65
|
|
|
58
66
|
const qs = params.toString();
|
|
@@ -493,9 +501,9 @@ const ViewToggle: FC<{
|
|
|
493
501
|
|
|
494
502
|
const ARCHIVE_VISIBILITIES: ArchiveVisibility[] = [
|
|
495
503
|
"public",
|
|
504
|
+
"featured",
|
|
496
505
|
"latest_hidden",
|
|
497
506
|
"private",
|
|
498
|
-
"featured",
|
|
499
507
|
];
|
|
500
508
|
|
|
501
509
|
function getVisibilityLabel(v: ArchiveVisibility): string {
|
|
@@ -547,9 +555,16 @@ const FILTER_ICONS = {
|
|
|
547
555
|
collection: "monitor",
|
|
548
556
|
format: "shapes",
|
|
549
557
|
media: "video",
|
|
558
|
+
thread: "git-branch",
|
|
550
559
|
visibility: "scan-eye",
|
|
551
560
|
} as const;
|
|
552
561
|
|
|
562
|
+
/** Icons for the thread filter options. */
|
|
563
|
+
const THREAD_ICONS = {
|
|
564
|
+
threads: "list-tree",
|
|
565
|
+
single: "git-commit-horizontal",
|
|
566
|
+
} as const;
|
|
567
|
+
|
|
553
568
|
const FilterBar: FC<{
|
|
554
569
|
filters: ArchiveFilters;
|
|
555
570
|
availableYears: number[];
|
|
@@ -756,6 +771,63 @@ const FilterBar: FC<{
|
|
|
756
771
|
})),
|
|
757
772
|
];
|
|
758
773
|
|
|
774
|
+
// --- Thread options ---------------------------------------------------------
|
|
775
|
+
|
|
776
|
+
const threadClearUrl = buildFilterUrl(
|
|
777
|
+
{ ...filters, hasReplies: undefined },
|
|
778
|
+
{ hasReplies: undefined },
|
|
779
|
+
sitePathPrefix,
|
|
780
|
+
);
|
|
781
|
+
|
|
782
|
+
const threadsLabel = i18n._(
|
|
783
|
+
msg({
|
|
784
|
+
message: "Threads",
|
|
785
|
+
comment: "@context: Archive thread filter - thread roots with replies",
|
|
786
|
+
}),
|
|
787
|
+
);
|
|
788
|
+
const singlePostsLabel = i18n._(
|
|
789
|
+
msg({
|
|
790
|
+
message: "Single posts",
|
|
791
|
+
comment: "@context: Archive thread filter - posts without replies",
|
|
792
|
+
}),
|
|
793
|
+
);
|
|
794
|
+
|
|
795
|
+
const threadOptions: ChipSelectOption[] = [
|
|
796
|
+
{
|
|
797
|
+
label: i18n._(
|
|
798
|
+
msg({
|
|
799
|
+
message: "All posts",
|
|
800
|
+
comment: "@context: Archive thread filter - threads and single posts",
|
|
801
|
+
}),
|
|
802
|
+
),
|
|
803
|
+
icon: FILTER_ICONS.thread,
|
|
804
|
+
value: threadClearUrl,
|
|
805
|
+
},
|
|
806
|
+
{
|
|
807
|
+
label: threadsLabel,
|
|
808
|
+
icon: THREAD_ICONS.threads,
|
|
809
|
+
value: buildFilterUrl(filters, { hasReplies: true }, sitePathPrefix),
|
|
810
|
+
},
|
|
811
|
+
{
|
|
812
|
+
label: singlePostsLabel,
|
|
813
|
+
icon: THREAD_ICONS.single,
|
|
814
|
+
value: buildFilterUrl(filters, { hasReplies: false }, sitePathPrefix),
|
|
815
|
+
},
|
|
816
|
+
];
|
|
817
|
+
|
|
818
|
+
const threadActiveLabel =
|
|
819
|
+
filters.hasReplies === true
|
|
820
|
+
? threadsLabel
|
|
821
|
+
: filters.hasReplies === false
|
|
822
|
+
? singlePostsLabel
|
|
823
|
+
: undefined;
|
|
824
|
+
const threadActiveIcon =
|
|
825
|
+
filters.hasReplies === true
|
|
826
|
+
? THREAD_ICONS.threads
|
|
827
|
+
: filters.hasReplies === false
|
|
828
|
+
? THREAD_ICONS.single
|
|
829
|
+
: undefined;
|
|
830
|
+
|
|
759
831
|
const activeKinds = filters.mediaKinds ?? [];
|
|
760
832
|
const mediaActiveLabel =
|
|
761
833
|
filters.hasMedia === false
|
|
@@ -829,6 +901,17 @@ const FilterBar: FC<{
|
|
|
829
901
|
iconOnly
|
|
830
902
|
/>
|
|
831
903
|
|
|
904
|
+
<ChipSelect
|
|
905
|
+
id="af-thread"
|
|
906
|
+
icon={FILTER_ICONS.thread}
|
|
907
|
+
options={threadOptions}
|
|
908
|
+
currentValue={currentUrl}
|
|
909
|
+
clearUrl={threadClearUrl}
|
|
910
|
+
activeLabel={threadActiveLabel}
|
|
911
|
+
activeIcon={threadActiveIcon}
|
|
912
|
+
iconOnly
|
|
913
|
+
/>
|
|
914
|
+
|
|
832
915
|
<ChipMediaSelect
|
|
833
916
|
id="af-media"
|
|
834
917
|
icon={FILTER_ICONS.media}
|
|
@@ -15,6 +15,7 @@ export const CollectionsPage: FC<CollectionsPageProps> = ({
|
|
|
15
15
|
items,
|
|
16
16
|
isAuthenticated,
|
|
17
17
|
sitePathPrefix = "",
|
|
18
|
+
siteOrigin = "",
|
|
18
19
|
}) => {
|
|
19
20
|
const { i18n } = useLingui();
|
|
20
21
|
const emptyMessage = i18n._(
|
|
@@ -27,7 +28,11 @@ export const CollectionsPage: FC<CollectionsPageProps> = ({
|
|
|
27
28
|
if (isAuthenticated) {
|
|
28
29
|
return (
|
|
29
30
|
<div class="py-6" data-page="collections">
|
|
30
|
-
<CollectionsManager
|
|
31
|
+
<CollectionsManager
|
|
32
|
+
items={items}
|
|
33
|
+
sitePathPrefix={sitePathPrefix}
|
|
34
|
+
siteOrigin={siteOrigin}
|
|
35
|
+
/>
|
|
31
36
|
</div>
|
|
32
37
|
);
|
|
33
38
|
}
|
|
@@ -54,6 +59,7 @@ export const CollectionsPage: FC<CollectionsPageProps> = ({
|
|
|
54
59
|
items={items}
|
|
55
60
|
emptyMessage={emptyMessage}
|
|
56
61
|
sitePathPrefix={sitePathPrefix}
|
|
62
|
+
siteOrigin={siteOrigin}
|
|
57
63
|
/>
|
|
58
64
|
</div>
|
|
59
65
|
</div>
|