@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.
Files changed (77) hide show
  1. package/dist/{app-GbfwoeDJ.js → app-C-L7wL6o.js} +485 -452
  2. package/dist/app-Hvqe7Ks_.js +5 -0
  3. package/dist/client/.vite/manifest.json +3 -3
  4. package/dist/client/_assets/client-DDs6NzB3.css +2 -0
  5. package/dist/client/_assets/{client-auth-CXILhW1b.js → client-auth-Dcon89Av.js} +30 -11
  6. package/dist/client/_assets/{client-D95FNDg5.js → client-dSfWfMe9.js} +7 -7
  7. package/dist/{github-sync-7y_nTXx1.js → github-sync-CQ1x271f.js} +3 -0
  8. package/dist/index.js +4 -87
  9. package/dist/node.js +3 -3
  10. package/package.json +1 -1
  11. package/src/client/components/jant-compose-dialog.ts +87 -9
  12. package/src/client/components/jant-compose-editor.ts +5 -1
  13. package/src/client/components/jant-post-menu.ts +23 -5
  14. package/src/client/compose-bridge.ts +2 -1
  15. package/src/client/toast.ts +29 -2
  16. package/src/client/upload-session.ts +1 -1
  17. package/src/db/migrations/0019_bored_magus.sql +2 -0
  18. package/src/db/migrations/0020_free_zaladane.sql +1 -0
  19. package/src/db/migrations/meta/0019_snapshot.json +2238 -0
  20. package/src/db/migrations/meta/0020_snapshot.json +2129 -0
  21. package/src/db/migrations/meta/_journal.json +14 -0
  22. package/src/db/migrations/pg/0017_bright_beyonder.sql +2 -0
  23. package/src/db/migrations/pg/0018_red_warlock.sql +1 -0
  24. package/src/db/migrations/pg/meta/0017_snapshot.json +2862 -0
  25. package/src/db/migrations/pg/meta/0018_snapshot.json +2739 -0
  26. package/src/db/migrations/pg/meta/_journal.json +14 -0
  27. package/src/db/pg/schema.ts +4 -30
  28. package/src/db/schema.ts +4 -39
  29. package/src/i18n/locales/public/en.po +10 -5
  30. package/src/i18n/locales/public/en.ts +1 -1
  31. package/src/i18n/locales/public/zh-Hans.po +10 -5
  32. package/src/i18n/locales/public/zh-Hans.ts +1 -1
  33. package/src/i18n/locales/public/zh-Hant.po +10 -5
  34. package/src/i18n/locales/public/zh-Hant.ts +1 -1
  35. package/src/index.ts +0 -3
  36. package/src/lib/__tests__/resolve-config.test.ts +4 -4
  37. package/src/lib/__tests__/startup-config.test.ts +27 -2
  38. package/src/lib/constants.ts +1 -0
  39. package/src/lib/github-sync-trigger.ts +7 -51
  40. package/src/lib/icons.ts +37 -0
  41. package/src/lib/startup-config.ts +53 -6
  42. package/src/routes/api/github-sync.tsx +36 -14
  43. package/src/routes/api/internal/sites.ts +1 -0
  44. package/src/routes/pages/home.tsx +2 -0
  45. package/src/routes/pages/latest.tsx +2 -0
  46. package/src/runtime/__tests__/readiness.test.ts +34 -0
  47. package/src/runtime/readiness.ts +8 -4
  48. package/src/services/__tests__/collection.test.ts +13 -11
  49. package/src/services/__tests__/site-admin.test.ts +85 -0
  50. package/src/services/github-sync.ts +6 -0
  51. package/src/services/site-admin.ts +66 -1
  52. package/src/styles/components.css +14 -0
  53. package/src/styles/ui.css +109 -0
  54. package/src/types/bindings.ts +0 -2
  55. package/src/types/config.ts +1 -1
  56. package/src/types/props.ts +2 -0
  57. package/src/ui/__tests__/font-themes.test.ts +2 -2
  58. package/src/ui/dash/settings/SettingsRootContent.tsx +17 -17
  59. package/src/ui/feed/LinkCard.tsx +3 -20
  60. package/src/ui/feed/LinkPreview.tsx +5 -19
  61. package/src/ui/feed/PostStatusBadges.tsx +4 -38
  62. package/src/ui/font-themes.ts +17 -17
  63. package/src/ui/layouts/BaseLayout.tsx +14 -29
  64. package/src/ui/pages/HomePage.tsx +21 -5
  65. package/src/ui/shared/DecorativeQuoteMark.tsx +2 -13
  66. package/src/ui/shared/Icon.tsx +60 -0
  67. package/src/ui/shared/IconSprite.tsx +57 -0
  68. package/src/ui/shared/PostFooter.tsx +6 -62
  69. package/src/ui/shared/custom-icons.ts +132 -0
  70. package/src/ui/shared/icon-collector.ts +37 -0
  71. package/dist/app-Ctl0T0zO.js +0 -5
  72. package/dist/client/_assets/client-C_kImWZj.css +0 -2
  73. package/src/lib/github-sync-queue-handler.ts +0 -69
  74. package/src/lib/github-sync-worker.ts +0 -72
  75. package/src/lib/job-queue-cf.ts +0 -18
  76. package/src/lib/job-queue-db.ts +0 -149
  77. 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;
@@ -87,6 +87,4 @@ export interface Bindings {
87
87
  CORS_ORIGINS?: EnvBindingValue;
88
88
  HOST?: string;
89
89
  PORT?: string;
90
- // GitHub Sync queue (Cloudflare Queues)
91
- GITHUB_SYNC_QUEUE?: Queue;
92
90
  }
@@ -63,7 +63,7 @@ export const CONFIG_FIELDS = {
63
63
  envKeys: ["DEFAULT_THEME"],
64
64
  },
65
65
  DEFAULT_FONT_THEME: {
66
- defaultValue: "tufte",
66
+ defaultValue: "classic",
67
67
  envOnly: true,
68
68
  envKeys: ["DEFAULT_FONT_THEME"],
69
69
  },
@@ -22,6 +22,8 @@ export interface HomePageProps {
22
22
  currentPage: number;
23
23
  totalPages: number;
24
24
  baseUrl: string;
25
+ isAuthenticated: boolean;
26
+ signinUrl: string;
25
27
  }
26
28
 
27
29
  /** Props for the single post page component */
@@ -11,8 +11,8 @@ describe("BUILTIN_FONT_THEMES", () => {
11
11
  expect(BUILTIN_FONT_THEMES).toHaveLength(7);
12
12
  });
13
13
 
14
- it("has 'tufte' as the first theme", () => {
15
- expect(BUILTIN_FONT_THEMES[0].id).toBe("tufte");
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}
@@ -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
- <svg
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
- <svg
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
- <svg
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
- <svg
51
+ <Icon
52
+ name="link-preview-badge-play"
62
53
  class="link-preview-badge-icon"
63
- viewBox="0 0 16 16"
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
- <svg
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
- <svg
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
- <svg
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>
@@ -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: {