@jant/core 0.3.35 → 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/module-RjUF93sV.js +716 -0
- package/dist/client/assets/native-48B9X9Wg.js +1 -0
- package/dist/client/assets/url-FWFqPJPb.js +1 -0
- package/dist/client/client.css +1 -1
- package/dist/client/client.js +4564 -3013
- package/dist/index.js +12885 -8161
- package/package.json +23 -6
- package/src/__tests__/helpers/app.ts +10 -10
- package/src/__tests__/helpers/db.ts +91 -87
- package/src/app.tsx +157 -31
- 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/{lib → client}/avatar-upload.ts +4 -3
- package/src/{lib → client}/collection-form-bridge.ts +2 -2
- package/src/{ui → client}/components/__tests__/jant-collection-form.test.ts +26 -9
- package/src/client/components/__tests__/jant-compose-dialog.test.ts +1140 -0
- package/src/client/components/__tests__/jant-compose-editor.test.ts +504 -0
- package/src/{ui → client}/components/__tests__/jant-post-form.test.ts +37 -17
- package/src/{ui → client}/components/__tests__/jant-settings-avatar.test.ts +2 -2
- package/src/{ui → client}/components/__tests__/jant-settings-general.test.ts +3 -3
- package/src/client/components/collection-sidebar-types.ts +43 -0
- package/src/{ui → client}/components/collection-types.ts +3 -4
- package/src/client/components/compose-types.ts +174 -0
- package/src/client/components/jant-collection-form.ts +667 -0
- package/src/client/components/jant-collection-sidebar.ts +805 -0
- package/src/client/components/jant-compose-dialog.ts +2161 -0
- package/src/client/components/jant-compose-editor.ts +1813 -0
- package/src/client/components/jant-compose-fullscreen.ts +283 -0
- package/src/client/components/jant-media-lightbox.ts +259 -0
- package/src/{ui → client}/components/jant-nav-manager.ts +97 -298
- package/src/{ui → client}/components/jant-post-form.ts +141 -12
- package/src/client/components/jant-post-menu.ts +1019 -0
- package/src/{ui → client}/components/jant-settings-avatar.ts +3 -3
- package/src/{ui → client}/components/jant-settings-general.ts +38 -4
- package/src/client/components/jant-text-preview.ts +232 -0
- package/src/{ui → client}/components/nav-manager-types.ts +6 -18
- package/src/{ui → client}/components/post-form-template.ts +137 -38
- package/src/{ui → client}/components/post-form-types.ts +15 -4
- package/src/client/compose-bridge.ts +583 -0
- package/src/{lib → client}/image-processor.ts +26 -8
- package/src/client/lazy-slugify.ts +51 -0
- package/src/client/media-metadata.ts +247 -0
- package/src/client/multipart-upload.ts +160 -0
- package/src/{lib → client}/nav-manager-bridge.ts +1 -1
- package/src/{lib → client}/post-form-bridge.ts +53 -2
- package/src/{lib → client}/settings-bridge.ts +3 -15
- package/src/client/thread-context.ts +140 -0
- package/src/client/tiptap/bubble-menu.ts +205 -0
- package/src/client/tiptap/create-editor.ts +86 -0
- package/src/client/tiptap/exitable-marks.ts +73 -0
- package/src/client/tiptap/extensions.ts +65 -0
- package/src/client/tiptap/image-node.ts +482 -0
- package/src/client/tiptap/link-toolbar.ts +371 -0
- package/src/client/tiptap/more-break.ts +50 -0
- package/src/client/tiptap/paste-image.ts +129 -0
- package/src/client/tiptap/slash-commands.ts +438 -0
- package/src/{lib → client}/toast.ts +101 -3
- package/src/client/types/sortablejs.d.ts +44 -0
- package/src/client/upload-with-metadata.ts +54 -0
- package/src/client/video-processor.ts +207 -0
- package/src/client.ts +27 -17
- 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 -140
- 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 +783 -1087
- package/src/i18n/locales/en.ts +1 -1
- package/src/i18n/locales/zh-Hans.po +867 -812
- package/src/i18n/locales/zh-Hans.ts +1 -1
- package/src/i18n/locales/zh-Hant.po +878 -823
- 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__/resolve-config.test.ts +2 -2
- package/src/lib/__tests__/schemas.test.ts +186 -65
- 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__/url.test.ts +2 -2
- package/src/lib/__tests__/view.test.ts +140 -65
- package/src/lib/blurhash-placeholder.ts +102 -0
- package/src/lib/constants.ts +3 -1
- package/src/lib/emoji-catalog.ts +963 -0
- package/src/lib/errors.ts +11 -8
- package/src/lib/feed.ts +77 -31
- 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 +22 -12
- package/src/lib/nanoid.ts +29 -0
- package/src/lib/navigation.ts +1 -1
- package/src/lib/render.tsx +24 -5
- package/src/lib/resolve-config.ts +13 -2
- package/src/lib/schemas.ts +226 -58
- 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 +158 -0
- package/src/lib/theme.ts +11 -8
- package/src/lib/timeline.ts +76 -34
- package/src/lib/tiptap-render.ts +191 -0
- package/src/lib/tiptap-to-markdown.ts +305 -0
- package/src/lib/upload.ts +263 -14
- package/src/lib/url.ts +37 -22
- package/src/lib/view.ts +236 -55
- 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/error-handler.ts +3 -3
- package/src/middleware/onboarding.ts +1 -1
- package/src/middleware/secure-headers.ts +40 -0
- package/src/preset.css +83 -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 +57 -31
- 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 +81 -62
- package/src/routes/api/search.ts +3 -4
- package/src/routes/api/upload-multipart.ts +245 -0
- package/src/routes/api/upload.ts +92 -24
- package/src/routes/auth/__tests__/setup.test.ts +20 -60
- package/src/routes/auth/reset.tsx +5 -4
- package/src/routes/auth/setup.tsx +39 -31
- package/src/routes/auth/signin.tsx +13 -14
- package/src/routes/compose.tsx +27 -63
- package/src/routes/dash/__tests__/settings-avatar.test.ts +44 -9
- package/src/routes/dash/custom-urls.tsx +414 -0
- package/src/routes/dash/settings.tsx +475 -99
- package/src/routes/feed/__tests__/rss.test.ts +22 -23
- package/src/routes/feed/rss.ts +6 -2
- 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 +36 -18
- package/src/routes/pages/archive.tsx +177 -37
- package/src/routes/pages/collection.tsx +43 -14
- package/src/routes/pages/collections.tsx +11 -2
- package/src/routes/pages/featured.tsx +27 -3
- package/src/routes/pages/home.tsx +15 -14
- 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 +800 -230
- package/src/services/__tests__/search.test.ts +67 -10
- package/src/services/__tests__/settings.test.ts +3 -3
- 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 +764 -172
- package/src/services/search.ts +161 -74
- package/src/services/settings.ts +6 -2
- package/src/styles/components.css +293 -62
- package/src/styles/tokens.css +93 -5
- package/src/styles/ui.css +4349 -766
- package/src/types/bindings.ts +8 -0
- package/src/types/config.ts +34 -4
- package/src/types/constants.ts +17 -2
- package/src/types/entities.ts +83 -37
- package/src/types/operations.ts +20 -27
- package/src/types/props.ts +52 -17
- package/src/types/views.ts +48 -24
- package/src/ui/color-themes.ts +133 -23
- package/src/ui/compose/ComposeDialog.tsx +255 -16
- 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 +12 -2
- package/src/ui/dash/appearance/AdvancedContent.tsx +71 -59
- package/src/ui/dash/appearance/ColorThemeContent.tsx +48 -33
- package/src/ui/dash/appearance/FontThemeContent.tsx +65 -73
- package/src/ui/dash/appearance/NavigationContent.tsx +106 -135
- package/src/ui/dash/index.ts +0 -3
- package/src/ui/dash/settings/AccountContent.tsx +87 -146
- 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 +78 -0
- package/src/ui/dash/settings/GeneralContent.tsx +3 -62
- package/src/ui/dash/settings/SessionsContent.tsx +159 -0
- package/src/ui/dash/settings/SettingsRootContent.tsx +266 -0
- 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 +116 -103
- package/src/ui/pages/ArchivePage.tsx +923 -95
- package/src/ui/pages/CollectionPage.tsx +6 -35
- package/src/ui/pages/CollectionsPage.tsx +2 -1
- package/src/ui/pages/ComposePage.tsx +54 -0
- package/src/ui/pages/FeaturedPage.tsx +2 -1
- package/src/ui/pages/HomePage.tsx +1 -1
- package/src/ui/pages/PostPage.tsx +30 -45
- package/src/ui/pages/SearchPage.tsx +182 -38
- package/src/ui/shared/AdminBreadcrumb.tsx +29 -0
- package/src/ui/shared/CollectionsSidebar.tsx +239 -4
- package/src/ui/shared/MediaGallery.tsx +475 -41
- 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/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/meta/0003_snapshot.json +0 -821
- package/src/lib/__tests__/sqid.test.ts +0 -65
- package/src/lib/collections-reorder.ts +0 -28
- package/src/lib/compose-bridge.ts +0 -280
- package/src/lib/media-upload.ts +0 -148
- 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/appearance.tsx +0 -240
- package/src/routes/dash/collections.tsx +0 -211
- package/src/routes/dash/index.tsx +0 -103
- package/src/routes/dash/media.tsx +0 -132
- package/src/routes/dash/pages.tsx +0 -239
- package/src/routes/dash/posts.tsx +0 -334
- package/src/routes/dash/redirects.tsx +0 -257
- 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 -203
- package/src/services/path-registry.ts +0 -160
- package/src/services/redirect.ts +0 -97
- package/src/types/sortablejs.d.ts +0 -29
- package/src/ui/components/__tests__/jant-compose-dialog.test.ts +0 -512
- package/src/ui/components/__tests__/jant-compose-editor.test.ts +0 -272
- package/src/ui/components/compose-types.ts +0 -75
- package/src/ui/components/jant-collection-form.ts +0 -512
- package/src/ui/components/jant-compose-dialog.ts +0 -495
- package/src/ui/components/jant-compose-editor.ts +0 -814
- package/src/ui/dash/PageForm.tsx +0 -185
- package/src/ui/dash/PostList.tsx +0 -95
- package/src/ui/dash/appearance/AppearanceNav.tsx +0 -60
- package/src/ui/dash/collections/CollectionForm.tsx +0 -166
- package/src/ui/dash/collections/CollectionsListContent.tsx +0 -146
- package/src/ui/dash/collections/IconPickerGrid.tsx +0 -50
- package/src/ui/dash/collections/ViewCollectionContent.tsx +0 -103
- package/src/ui/dash/media/MediaListContent.tsx +0 -201
- package/src/ui/dash/media/ViewMediaContent.tsx +0 -208
- package/src/ui/dash/pages/PagesContent.tsx +0 -74
- package/src/ui/dash/posts/PostForm.tsx +0 -248
- package/src/ui/dash/settings/SettingsNav.tsx +0 -52
- package/src/ui/layouts/DashLayout.tsx +0 -165
- package/src/ui/pages/SinglePage.tsx +0 -23
- package/src/ui/shared/ThreadView.tsx +0 -136
- /package/src/{ui → client}/components/settings-types.ts +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
2
|
import { Hono } from "hono";
|
|
3
|
-
import { requireAuth, requireAuthApi } from "../auth.js";
|
|
3
|
+
import { requireAuth, requireAuthApi, isLocalHostname } from "../auth.js";
|
|
4
4
|
import { errorHandler } from "../error-handler.js";
|
|
5
5
|
import type { Bindings } from "../../types.js";
|
|
6
6
|
import type { AppVariables } from "../../types/app-context.js";
|
|
@@ -21,6 +21,32 @@ function createMockAuth(authenticated: boolean) {
|
|
|
21
21
|
} as AppVariables["auth"];
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
function createMockApiTokenService(validToken?: string) {
|
|
25
|
+
const tokenId = "token-id-1";
|
|
26
|
+
return {
|
|
27
|
+
verify: vi.fn(async (raw: string) => (raw === validToken ? tokenId : null)),
|
|
28
|
+
updateLastUsed: vi.fn(async () => {}),
|
|
29
|
+
create: vi.fn(),
|
|
30
|
+
list: vi.fn(),
|
|
31
|
+
delete: vi.fn(),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
describe("isLocalHostname", () => {
|
|
36
|
+
it.each([
|
|
37
|
+
["localhost", true],
|
|
38
|
+
["127.0.0.1", true],
|
|
39
|
+
["::1", true],
|
|
40
|
+
["jant.localtest.me", true],
|
|
41
|
+
["sub.localtest.me", true],
|
|
42
|
+
["myblog.com", false],
|
|
43
|
+
["demo.jant.me", false],
|
|
44
|
+
["localtest.me.evil.com", false],
|
|
45
|
+
])("isLocalHostname(%s) → %s", (hostname, expected) => {
|
|
46
|
+
expect(isLocalHostname(hostname)).toBe(expected);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
24
50
|
describe("requireAuth", () => {
|
|
25
51
|
it("allows authenticated requests", async () => {
|
|
26
52
|
const app = new Hono<Env>();
|
|
@@ -28,11 +54,11 @@ describe("requireAuth", () => {
|
|
|
28
54
|
c.set("auth", createMockAuth(true));
|
|
29
55
|
await next();
|
|
30
56
|
});
|
|
31
|
-
app.get("/
|
|
57
|
+
app.get("/settings", requireAuth(), (c) => c.text("Settings"));
|
|
32
58
|
|
|
33
|
-
const res = await app.request("/
|
|
59
|
+
const res = await app.request("/settings");
|
|
34
60
|
expect(res.status).toBe(200);
|
|
35
|
-
expect(await res.text()).toBe("
|
|
61
|
+
expect(await res.text()).toBe("Settings");
|
|
36
62
|
});
|
|
37
63
|
|
|
38
64
|
it("redirects unauthenticated requests to /signin", async () => {
|
|
@@ -41,9 +67,9 @@ describe("requireAuth", () => {
|
|
|
41
67
|
c.set("auth", createMockAuth(false));
|
|
42
68
|
await next();
|
|
43
69
|
});
|
|
44
|
-
app.get("/
|
|
70
|
+
app.get("/settings", requireAuth(), (c) => c.text("Settings"));
|
|
45
71
|
|
|
46
|
-
const res = await app.request("/
|
|
72
|
+
const res = await app.request("/settings", { redirect: "manual" });
|
|
47
73
|
expect(res.status).toBe(302);
|
|
48
74
|
expect(res.headers.get("Location")).toBe("/signin");
|
|
49
75
|
});
|
|
@@ -54,20 +80,23 @@ describe("requireAuth", () => {
|
|
|
54
80
|
c.set("auth", createMockAuth(false));
|
|
55
81
|
await next();
|
|
56
82
|
});
|
|
57
|
-
app.get("/
|
|
83
|
+
app.get("/settings", requireAuth("/login"), (c) => c.text("Settings"));
|
|
58
84
|
|
|
59
|
-
const res = await app.request("/
|
|
85
|
+
const res = await app.request("/settings", { redirect: "manual" });
|
|
60
86
|
expect(res.status).toBe(302);
|
|
61
87
|
expect(res.headers.get("Location")).toBe("/login");
|
|
62
88
|
});
|
|
63
89
|
});
|
|
64
90
|
|
|
65
91
|
describe("requireAuthApi", () => {
|
|
66
|
-
it("allows authenticated requests", async () => {
|
|
92
|
+
it("allows authenticated requests via session", async () => {
|
|
67
93
|
const app = new Hono<Env>();
|
|
68
94
|
app.onError(errorHandler);
|
|
69
95
|
app.use("*", async (c, next) => {
|
|
70
96
|
c.set("auth", createMockAuth(true));
|
|
97
|
+
c.set("services", {
|
|
98
|
+
apiTokens: createMockApiTokenService(),
|
|
99
|
+
} as AppVariables["services"]);
|
|
71
100
|
await next();
|
|
72
101
|
});
|
|
73
102
|
app.get("/api/data", requireAuthApi(), (c) => c.json({ data: "secret" }));
|
|
@@ -79,11 +108,14 @@ describe("requireAuthApi", () => {
|
|
|
79
108
|
expect(body.data).toBe("secret");
|
|
80
109
|
});
|
|
81
110
|
|
|
82
|
-
it("returns 401 for unauthenticated requests", async () => {
|
|
111
|
+
it("returns 401 for unauthenticated requests without Bearer token", async () => {
|
|
83
112
|
const app = new Hono<Env>();
|
|
84
113
|
app.onError(errorHandler);
|
|
85
114
|
app.use("*", async (c, next) => {
|
|
86
115
|
c.set("auth", createMockAuth(false));
|
|
116
|
+
c.set("services", {
|
|
117
|
+
apiTokens: createMockApiTokenService(),
|
|
118
|
+
} as AppVariables["services"]);
|
|
87
119
|
await next();
|
|
88
120
|
});
|
|
89
121
|
app.get("/api/data", requireAuthApi(), (c) => c.json({ data: "secret" }));
|
|
@@ -107,6 +139,9 @@ describe("requireAuthApi", () => {
|
|
|
107
139
|
},
|
|
108
140
|
},
|
|
109
141
|
} as AppVariables["auth"]);
|
|
142
|
+
c.set("services", {
|
|
143
|
+
apiTokens: createMockApiTokenService(),
|
|
144
|
+
} as AppVariables["services"]);
|
|
110
145
|
await next();
|
|
111
146
|
});
|
|
112
147
|
app.get("/api/data", requireAuthApi(), (c) => c.json({ data: "secret" }));
|
|
@@ -114,4 +149,149 @@ describe("requireAuthApi", () => {
|
|
|
114
149
|
const res = await app.request("/api/data");
|
|
115
150
|
expect(res.status).toBe(401);
|
|
116
151
|
});
|
|
152
|
+
|
|
153
|
+
it("allows requests with valid Bearer token when session auth fails", async () => {
|
|
154
|
+
const validToken = "jnt_abc123";
|
|
155
|
+
const mockApiTokens = createMockApiTokenService(validToken);
|
|
156
|
+
|
|
157
|
+
const app = new Hono<Env>();
|
|
158
|
+
app.onError(errorHandler);
|
|
159
|
+
app.use("*", async (c, next) => {
|
|
160
|
+
c.set("auth", createMockAuth(false));
|
|
161
|
+
c.set("services", {
|
|
162
|
+
apiTokens: mockApiTokens,
|
|
163
|
+
} as AppVariables["services"]);
|
|
164
|
+
await next();
|
|
165
|
+
});
|
|
166
|
+
app.get("/api/data", requireAuthApi(), (c) => c.json({ data: "secret" }));
|
|
167
|
+
|
|
168
|
+
const res = await app.request("/api/data", {
|
|
169
|
+
headers: { Authorization: `Bearer ${validToken}` },
|
|
170
|
+
});
|
|
171
|
+
expect(res.status).toBe(200);
|
|
172
|
+
|
|
173
|
+
const body = await res.json();
|
|
174
|
+
expect(body.data).toBe("secret");
|
|
175
|
+
|
|
176
|
+
expect(mockApiTokens.verify).toHaveBeenCalledWith(validToken);
|
|
177
|
+
expect(mockApiTokens.updateLastUsed).toHaveBeenCalledWith("token-id-1");
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("returns 401 for invalid Bearer token", async () => {
|
|
181
|
+
const mockApiTokens = createMockApiTokenService("jnt_valid");
|
|
182
|
+
|
|
183
|
+
const app = new Hono<Env>();
|
|
184
|
+
app.onError(errorHandler);
|
|
185
|
+
app.use("*", async (c, next) => {
|
|
186
|
+
c.set("auth", createMockAuth(false));
|
|
187
|
+
c.set("services", {
|
|
188
|
+
apiTokens: mockApiTokens,
|
|
189
|
+
} as AppVariables["services"]);
|
|
190
|
+
await next();
|
|
191
|
+
});
|
|
192
|
+
app.get("/api/data", requireAuthApi(), (c) => c.json({ data: "secret" }));
|
|
193
|
+
|
|
194
|
+
const res = await app.request("/api/data", {
|
|
195
|
+
headers: { Authorization: "Bearer jnt_invalid" },
|
|
196
|
+
});
|
|
197
|
+
expect(res.status).toBe(401);
|
|
198
|
+
|
|
199
|
+
expect(mockApiTokens.verify).toHaveBeenCalledWith("jnt_invalid");
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it("prefers session auth over Bearer token", async () => {
|
|
203
|
+
const mockApiTokens = createMockApiTokenService("jnt_valid");
|
|
204
|
+
|
|
205
|
+
const app = new Hono<Env>();
|
|
206
|
+
app.onError(errorHandler);
|
|
207
|
+
app.use("*", async (c, next) => {
|
|
208
|
+
c.set("auth", createMockAuth(true));
|
|
209
|
+
c.set("services", {
|
|
210
|
+
apiTokens: mockApiTokens,
|
|
211
|
+
} as AppVariables["services"]);
|
|
212
|
+
await next();
|
|
213
|
+
});
|
|
214
|
+
app.get("/api/data", requireAuthApi(), (c) => c.json({ data: "secret" }));
|
|
215
|
+
|
|
216
|
+
const res = await app.request("/api/data", {
|
|
217
|
+
headers: { Authorization: "Bearer jnt_valid" },
|
|
218
|
+
});
|
|
219
|
+
expect(res.status).toBe(200);
|
|
220
|
+
|
|
221
|
+
// Should not check the token since session auth succeeded
|
|
222
|
+
expect(mockApiTokens.verify).not.toHaveBeenCalled();
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it("allows DEV_API_TOKEN on localhost", async () => {
|
|
226
|
+
const devToken = "jnt_dev_test123";
|
|
227
|
+
const mockApiTokens = createMockApiTokenService();
|
|
228
|
+
|
|
229
|
+
const app = new Hono<Env>();
|
|
230
|
+
app.onError(errorHandler);
|
|
231
|
+
app.use("*", async (c, next) => {
|
|
232
|
+
c.env = { ...c.env, DEV_API_TOKEN: devToken } as Bindings;
|
|
233
|
+
c.set("auth", createMockAuth(false));
|
|
234
|
+
c.set("services", {
|
|
235
|
+
apiTokens: mockApiTokens,
|
|
236
|
+
} as AppVariables["services"]);
|
|
237
|
+
await next();
|
|
238
|
+
});
|
|
239
|
+
app.get("/api/data", requireAuthApi(), (c) => c.json({ data: "secret" }));
|
|
240
|
+
|
|
241
|
+
const res = await app.request("http://localhost:9020/api/data", {
|
|
242
|
+
headers: { Authorization: `Bearer ${devToken}` },
|
|
243
|
+
});
|
|
244
|
+
expect(res.status).toBe(200);
|
|
245
|
+
|
|
246
|
+
// Should NOT hit DB verification
|
|
247
|
+
expect(mockApiTokens.verify).not.toHaveBeenCalled();
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it("rejects DEV_API_TOKEN on non-local hostname", async () => {
|
|
251
|
+
const devToken = "jnt_dev_test123";
|
|
252
|
+
const mockApiTokens = createMockApiTokenService();
|
|
253
|
+
|
|
254
|
+
const app = new Hono<Env>();
|
|
255
|
+
app.onError(errorHandler);
|
|
256
|
+
app.use("*", async (c, next) => {
|
|
257
|
+
c.env = { ...c.env, DEV_API_TOKEN: devToken } as Bindings;
|
|
258
|
+
c.set("auth", createMockAuth(false));
|
|
259
|
+
c.set("services", {
|
|
260
|
+
apiTokens: mockApiTokens,
|
|
261
|
+
} as AppVariables["services"]);
|
|
262
|
+
await next();
|
|
263
|
+
});
|
|
264
|
+
app.get("/api/data", requireAuthApi(), (c) => c.json({ data: "secret" }));
|
|
265
|
+
|
|
266
|
+
const res = await app.request("https://myblog.com/api/data", {
|
|
267
|
+
headers: { Authorization: `Bearer ${devToken}` },
|
|
268
|
+
});
|
|
269
|
+
expect(res.status).toBe(401);
|
|
270
|
+
|
|
271
|
+
// Falls through to normal DB verification (which also fails)
|
|
272
|
+
expect(mockApiTokens.verify).toHaveBeenCalledWith(devToken);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it("allows DEV_API_TOKEN on *.localtest.me", async () => {
|
|
276
|
+
const devToken = "jnt_dev_test123";
|
|
277
|
+
const mockApiTokens = createMockApiTokenService();
|
|
278
|
+
|
|
279
|
+
const app = new Hono<Env>();
|
|
280
|
+
app.onError(errorHandler);
|
|
281
|
+
app.use("*", async (c, next) => {
|
|
282
|
+
c.env = { ...c.env, DEV_API_TOKEN: devToken } as Bindings;
|
|
283
|
+
c.set("auth", createMockAuth(false));
|
|
284
|
+
c.set("services", {
|
|
285
|
+
apiTokens: mockApiTokens,
|
|
286
|
+
} as AppVariables["services"]);
|
|
287
|
+
await next();
|
|
288
|
+
});
|
|
289
|
+
app.get("/api/data", requireAuthApi(), (c) => c.json({ data: "secret" }));
|
|
290
|
+
|
|
291
|
+
const res = await app.request("https://jant.localtest.me/api/data", {
|
|
292
|
+
headers: { Authorization: `Bearer ${devToken}` },
|
|
293
|
+
});
|
|
294
|
+
expect(res.status).toBe(200);
|
|
295
|
+
expect(mockApiTokens.verify).not.toHaveBeenCalled();
|
|
296
|
+
});
|
|
117
297
|
});
|
|
@@ -31,14 +31,14 @@ function createApp(complete: boolean) {
|
|
|
31
31
|
|
|
32
32
|
// Register routes for testing
|
|
33
33
|
app.get("/", (c) => c.text("Home"));
|
|
34
|
-
app.get("/
|
|
35
|
-
app.get("/
|
|
34
|
+
app.get("/settings", (c) => c.text("Settings"));
|
|
35
|
+
app.get("/settings/general", (c) => c.text("General"));
|
|
36
36
|
app.get("/archive", (c) => c.text("Archive"));
|
|
37
37
|
app.get("/p/abc", (c) => c.text("Post"));
|
|
38
38
|
app.get("/setup", (c) => c.text("Setup"));
|
|
39
39
|
app.get("/health", (c) => c.text("OK"));
|
|
40
40
|
app.get("/signin", (c) => c.text("Signin"));
|
|
41
|
-
app.
|
|
41
|
+
app.post("/signout", (c) => c.text("Signout"));
|
|
42
42
|
app.get("/reset", (c) => c.text("Reset"));
|
|
43
43
|
app.get("/api/auth/session", (c) => c.json({ ok: true }));
|
|
44
44
|
app.get("/assets/client-B2b-1X3C.js", (c) => c.text("js"));
|
|
@@ -63,16 +63,18 @@ describe("requireOnboarding", () => {
|
|
|
63
63
|
expect(res.headers.get("Location")).toBe("/setup");
|
|
64
64
|
});
|
|
65
65
|
|
|
66
|
-
it("redirects /
|
|
66
|
+
it("redirects /settings to /setup when onboarding not complete", async () => {
|
|
67
67
|
const { app } = createApp(false);
|
|
68
|
-
const res = await app.request("/
|
|
68
|
+
const res = await app.request("/settings", { redirect: "manual" });
|
|
69
69
|
expect(res.status).toBe(302);
|
|
70
70
|
expect(res.headers.get("Location")).toBe("/setup");
|
|
71
71
|
});
|
|
72
72
|
|
|
73
|
-
it("redirects /
|
|
73
|
+
it("redirects /settings/* to /setup when onboarding not complete", async () => {
|
|
74
74
|
const { app } = createApp(false);
|
|
75
|
-
const res = await app.request("/
|
|
75
|
+
const res = await app.request("/settings/general", {
|
|
76
|
+
redirect: "manual",
|
|
77
|
+
});
|
|
76
78
|
expect(res.status).toBe(302);
|
|
77
79
|
expect(res.headers.get("Location")).toBe("/setup");
|
|
78
80
|
});
|
|
@@ -105,7 +107,7 @@ describe("requireOnboarding", () => {
|
|
|
105
107
|
await app.request("/");
|
|
106
108
|
expect(getCallCount()).toBe(1);
|
|
107
109
|
|
|
108
|
-
await app.request("/
|
|
110
|
+
await app.request("/settings");
|
|
109
111
|
expect(getCallCount()).toBe(1); // still 1 — cached
|
|
110
112
|
});
|
|
111
113
|
|
|
@@ -115,7 +117,7 @@ describe("requireOnboarding", () => {
|
|
|
115
117
|
await app.request("/", { redirect: "manual" });
|
|
116
118
|
expect(getCallCount()).toBe(1);
|
|
117
119
|
|
|
118
|
-
await app.request("/
|
|
120
|
+
await app.request("/settings", { redirect: "manual" });
|
|
119
121
|
expect(getCallCount()).toBe(2); // queried again
|
|
120
122
|
});
|
|
121
123
|
|
|
@@ -136,7 +138,7 @@ describe("requireOnboarding", () => {
|
|
|
136
138
|
|
|
137
139
|
it("allows /signout", async () => {
|
|
138
140
|
const { app, getCallCount } = createApp(false);
|
|
139
|
-
const res = await app.request("/signout");
|
|
141
|
+
const res = await app.request("/signout", { method: "POST" });
|
|
140
142
|
expect(res.status).toBe(200);
|
|
141
143
|
expect(getCallCount()).toBe(0);
|
|
142
144
|
});
|
package/src/middleware/auth.ts
CHANGED
|
@@ -1,19 +1,42 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Authentication Middleware
|
|
3
3
|
*
|
|
4
|
-
* Protects routes by requiring authentication
|
|
4
|
+
* Protects routes by requiring authentication via session cookies
|
|
5
|
+
* or Bearer API tokens.
|
|
5
6
|
*/
|
|
6
7
|
|
|
7
8
|
import type { MiddlewareHandler } from "hono";
|
|
8
9
|
import type { Bindings } from "../types.js";
|
|
9
10
|
import type { AppVariables } from "../types/app-context.js";
|
|
10
|
-
import {
|
|
11
|
+
import { UnauthorizedError } from "../lib/errors.js";
|
|
11
12
|
|
|
12
13
|
type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
13
14
|
|
|
15
|
+
/**
|
|
16
|
+
* Checks whether a hostname is local (dev environment).
|
|
17
|
+
*
|
|
18
|
+
* @param hostname - The hostname to check
|
|
19
|
+
* @returns `true` for localhost, 127.0.0.1, ::1, and *.localtest.me
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```ts
|
|
23
|
+
* isLocalHostname("localhost") // true
|
|
24
|
+
* isLocalHostname("myblog.com") // false
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
export function isLocalHostname(hostname: string): boolean {
|
|
28
|
+
return (
|
|
29
|
+
hostname === "localhost" ||
|
|
30
|
+
hostname === "127.0.0.1" ||
|
|
31
|
+
hostname === "::1" ||
|
|
32
|
+
hostname.endsWith(".localtest.me")
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
14
36
|
/**
|
|
15
37
|
* Middleware that requires authentication.
|
|
16
38
|
* Redirects to signin page if not authenticated.
|
|
39
|
+
* Session-only — Bearer tokens are not accepted for dashboard pages.
|
|
17
40
|
*/
|
|
18
41
|
export function requireAuth(redirectTo = "/signin"): MiddlewareHandler<Env> {
|
|
19
42
|
return async (c, next) => {
|
|
@@ -35,23 +58,54 @@ export function requireAuth(redirectTo = "/signin"): MiddlewareHandler<Env> {
|
|
|
35
58
|
|
|
36
59
|
/**
|
|
37
60
|
* Middleware for API routes that requires authentication.
|
|
38
|
-
*
|
|
61
|
+
* Tries session auth first, then falls back to Bearer API token.
|
|
62
|
+
* Returns 401 if neither method succeeds.
|
|
39
63
|
*/
|
|
40
64
|
export function requireAuthApi(): MiddlewareHandler<Env> {
|
|
41
65
|
return async (c, next) => {
|
|
66
|
+
// 1. Try session auth (existing behavior)
|
|
42
67
|
try {
|
|
43
68
|
const session = await c.var.auth.api.getSession({
|
|
44
69
|
headers: c.req.raw.headers,
|
|
45
70
|
});
|
|
46
71
|
|
|
47
|
-
if (
|
|
48
|
-
|
|
72
|
+
if (session?.user) {
|
|
73
|
+
await next();
|
|
74
|
+
return;
|
|
49
75
|
}
|
|
76
|
+
} catch {
|
|
77
|
+
// Session check failed — fall through to Bearer token
|
|
78
|
+
}
|
|
50
79
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
80
|
+
// 2. Try Bearer token auth
|
|
81
|
+
const authHeader = c.req.header("Authorization");
|
|
82
|
+
if (authHeader?.startsWith("Bearer ")) {
|
|
83
|
+
const rawToken = authHeader.slice(7);
|
|
84
|
+
|
|
85
|
+
// Dev shortcut: bypass DB lookup when DEV_API_TOKEN matches on a local hostname
|
|
86
|
+
const devToken = c.env?.DEV_API_TOKEN;
|
|
87
|
+
if (devToken && rawToken === devToken) {
|
|
88
|
+
const hostname = new URL(c.req.url).hostname;
|
|
89
|
+
if (isLocalHostname(hostname)) {
|
|
90
|
+
await next();
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const tokenId = await c.var.services.apiTokens.verify(rawToken);
|
|
96
|
+
if (tokenId) {
|
|
97
|
+
// Fire-and-forget last-used update (non-blocking)
|
|
98
|
+
const updatePromise = c.var.services.apiTokens.updateLastUsed(tokenId);
|
|
99
|
+
try {
|
|
100
|
+
c.executionCtx.waitUntil(updatePromise);
|
|
101
|
+
} catch {
|
|
102
|
+
// executionCtx not available (e.g. in tests) — ignore
|
|
103
|
+
}
|
|
104
|
+
await next();
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
55
107
|
}
|
|
108
|
+
|
|
109
|
+
throw new UnauthorizedError();
|
|
56
110
|
};
|
|
57
111
|
}
|
|
@@ -33,7 +33,7 @@ export const errorHandler: ErrorHandler<Env> = (err, c) => {
|
|
|
33
33
|
// Unknown API error
|
|
34
34
|
// eslint-disable-next-line no-console -- Server error logging is intentional
|
|
35
35
|
console.error("[Jant] Unhandled error:", err);
|
|
36
|
-
return c.json({ error: "
|
|
36
|
+
return c.json({ error: "Something went wrong on our end" }, 500);
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
// Datastar requests: return toast
|
|
@@ -43,7 +43,7 @@ export const errorHandler: ErrorHandler<Env> = (err, c) => {
|
|
|
43
43
|
}
|
|
44
44
|
// eslint-disable-next-line no-console -- Server error logging is intentional
|
|
45
45
|
console.error("[Jant] Unhandled error:", err);
|
|
46
|
-
return dsToast("
|
|
46
|
+
return dsToast("Something went wrong. Try refreshing the page.", "error");
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
// JSON-accepting requests (Lit bridges)
|
|
@@ -59,7 +59,7 @@ export const errorHandler: ErrorHandler<Env> = (err, c) => {
|
|
|
59
59
|
}
|
|
60
60
|
// eslint-disable-next-line no-console -- Server error logging is intentional
|
|
61
61
|
console.error("[Jant] Unhandled error:", err);
|
|
62
|
-
return c.json({ error: "
|
|
62
|
+
return c.json({ error: "Something went wrong on our end" }, 500);
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
// Non-API routes: map NotFoundError to Hono's built-in 404
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Security Headers Middleware
|
|
3
|
+
*
|
|
4
|
+
* Adds Content-Security-Policy and other security headers via Hono's
|
|
5
|
+
* built-in secureHeaders middleware. Uses a baseline CSP that works with
|
|
6
|
+
* the current tech stack (Datastar, Lit, inline theme styles).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { secureHeaders } from "hono/secure-headers";
|
|
10
|
+
import type { MiddlewareHandler } from "hono";
|
|
11
|
+
import type { Bindings } from "../types.js";
|
|
12
|
+
import type { AppVariables } from "../types/app-context.js";
|
|
13
|
+
import { IS_VITE_DEV } from "../lib/version.js";
|
|
14
|
+
|
|
15
|
+
type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
16
|
+
|
|
17
|
+
export function secureHeadersMiddleware(): MiddlewareHandler<Env> {
|
|
18
|
+
return secureHeaders({
|
|
19
|
+
contentSecurityPolicy: {
|
|
20
|
+
defaultSrc: ["'self'"],
|
|
21
|
+
scriptSrc: [
|
|
22
|
+
"'self'",
|
|
23
|
+
// Datastar evaluates expressions in data-on-* / data-signals attributes
|
|
24
|
+
"'unsafe-eval'",
|
|
25
|
+
],
|
|
26
|
+
styleSrc: [
|
|
27
|
+
"'self'",
|
|
28
|
+
// Theme styles and custom CSS are injected as inline <style> tags
|
|
29
|
+
"'unsafe-inline'",
|
|
30
|
+
],
|
|
31
|
+
imgSrc: ["'self'", "data:", "blob:", "https:"],
|
|
32
|
+
fontSrc: ["'self'"],
|
|
33
|
+
connectSrc: IS_VITE_DEV ? ["'self'", "ws:"] : ["'self'"],
|
|
34
|
+
frameSrc: ["'none'"],
|
|
35
|
+
objectSrc: ["'none'"],
|
|
36
|
+
baseUri: ["'self'"],
|
|
37
|
+
formAction: ["'self'"],
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
}
|
package/src/preset.css
CHANGED
|
@@ -13,6 +13,12 @@
|
|
|
13
13
|
@import "./styles/tokens.css";
|
|
14
14
|
@import "./styles/ui.css";
|
|
15
15
|
|
|
16
|
+
/*
|
|
17
|
+
* Override BaseCoat's class-based dark mode with media-query-based.
|
|
18
|
+
* Jant follows system preference automatically — no manual toggle.
|
|
19
|
+
*/
|
|
20
|
+
@custom-variant dark (@media (prefers-color-scheme: dark));
|
|
21
|
+
|
|
16
22
|
@theme {
|
|
17
23
|
--radius-default: 0.5rem;
|
|
18
24
|
--color-success: var(--success);
|
|
@@ -24,8 +30,45 @@
|
|
|
24
30
|
--success: oklch(0.518 0.16 145.071);
|
|
25
31
|
}
|
|
26
32
|
|
|
27
|
-
|
|
28
|
-
|
|
33
|
+
/*
|
|
34
|
+
* BaseCoat dark mode fallback — mirrors BaseCoat's `.dark { }` variables
|
|
35
|
+
* via media query so dark mode works without JS class toggling.
|
|
36
|
+
* These are overridden by the active color theme (higher specificity).
|
|
37
|
+
*
|
|
38
|
+
* Source: basecoat-css@0.3.11 .dark { } block
|
|
39
|
+
*/
|
|
40
|
+
@media (prefers-color-scheme: dark) {
|
|
41
|
+
:root {
|
|
42
|
+
color-scheme: dark;
|
|
43
|
+
--background: oklch(0.145 0 0);
|
|
44
|
+
--foreground: oklch(0.985 0 0);
|
|
45
|
+
--card: oklch(0.205 0 0);
|
|
46
|
+
--card-foreground: oklch(0.985 0 0);
|
|
47
|
+
--popover: oklch(0.269 0 0);
|
|
48
|
+
--popover-foreground: oklch(0.985 0 0);
|
|
49
|
+
--primary: oklch(0.922 0 0);
|
|
50
|
+
--primary-foreground: oklch(0.205 0 0);
|
|
51
|
+
--secondary: oklch(0.269 0 0);
|
|
52
|
+
--secondary-foreground: oklch(0.985 0 0);
|
|
53
|
+
--muted: oklch(0.269 0 0);
|
|
54
|
+
--muted-foreground: oklch(0.708 0 0);
|
|
55
|
+
--accent: oklch(0.371 0 0);
|
|
56
|
+
--accent-foreground: oklch(0.985 0 0);
|
|
57
|
+
--destructive: oklch(0.704 0.191 22.216);
|
|
58
|
+
--border: oklch(1 0 0 / 10%);
|
|
59
|
+
--input: oklch(1 0 0 / 15%);
|
|
60
|
+
--ring: oklch(0.556 0 0);
|
|
61
|
+
--sidebar: oklch(0.205 0 0);
|
|
62
|
+
--sidebar-foreground: oklch(0.985 0 0);
|
|
63
|
+
--sidebar-primary: oklch(0.488 0.243 264.376);
|
|
64
|
+
--sidebar-primary-foreground: oklch(0.985 0 0);
|
|
65
|
+
--sidebar-accent: oklch(0.269 0 0);
|
|
66
|
+
--sidebar-accent-foreground: oklch(0.985 0 0);
|
|
67
|
+
--sidebar-border: oklch(1 0 0 / 10%);
|
|
68
|
+
--sidebar-ring: oklch(0.439 0 0);
|
|
69
|
+
--scrollbar-thumb: rgba(255, 255, 255, 0.3);
|
|
70
|
+
--success: oklch(0.627 0.194 149.214);
|
|
71
|
+
}
|
|
29
72
|
}
|
|
30
73
|
|
|
31
74
|
/**
|
|
@@ -73,4 +116,42 @@
|
|
|
73
116
|
:where(h1, h2, h3, h4, h5, h6) {
|
|
74
117
|
font-family: var(--font-heading);
|
|
75
118
|
}
|
|
119
|
+
|
|
120
|
+
/* Image figures */
|
|
121
|
+
figure {
|
|
122
|
+
margin: 1.5em 0;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
figure img {
|
|
126
|
+
width: 100%;
|
|
127
|
+
max-height: 500px;
|
|
128
|
+
object-fit: contain;
|
|
129
|
+
border-radius: 6px;
|
|
130
|
+
cursor: pointer;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
figcaption {
|
|
134
|
+
text-align: center;
|
|
135
|
+
font-size: 0.875rem;
|
|
136
|
+
color: var(--tw-prose-captions);
|
|
137
|
+
margin-top: 0.5em;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/* Layout variants — center-breakout via margin+transform (no scrollbars) */
|
|
141
|
+
figure[data-layout="wide"] {
|
|
142
|
+
width: 1200px;
|
|
143
|
+
max-width: 100vw;
|
|
144
|
+
margin-left: 50%;
|
|
145
|
+
transform: translateX(-50%);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
figure[data-layout="full"] {
|
|
149
|
+
width: 100vw;
|
|
150
|
+
margin-left: 50%;
|
|
151
|
+
transform: translateX(-50%);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
figure[data-layout="full"] img {
|
|
155
|
+
border-radius: 0;
|
|
156
|
+
}
|
|
76
157
|
}
|