@jant/core 0.3.43 → 0.3.45
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-GbfwoeDJ.js → app-C-L7wL6o.js} +485 -452
- package/dist/app-Hvqe7Ks_.js +5 -0
- package/dist/client/.vite/manifest.json +3 -3
- package/dist/client/_assets/client-DDs6NzB3.css +2 -0
- package/dist/client/_assets/{client-auth-CXILhW1b.js → client-auth-Dcon89Av.js} +30 -11
- package/dist/client/_assets/{client-D95FNDg5.js → client-dSfWfMe9.js} +7 -7
- package/dist/{github-sync-7y_nTXx1.js → github-sync-CQ1x271f.js} +3 -0
- package/dist/index.js +4 -87
- package/dist/node.js +3 -3
- package/package.json +1 -1
- package/src/client/components/jant-compose-dialog.ts +87 -9
- package/src/client/components/jant-compose-editor.ts +5 -1
- package/src/client/components/jant-post-menu.ts +23 -5
- package/src/client/compose-bridge.ts +2 -1
- package/src/client/toast.ts +29 -2
- package/src/client/upload-session.ts +1 -1
- package/src/db/migrations/0019_bored_magus.sql +2 -0
- package/src/db/migrations/0020_free_zaladane.sql +1 -0
- package/src/db/migrations/meta/0019_snapshot.json +2238 -0
- package/src/db/migrations/meta/0020_snapshot.json +2129 -0
- package/src/db/migrations/meta/_journal.json +14 -0
- package/src/db/migrations/pg/0017_bright_beyonder.sql +2 -0
- package/src/db/migrations/pg/0018_red_warlock.sql +1 -0
- package/src/db/migrations/pg/meta/0017_snapshot.json +2862 -0
- package/src/db/migrations/pg/meta/0018_snapshot.json +2739 -0
- package/src/db/migrations/pg/meta/_journal.json +14 -0
- package/src/db/pg/schema.ts +4 -30
- package/src/db/schema.ts +4 -39
- package/src/i18n/locales/public/en.po +10 -5
- package/src/i18n/locales/public/en.ts +1 -1
- package/src/i18n/locales/public/zh-Hans.po +10 -5
- package/src/i18n/locales/public/zh-Hans.ts +1 -1
- package/src/i18n/locales/public/zh-Hant.po +10 -5
- package/src/i18n/locales/public/zh-Hant.ts +1 -1
- package/src/index.ts +0 -3
- package/src/lib/__tests__/resolve-config.test.ts +4 -4
- package/src/lib/__tests__/startup-config.test.ts +27 -2
- package/src/lib/constants.ts +1 -0
- package/src/lib/github-sync-trigger.ts +7 -51
- package/src/lib/icons.ts +37 -0
- package/src/lib/startup-config.ts +53 -6
- package/src/routes/api/github-sync.tsx +36 -14
- package/src/routes/api/internal/sites.ts +1 -0
- package/src/routes/pages/home.tsx +2 -0
- package/src/routes/pages/latest.tsx +2 -0
- package/src/runtime/__tests__/readiness.test.ts +34 -0
- package/src/runtime/readiness.ts +8 -4
- package/src/services/__tests__/collection.test.ts +13 -11
- package/src/services/__tests__/site-admin.test.ts +85 -0
- package/src/services/github-sync.ts +6 -0
- package/src/services/site-admin.ts +66 -1
- package/src/styles/components.css +14 -0
- package/src/styles/ui.css +109 -0
- package/src/types/bindings.ts +0 -2
- package/src/types/config.ts +1 -1
- package/src/types/props.ts +2 -0
- package/src/ui/__tests__/font-themes.test.ts +2 -2
- package/src/ui/dash/settings/SettingsRootContent.tsx +17 -17
- package/src/ui/feed/LinkCard.tsx +3 -20
- package/src/ui/feed/LinkPreview.tsx +5 -19
- package/src/ui/feed/PostStatusBadges.tsx +4 -38
- package/src/ui/font-themes.ts +17 -17
- package/src/ui/layouts/BaseLayout.tsx +14 -29
- package/src/ui/pages/HomePage.tsx +21 -5
- package/src/ui/shared/DecorativeQuoteMark.tsx +2 -13
- package/src/ui/shared/Icon.tsx +60 -0
- package/src/ui/shared/IconSprite.tsx +57 -0
- package/src/ui/shared/PostFooter.tsx +6 -62
- package/src/ui/shared/custom-icons.ts +132 -0
- package/src/ui/shared/icon-collector.ts +37 -0
- package/dist/app-Ctl0T0zO.js +0 -5
- package/dist/client/_assets/client-C_kImWZj.css +0 -2
- package/src/lib/github-sync-queue-handler.ts +0 -69
- package/src/lib/github-sync-worker.ts +0 -72
- package/src/lib/job-queue-cf.ts +0 -18
- package/src/lib/job-queue-db.ts +0 -149
- package/src/lib/job-queue.ts +0 -35
|
@@ -45,4 +45,89 @@ describe("SiteAdminService", () => {
|
|
|
45
45
|
|
|
46
46
|
expect(siteRows).toEqual([{ key: "demo-cloud" }]);
|
|
47
47
|
});
|
|
48
|
+
|
|
49
|
+
it("returns the existing site when replayed with the same idempotency key", async () => {
|
|
50
|
+
const { db } = createTestDatabase();
|
|
51
|
+
const service = createSiteAdminService(db, sqliteSchemaBundle, "sqlite", {
|
|
52
|
+
siteResolutionMode: "host-based",
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const first = await service.createManagedSite({
|
|
56
|
+
key: "idem-site",
|
|
57
|
+
primaryHost: "idem-site.example.com",
|
|
58
|
+
siteName: "Idempotent Site",
|
|
59
|
+
idempotencyKey: "job_abc",
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const second = await service.createManagedSite({
|
|
63
|
+
key: "idem-site",
|
|
64
|
+
primaryHost: "idem-site.example.com",
|
|
65
|
+
siteName: "Idempotent Site",
|
|
66
|
+
idempotencyKey: "job_abc",
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
expect(second.site.id).toBe(first.site.id);
|
|
70
|
+
expect(second.domain.id).toBe(first.domain.id);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("rejects reuse of an idempotency key with different key or primary host", async () => {
|
|
74
|
+
const { db } = createTestDatabase();
|
|
75
|
+
const service = createSiteAdminService(db, sqliteSchemaBundle, "sqlite", {
|
|
76
|
+
siteResolutionMode: "host-based",
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
await service.createManagedSite({
|
|
80
|
+
key: "idem-site",
|
|
81
|
+
primaryHost: "idem-site.example.com",
|
|
82
|
+
siteName: "Idempotent Site",
|
|
83
|
+
idempotencyKey: "job_xyz",
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
await expect(
|
|
87
|
+
service.createManagedSite({
|
|
88
|
+
key: "other-site",
|
|
89
|
+
primaryHost: "idem-site.example.com",
|
|
90
|
+
siteName: "Other Site",
|
|
91
|
+
idempotencyKey: "job_xyz",
|
|
92
|
+
}),
|
|
93
|
+
).rejects.toEqual(
|
|
94
|
+
new ConflictError(
|
|
95
|
+
"Idempotency key was reused with a different site key or primary host.",
|
|
96
|
+
),
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
await expect(
|
|
100
|
+
service.createManagedSite({
|
|
101
|
+
key: "idem-site",
|
|
102
|
+
primaryHost: "different-host.example.com",
|
|
103
|
+
siteName: "Idempotent Site",
|
|
104
|
+
idempotencyKey: "job_xyz",
|
|
105
|
+
}),
|
|
106
|
+
).rejects.toEqual(
|
|
107
|
+
new ConflictError(
|
|
108
|
+
"Idempotency key was reused with a different site key or primary host.",
|
|
109
|
+
),
|
|
110
|
+
);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("treats requests without an idempotency key as independent creations", async () => {
|
|
114
|
+
const { db } = createTestDatabase();
|
|
115
|
+
const service = createSiteAdminService(db, sqliteSchemaBundle, "sqlite", {
|
|
116
|
+
siteResolutionMode: "host-based",
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
await service.createManagedSite({
|
|
120
|
+
key: "no-idem-site",
|
|
121
|
+
primaryHost: "no-idem-site.example.com",
|
|
122
|
+
siteName: "No Idem Site",
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
await expect(
|
|
126
|
+
service.createManagedSite({
|
|
127
|
+
key: "no-idem-site",
|
|
128
|
+
primaryHost: "no-idem-site-2.example.com",
|
|
129
|
+
siteName: "No Idem Site",
|
|
130
|
+
}),
|
|
131
|
+
).rejects.toEqual(new ConflictError("Site key is already in use."));
|
|
132
|
+
});
|
|
48
133
|
});
|
|
@@ -651,9 +651,15 @@ export function createGitHubSyncService(
|
|
|
651
651
|
if (frontMatter.link_url !== undefined) {
|
|
652
652
|
updateData.url = frontMatter.link_url;
|
|
653
653
|
}
|
|
654
|
+
if (frontMatter.source_name !== undefined)
|
|
655
|
+
updateData.sourceName = frontMatter.source_name;
|
|
656
|
+
if (frontMatter.source_url !== undefined)
|
|
657
|
+
updateData.sourceUrl = frontMatter.source_url;
|
|
654
658
|
if (frontMatter.quote_text !== undefined) {
|
|
655
659
|
updateData.quoteText = frontMatter.quote_text;
|
|
656
660
|
}
|
|
661
|
+
if (frontMatter.rating !== undefined)
|
|
662
|
+
updateData.rating = frontMatter.rating;
|
|
657
663
|
|
|
658
664
|
if (Object.keys(updateData).length > 0) {
|
|
659
665
|
await services.posts.update(existingPost.id, updateData);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { eq, sql } from "drizzle-orm";
|
|
1
|
+
import { and, eq, sql } from "drizzle-orm";
|
|
2
2
|
import {
|
|
3
3
|
executeStatement,
|
|
4
4
|
type Database,
|
|
@@ -45,6 +45,13 @@ export interface CreateManagedSiteInput {
|
|
|
45
45
|
siteName: string;
|
|
46
46
|
siteLanguage?: string | null;
|
|
47
47
|
timeZone?: string | null;
|
|
48
|
+
/**
|
|
49
|
+
* Optional caller-supplied idempotency key. When provided, retrying the same
|
|
50
|
+
* request returns the previously created site instead of a 409 conflict.
|
|
51
|
+
* Reusing the key with a different `key` or `primaryHost` is rejected as a
|
|
52
|
+
* client bug.
|
|
53
|
+
*/
|
|
54
|
+
idempotencyKey?: string | null;
|
|
48
55
|
}
|
|
49
56
|
|
|
50
57
|
export interface ManagedSiteResult {
|
|
@@ -194,6 +201,47 @@ export function createSiteAdminService(
|
|
|
194
201
|
return `${protocol}//${domain.host}${pathPrefix}`;
|
|
195
202
|
}
|
|
196
203
|
|
|
204
|
+
async function loadByIdempotencyKey(
|
|
205
|
+
targetDb: Database,
|
|
206
|
+
idempotencyKey: string,
|
|
207
|
+
): Promise<ManagedSiteResult | null> {
|
|
208
|
+
const siteRow = (
|
|
209
|
+
await targetDb
|
|
210
|
+
.select()
|
|
211
|
+
.from(sites)
|
|
212
|
+
.where(eq(sites.provisioningIdempotencyKey, idempotencyKey))
|
|
213
|
+
.limit(1)
|
|
214
|
+
)[0];
|
|
215
|
+
if (!siteRow) {
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const domainRow = (
|
|
220
|
+
await targetDb
|
|
221
|
+
.select()
|
|
222
|
+
.from(siteDomains)
|
|
223
|
+
.where(
|
|
224
|
+
and(
|
|
225
|
+
eq(siteDomains.siteId, siteRow.id),
|
|
226
|
+
eq(siteDomains.kind, "primary"),
|
|
227
|
+
),
|
|
228
|
+
)
|
|
229
|
+
.limit(1)
|
|
230
|
+
)[0];
|
|
231
|
+
if (!domainRow) {
|
|
232
|
+
// A site row without a primary domain means the original creation aborted
|
|
233
|
+
// mid-transaction on a dialect without real transactions. Treat as not
|
|
234
|
+
// found so the caller can retry; the partial unique index will surface a
|
|
235
|
+
// genuine duplicate.
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return {
|
|
240
|
+
site: toSite(siteRow),
|
|
241
|
+
domain: toSiteDomain(domainRow),
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
197
245
|
async function createWithDatabase(
|
|
198
246
|
targetDb: Database,
|
|
199
247
|
input: CreateManagedSiteInput,
|
|
@@ -201,6 +249,22 @@ export function createSiteAdminService(
|
|
|
201
249
|
const siteKey = input.key.trim();
|
|
202
250
|
const primaryHost = input.primaryHost.trim().toLowerCase();
|
|
203
251
|
const siteName = input.siteName.trim();
|
|
252
|
+
const idempotencyKey = input.idempotencyKey?.trim() || null;
|
|
253
|
+
|
|
254
|
+
if (idempotencyKey) {
|
|
255
|
+
const existing = await loadByIdempotencyKey(targetDb, idempotencyKey);
|
|
256
|
+
if (existing) {
|
|
257
|
+
if (
|
|
258
|
+
existing.site.key !== siteKey ||
|
|
259
|
+
existing.domain.host !== primaryHost
|
|
260
|
+
) {
|
|
261
|
+
throw new ConflictError(
|
|
262
|
+
"Idempotency key was reused with a different site key or primary host.",
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
return existing;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
204
268
|
|
|
205
269
|
const existingSite = await targetDb
|
|
206
270
|
.select({ id: sites.id })
|
|
@@ -231,6 +295,7 @@ export function createSiteAdminService(
|
|
|
231
295
|
id: siteId,
|
|
232
296
|
key: siteKey,
|
|
233
297
|
status: "active",
|
|
298
|
+
provisioningIdempotencyKey: idempotencyKey,
|
|
234
299
|
createdAt: timestamp,
|
|
235
300
|
updatedAt: timestamp,
|
|
236
301
|
})
|
|
@@ -106,6 +106,20 @@ svg[stroke-width].icon-fine {
|
|
|
106
106
|
}
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
+
.toast-copy {
|
|
110
|
+
@apply shrink-0 translate-y-0.5 cursor-pointer rounded-sm p-0 border-0 bg-transparent;
|
|
111
|
+
color: var(--color-muted-foreground);
|
|
112
|
+
transition: color 0.15s;
|
|
113
|
+
|
|
114
|
+
&:hover {
|
|
115
|
+
color: var(--color-foreground);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
> svg {
|
|
119
|
+
@apply size-3.5;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
109
123
|
.toast-out {
|
|
110
124
|
animation: toast-out 0.3s ease-in forwards;
|
|
111
125
|
}
|
package/src/styles/ui.css
CHANGED
|
@@ -2100,6 +2100,10 @@
|
|
|
2100
2100
|
width: 30px;
|
|
2101
2101
|
height: 9px;
|
|
2102
2102
|
margin: 4rem 0;
|
|
2103
|
+
/* Center over the post-body column. On desktop the body column is
|
|
2104
|
+
`var(--layout-content-width)` (55%). Between 760-1024px it switches
|
|
2105
|
+
to `min(100%, 35rem)` via preset.css, so the hr must track that
|
|
2106
|
+
same rule — otherwise the glyph drifts left of the content center. */
|
|
2103
2107
|
margin-left: calc(var(--layout-content-width) / 2 - 15px);
|
|
2104
2108
|
color: var(--site-feed-divider-color);
|
|
2105
2109
|
background-color: currentColor;
|
|
@@ -2113,6 +2117,14 @@
|
|
|
2113
2117
|
mask-size: contain;
|
|
2114
2118
|
}
|
|
2115
2119
|
|
|
2120
|
+
@media (max-width: 1024px) {
|
|
2121
|
+
.site-content hr.feed-divider {
|
|
2122
|
+
/* Match preset.css `min(100%, 35rem)` body-column width so the hr
|
|
2123
|
+
stays visually aligned with the post column at this breakpoint. */
|
|
2124
|
+
margin-left: calc(min(100%, 35rem) / 2 - 15px);
|
|
2125
|
+
}
|
|
2126
|
+
}
|
|
2127
|
+
|
|
2116
2128
|
.feed-card {
|
|
2117
2129
|
position: relative;
|
|
2118
2130
|
padding: 1rem 1.1rem 0.95rem;
|
|
@@ -5784,6 +5796,87 @@
|
|
|
5784
5796
|
min-width: 0;
|
|
5785
5797
|
}
|
|
5786
5798
|
|
|
5799
|
+
.compose-quick-actions-row {
|
|
5800
|
+
display: flex;
|
|
5801
|
+
justify-content: flex-end;
|
|
5802
|
+
padding: 2px 12px 12px;
|
|
5803
|
+
background-color: var(--compose-paper-bg);
|
|
5804
|
+
}
|
|
5805
|
+
|
|
5806
|
+
.compose-action-row:has(+ .compose-quick-actions-row) {
|
|
5807
|
+
padding-bottom: 4px;
|
|
5808
|
+
}
|
|
5809
|
+
|
|
5810
|
+
@media (min-width: 700px) {
|
|
5811
|
+
.compose-quick-actions-row {
|
|
5812
|
+
padding: 2px 18px 14px;
|
|
5813
|
+
}
|
|
5814
|
+
|
|
5815
|
+
.compose-action-row:has(+ .compose-quick-actions-row) {
|
|
5816
|
+
padding-bottom: 4px;
|
|
5817
|
+
}
|
|
5818
|
+
}
|
|
5819
|
+
|
|
5820
|
+
.compose-publish-quick-toggle {
|
|
5821
|
+
display: inline-flex;
|
|
5822
|
+
align-items: center;
|
|
5823
|
+
gap: 0.4rem;
|
|
5824
|
+
padding: 0.5rem 0.55rem;
|
|
5825
|
+
margin: 0 -0.4rem;
|
|
5826
|
+
min-height: 36px;
|
|
5827
|
+
font-size: var(--type-ui-caption);
|
|
5828
|
+
color: color-mix(in srgb, var(--site-text-secondary) 78%, transparent);
|
|
5829
|
+
cursor: pointer;
|
|
5830
|
+
user-select: none;
|
|
5831
|
+
line-height: 1.1;
|
|
5832
|
+
border-radius: 6px;
|
|
5833
|
+
-webkit-tap-highlight-color: transparent;
|
|
5834
|
+
transition:
|
|
5835
|
+
color 0.15s ease,
|
|
5836
|
+
background-color 0.15s ease;
|
|
5837
|
+
}
|
|
5838
|
+
|
|
5839
|
+
.compose-publish-quick-toggle:active {
|
|
5840
|
+
background-color: color-mix(
|
|
5841
|
+
in srgb,
|
|
5842
|
+
var(--site-text-primary) 6%,
|
|
5843
|
+
transparent
|
|
5844
|
+
);
|
|
5845
|
+
}
|
|
5846
|
+
|
|
5847
|
+
@media (min-width: 700px) {
|
|
5848
|
+
.compose-publish-quick-toggle {
|
|
5849
|
+
padding: 0.3rem 0.35rem;
|
|
5850
|
+
margin: 0 -0.2rem;
|
|
5851
|
+
min-height: 0;
|
|
5852
|
+
}
|
|
5853
|
+
}
|
|
5854
|
+
|
|
5855
|
+
.compose-publish-quick-toggle:hover {
|
|
5856
|
+
color: var(--site-text-secondary);
|
|
5857
|
+
}
|
|
5858
|
+
|
|
5859
|
+
.compose-publish-quick-toggle:has(input:checked) {
|
|
5860
|
+
color: color-mix(in srgb, var(--site-text-primary) 85%, transparent);
|
|
5861
|
+
}
|
|
5862
|
+
|
|
5863
|
+
.compose-publish-quick-toggle-input {
|
|
5864
|
+
width: 0.88rem !important;
|
|
5865
|
+
height: 0.88rem !important;
|
|
5866
|
+
border-radius: 3px;
|
|
5867
|
+
}
|
|
5868
|
+
|
|
5869
|
+
.compose-publish-quick-toggle-input:checked:after {
|
|
5870
|
+
width: 0.74rem !important;
|
|
5871
|
+
height: 0.74rem !important;
|
|
5872
|
+
mask-size: 0.74rem !important;
|
|
5873
|
+
}
|
|
5874
|
+
|
|
5875
|
+
.compose-publish-quick-toggle:has(input:disabled) {
|
|
5876
|
+
opacity: 0.5;
|
|
5877
|
+
cursor: not-allowed;
|
|
5878
|
+
}
|
|
5879
|
+
|
|
5787
5880
|
.compose-publish-summaries {
|
|
5788
5881
|
display: flex;
|
|
5789
5882
|
flex-wrap: wrap;
|
|
@@ -6826,6 +6919,9 @@
|
|
|
6826
6919
|
border: none;
|
|
6827
6920
|
padding: 8px;
|
|
6828
6921
|
transition: background-color 0.15s;
|
|
6922
|
+
flex-direction: column;
|
|
6923
|
+
justify-content: center;
|
|
6924
|
+
gap: 6px;
|
|
6829
6925
|
}
|
|
6830
6926
|
|
|
6831
6927
|
.compose-attachment-retry:hover {
|
|
@@ -6855,6 +6951,19 @@
|
|
|
6855
6951
|
opacity: 0.9;
|
|
6856
6952
|
}
|
|
6857
6953
|
|
|
6954
|
+
.compose-attachment-error-msg {
|
|
6955
|
+
font-size: 10px;
|
|
6956
|
+
line-height: 1.3;
|
|
6957
|
+
color: white;
|
|
6958
|
+
text-align: center;
|
|
6959
|
+
word-break: break-all;
|
|
6960
|
+
padding: 0 4px;
|
|
6961
|
+
user-select: text;
|
|
6962
|
+
text-shadow:
|
|
6963
|
+
0 1px 3px rgba(0, 0, 0, 0.9),
|
|
6964
|
+
0 0 6px rgba(0, 0, 0, 0.7);
|
|
6965
|
+
}
|
|
6966
|
+
|
|
6858
6967
|
.compose-attachment-remove {
|
|
6859
6968
|
position: absolute;
|
|
6860
6969
|
top: 6px;
|
package/src/types/bindings.ts
CHANGED
package/src/types/config.ts
CHANGED
package/src/types/props.ts
CHANGED
|
@@ -11,8 +11,8 @@ describe("BUILTIN_FONT_THEMES", () => {
|
|
|
11
11
|
expect(BUILTIN_FONT_THEMES).toHaveLength(7);
|
|
12
12
|
});
|
|
13
13
|
|
|
14
|
-
it("has '
|
|
15
|
-
expect(BUILTIN_FONT_THEMES[0].id).toBe("
|
|
14
|
+
it("has 'classic' as the first theme", () => {
|
|
15
|
+
expect(BUILTIN_FONT_THEMES[0].id).toBe("classic");
|
|
16
16
|
});
|
|
17
17
|
|
|
18
18
|
it("each theme has required fields", () => {
|
|
@@ -97,6 +97,23 @@ export function SettingsRootContent({
|
|
|
97
97
|
}),
|
|
98
98
|
)}
|
|
99
99
|
/>
|
|
100
|
+
<SettingsDirectoryLink
|
|
101
|
+
href={toPublicPath("/settings/custom-urls", sitePathPrefix)}
|
|
102
|
+
icon={ICONS.arrowRightLeft}
|
|
103
|
+
tone="subtle"
|
|
104
|
+
name={i18n._(
|
|
105
|
+
msg({
|
|
106
|
+
message: "Custom URLs",
|
|
107
|
+
comment: "@context: Settings item — custom URL settings",
|
|
108
|
+
}),
|
|
109
|
+
)}
|
|
110
|
+
description={i18n._(
|
|
111
|
+
msg({
|
|
112
|
+
message: "Redirects, vanity paths, and URL control",
|
|
113
|
+
comment: "@context: Settings item description for custom URLs",
|
|
114
|
+
}),
|
|
115
|
+
)}
|
|
116
|
+
/>
|
|
100
117
|
</SettingsDirectorySection>
|
|
101
118
|
|
|
102
119
|
<SettingsDirectorySection
|
|
@@ -227,23 +244,6 @@ export function SettingsRootContent({
|
|
|
227
244
|
}),
|
|
228
245
|
)}
|
|
229
246
|
>
|
|
230
|
-
<SettingsDirectoryLink
|
|
231
|
-
href={toPublicPath("/settings/custom-urls", sitePathPrefix)}
|
|
232
|
-
icon={ICONS.arrowRightLeft}
|
|
233
|
-
tone="subtle"
|
|
234
|
-
name={i18n._(
|
|
235
|
-
msg({
|
|
236
|
-
message: "Custom URLs",
|
|
237
|
-
comment: "@context: Settings item — custom URL settings",
|
|
238
|
-
}),
|
|
239
|
-
)}
|
|
240
|
-
description={i18n._(
|
|
241
|
-
msg({
|
|
242
|
-
message: "Redirects, vanity paths, and URL control",
|
|
243
|
-
comment: "@context: Settings item description for custom URLs",
|
|
244
|
-
}),
|
|
245
|
-
)}
|
|
246
|
-
/>
|
|
247
247
|
<SettingsDirectoryLink
|
|
248
248
|
href={toPublicPath("/settings/code-injection", sitePathPrefix)}
|
|
249
249
|
icon={ICONS.terminal}
|
package/src/ui/feed/LinkCard.tsx
CHANGED
|
@@ -14,6 +14,7 @@ import { PostStatusBadges } from "./PostStatusBadges.js";
|
|
|
14
14
|
import { sanitizeUrl, extractDisplayDomain } from "../../lib/url.js";
|
|
15
15
|
import { MediaGallery } from "../shared/MediaGallery.js";
|
|
16
16
|
import { LinkPreview } from "./LinkPreview.js";
|
|
17
|
+
import { Icon } from "../shared/Icon.js";
|
|
17
18
|
|
|
18
19
|
export const LinkCard: FC<TimelineCardProps> = ({
|
|
19
20
|
post,
|
|
@@ -39,30 +40,12 @@ export const LinkCard: FC<TimelineCardProps> = ({
|
|
|
39
40
|
target="_blank"
|
|
40
41
|
rel="noopener noreferrer"
|
|
41
42
|
>
|
|
42
|
-
<
|
|
43
|
-
class="feed-link-domain-icon"
|
|
44
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
45
|
-
fill="none"
|
|
46
|
-
viewBox="0 0 24 24"
|
|
47
|
-
stroke-width="2"
|
|
48
|
-
stroke="currentColor"
|
|
49
|
-
>
|
|
50
|
-
<path d="M13.5 6H5.25A2.25 2.25 0 0 0 3 8.25v10.5A2.25 2.25 0 0 0 5.25 21h10.5A2.25 2.25 0 0 0 18 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25" />
|
|
51
|
-
</svg>
|
|
43
|
+
<Icon name="link-domain" class="feed-link-domain-icon" />
|
|
52
44
|
<span>{domain}</span>
|
|
53
45
|
</a>
|
|
54
46
|
) : (
|
|
55
47
|
<div class="feed-link-domain">
|
|
56
|
-
<
|
|
57
|
-
class="feed-link-domain-icon"
|
|
58
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
59
|
-
fill="none"
|
|
60
|
-
viewBox="0 0 24 24"
|
|
61
|
-
stroke-width="2"
|
|
62
|
-
stroke="currentColor"
|
|
63
|
-
>
|
|
64
|
-
<path d="M13.5 6H5.25A2.25 2.25 0 0 0 3 8.25v10.5A2.25 2.25 0 0 0 5.25 21h10.5A2.25 2.25 0 0 0 18 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25" />
|
|
65
|
-
</svg>
|
|
48
|
+
<Icon name="link-domain" class="feed-link-domain-icon" />
|
|
66
49
|
<span>{domain}</span>
|
|
67
50
|
</div>
|
|
68
51
|
));
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import type { FC } from "hono/jsx";
|
|
9
|
+
import { Icon } from "../shared/Icon.js";
|
|
9
10
|
|
|
10
11
|
interface LinkPreviewProps {
|
|
11
12
|
imageUrl: string;
|
|
@@ -41,31 +42,16 @@ export const LinkPreview: FC<LinkPreviewProps> = ({
|
|
|
41
42
|
<img src={imageUrl} alt="" class="link-preview-image" loading="lazy" />
|
|
42
43
|
{isVideo && (
|
|
43
44
|
<div class="link-preview-play" aria-hidden="true">
|
|
44
|
-
<
|
|
45
|
-
class="link-preview-play-icon"
|
|
46
|
-
viewBox="0 0 68 48"
|
|
47
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
48
|
-
>
|
|
49
|
-
<path
|
|
50
|
-
class="link-preview-play-bg"
|
|
51
|
-
d="M66.52 7.74c-.78-2.93-2.49-5.41-5.42-6.19C55.79.13 34 0 34 0S12.21.13 6.9 1.55C3.97 2.33 2.27 4.81 1.48 7.74.06 13.05 0 24 0 24s.06 10.95 1.48 16.26c.78 2.93 2.49 5.41 5.42 6.19C12.21 47.87 34 48 34 48s21.79-.13 27.1-1.55c2.93-.78 4.64-3.26 5.42-6.19C67.94 34.95 68 24 68 24s-.06-10.95-1.48-16.26z"
|
|
52
|
-
fill="rgba(0,0,0,.65)"
|
|
53
|
-
/>
|
|
54
|
-
<path d="M45 24L27 14v20" fill="#fff" />
|
|
55
|
-
</svg>
|
|
45
|
+
<Icon name="link-preview-play" class="link-preview-play-icon" />
|
|
56
46
|
</div>
|
|
57
47
|
)}
|
|
58
48
|
{providerLabel && (
|
|
59
49
|
<span class="link-preview-badge" aria-hidden="true">
|
|
60
50
|
{isVideo && (
|
|
61
|
-
<
|
|
51
|
+
<Icon
|
|
52
|
+
name="link-preview-badge-play"
|
|
62
53
|
class="link-preview-badge-icon"
|
|
63
|
-
|
|
64
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
65
|
-
fill="currentColor"
|
|
66
|
-
>
|
|
67
|
-
<path d="M5.5 3.5v9l7-4.5z" />
|
|
68
|
-
</svg>
|
|
54
|
+
/>
|
|
69
55
|
)}
|
|
70
56
|
{providerLabel}
|
|
71
57
|
</span>
|
|
@@ -9,55 +9,21 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import type { FC } from "hono/jsx";
|
|
12
|
+
import { Icon } from "../shared/Icon.js";
|
|
12
13
|
|
|
13
14
|
export const PostStatusBadges: FC = () => {
|
|
14
15
|
return (
|
|
15
16
|
<div class="post-status-badges">
|
|
16
17
|
<span class="post-status-badge post-status-pinned">
|
|
17
|
-
<
|
|
18
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
19
|
-
viewBox="0 0 24 24"
|
|
20
|
-
fill="none"
|
|
21
|
-
stroke="currentColor"
|
|
22
|
-
stroke-width="1.75"
|
|
23
|
-
stroke-linecap="round"
|
|
24
|
-
stroke-linejoin="round"
|
|
25
|
-
>
|
|
26
|
-
<line x1="12" x2="12" y1="17" y2="22" />
|
|
27
|
-
<path d="M5 17h14v-1.76a2 2 0 0 0-1.11-1.79l-1.78-.9A2 2 0 0 1 15 10.76V6h1a2 2 0 0 0 0-4H8a2 2 0 0 0 0 4h1v4.76a2 2 0 0 1-1.11 1.79l-1.78.9A2 2 0 0 0 5 15.24Z" />
|
|
28
|
-
</svg>
|
|
18
|
+
<Icon name="post-status-pin" />
|
|
29
19
|
Pinned
|
|
30
20
|
</span>
|
|
31
21
|
<span class="post-status-badge post-status-pinned-in-collection">
|
|
32
|
-
<
|
|
33
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
34
|
-
viewBox="0 0 24 24"
|
|
35
|
-
fill="none"
|
|
36
|
-
stroke="currentColor"
|
|
37
|
-
stroke-width="1.75"
|
|
38
|
-
stroke-linecap="round"
|
|
39
|
-
stroke-linejoin="round"
|
|
40
|
-
>
|
|
41
|
-
<line x1="12" x2="12" y1="17" y2="22" />
|
|
42
|
-
<path d="M5 17h14v-1.76a2 2 0 0 0-1.11-1.79l-1.78-.9A2 2 0 0 1 15 10.76V6h1a2 2 0 0 0 0-4H8a2 2 0 0 0 0 4h1v4.76a2 2 0 0 1-1.11 1.79l-1.78.9A2 2 0 0 0 5 15.24Z" />
|
|
43
|
-
</svg>
|
|
22
|
+
<Icon name="post-status-pin" />
|
|
44
23
|
Pinned
|
|
45
24
|
</span>
|
|
46
25
|
<span class="post-status-badge post-status-private">
|
|
47
|
-
<
|
|
48
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
49
|
-
viewBox="0 0 24 24"
|
|
50
|
-
fill="none"
|
|
51
|
-
stroke="currentColor"
|
|
52
|
-
stroke-width="1.75"
|
|
53
|
-
stroke-linecap="round"
|
|
54
|
-
stroke-linejoin="round"
|
|
55
|
-
>
|
|
56
|
-
<path d="M10.733 5.076a10.744 10.744 0 0 1 11.205 6.575 1 1 0 0 1 0 .696 10.747 10.747 0 0 1-1.444 2.49" />
|
|
57
|
-
<path d="M14.084 14.158a3 3 0 0 1-4.242-4.242" />
|
|
58
|
-
<path d="M17.479 17.499a10.75 10.75 0 0 1-15.417-5.151 1 1 0 0 1 0-.696 10.75 10.75 0 0 1 4.446-5.143" />
|
|
59
|
-
<path d="m2 2 20 20" />
|
|
60
|
-
</svg>
|
|
26
|
+
<Icon name="post-status-private" />
|
|
61
27
|
Private
|
|
62
28
|
</span>
|
|
63
29
|
</div>
|
package/src/ui/font-themes.ts
CHANGED
|
@@ -138,23 +138,6 @@ export function getCjkSerifCssVariables(
|
|
|
138
138
|
export const DEFAULT_FONT_CJK_SERIF_FALLBACK = DEFAULT_CJK_SERIF_FALLBACK;
|
|
139
139
|
|
|
140
140
|
export const BUILTIN_FONT_THEMES: FontTheme[] = [
|
|
141
|
-
{
|
|
142
|
-
id: "tufte",
|
|
143
|
-
name: {
|
|
144
|
-
id: "Tufte",
|
|
145
|
-
message: "Tufte",
|
|
146
|
-
comment: "@context: Font theme name",
|
|
147
|
-
},
|
|
148
|
-
headingFontFamily: TUFTE_SERIF,
|
|
149
|
-
bodyFontFamily: TUFTE_SERIF,
|
|
150
|
-
cssVariables: {},
|
|
151
|
-
description: {
|
|
152
|
-
id: "Palatino-based old-style serif matching Tufte CSS proportions",
|
|
153
|
-
message: "Palatino-based old-style serif matching Tufte CSS proportions",
|
|
154
|
-
comment: "@context: Font theme description",
|
|
155
|
-
},
|
|
156
|
-
},
|
|
157
|
-
|
|
158
141
|
{
|
|
159
142
|
id: "classic",
|
|
160
143
|
name: {
|
|
@@ -177,6 +160,23 @@ export const BUILTIN_FONT_THEMES: FontTheme[] = [
|
|
|
177
160
|
},
|
|
178
161
|
},
|
|
179
162
|
|
|
163
|
+
{
|
|
164
|
+
id: "tufte",
|
|
165
|
+
name: {
|
|
166
|
+
id: "Tufte",
|
|
167
|
+
message: "Tufte",
|
|
168
|
+
comment: "@context: Font theme name",
|
|
169
|
+
},
|
|
170
|
+
headingFontFamily: TUFTE_SERIF,
|
|
171
|
+
bodyFontFamily: TUFTE_SERIF,
|
|
172
|
+
cssVariables: {},
|
|
173
|
+
description: {
|
|
174
|
+
id: "Palatino-based old-style serif matching Tufte CSS proportions",
|
|
175
|
+
message: "Palatino-based old-style serif matching Tufte CSS proportions",
|
|
176
|
+
comment: "@context: Font theme description",
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
|
|
180
180
|
{
|
|
181
181
|
id: "system-sans",
|
|
182
182
|
name: {
|