@jant/core 0.6.8 → 0.6.10
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-CGHkOdme.js} +3450 -3121
- package/dist/app-D24n0DoH.js +6 -0
- package/dist/client/.vite/manifest.json +3 -3
- package/dist/client/_assets/{client-CXnEhyyv.js → client-DYrWuaIk.js} +1 -1
- package/dist/client/_assets/{client-auth-CSItbyU8.js → client-auth-B5Re0uCd.js} +187 -167
- package/dist/client/_assets/client-xWDl78yi.css +2 -0
- package/dist/{export-Be082J0n.js → export-DY1v5Iqu.js} +2 -2
- package/dist/{github-sync-D1Cw8mOY.js → github-sync-2_T7nbOv.js} +1 -1
- package/dist/{github-sync-_kPWM4m9.js → github-sync-LefaslGJ.js} +2 -2
- 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 +8 -2
- package/src/client/components/__tests__/jant-settings-general.test.ts +64 -12
- package/src/client/components/jant-compose-dialog.ts +12 -0
- package/src/client/components/jant-settings-general.ts +74 -21
- package/src/client/components/settings-types.ts +13 -0
- package/src/client/settings-bridge.ts +3 -0
- package/src/client/tiptap/__tests__/link-toolbar.test.ts +41 -0
- package/src/client/tiptap/__tests__/mark-exit.test.ts +99 -0
- package/src/client/tiptap/bubble-menu.ts +37 -4
- package/src/client/tiptap/link-toolbar.ts +63 -1
- 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 +282 -27
- package/src/i18n/locales/settings/en.ts +1 -1
- package/src/i18n/locales/settings/zh-Hans.po +282 -27
- package/src/i18n/locales/settings/zh-Hans.ts +1 -1
- package/src/i18n/locales/settings/zh-Hant.po +282 -27
- 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__/feed.test.ts +5 -1
- package/src/lib/feed.ts +6 -3
- 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 +22 -4
- package/src/routes/feed/__tests__/feed.test.ts +58 -19
- package/src/routes/feed/feed.ts +37 -28
- package/src/routes/pages/featured.tsx +17 -0
- package/src/routes/pages/latest.tsx +25 -0
- 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/post.ts +1 -1
- package/src/services/settings.ts +49 -15
- package/src/services/upload-session.ts +13 -3
- package/src/styles/tokens.css +21 -4
- package/src/styles/ui.css +44 -1
- package/src/types/bindings.ts +1 -0
- package/src/types/config.ts +13 -0
- package/src/ui/__tests__/color-themes.test.ts +2 -2
- package/src/ui/color-themes.ts +32 -0
- package/src/ui/dash/appearance/ColorThemeContent.tsx +264 -29
- package/src/ui/dash/settings/GeneralContent.tsx +54 -4
- package/src/ui/dash/settings/__tests__/GeneralContent.test.tsx +3 -2
- package/src/ui/layouts/BaseLayout.tsx +3 -2
- package/src/ui/layouts/__tests__/BaseLayout.test.tsx +17 -4
- package/dist/app-DaxS_Cz-.js +0 -6
- package/dist/client/_assets/client-C6peCkkD.css +0 -2
package/src/services/post.ts
CHANGED
|
@@ -2483,7 +2483,7 @@ export function createPostService(
|
|
|
2483
2483
|
.select()
|
|
2484
2484
|
.from(posts)
|
|
2485
2485
|
.where(and(eq(posts.siteId, siteId), eq(posts.threadId, rootId)))
|
|
2486
|
-
.orderBy(posts.createdAt);
|
|
2486
|
+
.orderBy(posts.createdAt, posts.id);
|
|
2487
2487
|
|
|
2488
2488
|
return hydratePosts(rows);
|
|
2489
2489
|
},
|
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
|
@@ -381,6 +381,21 @@
|
|
|
381
381
|
@media (max-width: 760px) {
|
|
382
382
|
:root {
|
|
383
383
|
--site-padding: 1.875rem;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/* Tufte two-column → single-column collapse.
|
|
388
|
+
*
|
|
389
|
+
* Below this width the post column is already narrowed to `min(100%, 35rem)`
|
|
390
|
+
* (preset.css) and there is no room for a 45% sidenote gutter, so all content
|
|
391
|
+
* goes full-width. Single images (MediaGallery getSingleVisualWidth) and link
|
|
392
|
+
* previews (.link-preview) read this token directly, so they collapse here too.
|
|
393
|
+
*
|
|
394
|
+
* Keep this breakpoint in sync with the post-column collapse (preset.css /
|
|
395
|
+
* ui.css feed-divider, both `max-width: 1024px`) and the sidenote float→inline
|
|
396
|
+
* collapse (ui.css). They are one layout switch; do not let them drift apart. */
|
|
397
|
+
@media (max-width: 1024px) {
|
|
398
|
+
:root {
|
|
384
399
|
--layout-content-width: 100%;
|
|
385
400
|
}
|
|
386
401
|
}
|
|
@@ -467,13 +482,15 @@
|
|
|
467
482
|
|
|
468
483
|
@media (max-width: 760px), (hover: none) and (pointer: coarse) {
|
|
469
484
|
:root {
|
|
470
|
-
/* Content layer — tighter scale for mobile reading
|
|
471
|
-
Ratio ≈ 1.
|
|
472
|
-
|
|
485
|
+
/* Content layer — tighter scale for mobile reading.
|
|
486
|
+
Ratio ≈ 1.88 : 1.42 : 1.15 : 1 (display:title:subtitle:body).
|
|
487
|
+
At 15px root × 0.78 content-scale: 28.7 / 21.6 / 17.6 / 15.2px.
|
|
488
|
+
Body is floored to a 16px minimum below (max()) for readability;
|
|
489
|
+
the floor still yields to calc() when the user zooms the root up. */
|
|
473
490
|
--type-content-display: calc(2.45rem * var(--type-content-scale));
|
|
474
491
|
--type-content-title: calc(1.85rem * var(--type-content-scale));
|
|
475
492
|
--type-content-subtitle: calc(1.5rem * var(--type-content-scale));
|
|
476
|
-
--type-content-body: calc(1.3rem * var(--type-content-scale));
|
|
493
|
+
--type-content-body: max(16px, calc(1.3rem * var(--type-content-scale)));
|
|
477
494
|
|
|
478
495
|
/* UI layer — pixel floors for touch targets */
|
|
479
496
|
--type-ui-title: max(16px, var(--type-secondary));
|
package/src/styles/ui.css
CHANGED
|
@@ -2433,7 +2433,12 @@
|
|
|
2433
2433
|
display: none;
|
|
2434
2434
|
}
|
|
2435
2435
|
|
|
2436
|
-
|
|
2436
|
+
/* Collapse sidenotes from floated margin notes to inline tap-to-toggle at the
|
|
2437
|
+
same width the post column drops to `min(100%, 35rem)` (preset.css) and the
|
|
2438
|
+
Tufte gutter disappears — see --layout-content-width in tokens.css. Below
|
|
2439
|
+
1024px a floated `width: 50%; margin-right: -60%` note has no gutter to land
|
|
2440
|
+
in and overflows the right edge, so it must inline instead. */
|
|
2441
|
+
@media (max-width: 1024px) {
|
|
2437
2442
|
label.margin-toggle:not(.sidenote-number) {
|
|
2438
2443
|
display: inline;
|
|
2439
2444
|
cursor: pointer;
|
|
@@ -11110,6 +11115,24 @@
|
|
|
11110
11115
|
line-height: 1.28;
|
|
11111
11116
|
}
|
|
11112
11117
|
|
|
11118
|
+
.theme-preview-accent {
|
|
11119
|
+
display: inline-flex;
|
|
11120
|
+
align-items: center;
|
|
11121
|
+
gap: 0.3rem;
|
|
11122
|
+
white-space: nowrap;
|
|
11123
|
+
color: var(--preview-muted-light);
|
|
11124
|
+
}
|
|
11125
|
+
|
|
11126
|
+
.theme-preview-swatch {
|
|
11127
|
+
flex: none;
|
|
11128
|
+
width: 0.62rem;
|
|
11129
|
+
height: 0.62rem;
|
|
11130
|
+
border-radius: 3px;
|
|
11131
|
+
background: var(--preview-primary-light);
|
|
11132
|
+
box-shadow: 0 0 0 1px
|
|
11133
|
+
color-mix(in srgb, var(--preview-fg-light) 18%, transparent);
|
|
11134
|
+
}
|
|
11135
|
+
|
|
11113
11136
|
.theme-preview-meta {
|
|
11114
11137
|
color: var(--preview-muted-light);
|
|
11115
11138
|
line-height: 1.55;
|
|
@@ -11143,6 +11166,16 @@
|
|
|
11143
11166
|
color: var(--preview-muted-dark);
|
|
11144
11167
|
}
|
|
11145
11168
|
|
|
11169
|
+
.theme-preview-panel[data-theme-preview-mode="dark"] .theme-preview-accent {
|
|
11170
|
+
color: var(--preview-muted-dark);
|
|
11171
|
+
}
|
|
11172
|
+
|
|
11173
|
+
.theme-preview-panel[data-theme-preview-mode="dark"] .theme-preview-swatch {
|
|
11174
|
+
background: var(--preview-primary-dark);
|
|
11175
|
+
box-shadow: 0 0 0 1px
|
|
11176
|
+
color-mix(in srgb, var(--preview-fg-dark) 18%, transparent);
|
|
11177
|
+
}
|
|
11178
|
+
|
|
11146
11179
|
.theme-preview-panel[data-theme-preview-mode="dark"] .theme-preview-divider {
|
|
11147
11180
|
border-color: var(--preview-border-dark);
|
|
11148
11181
|
}
|
|
@@ -11671,6 +11704,16 @@
|
|
|
11671
11704
|
color: var(--preview-muted-dark);
|
|
11672
11705
|
}
|
|
11673
11706
|
|
|
11707
|
+
.theme-preview-panel[data-theme-preview-mode="auto"] .theme-preview-accent {
|
|
11708
|
+
color: var(--preview-muted-dark);
|
|
11709
|
+
}
|
|
11710
|
+
|
|
11711
|
+
.theme-preview-panel[data-theme-preview-mode="auto"] .theme-preview-swatch {
|
|
11712
|
+
background: var(--preview-primary-dark);
|
|
11713
|
+
box-shadow: 0 0 0 1px
|
|
11714
|
+
color-mix(in srgb, var(--preview-fg-dark) 18%, transparent);
|
|
11715
|
+
}
|
|
11716
|
+
|
|
11674
11717
|
.theme-preview-panel[data-theme-preview-mode="auto"]
|
|
11675
11718
|
.theme-preview-divider {
|
|
11676
11719
|
border-color: var(--preview-border-dark);
|
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;
|
|
@@ -2,8 +2,8 @@ import { describe, expect, it } from "vitest";
|
|
|
2
2
|
import { BUILTIN_COLOR_THEMES } from "../color-themes.js";
|
|
3
3
|
|
|
4
4
|
describe("BUILTIN_COLOR_THEMES", () => {
|
|
5
|
-
it("contains
|
|
6
|
-
expect(BUILTIN_COLOR_THEMES).toHaveLength(
|
|
5
|
+
it("contains 15 themes", () => {
|
|
6
|
+
expect(BUILTIN_COLOR_THEMES).toHaveLength(15);
|
|
7
7
|
});
|
|
8
8
|
|
|
9
9
|
it("keeps Tufte as the first theme", () => {
|
package/src/ui/color-themes.ts
CHANGED
|
@@ -624,6 +624,38 @@ export const BUILTIN_COLOR_THEMES: ColorTheme[] = [
|
|
|
624
624
|
},
|
|
625
625
|
}),
|
|
626
626
|
|
|
627
|
+
// Pure white with neutral grays — zero tint, clean and clinical
|
|
628
|
+
defineTheme({
|
|
629
|
+
id: "snow",
|
|
630
|
+
name: "Snow",
|
|
631
|
+
light: {
|
|
632
|
+
bg: "oklch(1 0 0)",
|
|
633
|
+
fg: "oklch(0.205 0 0)",
|
|
634
|
+
primary: "oklch(0.25 0 0)",
|
|
635
|
+
primaryFg: "oklch(0.99 0 0)",
|
|
636
|
+
siteAccent: "oklch(0.37 0 0)",
|
|
637
|
+
muted: "oklch(0.965 0 0)",
|
|
638
|
+
mutedFg: "oklch(0.5 0 0)",
|
|
639
|
+
border: "oklch(0.91 0 0)",
|
|
640
|
+
readingTitle: "oklch(0.17 0 0)",
|
|
641
|
+
readingHeading: "oklch(0.205 0 0)",
|
|
642
|
+
readingBody: "oklch(0.24 0 0)",
|
|
643
|
+
readingQuote: "oklch(0.4 0 0)",
|
|
644
|
+
dashBg: "oklch(0.975 0 0)",
|
|
645
|
+
},
|
|
646
|
+
dark: {
|
|
647
|
+
bg: "oklch(0.17 0 0)",
|
|
648
|
+
fg: "oklch(0.92 0 0)",
|
|
649
|
+
primary: "oklch(0.85 0 0)",
|
|
650
|
+
primaryFg: "oklch(0.17 0 0)",
|
|
651
|
+
siteAccent: "oklch(0.78 0 0)",
|
|
652
|
+
muted: "oklch(0.235 0 0)",
|
|
653
|
+
mutedFg: "oklch(0.65 0 0)",
|
|
654
|
+
border: "oklch(0.3 0 0)",
|
|
655
|
+
dashBg: "oklch(0.15 0 0)",
|
|
656
|
+
},
|
|
657
|
+
}),
|
|
658
|
+
|
|
627
659
|
// Deep coffee brown — rich and grounded
|
|
628
660
|
defineTheme({
|
|
629
661
|
id: "espresso",
|