@jant/core 0.6.8 → 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 +1 -0
- package/dist/{app-9P4rVCe2.js → app-C-jxWmAV.js} +12324 -12157
- package/dist/app-DqHzOwL5.js +6 -0
- package/dist/client/.vite/manifest.json +3 -3
- package/dist/client/_assets/{client-C6peCkkD.css → client-CGf2m3qp.css} +1 -1
- package/dist/client/_assets/{client-CXnEhyyv.js → client-DWy1LEEk.js} +1 -1
- package/dist/client/_assets/{client-auth-CSItbyU8.js → client-auth-Blg-a5Ep.js} +180 -162
- package/dist/{export-Be082J0n.js → export-C2DIB7mm.js} +2 -2
- package/dist/{github-sync-_kPWM4m9.js → github-sync-7XQ5ZM6z.js} +2 -2
- package/dist/{github-sync-D1Cw8mOY.js → github-sync-BEFCfLKK.js} +1 -1
- package/dist/index.js +3 -3
- package/dist/node.js +4 -4
- package/package.json +1 -1
- 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/jant-compose-dialog.ts +12 -0
- package/src/client/components/jant-settings-general.ts +56 -18
- package/src/client/components/settings-types.ts +11 -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/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/settings/en.po +25 -10
- package/src/i18n/locales/settings/en.ts +1 -1
- package/src/i18n/locales/settings/zh-Hans.po +25 -10
- package/src/i18n/locales/settings/zh-Hans.ts +1 -1
- package/src/i18n/locales/settings/zh-Hant.po +25 -10
- 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/ids.ts +1 -0
- package/src/lib/resolve-config.ts +1 -0
- package/src/lib/upload.ts +14 -0
- 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 +19 -1
- package/src/routes/api/settings.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/settings.tsx +15 -2
- package/src/services/__tests__/media.test.ts +191 -30
- package/src/services/__tests__/settings.test.ts +55 -0
- package/src/services/bootstrap.ts +7 -0
- package/src/services/export-theme/layouts/_default/baseof.html +2 -1
- package/src/services/media.ts +169 -42
- package/src/services/settings.ts +49 -15
- package/src/services/upload-session.ts +13 -3
- package/src/styles/tokens.css +6 -4
- package/src/types/bindings.ts +1 -0
- package/src/types/config.ts +13 -0
- package/src/ui/dash/settings/GeneralContent.tsx +38 -4
- package/src/ui/layouts/BaseLayout.tsx +1 -0
- package/src/ui/layouts/__tests__/BaseLayout.test.tsx +13 -0
- package/dist/app-DaxS_Cz-.js +0 -6
package/src/services/settings.ts
CHANGED
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
} from "../lib/constants.js";
|
|
20
20
|
import {
|
|
21
21
|
baseLocale,
|
|
22
|
+
isLocale,
|
|
22
23
|
isValidContentLanguage,
|
|
23
24
|
normalizeContentLanguage,
|
|
24
25
|
} from "../i18n/locales.js";
|
|
@@ -41,6 +42,8 @@ export interface GeneralSettingsData {
|
|
|
41
42
|
siteDescription: string;
|
|
42
43
|
siteFooter: string;
|
|
43
44
|
siteLanguage: string;
|
|
45
|
+
/** Admin UI locale; empty string follows the content language. */
|
|
46
|
+
dashboardLanguage?: string;
|
|
44
47
|
cjkSerifFont: string;
|
|
45
48
|
showJantBrandingOnHome: boolean;
|
|
46
49
|
homeDefaultView?: FeedKind;
|
|
@@ -61,6 +64,11 @@ export interface SiteSettingsResult {
|
|
|
61
64
|
|
|
62
65
|
export interface LocaleSettingsData {
|
|
63
66
|
siteLanguage: string;
|
|
67
|
+
/**
|
|
68
|
+
* Admin dashboard UI locale. Empty string clears the explicit setting so the
|
|
69
|
+
* dashboard follows the content language. When set, must be a catalog locale.
|
|
70
|
+
*/
|
|
71
|
+
dashboardLanguage?: string;
|
|
64
72
|
cjkSerifFont: string;
|
|
65
73
|
timeZone: string;
|
|
66
74
|
}
|
|
@@ -97,7 +105,11 @@ export interface SettingsService {
|
|
|
97
105
|
): Promise<SiteSettingsResult>;
|
|
98
106
|
updateLocaleSettings(
|
|
99
107
|
data: LocaleSettingsData,
|
|
100
|
-
opts: {
|
|
108
|
+
opts: {
|
|
109
|
+
oldLanguage: string;
|
|
110
|
+
oldCjkSerifFont?: string;
|
|
111
|
+
oldDashboardLanguage?: string;
|
|
112
|
+
},
|
|
101
113
|
): Promise<{ languageChanged: boolean }>;
|
|
102
114
|
updateFeedSettings(data: { mainRssFeed?: FeedKind }): Promise<void>;
|
|
103
115
|
updateHomeBranding(showJantBrandingOnHome: boolean): Promise<void>;
|
|
@@ -131,15 +143,16 @@ export interface SettingsService {
|
|
|
131
143
|
*/
|
|
132
144
|
uploadAvatar(data: AvatarUploadData, deps: AvatarUploadDeps): Promise<void>;
|
|
133
145
|
/**
|
|
134
|
-
* Remove avatar and all favicon-related settings.
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
*
|
|
146
|
+
* Remove avatar and all favicon-related settings. The apple-touch-icon's
|
|
147
|
+
* media row is deleted and its storage object is retired (moved to the
|
|
148
|
+
* recycle bin, or deleted) via the media service when supplied — so removal
|
|
149
|
+
* is recoverable rather than erasing the bytes with no trace.
|
|
138
150
|
*/
|
|
139
|
-
removeAvatar(
|
|
140
|
-
storage?: StorageDriver | null
|
|
141
|
-
|
|
142
|
-
|
|
151
|
+
removeAvatar(deps?: {
|
|
152
|
+
storage?: StorageDriver | null;
|
|
153
|
+
media?: MediaService;
|
|
154
|
+
storageProvider?: string;
|
|
155
|
+
}): Promise<void>;
|
|
143
156
|
}
|
|
144
157
|
|
|
145
158
|
export function createSettingsService(
|
|
@@ -301,6 +314,26 @@ export function createSettingsService(
|
|
|
301
314
|
normalizeContentLanguage(trimmedLanguage),
|
|
302
315
|
);
|
|
303
316
|
|
|
317
|
+
// Dashboard UI locale. undefined = leave untouched; "" = clear so the
|
|
318
|
+
// dashboard follows the content language; otherwise it must be one of the
|
|
319
|
+
// translated catalog locales.
|
|
320
|
+
let dashboardChanged = false;
|
|
321
|
+
if (data.dashboardLanguage !== undefined) {
|
|
322
|
+
const dashboardLanguage = data.dashboardLanguage.trim();
|
|
323
|
+
if (dashboardLanguage && !isLocale(dashboardLanguage)) {
|
|
324
|
+
throw new ValidationError(
|
|
325
|
+
"Choose a dashboard language Jant is translated into.",
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
if (dashboardLanguage) {
|
|
329
|
+
await this.set("DASHBOARD_LANGUAGE", dashboardLanguage);
|
|
330
|
+
} else {
|
|
331
|
+
await this.remove("DASHBOARD_LANGUAGE");
|
|
332
|
+
}
|
|
333
|
+
dashboardChanged =
|
|
334
|
+
(opts.oldDashboardLanguage ?? "") !== dashboardLanguage;
|
|
335
|
+
}
|
|
336
|
+
|
|
304
337
|
// CJK serif font setting
|
|
305
338
|
const cjkFont = data.cjkSerifFont?.trim() ?? "";
|
|
306
339
|
if (cjkFont && isCjkSerifFont(cjkFont) && cjkFont !== "off") {
|
|
@@ -329,7 +362,8 @@ export function createSettingsService(
|
|
|
329
362
|
return {
|
|
330
363
|
languageChanged:
|
|
331
364
|
opts.oldLanguage !== trimmedLanguage ||
|
|
332
|
-
(opts.oldCjkSerifFont ?? "off") !== effectiveCjkFont
|
|
365
|
+
(opts.oldCjkSerifFont ?? "off") !== effectiveCjkFont ||
|
|
366
|
+
dashboardChanged,
|
|
333
367
|
};
|
|
334
368
|
},
|
|
335
369
|
|
|
@@ -469,19 +503,19 @@ export function createSettingsService(
|
|
|
469
503
|
await this.set("SITE_FAVICON_VERSION", version);
|
|
470
504
|
},
|
|
471
505
|
|
|
472
|
-
async removeAvatar(
|
|
506
|
+
async removeAvatar(deps) {
|
|
473
507
|
const appleTouchKey = await this.get("SITE_FAVICON_APPLE_TOUCH");
|
|
474
|
-
if (storage && appleTouchKey) {
|
|
475
|
-
await storage.delete(appleTouchKey);
|
|
476
|
-
}
|
|
477
508
|
|
|
509
|
+
// Retire the apple-touch-icon through the media service so its object goes
|
|
510
|
+
// to the recycle bin (recoverable) rather than being erased now. Also
|
|
511
|
+
// removes its media row.
|
|
478
512
|
if (deps?.media && deps.storageProvider && appleTouchKey) {
|
|
479
513
|
const existing = await deps.media.getByStorageKey(
|
|
480
514
|
appleTouchKey,
|
|
481
515
|
deps.storageProvider,
|
|
482
516
|
);
|
|
483
517
|
if (existing) {
|
|
484
|
-
await deps.media.delete(existing.id);
|
|
518
|
+
await deps.media.delete(existing.id, deps.storage);
|
|
485
519
|
}
|
|
486
520
|
}
|
|
487
521
|
|
|
@@ -148,6 +148,7 @@ export interface UploadSessionService {
|
|
|
148
148
|
abortedMultipartUploads: number;
|
|
149
149
|
deletedSessions: number;
|
|
150
150
|
deletedOrphanMedia: number;
|
|
151
|
+
purgedStorageObjects: number;
|
|
151
152
|
}>;
|
|
152
153
|
abort(id: string, deps: { storage: StorageDriver }): Promise<void>;
|
|
153
154
|
}
|
|
@@ -766,9 +767,10 @@ export function createUploadSessionService(
|
|
|
766
767
|
}
|
|
767
768
|
|
|
768
769
|
// Reap finalized media that was uploaded during compose but never
|
|
769
|
-
// attached to a post (`postId IS NULL`) past the grace window.
|
|
770
|
-
// the
|
|
771
|
-
//
|
|
770
|
+
// attached to a post (`postId IS NULL`) past the grace window. This now
|
|
771
|
+
// soft-deletes via the media service: the DB row is removed and the
|
|
772
|
+
// storage object is enqueued for deferred deletion. Bounded by the same
|
|
773
|
+
// per-run `limit`; any backlog drains over subsequent runs.
|
|
772
774
|
const orphanIds = await media.listOrphanedMediaIds({
|
|
773
775
|
before: now() - ORPHAN_MEDIA_GRACE_SECONDS,
|
|
774
776
|
limit,
|
|
@@ -777,10 +779,18 @@ export function createUploadSessionService(
|
|
|
777
779
|
await media.deleteByIds(orphanIds, deps.storage);
|
|
778
780
|
}
|
|
779
781
|
|
|
782
|
+
// Physically delete storage objects whose recycle window has elapsed.
|
|
783
|
+
// Skips any object a live media row still references (re-uploads).
|
|
784
|
+
const purgedStorageObjects = await media.purgeDueStorageObjects(
|
|
785
|
+
{ before: now(), limit, provider: deps.storageDriver },
|
|
786
|
+
deps.storage,
|
|
787
|
+
);
|
|
788
|
+
|
|
780
789
|
return {
|
|
781
790
|
abortedMultipartUploads,
|
|
782
791
|
deletedSessions,
|
|
783
792
|
deletedOrphanMedia: orphanIds.length,
|
|
793
|
+
purgedStorageObjects,
|
|
784
794
|
};
|
|
785
795
|
},
|
|
786
796
|
};
|
package/src/styles/tokens.css
CHANGED
|
@@ -467,13 +467,15 @@
|
|
|
467
467
|
|
|
468
468
|
@media (max-width: 760px), (hover: none) and (pointer: coarse) {
|
|
469
469
|
:root {
|
|
470
|
-
/* Content layer — tighter scale for mobile reading
|
|
471
|
-
Ratio ≈ 1.
|
|
472
|
-
|
|
470
|
+
/* Content layer — tighter scale for mobile reading.
|
|
471
|
+
Ratio ≈ 1.88 : 1.42 : 1.15 : 1 (display:title:subtitle:body).
|
|
472
|
+
At 15px root × 0.78 content-scale: 28.7 / 21.6 / 17.6 / 15.2px.
|
|
473
|
+
Body is floored to a 16px minimum below (max()) for readability;
|
|
474
|
+
the floor still yields to calc() when the user zooms the root up. */
|
|
473
475
|
--type-content-display: calc(2.45rem * var(--type-content-scale));
|
|
474
476
|
--type-content-title: calc(1.85rem * var(--type-content-scale));
|
|
475
477
|
--type-content-subtitle: calc(1.5rem * var(--type-content-scale));
|
|
476
|
-
--type-content-body: calc(1.3rem * var(--type-content-scale));
|
|
478
|
+
--type-content-body: max(16px, calc(1.3rem * var(--type-content-scale)));
|
|
477
479
|
|
|
478
480
|
/* UI layer — pixel floors for touch targets */
|
|
479
481
|
--type-ui-title: max(16px, var(--type-secondary));
|
package/src/types/bindings.ts
CHANGED
|
@@ -35,6 +35,7 @@ export interface Bindings {
|
|
|
35
35
|
SITE_NAME?: EnvBindingValue;
|
|
36
36
|
SITE_DESCRIPTION?: EnvBindingValue;
|
|
37
37
|
SITE_LANGUAGE?: EnvBindingValue;
|
|
38
|
+
DASHBOARD_LANGUAGE?: EnvBindingValue;
|
|
38
39
|
HOME_DEFAULT_VIEW?: EnvBindingValue;
|
|
39
40
|
MAIN_RSS_FEED?: EnvBindingValue;
|
|
40
41
|
TIME_ZONE?: EnvBindingValue;
|
package/src/types/config.ts
CHANGED
|
@@ -41,6 +41,13 @@ export const CONFIG_FIELDS = {
|
|
|
41
41
|
envOnly: false,
|
|
42
42
|
envKeys: ["SITE_LANGUAGE"],
|
|
43
43
|
},
|
|
44
|
+
// Admin dashboard UI locale. Empty means "follow the content language"
|
|
45
|
+
// (resolved through the catalog fallback chain, i.e. today's behaviour).
|
|
46
|
+
DASHBOARD_LANGUAGE: {
|
|
47
|
+
defaultValue: "",
|
|
48
|
+
envOnly: false,
|
|
49
|
+
envKeys: ["DASHBOARD_LANGUAGE"],
|
|
50
|
+
},
|
|
44
51
|
CJK_SERIF_FONT: {
|
|
45
52
|
defaultValue: "off",
|
|
46
53
|
envOnly: false,
|
|
@@ -455,6 +462,12 @@ export interface AppConfig {
|
|
|
455
462
|
/** true only when description is set in DB or ENV (not just the default) */
|
|
456
463
|
siteDescriptionExplicit: boolean;
|
|
457
464
|
siteLanguage: string;
|
|
465
|
+
/**
|
|
466
|
+
* Admin dashboard UI locale. Empty string means "follow the content
|
|
467
|
+
* language" (derived via the catalog fallback chain). When set, it is one of
|
|
468
|
+
* the translated catalog locales ("en", "zh-Hans", "zh-Hant").
|
|
469
|
+
*/
|
|
470
|
+
dashboardLanguage: string;
|
|
458
471
|
/** CJK serif font locale: "off", "zh-Hans", "zh-Hant", "ja", or "ko" */
|
|
459
472
|
cjkSerifFont: string;
|
|
460
473
|
homeDefaultView: string;
|
|
@@ -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}
|
|
@@ -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(
|