@jant/core 0.3.42 → 0.3.44

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 (99) hide show
  1. package/bin/commands/import-site.js +1 -1
  2. package/bin/commands/search-reindex.js +175 -0
  3. package/bin/lib/hugo-markdown.js +102 -0
  4. package/bin/lib/site-pull-media.js +1 -4
  5. package/dist/app-BI9bnCkO.js +5 -0
  6. package/dist/{app-Cu3lveYI.js → app-CtJDxZBb.js} +969 -373
  7. package/dist/client/.vite/manifest.json +2 -2
  8. package/dist/client/_assets/client-BQH7AQ24.css +2 -0
  9. package/dist/client/_assets/{client-auth-BRFl5zQA.js → client-auth-CXILhW1b.js} +144 -144
  10. package/dist/{env-wCpMcNXs.js → env-CgaH9Mut.js} +1 -1
  11. package/dist/{github-api-CficQztC.js → github-api-BkRWnqMx.js} +1 -1
  12. package/dist/{github-app-F4qZ05xk.js → github-app-WeadXMb8.js} +1 -1
  13. package/dist/{github-sync-zohnA9qv.js → github-sync-7y_nTXx1.js} +41 -14
  14. package/dist/index.js +5 -5
  15. package/dist/node.js +5 -5
  16. package/dist/{url-FvvgARU9.js → url-umUptr5z.js} +30 -1
  17. package/package.json +1 -1
  18. package/src/__tests__/helpers/app.ts +15 -4
  19. package/src/app.tsx +8 -0
  20. package/src/client/tiptap/__tests__/insert-paragraph-around.test.ts +228 -0
  21. package/src/client/tiptap/extensions.ts +3 -0
  22. package/src/client/tiptap/insert-paragraph-around.ts +79 -0
  23. package/src/db/migrations/0018_yummy_franklin_richards.sql +6 -0
  24. package/src/db/migrations/0019_bored_magus.sql +2 -0
  25. package/src/db/migrations/meta/0018_snapshot.json +2225 -0
  26. package/src/db/migrations/meta/0019_snapshot.json +2238 -0
  27. package/src/db/migrations/meta/_journal.json +15 -1
  28. package/src/db/migrations/pg/0016_familiar_lionheart.sql +6 -0
  29. package/src/db/migrations/pg/0017_bright_beyonder.sql +2 -0
  30. package/src/db/migrations/pg/meta/0016_snapshot.json +2840 -0
  31. package/src/db/migrations/pg/meta/0017_snapshot.json +2862 -0
  32. package/src/db/migrations/pg/meta/_journal.json +15 -1
  33. package/src/db/pg/schema.ts +22 -0
  34. package/src/db/schema.ts +27 -0
  35. package/src/index.ts +1 -2
  36. package/src/lib/__tests__/hosted-signin.test.ts +30 -0
  37. package/src/lib/__tests__/navigation.test.ts +4 -20
  38. package/src/lib/__tests__/rate-limit-d1.test.ts +82 -0
  39. package/src/lib/__tests__/rate-limit-memory.test.ts +69 -0
  40. package/src/lib/__tests__/summary.test.ts +140 -0
  41. package/src/lib/__tests__/view.test.ts +66 -0
  42. package/src/lib/feed.ts +70 -34
  43. package/src/lib/hosted-signin.ts +9 -3
  44. package/src/lib/icons.ts +37 -0
  45. package/src/lib/navigation.ts +11 -12
  46. package/src/lib/post-meta.ts +20 -2
  47. package/src/lib/rate-limit-d1.ts +99 -0
  48. package/src/lib/rate-limit-memory.ts +105 -0
  49. package/src/lib/rate-limit.ts +63 -0
  50. package/src/lib/render.tsx +9 -0
  51. package/src/lib/resolve-config.ts +9 -0
  52. package/src/lib/summary.ts +42 -7
  53. package/src/lib/url.ts +34 -0
  54. package/src/lib/view.ts +42 -8
  55. package/src/middleware/__tests__/auth.test.ts +44 -4
  56. package/src/middleware/__tests__/rate-limit.test.ts +113 -0
  57. package/src/middleware/__tests__/session.test.ts +85 -0
  58. package/src/middleware/auth.ts +62 -25
  59. package/src/middleware/rate-limit.ts +54 -0
  60. package/src/middleware/session.ts +36 -0
  61. package/src/routes/__tests__/compose.test.ts +1 -1
  62. package/src/routes/api/__tests__/search.test.ts +48 -0
  63. package/src/routes/api/__tests__/upload-multipart.test.ts +11 -4
  64. package/src/routes/api/internal/search-reindex.ts +40 -0
  65. package/src/routes/api/internal/sites.ts +1 -0
  66. package/src/routes/api/search.ts +13 -0
  67. package/src/routes/auth/dev.ts +1 -1
  68. package/src/routes/auth/signin.tsx +23 -5
  69. package/src/routes/dash/settings.tsx +3 -5
  70. package/src/routes/feed/__tests__/sitemap.test.ts +320 -4
  71. package/src/routes/feed/sitemap.ts +208 -33
  72. package/src/routes/pages/__tests__/page-canonical.test.ts +101 -0
  73. package/src/routes/pages/home.tsx +24 -15
  74. package/src/routes/pages/page.tsx +34 -0
  75. package/src/routes/pages/partials.tsx +4 -15
  76. package/src/runtime/cloudflare.ts +4 -0
  77. package/src/runtime/node.ts +16 -0
  78. package/src/services/__tests__/post.test.ts +205 -0
  79. package/src/services/__tests__/search.test.ts +44 -0
  80. package/src/services/__tests__/site-admin.test.ts +85 -0
  81. package/src/services/export.ts +9 -2
  82. package/src/services/post.ts +200 -2
  83. package/src/services/site-admin.ts +66 -1
  84. package/src/styles/ui.css +12 -0
  85. package/src/types/app-context.ts +20 -0
  86. package/src/types/config.ts +8 -0
  87. package/src/types/props.ts +0 -7
  88. package/src/ui/feed/LinkCard.tsx +3 -20
  89. package/src/ui/feed/LinkPreview.tsx +5 -19
  90. package/src/ui/feed/PostStatusBadges.tsx +4 -38
  91. package/src/ui/layouts/BaseLayout.tsx +23 -29
  92. package/src/ui/shared/DecorativeQuoteMark.tsx +2 -13
  93. package/src/ui/shared/Icon.tsx +60 -0
  94. package/src/ui/shared/IconSprite.tsx +57 -0
  95. package/src/ui/shared/PostFooter.tsx +6 -62
  96. package/src/ui/shared/custom-icons.ts +132 -0
  97. package/src/ui/shared/icon-collector.ts +37 -0
  98. package/dist/app-DzCB4yOp.js +0 -5
  99. package/dist/client/_assets/client-C_kImWZj.css +0 -2
@@ -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>
@@ -32,6 +32,9 @@ import {
32
32
  IS_VITE_DEV,
33
33
  } from "../../lib/version.js";
34
34
  import { I18nProvider } from "../../i18n/index.js";
35
+ import { resetIconCollector } from "../shared/icon-collector.js";
36
+ import { Icon } from "../shared/Icon.js";
37
+ import { IconSprite } from "../shared/IconSprite.js";
35
38
 
36
39
  export interface ToastProps {
37
40
  message: string;
@@ -49,6 +52,13 @@ export interface BaseLayoutProps {
49
52
  faviconUrl?: string;
50
53
  faviconVersion?: string;
51
54
  socialImageUrl?: string;
55
+ /**
56
+ * Absolute canonical URL for the current page. Rendered as
57
+ * `<link rel="canonical">` when set. Use on pages whose primary content is
58
+ * also reachable via another URL (e.g. reply posts, which render the full
59
+ * thread at both the reply URL and the thread-root URL).
60
+ */
61
+ canonicalHref?: string;
52
62
  noindex?: boolean;
53
63
  isAuthenticated?: boolean;
54
64
  clientBundle?: "public" | "full";
@@ -65,11 +75,17 @@ export const BaseLayout: FC<PropsWithChildren<BaseLayoutProps>> = ({
65
75
  faviconUrl,
66
76
  faviconVersion,
67
77
  socialImageUrl,
78
+ canonicalHref,
68
79
  noindex,
69
80
  isAuthenticated = false,
70
81
  clientBundle,
71
82
  children,
72
83
  }) => {
84
+ // Start a fresh icon collection scope for this request. <Icon> usages in
85
+ // children register names here; <IconSprite> at the end of <body> reads
86
+ // the collected set and emits the <symbol> definitions once.
87
+ resetIconCollector();
88
+
73
89
  // Read lang from Hono context if available, otherwise use prop or default
74
90
  const resolvedLang = lang ?? (c ? c.get("lang") : "en");
75
91
 
@@ -265,6 +281,7 @@ export const BaseLayout: FC<PropsWithChildren<BaseLayoutProps>> = ({
265
281
  {resolvedNoindex && (
266
282
  <meta name="robots" content="noindex, nofollow" />
267
283
  )}
284
+ {canonicalHref && <link rel="canonical" href={canonicalHref} />}
268
285
  <link rel="icon" href={resolvedFaviconHref} sizes="16x16 32x32" />
269
286
  <link rel="apple-touch-icon" href={resolvedAppleTouchHref} />
270
287
  <link
@@ -334,47 +351,24 @@ export const BaseLayout: FC<PropsWithChildren<BaseLayoutProps>> = ({
334
351
  data-init="el.closest('[popover]').showPopover(); history.replaceState({}, '', location.pathname); setTimeout(() => { el.classList.add('toast-out'); el.addEventListener('animationend', () => el.remove()) }, 3000)"
335
352
  >
336
353
  {toast.type === "error" ? (
337
- <svg
338
- xmlns="http://www.w3.org/2000/svg"
339
- fill="none"
340
- viewBox="0 0 24 24"
341
- stroke-width="2"
342
- stroke="currentColor"
343
- >
344
- <circle cx="12" cy="12" r="10" />
345
- <path d="m15 9-6 6M9 9l6 6" />
346
- </svg>
354
+ <Icon name="toast-error" />
347
355
  ) : (
348
- <svg
349
- xmlns="http://www.w3.org/2000/svg"
350
- fill="none"
351
- viewBox="0 0 24 24"
352
- stroke-width="2"
353
- stroke="currentColor"
354
- >
355
- <circle cx="12" cy="12" r="10" />
356
- <path d="m9 12 2 2 4-4" />
357
- </svg>
356
+ <Icon name="toast-success" />
358
357
  )}
359
358
  <span>{toast.message}</span>
360
359
  <button
361
360
  class="toast-close"
362
361
  data-on:click="el.closest('.toast').classList.add('toast-out'); el.closest('.toast').addEventListener('animationend', () => el.closest('.toast').remove())"
363
362
  >
364
- <svg
365
- xmlns="http://www.w3.org/2000/svg"
366
- fill="none"
367
- viewBox="0 0 24 24"
368
- stroke-width="2"
369
- stroke="currentColor"
370
- >
371
- <path d="M18 6 6 18M6 6l12 12" />
372
- </svg>
363
+ <Icon name="toast-close" />
373
364
  </button>
374
365
  </div>
375
366
  )}
376
367
  </div>
377
368
  {customBodyEndHtml && raw(customBodyEndHtml)}
369
+ {/* Icon sprite: must come after all <Icon> usages so the
370
+ request-scoped collector has seen every name. */}
371
+ <IconSprite />
378
372
  </body>
379
373
  </html>
380
374
  </>
@@ -1,8 +1,5 @@
1
1
  import type { FC } from "hono/jsx";
2
- import {
3
- DECORATIVE_QUOTE_MARK_PATHS,
4
- DECORATIVE_QUOTE_MARK_VIEWBOX,
5
- } from "../../lib/decorative-quote-mark.js";
2
+ import { Icon } from "./Icon.js";
6
3
 
7
4
  interface DecorativeQuoteMarkProps {
8
5
  class?: string;
@@ -22,14 +19,6 @@ export const DecorativeQuoteMark: FC<DecorativeQuoteMarkProps> = ({
22
19
  data-direction={direction}
23
20
  aria-hidden="true"
24
21
  >
25
- <svg
26
- viewBox={DECORATIVE_QUOTE_MARK_VIEWBOX}
27
- role="presentation"
28
- focusable="false"
29
- >
30
- {DECORATIVE_QUOTE_MARK_PATHS.map((path) => (
31
- <path fill="currentColor" d={path} />
32
- ))}
33
- </svg>
22
+ <Icon name="decorative-quote" />
34
23
  </span>
35
24
  );
@@ -0,0 +1,60 @@
1
+ /**
2
+ * <Icon> — sprite-based SVG icon for SSR pages.
3
+ *
4
+ * Renders a lightweight <svg><use href="#icon-${name}"/></svg> stub and
5
+ * registers the icon name with the request-scoped collector so the final
6
+ * sprite (rendered by <IconSprite>) contains exactly the icons used on
7
+ * this page.
8
+ *
9
+ * Name can refer to any lucide-static icon (kebab-case) or one of the
10
+ * custom symbols defined in `custom-icons.ts`. Unknown names render an
11
+ * empty <svg> — the same failure mode as the previous getIconSvg() path.
12
+ *
13
+ * Size: outer <svg> width/height in pixels. Defaults to 24 (lucide default).
14
+ * Pass `class` to add CSS classes, e.g. for sizing via stylesheet instead
15
+ * of inline width/height.
16
+ */
17
+
18
+ import type { FC } from "hono/jsx";
19
+ import { collectIcon } from "./icon-collector.js";
20
+ import { getIconViewBox } from "./custom-icons.js";
21
+
22
+ export interface IconProps {
23
+ /** Kebab-case icon name (lucide or custom). */
24
+ name: string;
25
+ /** Width/height in px. Omit to size via CSS. */
26
+ size?: number;
27
+ /** CSS class for the outer <svg>. */
28
+ class?: string;
29
+ /** Accessible label. If set, the icon is not aria-hidden. */
30
+ "aria-label"?: string;
31
+ /**
32
+ * Whether this icon is decorative. Defaults to true (aria-hidden) when
33
+ * no aria-label is provided.
34
+ */
35
+ "aria-hidden"?: boolean | "true" | "false";
36
+ }
37
+
38
+ export const Icon: FC<IconProps> = ({
39
+ name,
40
+ size,
41
+ class: cls,
42
+ "aria-label": ariaLabel,
43
+ "aria-hidden": ariaHidden,
44
+ }) => {
45
+ collectIcon(name);
46
+
47
+ const hidden = ariaHidden ?? (ariaLabel ? undefined : true);
48
+
49
+ return (
50
+ <svg
51
+ viewBox={getIconViewBox(name)}
52
+ {...(size !== undefined ? { width: size, height: size } : {})}
53
+ {...(cls ? { class: cls } : {})}
54
+ {...(ariaLabel ? { "aria-label": ariaLabel, role: "img" } : {})}
55
+ {...(hidden ? { "aria-hidden": "true" } : {})}
56
+ >
57
+ <use href={`#icon-${name}`} />
58
+ </svg>
59
+ );
60
+ };
@@ -0,0 +1,57 @@
1
+ /**
2
+ * <IconSprite> — emits the SVG symbol definitions used by this render.
3
+ *
4
+ * Must be rendered AFTER all <Icon> usages in the document (e.g. at the
5
+ * end of <body>) so the collector has the full set of icon names. Hono
6
+ * JSX stringifies synchronously in document order, so children declared
7
+ * earlier in the tree are evaluated before this component.
8
+ *
9
+ * <use href="#icon-x"> anywhere in the document resolves correctly even
10
+ * when the <symbol> definition comes after the reference, since browsers
11
+ * wire up the references after the full document is parsed.
12
+ */
13
+
14
+ import type { FC } from "hono/jsx";
15
+ import { raw } from "hono/html";
16
+ import {
17
+ getIconInnerSvg,
18
+ LUCIDE_SYMBOL_ATTRS,
19
+ LUCIDE_VIEWBOX,
20
+ } from "../../lib/icons.js";
21
+ import { getCollectedIcons } from "./icon-collector.js";
22
+ import { getCustomSymbol } from "./custom-icons.js";
23
+
24
+ function buildSymbol(name: string): string | null {
25
+ const custom = getCustomSymbol(name);
26
+ if (custom) {
27
+ return `<symbol id="icon-${name}" viewBox="${custom.viewBox}">${custom.inner}</symbol>`;
28
+ }
29
+ const inner = getIconInnerSvg(name);
30
+ if (inner === null) return null;
31
+ return `<symbol id="icon-${name}" viewBox="${LUCIDE_VIEWBOX}" ${LUCIDE_SYMBOL_ATTRS}>${inner}</symbol>`;
32
+ }
33
+
34
+ export const IconSprite: FC = () => {
35
+ const names = Array.from(getCollectedIcons()).sort();
36
+ const symbols = names
37
+ .map(buildSymbol)
38
+ .filter((s): s is string => s !== null)
39
+ .join("");
40
+
41
+ if (!symbols) return null;
42
+
43
+ // Hidden from layout and AT; provides the <symbol> definitions only.
44
+ // Use raw() as a child instead of dangerouslySetInnerHTML because Hono
45
+ // wraps <svg> with an internal nameSpaceContext child, which makes
46
+ // dangerouslySetInnerHTML conflict with children and throw.
47
+ return (
48
+ <svg
49
+ xmlns="http://www.w3.org/2000/svg"
50
+ style="display:none"
51
+ aria-hidden="true"
52
+ data-icon-sprite
53
+ >
54
+ {raw(symbols)}
55
+ </svg>
56
+ );
57
+ };
@@ -13,8 +13,8 @@ import type {
13
13
  PostFooterDisplayOptions,
14
14
  } from "../../types.js";
15
15
  import { useLingui } from "../../i18n/context.js";
16
- import { FEATURED_SPARKLE_PATH } from "../../lib/featured-icons.js";
17
16
  import { sanitizeUrl } from "../../lib/url.js";
17
+ import { Icon } from "./Icon.js";
18
18
 
19
19
  interface PostFooterProps {
20
20
  post: PostView;
@@ -46,18 +46,7 @@ export const CompactCollectionTags: FC<{
46
46
  <a href={first.url} class="post-collection-tag">
47
47
  {showIcon && (
48
48
  <span class="post-collection-primary-icon" aria-hidden="true">
49
- <svg
50
- xmlns="http://www.w3.org/2000/svg"
51
- viewBox="0 0 16 16"
52
- fill="none"
53
- stroke="currentColor"
54
- stroke-width="1.35"
55
- stroke-linecap="round"
56
- stroke-linejoin="round"
57
- >
58
- <rect x="3" y="5.05" width="10" height="8.15" rx="2.2" />
59
- <path d="M5.1 5.05V4.2a1.1 1.1 0 0 1 1.1-1.1h3.6a1.1 1.1 0 0 1 1.1 1.1v.85" />
60
- </svg>
49
+ <Icon name="post-collection-lock" />
61
50
  </span>
62
51
  )}
63
52
  <span class="post-collection-tag-text">{first.title}</span>
@@ -162,17 +151,7 @@ export const PostMenuTriggerButton: FC<{ className?: string }> = ({
162
151
  aria-expanded="false"
163
152
  data-post-menu-trigger
164
153
  >
165
- <svg
166
- xmlns="http://www.w3.org/2000/svg"
167
- width="15"
168
- height="15"
169
- viewBox="0 0 24 24"
170
- fill="currentColor"
171
- >
172
- <circle cx="5" cy="12" r="1.75" />
173
- <circle cx="12" cy="12" r="1.75" />
174
- <circle cx="19" cy="12" r="1.75" />
175
- </svg>
154
+ <Icon name="post-menu-dots" size={15} />
176
155
  </button>
177
156
  );
178
157
  };
@@ -222,18 +201,7 @@ export const PostFooter: FC<PostFooterProps> = ({ post, detail, display }) => {
222
201
  data-tooltip={featuredLabel}
223
202
  data-align="center"
224
203
  >
225
- <svg
226
- xmlns="http://www.w3.org/2000/svg"
227
- viewBox="0 0 24 24"
228
- fill="none"
229
- stroke="currentColor"
230
- stroke-width="1.35"
231
- stroke-linecap="round"
232
- stroke-linejoin="round"
233
- aria-hidden="true"
234
- >
235
- <path d={FEATURED_SPARKLE_PATH} />
236
- </svg>
204
+ <Icon name="featured-sparkle" />
237
205
  </span>
238
206
  {showTimestamp && (
239
207
  <PostPublishedLink post={post} className="u-url post-footer-link" />
@@ -252,18 +220,7 @@ export const PostFooter: FC<PostFooterProps> = ({ post, detail, display }) => {
252
220
  }),
253
221
  )}
254
222
  >
255
- <svg
256
- xmlns="http://www.w3.org/2000/svg"
257
- viewBox="0 0 24 24"
258
- fill="none"
259
- stroke="currentColor"
260
- stroke-width="2"
261
- stroke-linecap="round"
262
- stroke-linejoin="round"
263
- >
264
- <path d="M7 17 17 7" />
265
- <path d="M9 7h8v8" />
266
- </svg>
223
+ <Icon name="post-external-link" />
267
224
  </a>
268
225
  )}
269
226
  <CompactCollectionTags
@@ -286,20 +243,7 @@ export const PostFooter: FC<PostFooterProps> = ({ post, detail, display }) => {
286
243
  )}
287
244
  data-reply-trigger
288
245
  >
289
- <svg
290
- xmlns="http://www.w3.org/2000/svg"
291
- width="14"
292
- height="14"
293
- viewBox="0 0 24 24"
294
- fill="none"
295
- stroke="currentColor"
296
- stroke-width="2"
297
- stroke-linecap="round"
298
- stroke-linejoin="round"
299
- >
300
- <polyline points="9 17 4 12 9 7" />
301
- <path d="M20 18v-2a4 4 0 0 0-4-4H4" />
302
- </svg>
246
+ <Icon name="post-reply" size={14} />
303
247
  </button>
304
248
  )}
305
249
  <PostMenuTriggerButton />
@@ -0,0 +1,132 @@
1
+ /**
2
+ * Custom (non-lucide) SVG symbol definitions used by the icon sprite.
3
+ *
4
+ * Icons here fall into three groups:
5
+ * 1. Jant-specific paths (decorative quote mark, featured sparkle).
6
+ * 2. Lucide-equivalent paths the UI uses with non-default stroke widths
7
+ * or sizes that don't match the stock lucide symbol (we keep them as
8
+ * custom symbols to preserve exact visual fidelity during refactor).
9
+ * 3. Fixed-color SVGs (video play overlay) that don't use currentColor.
10
+ *
11
+ * Each entry provides everything needed to render a <symbol> element:
12
+ * <symbol id="icon-${name}" viewBox={viewBox}>{inner}</symbol>
13
+ * Consumers of <Icon name="..."> pass `size` / `className` on the outer
14
+ * <svg><use/></svg>; `<symbol>` children inherit the outer attributes.
15
+ */
16
+
17
+ import {
18
+ FEATURED_SPARKLE_PATH,
19
+ FEATURED_SPARKLE_OFF_SLASH_PATH,
20
+ } from "../../lib/featured-icons.js";
21
+ import {
22
+ DECORATIVE_QUOTE_MARK_PATHS,
23
+ DECORATIVE_QUOTE_MARK_VIEWBOX,
24
+ } from "../../lib/decorative-quote-mark.js";
25
+
26
+ export interface CustomSymbol {
27
+ viewBox: string;
28
+ /** Inner SVG markup (paths, circles, etc). Must be trusted HTML. */
29
+ inner: string;
30
+ }
31
+
32
+ const STROKE_THIN =
33
+ 'fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"';
34
+ const STROKE_POST_BADGE =
35
+ 'fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"';
36
+
37
+ export const CUSTOM_SYMBOLS: Record<string, CustomSymbol> = {
38
+ // Featured sparkle (thinner stroke than lucide's stock "sparkles").
39
+ "featured-sparkle": {
40
+ viewBox: "0 0 24 24",
41
+ inner: `<path ${STROKE_THIN} d="${FEATURED_SPARKLE_PATH}" />`,
42
+ },
43
+ // Featured sparkle with a diagonal slash (for "unfeature" affordances).
44
+ "featured-sparkle-off": {
45
+ viewBox: "0 0 24 24",
46
+ inner: `<path ${STROKE_THIN} d="${FEATURED_SPARKLE_PATH}" /><path ${STROKE_THIN} d="${FEATURED_SPARKLE_OFF_SLASH_PATH}" />`,
47
+ },
48
+ // Decorative double-quote glyph (96×96, filled).
49
+ "decorative-quote": {
50
+ viewBox: DECORATIVE_QUOTE_MARK_VIEWBOX,
51
+ inner: DECORATIVE_QUOTE_MARK_PATHS.map(
52
+ (path) => `<path fill="currentColor" d="${path}" />`,
53
+ ).join(""),
54
+ },
55
+ // Post collection lock (thin 1.35 stroke, 16×16 viewBox).
56
+ "post-collection-lock": {
57
+ viewBox: "0 0 16 16",
58
+ inner: `<rect ${STROKE_THIN} x="3" y="5.05" width="10" height="8.15" rx="2.2" /><path ${STROKE_THIN} d="M5.1 5.05V4.2a1.1 1.1 0 0 1 1.1-1.1h3.6a1.1 1.1 0 0 1 1.1 1.1v.85" />`,
59
+ },
60
+ // Post menu trigger: three dots, filled.
61
+ "post-menu-dots": {
62
+ viewBox: "0 0 24 24",
63
+ inner: `<circle cx="5" cy="12" r="1.75" fill="currentColor" /><circle cx="12" cy="12" r="1.75" fill="currentColor" /><circle cx="19" cy="12" r="1.75" fill="currentColor" />`,
64
+ },
65
+ // External arrow in a box (used in post footer external link affordance).
66
+ "post-external-link": {
67
+ viewBox: "0 0 24 24",
68
+ inner: `<path fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" d="M7 17 17 7" /><path fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" d="M9 7h8v8" />`,
69
+ },
70
+ // Reply arrow (used on "Reply" button).
71
+ "post-reply": {
72
+ viewBox: "0 0 24 24",
73
+ inner: `<polyline fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" points="9 17 4 12 9 7" /><path fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" d="M20 18v-2a4 4 0 0 0-4-4H4" />`,
74
+ },
75
+ // Link card domain favicon fallback (external link arrow to new page).
76
+ "link-domain": {
77
+ viewBox: "0 0 24 24",
78
+ inner: `<path fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" 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" />`,
79
+ },
80
+ // Video preview play button overlay (YouTube-style, fixed colors).
81
+ "link-preview-play": {
82
+ viewBox: "0 0 68 48",
83
+ inner:
84
+ '<path class="link-preview-play-bg" fill="rgba(0,0,0,.65)" 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" /><path fill="#fff" d="M45 24L27 14v20" />',
85
+ },
86
+ // Small play triangle (for provider badge).
87
+ "link-preview-badge-play": {
88
+ viewBox: "0 0 16 16",
89
+ inner: '<path fill="currentColor" d="M5.5 3.5v9l7-4.5z" />',
90
+ },
91
+ // Toast icons (no lucide equivalent at these exact sizes).
92
+ "toast-success": {
93
+ viewBox: "0 0 24 24",
94
+ inner: `<circle fill="none" stroke="currentColor" stroke-width="2" cx="12" cy="12" r="10" /><path fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" d="m9 12 2 2 4-4" />`,
95
+ },
96
+ "toast-error": {
97
+ viewBox: "0 0 24 24",
98
+ inner: `<circle fill="none" stroke="currentColor" stroke-width="2" cx="12" cy="12" r="10" /><path fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" d="m15 9-6 6M9 9l6 6" />`,
99
+ },
100
+ "toast-close": {
101
+ viewBox: "0 0 24 24",
102
+ inner: `<path fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" d="M18 6 6 18M6 6l12 12" />`,
103
+ },
104
+ // Post status badge: pinned (custom pin, thin 1.75 stroke).
105
+ "post-status-pin": {
106
+ viewBox: "0 0 24 24",
107
+ inner: `<line ${STROKE_POST_BADGE} x1="12" x2="12" y1="17" y2="22" /><path ${STROKE_POST_BADGE} 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" />`,
108
+ },
109
+ // Post status badge: private (lucide eye-off, thin 1.75 stroke).
110
+ "post-status-private": {
111
+ viewBox: "0 0 24 24",
112
+ inner: `<path ${STROKE_POST_BADGE} 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" /><path ${STROKE_POST_BADGE} d="M14.084 14.158a3 3 0 0 1-4.242-4.242" /><path ${STROKE_POST_BADGE} 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" /><path ${STROKE_POST_BADGE} d="m2 2 20 20" />`,
113
+ },
114
+ };
115
+
116
+ export function getCustomSymbol(name: string): CustomSymbol | null {
117
+ return CUSTOM_SYMBOLS[name] ?? null;
118
+ }
119
+
120
+ /**
121
+ * Return the viewBox for an icon's outer <svg> wrapper.
122
+ *
123
+ * This must match the <symbol>'s viewBox so the browser computes the correct
124
+ * intrinsic aspect ratio. Without this, outer <svg> with `height: auto` in
125
+ * CSS falls back to the 300×150 replaced-element default instead of the
126
+ * icon's real aspect ratio.
127
+ *
128
+ * Falls back to lucide's "0 0 24 24" for lucide-sourced icons.
129
+ */
130
+ export function getIconViewBox(name: string): string {
131
+ return CUSTOM_SYMBOLS[name]?.viewBox ?? "0 0 24 24";
132
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Request-scoped icon collector for SSR SVG sprite pattern.
3
+ *
4
+ * The <Icon> component registers each used icon name here during render.
5
+ * At the end of <body>, <IconSprite> reads the collected set and emits a
6
+ * single <svg><symbol>...</symbol></svg> block so every <use href="#icon-x">
7
+ * reference in the page resolves to a definition.
8
+ *
9
+ * Mirrors the I18nProvider pattern in i18n/context.tsx: Hono JSX renders
10
+ * synchronously per request, so a module-level singleton is safe.
11
+ */
12
+
13
+ let currentCollector: Set<string> | null = null;
14
+
15
+ /**
16
+ * Start a new collection scope for the current render pass.
17
+ * Call at the top of the root layout before children render.
18
+ */
19
+ export function resetIconCollector(): void {
20
+ currentCollector = new Set<string>();
21
+ }
22
+
23
+ /**
24
+ * Register an icon as used during this render.
25
+ * Safe to call even when no collector is active (no-op).
26
+ */
27
+ export function collectIcon(name: string): void {
28
+ currentCollector?.add(name);
29
+ }
30
+
31
+ /**
32
+ * Get the icon names collected so far during this render.
33
+ * Returns an empty set if no collection scope is active.
34
+ */
35
+ export function getCollectedIcons(): ReadonlySet<string> {
36
+ return currentCollector ?? new Set<string>();
37
+ }
@@ -1,5 +0,0 @@
1
- import "./url-FvvgARU9.js";
2
- import { t as createApp } from "./app-Cu3lveYI.js";
3
- import "./github-sync-zohnA9qv.js";
4
- import "./env-wCpMcNXs.js";
5
- export { createApp };