@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
|
@@ -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;
|
|
@@ -78,6 +81,11 @@ export const BaseLayout: FC<PropsWithChildren<BaseLayoutProps>> = ({
|
|
|
78
81
|
clientBundle,
|
|
79
82
|
children,
|
|
80
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
|
+
|
|
81
89
|
// Read lang from Hono context if available, otherwise use prop or default
|
|
82
90
|
const resolvedLang = lang ?? (c ? c.get("lang") : "en");
|
|
83
91
|
|
|
@@ -343,47 +351,24 @@ export const BaseLayout: FC<PropsWithChildren<BaseLayoutProps>> = ({
|
|
|
343
351
|
data-init="el.closest('[popover]').showPopover(); history.replaceState({}, '', location.pathname); setTimeout(() => { el.classList.add('toast-out'); el.addEventListener('animationend', () => el.remove()) }, 3000)"
|
|
344
352
|
>
|
|
345
353
|
{toast.type === "error" ? (
|
|
346
|
-
<
|
|
347
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
348
|
-
fill="none"
|
|
349
|
-
viewBox="0 0 24 24"
|
|
350
|
-
stroke-width="2"
|
|
351
|
-
stroke="currentColor"
|
|
352
|
-
>
|
|
353
|
-
<circle cx="12" cy="12" r="10" />
|
|
354
|
-
<path d="m15 9-6 6M9 9l6 6" />
|
|
355
|
-
</svg>
|
|
354
|
+
<Icon name="toast-error" />
|
|
356
355
|
) : (
|
|
357
|
-
<
|
|
358
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
359
|
-
fill="none"
|
|
360
|
-
viewBox="0 0 24 24"
|
|
361
|
-
stroke-width="2"
|
|
362
|
-
stroke="currentColor"
|
|
363
|
-
>
|
|
364
|
-
<circle cx="12" cy="12" r="10" />
|
|
365
|
-
<path d="m9 12 2 2 4-4" />
|
|
366
|
-
</svg>
|
|
356
|
+
<Icon name="toast-success" />
|
|
367
357
|
)}
|
|
368
358
|
<span>{toast.message}</span>
|
|
369
359
|
<button
|
|
370
360
|
class="toast-close"
|
|
371
361
|
data-on:click="el.closest('.toast').classList.add('toast-out'); el.closest('.toast').addEventListener('animationend', () => el.closest('.toast').remove())"
|
|
372
362
|
>
|
|
373
|
-
<
|
|
374
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
375
|
-
fill="none"
|
|
376
|
-
viewBox="0 0 24 24"
|
|
377
|
-
stroke-width="2"
|
|
378
|
-
stroke="currentColor"
|
|
379
|
-
>
|
|
380
|
-
<path d="M18 6 6 18M6 6l12 12" />
|
|
381
|
-
</svg>
|
|
363
|
+
<Icon name="toast-close" />
|
|
382
364
|
</button>
|
|
383
365
|
</div>
|
|
384
366
|
)}
|
|
385
367
|
</div>
|
|
386
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 />
|
|
387
372
|
</body>
|
|
388
373
|
</html>
|
|
389
374
|
</>
|
|
@@ -16,6 +16,8 @@ export const HomePage: FC<HomePageProps> = ({
|
|
|
16
16
|
baseUrl,
|
|
17
17
|
currentPage,
|
|
18
18
|
totalPages,
|
|
19
|
+
isAuthenticated,
|
|
20
|
+
signinUrl,
|
|
19
21
|
}) => {
|
|
20
22
|
const { i18n } = useLingui();
|
|
21
23
|
|
|
@@ -37,16 +39,30 @@ export const HomePage: FC<HomePageProps> = ({
|
|
|
37
39
|
<div data-feed>
|
|
38
40
|
<div id="timeline-feed">
|
|
39
41
|
<div id="timeline-items" class="flex flex-col">
|
|
40
|
-
<p
|
|
41
|
-
id="empty-timeline"
|
|
42
|
-
class="py-12 text-center text-muted-foreground"
|
|
43
|
-
>
|
|
42
|
+
<p id="empty-timeline" class="py-8 text-muted-foreground">
|
|
44
43
|
{i18n._(
|
|
45
44
|
msg({
|
|
46
|
-
message: "
|
|
45
|
+
message: "Quiet here for now.",
|
|
47
46
|
comment: "@context: Empty state message on home page",
|
|
48
47
|
}),
|
|
49
48
|
)}
|
|
49
|
+
{!isAuthenticated && (
|
|
50
|
+
<>
|
|
51
|
+
{" "}
|
|
52
|
+
<a
|
|
53
|
+
href={signinUrl}
|
|
54
|
+
class="underline-offset-2 hover:underline"
|
|
55
|
+
>
|
|
56
|
+
{i18n._(
|
|
57
|
+
msg({
|
|
58
|
+
message: "Sign in if this is your space.",
|
|
59
|
+
comment:
|
|
60
|
+
"@context: Sign-in nudge shown to visitors on an empty home page, hinting that the site owner can sign in to start writing",
|
|
61
|
+
}),
|
|
62
|
+
)}
|
|
63
|
+
</a>
|
|
64
|
+
</>
|
|
65
|
+
)}
|
|
50
66
|
</p>
|
|
51
67
|
</div>
|
|
52
68
|
</div>
|
|
@@ -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
|
-
<
|
|
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
|
-
<
|
|
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
|
-
<
|
|
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
|
-
<
|
|
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
|
-
<
|
|
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
|
-
<
|
|
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
|
+
}
|