@jant/core 0.3.43 → 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.
@@ -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
+ }