@jant/core 0.3.25 → 0.3.27
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/dist/app.js +70 -563
- package/dist/auth.js +3 -0
- package/dist/client.js +1 -0
- package/dist/i18n/locales/en.js +1 -1
- package/dist/i18n/locales/zh-Hans.js +1 -1
- package/dist/i18n/locales/zh-Hant.js +1 -1
- package/dist/lib/avatar-upload.js +134 -0
- package/dist/lib/config.js +39 -0
- package/dist/lib/constants.js +10 -10
- package/dist/lib/favicon.js +102 -0
- package/dist/lib/image.js +13 -17
- package/dist/lib/media-helpers.js +2 -2
- package/dist/lib/navigation.js +23 -3
- package/dist/lib/render.js +10 -1
- package/dist/lib/schemas.js +31 -0
- package/dist/lib/timezones.js +388 -0
- package/dist/lib/view.js +1 -1
- package/dist/routes/api/posts.js +1 -1
- package/dist/routes/api/upload.js +3 -3
- package/dist/routes/auth/reset.js +221 -0
- package/dist/routes/auth/setup.js +194 -0
- package/dist/routes/auth/signin.js +176 -0
- package/dist/routes/dash/collections.js +23 -415
- package/dist/routes/dash/media.js +12 -392
- package/dist/routes/dash/pages.js +7 -330
- package/dist/routes/dash/redirects.js +18 -12
- package/dist/routes/dash/settings.js +198 -577
- package/dist/routes/feed/rss.js +2 -1
- package/dist/routes/feed/sitemap.js +4 -2
- package/dist/routes/pages/featured.js +5 -1
- package/dist/routes/pages/home.js +26 -1
- package/dist/routes/pages/latest.js +45 -0
- package/dist/services/post.js +30 -50
- package/dist/types/bindings.js +3 -0
- package/dist/types/config.js +147 -0
- package/dist/types/constants.js +27 -0
- package/dist/types/entities.js +3 -0
- package/dist/types/operations.js +3 -0
- package/dist/types/props.js +3 -0
- package/dist/types/views.js +5 -0
- package/dist/types.js +8 -111
- package/dist/ui/color-themes.js +33 -33
- package/dist/ui/compose/ComposeDialog.js +36 -21
- package/dist/ui/dash/PageForm.js +21 -15
- package/dist/ui/dash/PostForm.js +22 -16
- package/dist/ui/dash/collections/CollectionForm.js +152 -0
- package/dist/ui/dash/collections/CollectionsListContent.js +68 -0
- package/dist/ui/dash/collections/ViewCollectionContent.js +96 -0
- package/dist/ui/dash/media/MediaListContent.js +166 -0
- package/dist/ui/dash/media/ViewMediaContent.js +212 -0
- package/dist/ui/dash/pages/LinkFormContent.js +130 -0
- package/dist/ui/dash/pages/UnifiedPagesContent.js +193 -0
- package/dist/ui/dash/settings/AccountContent.js +209 -0
- package/dist/ui/dash/settings/AppearanceContent.js +259 -0
- package/dist/ui/dash/settings/GeneralContent.js +536 -0
- package/dist/ui/dash/settings/SettingsNav.js +41 -0
- package/dist/ui/font-themes.js +36 -0
- package/dist/ui/layouts/BaseLayout.js +24 -2
- package/dist/ui/layouts/SiteLayout.js +47 -19
- package/package.json +1 -1
- package/src/app.tsx +95 -553
- package/src/auth.ts +4 -1
- package/src/client.ts +1 -0
- package/src/i18n/locales/en.po +240 -175
- package/src/i18n/locales/en.ts +1 -1
- package/src/i18n/locales/zh-Hans.po +240 -175
- package/src/i18n/locales/zh-Hans.ts +1 -1
- package/src/i18n/locales/zh-Hant.po +240 -175
- package/src/i18n/locales/zh-Hant.ts +1 -1
- package/src/lib/__tests__/config.test.ts +192 -0
- package/src/lib/__tests__/favicon.test.ts +151 -0
- package/src/lib/__tests__/image.test.ts +2 -6
- package/src/lib/__tests__/timezones.test.ts +61 -0
- package/src/lib/__tests__/view.test.ts +2 -2
- package/src/lib/avatar-upload.ts +165 -0
- package/src/lib/config.ts +47 -0
- package/src/lib/constants.ts +19 -11
- package/src/lib/favicon.ts +115 -0
- package/src/lib/image.ts +13 -21
- package/src/lib/media-helpers.ts +2 -2
- package/src/lib/navigation.ts +33 -2
- package/src/lib/render.tsx +15 -1
- package/src/lib/schemas.ts +39 -0
- package/src/lib/timezones.ts +325 -0
- package/src/lib/view.ts +1 -1
- package/src/routes/api/posts.ts +1 -1
- package/src/routes/api/upload.ts +2 -3
- package/src/routes/auth/reset.tsx +239 -0
- package/src/routes/auth/setup.tsx +189 -0
- package/src/routes/auth/signin.tsx +163 -0
- package/src/routes/dash/__tests__/settings-avatar.test.ts +89 -0
- package/src/routes/dash/collections.tsx +17 -366
- package/src/routes/dash/media.tsx +12 -414
- package/src/routes/dash/pages.tsx +8 -348
- package/src/routes/dash/redirects.tsx +20 -14
- package/src/routes/dash/settings.tsx +243 -534
- package/src/routes/feed/__tests__/rss.test.ts +141 -0
- package/src/routes/feed/rss.ts +3 -1
- package/src/routes/feed/sitemap.ts +4 -2
- package/src/routes/pages/featured.tsx +7 -1
- package/src/routes/pages/home.tsx +25 -2
- package/src/routes/pages/latest.tsx +59 -0
- package/src/services/post.ts +34 -66
- package/src/styles/components.css +0 -65
- package/src/styles/tokens.css +1 -1
- package/src/styles/ui.css +24 -40
- package/src/types/bindings.ts +30 -0
- package/src/types/config.ts +183 -0
- package/src/types/constants.ts +26 -0
- package/src/types/entities.ts +109 -0
- package/src/types/operations.ts +88 -0
- package/src/types/props.ts +115 -0
- package/src/types/views.ts +172 -0
- package/src/types.ts +8 -644
- package/src/ui/__tests__/font-themes.test.ts +34 -0
- package/src/ui/color-themes.ts +34 -34
- package/src/ui/compose/ComposeDialog.tsx +40 -21
- package/src/ui/dash/PageForm.tsx +25 -19
- package/src/ui/dash/PostForm.tsx +26 -20
- package/src/ui/dash/collections/CollectionForm.tsx +153 -0
- package/src/ui/dash/collections/CollectionsListContent.tsx +85 -0
- package/src/ui/dash/collections/ViewCollectionContent.tsx +92 -0
- package/src/ui/dash/media/MediaListContent.tsx +201 -0
- package/src/ui/dash/media/ViewMediaContent.tsx +208 -0
- package/src/ui/dash/pages/LinkFormContent.tsx +119 -0
- package/src/ui/dash/pages/UnifiedPagesContent.tsx +203 -0
- package/src/ui/dash/settings/AccountContent.tsx +176 -0
- package/src/ui/dash/settings/AppearanceContent.tsx +254 -0
- package/src/ui/dash/settings/GeneralContent.tsx +533 -0
- package/src/ui/dash/settings/SettingsNav.tsx +56 -0
- package/src/ui/font-themes.ts +54 -0
- package/src/ui/layouts/BaseLayout.tsx +17 -0
- package/src/ui/layouts/SiteLayout.tsx +45 -31
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified pages list - navigation items + other pages
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { useLingui } from "@lingui/react/macro";
|
|
6
|
+
import type { Page, NavItem } from "../../../types.js";
|
|
7
|
+
import { ListItemRow, ActionButtons, CrudPageHeader } from "../index.js";
|
|
8
|
+
|
|
9
|
+
export function UnifiedPagesContent({
|
|
10
|
+
navItems,
|
|
11
|
+
otherPages,
|
|
12
|
+
}: {
|
|
13
|
+
navItems: NavItem[];
|
|
14
|
+
otherPages: Page[];
|
|
15
|
+
}) {
|
|
16
|
+
const { t } = useLingui();
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<>
|
|
20
|
+
<CrudPageHeader
|
|
21
|
+
title={t({
|
|
22
|
+
message: "Pages",
|
|
23
|
+
comment: "@context: Pages main heading",
|
|
24
|
+
})}
|
|
25
|
+
>
|
|
26
|
+
<div class="flex gap-2">
|
|
27
|
+
<a href="/dash/pages/links/new" class="btn-outline">
|
|
28
|
+
{t({
|
|
29
|
+
message: "Add Link",
|
|
30
|
+
comment: "@context: Button to add a navigation link",
|
|
31
|
+
})}
|
|
32
|
+
</a>
|
|
33
|
+
<a href="/dash/pages/new" class="btn">
|
|
34
|
+
{t({
|
|
35
|
+
message: "New Page",
|
|
36
|
+
comment: "@context: Button to create new page",
|
|
37
|
+
})}
|
|
38
|
+
</a>
|
|
39
|
+
</div>
|
|
40
|
+
</CrudPageHeader>
|
|
41
|
+
|
|
42
|
+
{/* Navigation section */}
|
|
43
|
+
<section class="mb-8">
|
|
44
|
+
<h2 class="text-lg font-medium mb-3">
|
|
45
|
+
{t({
|
|
46
|
+
message: "Your site navigation",
|
|
47
|
+
comment: "@context: Section heading for navigation items",
|
|
48
|
+
})}
|
|
49
|
+
</h2>
|
|
50
|
+
{navItems.length === 0 ? (
|
|
51
|
+
<p class="text-sm text-muted-foreground py-4">
|
|
52
|
+
{t({
|
|
53
|
+
message:
|
|
54
|
+
"No navigation links yet. Add pages to navigation or create links.",
|
|
55
|
+
comment: "@context: Empty state for navigation section",
|
|
56
|
+
})}
|
|
57
|
+
</p>
|
|
58
|
+
) : (
|
|
59
|
+
<div id="nav-links-list" class="flex flex-col divide-y">
|
|
60
|
+
{navItems.map((item) => (
|
|
61
|
+
<ListItemRow
|
|
62
|
+
key={item.id}
|
|
63
|
+
actions={
|
|
64
|
+
item.type === "page" ? (
|
|
65
|
+
<>
|
|
66
|
+
<ActionButtons
|
|
67
|
+
editHref={
|
|
68
|
+
item.pageId
|
|
69
|
+
? `/dash/pages/${item.pageId}/edit`
|
|
70
|
+
: undefined
|
|
71
|
+
}
|
|
72
|
+
editLabel={t({
|
|
73
|
+
message: "Edit",
|
|
74
|
+
comment: "@context: Button to edit page",
|
|
75
|
+
})}
|
|
76
|
+
/>
|
|
77
|
+
<button
|
|
78
|
+
type="button"
|
|
79
|
+
class="btn-sm-ghost"
|
|
80
|
+
data-on:click__prevent={`@post('/dash/pages/${item.pageId}/remove-from-nav')`}
|
|
81
|
+
>
|
|
82
|
+
{t({
|
|
83
|
+
message: "Un-nav",
|
|
84
|
+
comment:
|
|
85
|
+
"@context: Button to remove page from navigation",
|
|
86
|
+
})}
|
|
87
|
+
</button>
|
|
88
|
+
</>
|
|
89
|
+
) : (
|
|
90
|
+
<>
|
|
91
|
+
<ActionButtons
|
|
92
|
+
editHref={`/dash/pages/links/${item.id}/edit`}
|
|
93
|
+
editLabel={t({
|
|
94
|
+
message: "Edit",
|
|
95
|
+
comment: "@context: Button to edit link",
|
|
96
|
+
})}
|
|
97
|
+
deleteAction={`/dash/pages/links/${item.id}/delete`}
|
|
98
|
+
deleteLabel={t({
|
|
99
|
+
message: "Delete",
|
|
100
|
+
comment: "@context: Button to delete link",
|
|
101
|
+
})}
|
|
102
|
+
/>
|
|
103
|
+
</>
|
|
104
|
+
)
|
|
105
|
+
}
|
|
106
|
+
>
|
|
107
|
+
<div
|
|
108
|
+
class="flex items-center gap-3 cursor-grab"
|
|
109
|
+
data-id={item.id}
|
|
110
|
+
>
|
|
111
|
+
<span class="text-muted-foreground select-none">⠿</span>
|
|
112
|
+
<div class="flex items-center gap-2">
|
|
113
|
+
<span class="font-medium">{item.label}</span>
|
|
114
|
+
<code class="text-sm text-muted-foreground bg-muted px-1 rounded">
|
|
115
|
+
{item.url}
|
|
116
|
+
</code>
|
|
117
|
+
<span class="badge-secondary">
|
|
118
|
+
{item.type === "page"
|
|
119
|
+
? t({
|
|
120
|
+
message: "page",
|
|
121
|
+
comment: "@context: Nav item type badge",
|
|
122
|
+
})
|
|
123
|
+
: t({
|
|
124
|
+
message: "link",
|
|
125
|
+
comment: "@context: Nav item type badge",
|
|
126
|
+
})}
|
|
127
|
+
</span>
|
|
128
|
+
</div>
|
|
129
|
+
</div>
|
|
130
|
+
</ListItemRow>
|
|
131
|
+
))}
|
|
132
|
+
</div>
|
|
133
|
+
)}
|
|
134
|
+
</section>
|
|
135
|
+
|
|
136
|
+
{/* Other pages section */}
|
|
137
|
+
<section>
|
|
138
|
+
<h2 class="text-lg font-medium mb-3">
|
|
139
|
+
{t({
|
|
140
|
+
message: "Other pages",
|
|
141
|
+
comment: "@context: Section heading for pages not in navigation",
|
|
142
|
+
})}
|
|
143
|
+
</h2>
|
|
144
|
+
{otherPages.length === 0 ? (
|
|
145
|
+
<p class="text-sm text-muted-foreground py-4">
|
|
146
|
+
{t({
|
|
147
|
+
message: "All pages are in your navigation.",
|
|
148
|
+
comment: "@context: Empty state when all pages are in nav",
|
|
149
|
+
})}
|
|
150
|
+
</p>
|
|
151
|
+
) : (
|
|
152
|
+
<div class="flex flex-col divide-y">
|
|
153
|
+
{otherPages.map((page) => (
|
|
154
|
+
<ListItemRow
|
|
155
|
+
key={page.id}
|
|
156
|
+
actions={
|
|
157
|
+
<>
|
|
158
|
+
<button
|
|
159
|
+
type="button"
|
|
160
|
+
class="btn-sm-outline"
|
|
161
|
+
data-on:click__prevent={`@post('/dash/pages/${page.id}/add-to-nav')`}
|
|
162
|
+
>
|
|
163
|
+
{t({
|
|
164
|
+
message: "Add to nav",
|
|
165
|
+
comment: "@context: Button to add page to navigation",
|
|
166
|
+
})}
|
|
167
|
+
</button>
|
|
168
|
+
<ActionButtons
|
|
169
|
+
editHref={`/dash/pages/${page.id}/edit`}
|
|
170
|
+
editLabel={t({
|
|
171
|
+
message: "Edit",
|
|
172
|
+
comment: "@context: Button to edit page",
|
|
173
|
+
})}
|
|
174
|
+
viewHref={
|
|
175
|
+
page.status !== "draft" ? `/${page.slug}` : undefined
|
|
176
|
+
}
|
|
177
|
+
viewLabel={t({
|
|
178
|
+
message: "View",
|
|
179
|
+
comment: "@context: Button to view page on public site",
|
|
180
|
+
})}
|
|
181
|
+
/>
|
|
182
|
+
</>
|
|
183
|
+
}
|
|
184
|
+
>
|
|
185
|
+
<a
|
|
186
|
+
href={`/dash/pages/${page.id}`}
|
|
187
|
+
class="font-medium hover:underline"
|
|
188
|
+
>
|
|
189
|
+
{page.title ||
|
|
190
|
+
t({
|
|
191
|
+
message: "Untitled",
|
|
192
|
+
comment: "@context: Default title for untitled page",
|
|
193
|
+
})}
|
|
194
|
+
</a>
|
|
195
|
+
<p class="text-sm text-muted-foreground mt-1">/{page.slug}</p>
|
|
196
|
+
</ListItemRow>
|
|
197
|
+
))}
|
|
198
|
+
</div>
|
|
199
|
+
)}
|
|
200
|
+
</section>
|
|
201
|
+
</>
|
|
202
|
+
);
|
|
203
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Account settings: profile + password change forms
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { useLingui } from "@lingui/react/macro";
|
|
6
|
+
import { SettingsNav } from "./SettingsNav.js";
|
|
7
|
+
|
|
8
|
+
export function AccountContent({ userName }: { userName: string }) {
|
|
9
|
+
const { t } = useLingui();
|
|
10
|
+
|
|
11
|
+
const profileSignals = JSON.stringify({ userName }).replace(/</g, "\\u003c");
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<>
|
|
15
|
+
<h1 class="text-2xl font-semibold mb-2">
|
|
16
|
+
{t({ message: "Settings", comment: "@context: Dashboard heading" })}
|
|
17
|
+
</h1>
|
|
18
|
+
<SettingsNav currentTab="account" />
|
|
19
|
+
|
|
20
|
+
<div class="flex flex-col gap-6 max-w-lg">
|
|
21
|
+
<form
|
|
22
|
+
data-signals={profileSignals}
|
|
23
|
+
data-on:submit__prevent="@post('/dash/settings/account')"
|
|
24
|
+
data-indicator="_profileLoading"
|
|
25
|
+
>
|
|
26
|
+
<div class="card">
|
|
27
|
+
<header>
|
|
28
|
+
<h2>
|
|
29
|
+
{t({
|
|
30
|
+
message: "Profile",
|
|
31
|
+
comment: "@context: Account settings section heading",
|
|
32
|
+
})}
|
|
33
|
+
</h2>
|
|
34
|
+
</header>
|
|
35
|
+
<section class="flex flex-col gap-4">
|
|
36
|
+
<div class="field">
|
|
37
|
+
<label class="label">
|
|
38
|
+
{t({
|
|
39
|
+
message: "Name",
|
|
40
|
+
comment: "@context: Account settings form field",
|
|
41
|
+
})}
|
|
42
|
+
</label>
|
|
43
|
+
<input
|
|
44
|
+
type="text"
|
|
45
|
+
data-bind="userName"
|
|
46
|
+
class="input"
|
|
47
|
+
required
|
|
48
|
+
/>
|
|
49
|
+
</div>
|
|
50
|
+
</section>
|
|
51
|
+
</div>
|
|
52
|
+
|
|
53
|
+
<button
|
|
54
|
+
type="submit"
|
|
55
|
+
class="btn mt-4"
|
|
56
|
+
data-attr:disabled="$_profileLoading"
|
|
57
|
+
>
|
|
58
|
+
<svg
|
|
59
|
+
data-show="$_profileLoading"
|
|
60
|
+
style="display:none"
|
|
61
|
+
class="animate-spin size-4"
|
|
62
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
63
|
+
viewBox="0 0 24 24"
|
|
64
|
+
fill="none"
|
|
65
|
+
stroke="currentColor"
|
|
66
|
+
stroke-width="2"
|
|
67
|
+
stroke-linecap="round"
|
|
68
|
+
stroke-linejoin="round"
|
|
69
|
+
role="status"
|
|
70
|
+
>
|
|
71
|
+
<path d="M21 12a9 9 0 1 1-6.219-8.56" />
|
|
72
|
+
</svg>
|
|
73
|
+
{t({
|
|
74
|
+
message: "Save Profile",
|
|
75
|
+
comment: "@context: Button to save profile",
|
|
76
|
+
})}
|
|
77
|
+
</button>
|
|
78
|
+
</form>
|
|
79
|
+
|
|
80
|
+
<form
|
|
81
|
+
data-signals="{currentPassword: '', newPassword: '', confirmPassword: ''}"
|
|
82
|
+
data-on:submit__prevent="@post('/dash/settings/password')"
|
|
83
|
+
data-indicator="_passwordLoading"
|
|
84
|
+
>
|
|
85
|
+
<div class="card">
|
|
86
|
+
<header>
|
|
87
|
+
<h2>
|
|
88
|
+
{t({
|
|
89
|
+
message: "Change Password",
|
|
90
|
+
comment: "@context: Settings section heading",
|
|
91
|
+
})}
|
|
92
|
+
</h2>
|
|
93
|
+
</header>
|
|
94
|
+
<section class="flex flex-col gap-4">
|
|
95
|
+
<div class="field">
|
|
96
|
+
<label class="label">
|
|
97
|
+
{t({
|
|
98
|
+
message: "Current Password",
|
|
99
|
+
comment: "@context: Password form field",
|
|
100
|
+
})}
|
|
101
|
+
</label>
|
|
102
|
+
<input
|
|
103
|
+
type="password"
|
|
104
|
+
data-bind="currentPassword"
|
|
105
|
+
class="input"
|
|
106
|
+
required
|
|
107
|
+
autocomplete="current-password"
|
|
108
|
+
/>
|
|
109
|
+
</div>
|
|
110
|
+
|
|
111
|
+
<div class="field">
|
|
112
|
+
<label class="label">
|
|
113
|
+
{t({
|
|
114
|
+
message: "New Password",
|
|
115
|
+
comment: "@context: Password form field",
|
|
116
|
+
})}
|
|
117
|
+
</label>
|
|
118
|
+
<input
|
|
119
|
+
type="password"
|
|
120
|
+
data-bind="newPassword"
|
|
121
|
+
class="input"
|
|
122
|
+
required
|
|
123
|
+
minlength={8}
|
|
124
|
+
autocomplete="new-password"
|
|
125
|
+
/>
|
|
126
|
+
</div>
|
|
127
|
+
|
|
128
|
+
<div class="field">
|
|
129
|
+
<label class="label">
|
|
130
|
+
{t({
|
|
131
|
+
message: "Confirm New Password",
|
|
132
|
+
comment: "@context: Password form field",
|
|
133
|
+
})}
|
|
134
|
+
</label>
|
|
135
|
+
<input
|
|
136
|
+
type="password"
|
|
137
|
+
data-bind="confirmPassword"
|
|
138
|
+
class="input"
|
|
139
|
+
required
|
|
140
|
+
minlength={8}
|
|
141
|
+
autocomplete="new-password"
|
|
142
|
+
/>
|
|
143
|
+
</div>
|
|
144
|
+
</section>
|
|
145
|
+
</div>
|
|
146
|
+
|
|
147
|
+
<button
|
|
148
|
+
type="submit"
|
|
149
|
+
class="btn mt-4"
|
|
150
|
+
data-attr:disabled="$_passwordLoading"
|
|
151
|
+
>
|
|
152
|
+
<svg
|
|
153
|
+
data-show="$_passwordLoading"
|
|
154
|
+
style="display:none"
|
|
155
|
+
class="animate-spin size-4"
|
|
156
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
157
|
+
viewBox="0 0 24 24"
|
|
158
|
+
fill="none"
|
|
159
|
+
stroke="currentColor"
|
|
160
|
+
stroke-width="2"
|
|
161
|
+
stroke-linecap="round"
|
|
162
|
+
stroke-linejoin="round"
|
|
163
|
+
role="status"
|
|
164
|
+
>
|
|
165
|
+
<path d="M21 12a9 9 0 1 1-6.219-8.56" />
|
|
166
|
+
</svg>
|
|
167
|
+
{t({
|
|
168
|
+
message: "Change Password",
|
|
169
|
+
comment: "@context: Button to change password",
|
|
170
|
+
})}
|
|
171
|
+
</button>
|
|
172
|
+
</form>
|
|
173
|
+
</div>
|
|
174
|
+
</>
|
|
175
|
+
);
|
|
176
|
+
}
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Appearance settings: color theme picker + custom CSS form
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { useLingui } from "@lingui/react/macro";
|
|
6
|
+
import type { ColorTheme } from "../../color-themes.js";
|
|
7
|
+
import type { FontTheme } from "../../font-themes.js";
|
|
8
|
+
import { SettingsNav } from "./SettingsNav.js";
|
|
9
|
+
|
|
10
|
+
function ThemeCard({
|
|
11
|
+
theme,
|
|
12
|
+
selected,
|
|
13
|
+
}: {
|
|
14
|
+
theme: ColorTheme;
|
|
15
|
+
selected: boolean;
|
|
16
|
+
}) {
|
|
17
|
+
const expr = `$theme === '${theme.id}'`;
|
|
18
|
+
const { preview } = theme;
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<label
|
|
22
|
+
class={`block cursor-pointer rounded-lg border overflow-hidden transition-colors ${selected ? "border-primary" : "border-border"}`}
|
|
23
|
+
data-class:border-primary={expr}
|
|
24
|
+
data-class:border-border={`$theme !== '${theme.id}'`}
|
|
25
|
+
>
|
|
26
|
+
<div class="grid grid-cols-2">
|
|
27
|
+
<div
|
|
28
|
+
class="p-5"
|
|
29
|
+
style={`background-color:${preview.lightBg};color:${preview.lightText}`}
|
|
30
|
+
>
|
|
31
|
+
<input
|
|
32
|
+
type="radio"
|
|
33
|
+
name="theme"
|
|
34
|
+
value={theme.id}
|
|
35
|
+
data-bind="theme"
|
|
36
|
+
checked={selected || undefined}
|
|
37
|
+
class="mb-1"
|
|
38
|
+
/>
|
|
39
|
+
<h3 class="font-bold text-lg">{theme.name}</h3>
|
|
40
|
+
<p class="text-sm mt-2 leading-relaxed">
|
|
41
|
+
This is the {theme.name} theme in light mode. Links{" "}
|
|
42
|
+
<a
|
|
43
|
+
tabIndex={-1}
|
|
44
|
+
class="underline"
|
|
45
|
+
style={`color:${preview.lightLink}`}
|
|
46
|
+
>
|
|
47
|
+
look like this
|
|
48
|
+
</a>
|
|
49
|
+
. We'll show the correct light or dark mode based on your visitor's
|
|
50
|
+
settings.
|
|
51
|
+
</p>
|
|
52
|
+
</div>
|
|
53
|
+
<div
|
|
54
|
+
class="p-5"
|
|
55
|
+
style={`background-color:${preview.darkBg};color:${preview.darkText}`}
|
|
56
|
+
>
|
|
57
|
+
<h3 class="font-bold text-lg">{theme.name}</h3>
|
|
58
|
+
<p class="text-sm mt-2 leading-relaxed">
|
|
59
|
+
This is the {theme.name} theme in dark mode. Links{" "}
|
|
60
|
+
<a
|
|
61
|
+
tabIndex={-1}
|
|
62
|
+
class="underline"
|
|
63
|
+
style={`color:${preview.darkLink}`}
|
|
64
|
+
>
|
|
65
|
+
look like this
|
|
66
|
+
</a>
|
|
67
|
+
. We'll show the correct light or dark mode based on your visitor's
|
|
68
|
+
settings.
|
|
69
|
+
</p>
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
</label>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function AppearanceContent({
|
|
77
|
+
themes,
|
|
78
|
+
currentThemeId,
|
|
79
|
+
fontThemes,
|
|
80
|
+
currentFontThemeId,
|
|
81
|
+
customCSS,
|
|
82
|
+
}: {
|
|
83
|
+
themes: ColorTheme[];
|
|
84
|
+
currentThemeId: string;
|
|
85
|
+
fontThemes: FontTheme[];
|
|
86
|
+
currentFontThemeId: string;
|
|
87
|
+
customCSS: string;
|
|
88
|
+
}) {
|
|
89
|
+
const { t } = useLingui();
|
|
90
|
+
|
|
91
|
+
const themeSignals = JSON.stringify({ theme: currentThemeId }).replace(
|
|
92
|
+
/</g,
|
|
93
|
+
"\\u003c",
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
const cssSignals = JSON.stringify({ customCSS }).replace(/</g, "\\u003c");
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
<>
|
|
100
|
+
<h1 class="text-2xl font-semibold mb-2">
|
|
101
|
+
{t({ message: "Settings", comment: "@context: Dashboard heading" })}
|
|
102
|
+
</h1>
|
|
103
|
+
<SettingsNav currentTab="appearance" />
|
|
104
|
+
|
|
105
|
+
<div
|
|
106
|
+
data-signals={themeSignals}
|
|
107
|
+
data-on:change="@post('/dash/settings/appearance')"
|
|
108
|
+
class="max-w-3xl mb-8"
|
|
109
|
+
>
|
|
110
|
+
<fieldset>
|
|
111
|
+
<legend class="text-lg font-semibold">
|
|
112
|
+
{t({
|
|
113
|
+
message: "Color theme",
|
|
114
|
+
comment: "@context: Appearance settings heading",
|
|
115
|
+
})}
|
|
116
|
+
</legend>
|
|
117
|
+
<p class="text-sm text-muted-foreground mb-4">
|
|
118
|
+
{t({
|
|
119
|
+
message:
|
|
120
|
+
"This will theme both your site and your dashboard. All color themes support dark mode.",
|
|
121
|
+
comment: "@context: Appearance settings description",
|
|
122
|
+
})}
|
|
123
|
+
</p>
|
|
124
|
+
|
|
125
|
+
<div class="flex flex-col gap-4">
|
|
126
|
+
{themes.map((theme) => (
|
|
127
|
+
<ThemeCard
|
|
128
|
+
key={theme.id}
|
|
129
|
+
theme={theme}
|
|
130
|
+
selected={theme.id === currentThemeId}
|
|
131
|
+
/>
|
|
132
|
+
))}
|
|
133
|
+
</div>
|
|
134
|
+
</fieldset>
|
|
135
|
+
</div>
|
|
136
|
+
|
|
137
|
+
<div
|
|
138
|
+
data-signals={JSON.stringify({ fontTheme: currentFontThemeId }).replace(
|
|
139
|
+
/</g,
|
|
140
|
+
"\\u003c",
|
|
141
|
+
)}
|
|
142
|
+
data-on:change="@post('/dash/settings/font-theme')"
|
|
143
|
+
class="max-w-3xl"
|
|
144
|
+
>
|
|
145
|
+
<fieldset>
|
|
146
|
+
<legend class="text-lg font-semibold">
|
|
147
|
+
{t({
|
|
148
|
+
message: "Font theme",
|
|
149
|
+
comment: "@context: Appearance settings heading for font theme",
|
|
150
|
+
})}
|
|
151
|
+
</legend>
|
|
152
|
+
<p class="text-sm text-muted-foreground mb-4">
|
|
153
|
+
{t({
|
|
154
|
+
message:
|
|
155
|
+
"Choose a font for your site. All options use system fonts for fast loading.",
|
|
156
|
+
comment: "@context: Font theme settings description",
|
|
157
|
+
})}
|
|
158
|
+
</p>
|
|
159
|
+
<div class="flex flex-col gap-2">
|
|
160
|
+
{fontThemes.map((ft) => (
|
|
161
|
+
<label
|
|
162
|
+
key={ft.id}
|
|
163
|
+
class={`flex items-start gap-3 p-3 rounded-lg border cursor-pointer transition-colors ${ft.id === currentFontThemeId ? "border-primary" : "border-border"}`}
|
|
164
|
+
data-class:border-primary={`$fontTheme === '${ft.id}'`}
|
|
165
|
+
data-class:border-border={`$fontTheme !== '${ft.id}'`}
|
|
166
|
+
>
|
|
167
|
+
<input
|
|
168
|
+
type="radio"
|
|
169
|
+
name="fontTheme"
|
|
170
|
+
value={ft.id}
|
|
171
|
+
data-bind="fontTheme"
|
|
172
|
+
checked={ft.id === currentFontThemeId || undefined}
|
|
173
|
+
class="mt-1"
|
|
174
|
+
/>
|
|
175
|
+
<div>
|
|
176
|
+
<div class="font-medium">{ft.name}</div>
|
|
177
|
+
<div class="text-sm text-muted-foreground">
|
|
178
|
+
{ft.description}
|
|
179
|
+
</div>
|
|
180
|
+
<div
|
|
181
|
+
class="mt-1 text-sm"
|
|
182
|
+
style={`font-family:${ft.fontFamily}`}
|
|
183
|
+
>
|
|
184
|
+
The quick brown fox jumps over the lazy dog.{" "}
|
|
185
|
+
敏捷的棕色狐狸跳过了懒狗。
|
|
186
|
+
</div>
|
|
187
|
+
</div>
|
|
188
|
+
</label>
|
|
189
|
+
))}
|
|
190
|
+
</div>
|
|
191
|
+
</fieldset>
|
|
192
|
+
</div>
|
|
193
|
+
|
|
194
|
+
<form
|
|
195
|
+
data-signals={cssSignals}
|
|
196
|
+
data-on:submit__prevent="@post('/dash/settings/custom-css')"
|
|
197
|
+
data-indicator="_cssLoading"
|
|
198
|
+
class="max-w-3xl mt-8"
|
|
199
|
+
>
|
|
200
|
+
<fieldset>
|
|
201
|
+
<legend class="text-lg font-semibold">
|
|
202
|
+
{t({
|
|
203
|
+
message: "Custom CSS",
|
|
204
|
+
comment: "@context: Appearance settings heading for custom CSS",
|
|
205
|
+
})}
|
|
206
|
+
</legend>
|
|
207
|
+
<p class="text-sm text-muted-foreground mb-4">
|
|
208
|
+
{t({
|
|
209
|
+
message:
|
|
210
|
+
"Add custom CSS to override any styles. Use data attributes like [data-page], [data-post], [data-format] to target specific elements.",
|
|
211
|
+
comment: "@context: Custom CSS settings description",
|
|
212
|
+
})}
|
|
213
|
+
</p>
|
|
214
|
+
<textarea
|
|
215
|
+
data-bind="customCSS"
|
|
216
|
+
class="textarea font-mono text-sm min-h-32"
|
|
217
|
+
rows={8}
|
|
218
|
+
placeholder={t({
|
|
219
|
+
message: "/* Your custom CSS here */",
|
|
220
|
+
comment: "@context: Custom CSS textarea placeholder",
|
|
221
|
+
})}
|
|
222
|
+
>
|
|
223
|
+
{customCSS}
|
|
224
|
+
</textarea>
|
|
225
|
+
</fieldset>
|
|
226
|
+
<button
|
|
227
|
+
type="submit"
|
|
228
|
+
class="btn mt-4"
|
|
229
|
+
data-attr:disabled="$_cssLoading"
|
|
230
|
+
>
|
|
231
|
+
<svg
|
|
232
|
+
data-show="$_cssLoading"
|
|
233
|
+
style="display:none"
|
|
234
|
+
class="animate-spin size-4"
|
|
235
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
236
|
+
viewBox="0 0 24 24"
|
|
237
|
+
fill="none"
|
|
238
|
+
stroke="currentColor"
|
|
239
|
+
stroke-width="2"
|
|
240
|
+
stroke-linecap="round"
|
|
241
|
+
stroke-linejoin="round"
|
|
242
|
+
role="status"
|
|
243
|
+
>
|
|
244
|
+
<path d="M21 12a9 9 0 1 1-6.219-8.56" />
|
|
245
|
+
</svg>
|
|
246
|
+
{t({
|
|
247
|
+
message: "Save CSS",
|
|
248
|
+
comment: "@context: Button to save custom CSS",
|
|
249
|
+
})}
|
|
250
|
+
</button>
|
|
251
|
+
</form>
|
|
252
|
+
</>
|
|
253
|
+
);
|
|
254
|
+
}
|