@nextclaw/ui 0.12.24 → 0.12.26
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/CHANGELOG.md +136 -29
- package/dist/assets/api-DGD9_Bg4.js +15 -0
- package/dist/assets/app-manager-provider-oYdeYPSv.js +1 -0
- package/dist/assets/{book-open-DDlN5MvX.js → book-open-BcnAiKde.js} +1 -1
- package/dist/assets/channels-list-page-HgLgrEg4.js +8 -0
- package/dist/assets/chat-page-DAKMFDrS.js +1 -0
- package/dist/assets/config-split-page-CcrEUtwu.js +1 -0
- package/dist/assets/cpu-DPPwMzoC.js +3 -0
- package/dist/assets/{createLucideIcon-BLMK3QUd.js → createLucideIcon-DzY6wN61.js} +1 -1
- package/dist/assets/desktop-DVUbOWbR.js +3 -0
- package/dist/assets/desktop-update-config-CP8dFYXK.js +1 -0
- package/dist/assets/{dialog-C3D7Be0p.js → dialog-BKo0RItd.js} +1 -1
- package/dist/assets/{dist-CPlbUgwU.js → dist-CFiwgaLs.js} +1 -1
- package/dist/assets/doc-browser-CAhfnm0D.js +1 -0
- package/dist/assets/{doc-browser-context-BJuMaI3o.js → doc-browser-context-FukQHvyo.js} +1 -1
- package/dist/assets/doc-browser-p9DDNPWB.js +1 -0
- package/dist/assets/doc-browser-rZIQIjuw.js +1 -0
- package/dist/assets/download-CMM8po31.js +1 -0
- package/dist/assets/{es2015-xqN1slyW.js → es2015-BhznEEyJ.js} +1 -1
- package/dist/assets/{external-link-DwfSfTLB.js → external-link-CpEvG65F.js} +1 -1
- package/dist/assets/i18n-D1144VAA.js +1 -0
- package/dist/assets/index-Cuwst6cc.js +100 -0
- package/dist/assets/index-dlcqieQ0.css +1 -0
- package/dist/assets/{key-round-CJ5gDAAG.js → key-round-DUq47t0P.js} +1 -1
- package/dist/assets/marketplace-page-BeFbwxR-.js +105 -0
- package/dist/assets/marketplace-page-CR4xq-TM.js +1 -0
- package/dist/assets/mcp-marketplace-page-DlRrSCj3.js +1 -0
- package/dist/assets/mcp-marketplace-page-DwnaLNTx.js +40 -0
- package/dist/assets/model-config-L2l6YAlQ.js +1 -0
- package/dist/assets/{notice-card-BFDbKQDA.js → notice-card-Dr6xCwva.js} +1 -1
- package/dist/assets/play-AqrNslHI.js +1 -0
- package/dist/assets/plus-B-YHtTNC.js +1 -0
- package/dist/assets/{popover-B86Dbfhf.js → popover-BDFNiLlg.js} +1 -1
- package/dist/assets/provider-scoped-model-input-BMTp4BEH.js +1 -0
- package/dist/assets/providers-list-DYAEunOp.js +1 -0
- package/dist/assets/refresh-cw-CrbD8EkT.js +1 -0
- package/dist/assets/remote-Dr3jcfWP.js +1 -0
- package/dist/assets/{rotate-cw-BZ2JObNs.js → rotate-cw-BN9yjccP.js} +1 -1
- package/dist/assets/runtime-config-page-BdeU8PEK.js +1 -0
- package/dist/assets/{save-euRxl8pI.js → save-CO_4qf6b.js} +1 -1
- package/dist/assets/{search-CLd7m0M7.js → search-CRtQwr-h.js} +1 -1
- package/dist/assets/search-config-CQUhd5RU.js +1 -0
- package/dist/assets/secrets-config-D-NWlW9q.js +3 -0
- package/dist/assets/{select-CJ0wbo3D.js → select-BUTwE_lC.js} +1 -1
- package/dist/assets/{setting-row-D1Yygqp7.js → setting-row-BavcnXw1.js} +1 -1
- package/dist/assets/settings-MWL2SMyk.js +1 -0
- package/dist/assets/{sparkles-DVfeSVJQ.js → sparkles-BmgOD4nY.js} +1 -1
- package/dist/assets/{status-dot-ChvPCib9.js → status-dot-l3kPFdq_.js} +1 -1
- package/dist/assets/{tabs-custom-Hia_ong0.js → tabs-custom-D48zdZoc.js} +1 -1
- package/dist/assets/{tag-chip-FrkmkT8r.js → tag-chip-Dm2Lqnpu.js} +1 -1
- package/dist/assets/use-config-Cyv5IuSt.js +1 -0
- package/dist/assets/use-infinite-scroll-loader-CFVdPpNv.js +1 -0
- package/dist/assets/x-BeyYA_h6.js +1 -0
- package/dist/index.html +29 -40
- package/package.json +9 -9
- package/src/app/components/layout/sidebar.layout.test.tsx +2 -4
- package/src/app/components/theme-provider.tsx +1 -0
- package/src/app/configs/app-navigation.config.ts +0 -6
- package/src/app/index.tsx +4 -7
- package/src/features/agents/components/agents-page.test.tsx +25 -15
- package/src/features/agents/components/agents-page.tsx +133 -172
- package/src/features/channels/components/config/channel-form.test.tsx +1 -0
- package/src/features/channels/components/config/channel-form.tsx +4 -3
- package/src/features/channels/components/config/weixin-channel-auth-section.test.tsx +38 -1
- package/src/features/channels/components/config/weixin-channel-auth-section.tsx +137 -40
- package/src/features/channels/index.ts +1 -1
- package/src/features/channels/utils/channel-form-fields.utils.test.ts +26 -0
- package/src/features/channels/utils/channel-form-fields.utils.ts +32 -18
- package/src/features/chat/components/chat-session-workspace-panel-nav.tsx +23 -4
- package/src/features/chat/components/chat-session-workspace-panel.tsx +53 -35
- package/src/features/chat/components/chat-sidebar-session-item.tsx +16 -12
- package/src/features/chat/components/conversation/chat-conversation-header.test.tsx +74 -0
- package/src/features/chat/components/conversation/chat-conversation-header.tsx +8 -2
- package/src/features/chat/components/conversation/chat-conversation-panel.test.tsx +262 -114
- package/src/features/chat/components/conversation/chat-conversation-panel.tsx +210 -174
- package/src/features/chat/components/conversation/chat-input-bar.container.tsx +11 -1
- package/src/features/chat/components/conversation/session-header/chat-session-header-actions.test.tsx +24 -0
- package/src/features/chat/components/conversation/session-header/chat-session-header-actions.tsx +27 -6
- package/src/features/chat/components/layout/chat-sidebar-utility-menu.tsx +174 -0
- package/src/features/chat/components/layout/chat-sidebar.test.tsx +45 -8
- package/src/features/chat/components/layout/chat-sidebar.tsx +29 -46
- package/src/features/chat/components/providers/chat-presenter.provider.tsx +4 -0
- package/src/features/chat/components/workspace/session-cron-job-content.tsx +103 -0
- package/src/features/chat/hooks/use-ncp-agent-runtime.test.tsx +153 -80
- package/src/features/chat/hooks/use-ncp-chat-page-data.test.tsx +70 -0
- package/src/features/chat/hooks/use-ncp-chat-page-data.ts +1 -1
- package/src/features/chat/hooks/use-ncp-child-session-tabs-view.ts +2 -8
- package/src/features/chat/hooks/use-ncp-session-list-view.ts +1 -2
- package/src/features/chat/managers/chat-session-list.manager.test.ts +7 -9
- package/src/features/chat/managers/chat-session-list.manager.ts +5 -10
- package/src/features/chat/managers/ncp-chat-input.manager.test.ts +20 -2
- package/src/features/chat/managers/ncp-chat-input.manager.ts +18 -0
- package/src/features/chat/managers/ncp-chat-presenter.manager.ts +7 -0
- package/src/features/chat/managers/ncp-chat-thread.manager.test.ts +52 -1
- package/src/features/chat/managers/ncp-chat-thread.manager.ts +21 -0
- package/src/features/chat/pages/ncp-chat-page.tsx +9 -5
- package/src/features/chat/stores/chat-input.store.ts +3 -1
- package/src/features/chat/stores/chat-session-list.store.ts +0 -2
- package/src/features/chat/stores/chat-thread.store.ts +4 -0
- package/src/features/chat/utils/chat-session-display.utils.test.ts +83 -1
- package/src/features/chat/utils/chat-session-display.utils.ts +73 -0
- package/src/features/chat/utils/ncp-chat-input-availability.utils.test.ts +1 -0
- package/src/features/chat/utils/ncp-session-adapter.utils.test.ts +22 -0
- package/src/features/chat/utils/ncp-session-adapter.utils.ts +32 -0
- package/src/features/marketplace/components/curated-shelves/marketplace-curated-scene-route.test.tsx +235 -0
- package/src/features/marketplace/components/curated-shelves/marketplace-curated-shelves.config.ts +162 -0
- package/src/features/marketplace/components/curated-shelves/marketplace-curated-shelves.tsx +355 -0
- package/src/features/marketplace/components/curated-shelves/marketplace-shelf-card.tsx +118 -0
- package/src/features/marketplace/components/detail-doc/marketplace-detail-doc-renderer.ts +201 -0
- package/src/features/marketplace/components/detail-doc/marketplace-detail-doc.test.ts +40 -0
- package/src/features/marketplace/components/marketplace-catalog-grid.tsx +114 -0
- package/src/features/marketplace/components/marketplace-detail-doc.ts +73 -24
- package/src/features/marketplace/components/marketplace-item-icon.tsx +45 -0
- package/src/features/marketplace/components/marketplace-list-card.tsx +177 -93
- package/src/features/marketplace/components/marketplace-page-detail.test.tsx +9 -2
- package/src/features/marketplace/components/marketplace-page-parts.tsx +1 -1
- package/src/features/marketplace/components/marketplace-page.test.tsx +25 -6
- package/src/features/marketplace/components/marketplace-page.tsx +154 -132
- package/src/features/marketplace/hooks/use-marketplace-curated-scene-route.ts +97 -0
- package/src/features/marketplace/hooks/use-marketplace.ts +59 -3
- package/src/features/system-status/components/config/runtime-agent-list-card.tsx +4 -8
- package/src/features/system-status/components/config/runtime-binding-list-card.tsx +5 -7
- package/src/features/system-status/components/config/runtime-config-editor.tsx +1 -19
- package/src/features/system-status/components/config/runtime-entry-list-card.tsx +10 -11
- package/src/features/system-status/components/config/runtime-settings-card.tsx +15 -23
- package/src/features/system-status/components/runtime-control-card.test.tsx +8 -6
- package/src/features/system-status/components/runtime-control-card.tsx +7 -6
- package/src/features/system-status/pages/runtime-config-page.test.tsx +19 -9
- package/src/features/system-status/pages/runtime-config-page.tsx +2 -3
- package/src/features/system-status/utils/runtime-config-agent.utils.ts +4 -4
- package/src/features/system-status/utils/system-status.utils.ts +31 -6
- package/src/index.css +8 -0
- package/src/platforms/desktop/components/desktop-app-shell.test.tsx +68 -0
- package/src/platforms/desktop/components/desktop-app-shell.tsx +46 -18
- package/src/platforms/desktop/components/desktop-window-chrome.tsx +30 -0
- package/src/platforms/desktop/index.ts +6 -0
- package/src/platforms/desktop/types/desktop-update.types.ts +3 -0
- package/src/platforms/desktop/utils/desktop-host.utils.ts +56 -0
- package/src/shared/components/common/brand-header.tsx +36 -16
- package/src/shared/components/config/provider-form-support.ts +2 -22
- package/src/shared/components/cron-config.tsx +12 -58
- package/src/shared/components/doc-browser/doc-browser.tsx +4 -4
- package/src/shared/components/ui/select.tsx +19 -7
- package/src/shared/lib/api/channel-auth.types.ts +1 -0
- package/src/shared/lib/api/ncp-session.types.ts +9 -0
- package/src/shared/lib/api/types.ts +12 -1
- package/src/shared/lib/api/utils/marketplace.utils.ts +7 -1
- package/src/shared/lib/cron/cron-job-view.utils.ts +59 -0
- package/src/shared/lib/cron/index.ts +1 -0
- package/src/shared/lib/i18n/{channel-auth.ts → channel-auth.constants.ts} +31 -0
- package/src/shared/lib/i18n/chat-labels.utils.ts +3 -2
- package/src/shared/lib/i18n/index.ts +20 -59
- package/src/shared/lib/i18n/{runtime-control.ts → runtime-control-labels.utils.ts} +30 -1
- package/src/shared/lib/provider-models/index.test.ts +39 -0
- package/src/shared/lib/provider-models/index.ts +1 -3
- package/src/shared/lib/ui-document-title/index.ts +0 -1
- package/tsconfig.json +1 -0
- package/vite.config.ts +1 -1
- package/vitest.config.ts +1 -1
- package/dist/assets/api-D2xRKmZd.js +0 -15
- package/dist/assets/app-manager-provider-CNaZboG4.js +0 -1
- package/dist/assets/app-navigation.config-Ihhrrt--.js +0 -1
- package/dist/assets/channels-list-page-p26lgxLk.js +0 -8
- package/dist/assets/chat-Dkh2qtuz.js +0 -61
- package/dist/assets/chat-page-DoTmE2wx.js +0 -1
- package/dist/assets/chunk-JZWAC4HX-Kydj4yEz.js +0 -3
- package/dist/assets/config-split-page-DIOCjj2Q.js +0 -1
- package/dist/assets/desktop-update-config-DlpzDfKM.js +0 -1
- package/dist/assets/doc-browser-C8FM5fC0.js +0 -1
- package/dist/assets/doc-browser-RJUOL_GO.js +0 -1
- package/dist/assets/doc-browser-p82AdNO-.js +0 -1
- package/dist/assets/folder-CeJKPx5P.js +0 -1
- package/dist/assets/hash-BqxRTZW5.js +0 -1
- package/dist/assets/i18n-DnTGDIRw.js +0 -1
- package/dist/assets/index-D8MKmXtO.css +0 -1
- package/dist/assets/index-pBvbJ5Mt.js +0 -2
- package/dist/assets/loader-circle-fd-vQKtW.js +0 -1
- package/dist/assets/logo-badge-KAe-7d8c.js +0 -1
- package/dist/assets/logos-C4sYP1Vl.js +0 -1
- package/dist/assets/marketplace-page-Cql0kDi-.js +0 -1
- package/dist/assets/marketplace-page-m4P5g_Ht.js +0 -49
- package/dist/assets/mcp-marketplace-page-9WVKl1m1.js +0 -1
- package/dist/assets/mcp-marketplace-page-ByzBQZcx.js +0 -40
- package/dist/assets/message-square-z_osm9c0.js +0 -1
- package/dist/assets/model-config-Dbr_0APb.js +0 -1
- package/dist/assets/play-Dv6Nr1Ew.js +0 -1
- package/dist/assets/plus-D8eKFY7h.js +0 -1
- package/dist/assets/provider-scoped-model-input-DFm6N2f7.js +0 -1
- package/dist/assets/providers-list-BJcLOjun.js +0 -1
- package/dist/assets/refresh-ccw-ByVwmnN_.js +0 -1
- package/dist/assets/refresh-cw-PcqoYB3K.js +0 -1
- package/dist/assets/remote-BOxo9iwd.js +0 -1
- package/dist/assets/runtime-config-page-CjLhnbSl.js +0 -1
- package/dist/assets/search-config-J4Htco-P.js +0 -1
- package/dist/assets/secrets-config-CUdERjco.js +0 -3
- package/dist/assets/sessions-config-page-DpK991fs.js +0 -2
- package/dist/assets/settings-drbWqzA4.js +0 -1
- package/dist/assets/skeleton-BK1SOSRA.js +0 -1
- package/dist/assets/theme-provider-0hxjiPc_.js +0 -2
- package/dist/assets/tooltip-Cj4yA0gH.js +0 -1
- package/dist/assets/trash-2-CBsHCfqq.js +0 -1
- package/dist/assets/use-config-38Ur-89i.js +0 -1
- package/dist/assets/use-confirm-dialog-DPQThaeU.js +0 -1
- package/dist/assets/use-infinite-scroll-loader-5Gf1xQi7.js +0 -1
- package/dist/assets/use-viewport-layout-D1XzKeip.js +0 -1
- package/dist/assets/x-CM-XDMpk.js +0 -1
- package/src/features/chat/components/config/sessions-config-detail-pane.tsx +0 -244
- package/src/features/chat/pages/sessions-config-page.test.tsx +0 -152
- package/src/features/chat/pages/sessions-config-page.tsx +0 -192
- /package/dist/assets/{config-hints-MogHYQ8G.js → config-hints-BNfpOL4J.js} +0 -0
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
MarketplaceInstalledRecord,
|
|
3
|
+
MarketplaceItemSummary,
|
|
4
|
+
MarketplaceSceneView,
|
|
5
|
+
} from "@/shared/lib/api";
|
|
6
|
+
import type { InstallState } from "@/features/marketplace/components/marketplace-list-card";
|
|
7
|
+
import {
|
|
8
|
+
buildLocaleFallbacks,
|
|
9
|
+
} from "@/features/marketplace/components/marketplace-localization";
|
|
10
|
+
import { MarketplaceListSkeleton } from "@/features/marketplace/components/marketplace-page-parts";
|
|
11
|
+
import { cn } from "@/shared/lib/utils";
|
|
12
|
+
import {
|
|
13
|
+
ArrowLeft,
|
|
14
|
+
Clock3,
|
|
15
|
+
Compass,
|
|
16
|
+
} from "lucide-react";
|
|
17
|
+
import { type ComponentType } from "react";
|
|
18
|
+
import { SkillShelfCard } from "@/features/marketplace/components/curated-shelves/marketplace-shelf-card";
|
|
19
|
+
import {
|
|
20
|
+
MARKETPLACE_SHELF_FALLBACK_VISUAL,
|
|
21
|
+
MARKETPLACE_SHELF_SCENE_VISUALS,
|
|
22
|
+
MARKETPLACE_SHELF_TONE_STYLES,
|
|
23
|
+
type MarketplaceShelfLocalizedText,
|
|
24
|
+
type MarketplaceShelfSceneVisual,
|
|
25
|
+
} from "@/features/marketplace/components/curated-shelves/marketplace-curated-shelves.config";
|
|
26
|
+
|
|
27
|
+
const SCENE_CARD_GRID_CLASS =
|
|
28
|
+
"grid grid-cols-[repeat(auto-fill,minmax(240px,320px))] justify-start gap-3";
|
|
29
|
+
const SCENE_SKELETON_CARD_COUNT = 24;
|
|
30
|
+
|
|
31
|
+
export type MarketplaceShelfEntry = {
|
|
32
|
+
item: MarketplaceItemSummary;
|
|
33
|
+
record?: MarketplaceInstalledRecord;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export function MarketplaceCuratedShelves(props: {
|
|
37
|
+
entries: MarketplaceShelfEntry[];
|
|
38
|
+
scenes: MarketplaceSceneView[];
|
|
39
|
+
language: string;
|
|
40
|
+
installState: InstallState;
|
|
41
|
+
onOpen: (entry: MarketplaceShelfEntry) => void;
|
|
42
|
+
onInstall: (item: MarketplaceItemSummary) => void;
|
|
43
|
+
onOpenScene: (scene: string) => void;
|
|
44
|
+
}) {
|
|
45
|
+
const {
|
|
46
|
+
entries,
|
|
47
|
+
scenes,
|
|
48
|
+
language,
|
|
49
|
+
installState,
|
|
50
|
+
onOpen,
|
|
51
|
+
onInstall,
|
|
52
|
+
onOpenScene,
|
|
53
|
+
} = props;
|
|
54
|
+
const localeFallbacks = buildLocaleFallbacks(language);
|
|
55
|
+
const recentEntries = [...entries]
|
|
56
|
+
.sort((left, right) => compareUpdatedAt(left.item, right.item))
|
|
57
|
+
.slice(0, 6);
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<div className="mb-4 space-y-5">
|
|
61
|
+
<section className="space-y-2.5">
|
|
62
|
+
<ShelfHeader
|
|
63
|
+
icon={Compass}
|
|
64
|
+
title={readLocalized({ zh: "场景", en: "Scenes" }, language)}
|
|
65
|
+
description={readLocalized(
|
|
66
|
+
{
|
|
67
|
+
zh: "按使用场景浏览适合的技能组合。",
|
|
68
|
+
en: "Browse skills by how you plan to use them.",
|
|
69
|
+
},
|
|
70
|
+
language,
|
|
71
|
+
)}
|
|
72
|
+
/>
|
|
73
|
+
<div className="grid grid-cols-2 gap-2 md:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5">
|
|
74
|
+
{scenes.map((scene) => (
|
|
75
|
+
<GoalCard
|
|
76
|
+
key={scene.scene}
|
|
77
|
+
scene={scene}
|
|
78
|
+
language={language}
|
|
79
|
+
onSelect={onOpenScene}
|
|
80
|
+
/>
|
|
81
|
+
))}
|
|
82
|
+
</div>
|
|
83
|
+
</section>
|
|
84
|
+
|
|
85
|
+
{recentEntries.length > 0 && (
|
|
86
|
+
<ShelfItemRow
|
|
87
|
+
icon={Clock3}
|
|
88
|
+
title={readLocalized({ zh: "最近更新", en: "Recently updated" }, language)}
|
|
89
|
+
description={readLocalized(
|
|
90
|
+
{
|
|
91
|
+
zh: "看看生态最近在补齐哪些能力。",
|
|
92
|
+
en: "See what the ecosystem has been improving lately.",
|
|
93
|
+
},
|
|
94
|
+
language,
|
|
95
|
+
)}
|
|
96
|
+
entries={recentEntries}
|
|
97
|
+
language={language}
|
|
98
|
+
localeFallbacks={localeFallbacks}
|
|
99
|
+
installState={installState}
|
|
100
|
+
onOpen={onOpen}
|
|
101
|
+
onInstall={onInstall}
|
|
102
|
+
/>
|
|
103
|
+
)}
|
|
104
|
+
</div>
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function GoalCard(props: {
|
|
109
|
+
scene: MarketplaceSceneView;
|
|
110
|
+
language: string;
|
|
111
|
+
onSelect: (scene: string) => void;
|
|
112
|
+
}) {
|
|
113
|
+
const { scene, language, onSelect } = props;
|
|
114
|
+
const visual = resolveSceneVisual(scene.scene);
|
|
115
|
+
const Icon = visual.icon;
|
|
116
|
+
const tone = MARKETPLACE_SHELF_TONE_STYLES[visual.tone];
|
|
117
|
+
|
|
118
|
+
return (
|
|
119
|
+
<button
|
|
120
|
+
type="button"
|
|
121
|
+
onClick={() => onSelect(scene.scene)}
|
|
122
|
+
className={cn(
|
|
123
|
+
"group flex min-h-[74px] flex-col justify-center rounded-lg border px-3 py-2.5 text-left shadow-sm transition-colors hover:border-gray-300 hover:bg-gray-50/70",
|
|
124
|
+
tone.card,
|
|
125
|
+
)}
|
|
126
|
+
>
|
|
127
|
+
<div className="min-w-0">
|
|
128
|
+
<div className="flex min-w-0 items-center gap-2">
|
|
129
|
+
<span
|
|
130
|
+
className={cn(
|
|
131
|
+
"flex h-7 w-7 shrink-0 items-center justify-center rounded-md border",
|
|
132
|
+
tone.icon,
|
|
133
|
+
)}
|
|
134
|
+
>
|
|
135
|
+
<Icon className="h-3.5 w-3.5" />
|
|
136
|
+
</span>
|
|
137
|
+
<div className="min-w-0 flex-1 truncate text-[13px] font-semibold text-gray-950">
|
|
138
|
+
{readSceneTitle(scene, visual, language)}
|
|
139
|
+
</div>
|
|
140
|
+
{typeof scene.count === "number" && (
|
|
141
|
+
<span className="shrink-0 text-[11px] font-medium text-gray-400">
|
|
142
|
+
{readSceneCount(scene.count, language)}
|
|
143
|
+
</span>
|
|
144
|
+
)}
|
|
145
|
+
</div>
|
|
146
|
+
<p className={cn("mt-1 line-clamp-1 text-[11px] leading-relaxed", tone.text)}>
|
|
147
|
+
{readSceneSummary(scene, visual, language)}
|
|
148
|
+
</p>
|
|
149
|
+
</div>
|
|
150
|
+
</button>
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function MarketplaceCuratedSceneView(props: {
|
|
155
|
+
scene: MarketplaceSceneView;
|
|
156
|
+
entries: MarketplaceShelfEntry[];
|
|
157
|
+
isLoading: boolean;
|
|
158
|
+
language: string;
|
|
159
|
+
localeFallbacks: string[];
|
|
160
|
+
installState: InstallState;
|
|
161
|
+
onBack: () => void;
|
|
162
|
+
onOpen: (entry: MarketplaceShelfEntry) => void;
|
|
163
|
+
onInstall: (item: MarketplaceItemSummary) => void;
|
|
164
|
+
}) {
|
|
165
|
+
const {
|
|
166
|
+
scene,
|
|
167
|
+
entries,
|
|
168
|
+
isLoading,
|
|
169
|
+
language,
|
|
170
|
+
localeFallbacks,
|
|
171
|
+
installState,
|
|
172
|
+
onBack,
|
|
173
|
+
onOpen,
|
|
174
|
+
onInstall,
|
|
175
|
+
} = props;
|
|
176
|
+
const visual = resolveSceneVisual(scene.scene);
|
|
177
|
+
const Icon = visual.icon;
|
|
178
|
+
const tone = MARKETPLACE_SHELF_TONE_STYLES[visual.tone];
|
|
179
|
+
|
|
180
|
+
return (
|
|
181
|
+
<section className="flex min-h-full flex-col">
|
|
182
|
+
<div className="mb-4 flex min-w-0 items-start gap-2.5">
|
|
183
|
+
<button
|
|
184
|
+
type="button"
|
|
185
|
+
onClick={onBack}
|
|
186
|
+
className="inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-lg text-gray-600 transition-colors hover:bg-gray-100 hover:text-gray-900"
|
|
187
|
+
aria-label={readLocalized({ zh: "返回", en: "Back" }, language)}
|
|
188
|
+
>
|
|
189
|
+
<ArrowLeft className="h-4 w-4" />
|
|
190
|
+
</button>
|
|
191
|
+
<div className="min-w-0 flex-1">
|
|
192
|
+
<div className="flex min-w-0 items-center gap-2">
|
|
193
|
+
<span
|
|
194
|
+
className={cn(
|
|
195
|
+
"flex h-8 w-8 shrink-0 items-center justify-center rounded-lg border",
|
|
196
|
+
tone.icon,
|
|
197
|
+
)}
|
|
198
|
+
>
|
|
199
|
+
<Icon className="h-4 w-4" />
|
|
200
|
+
</span>
|
|
201
|
+
<h3 className="min-w-0 flex-1 truncate text-[15px] font-semibold text-gray-950">
|
|
202
|
+
{readSceneTitle(scene, visual, language)}
|
|
203
|
+
</h3>
|
|
204
|
+
<span className="shrink-0 text-[11px] font-medium text-gray-400">
|
|
205
|
+
{readLocalized(
|
|
206
|
+
isLoading
|
|
207
|
+
? { zh: "加载中", en: "Loading" }
|
|
208
|
+
: { zh: `${entries.length} 个技能`, en: `${entries.length} skills` },
|
|
209
|
+
language,
|
|
210
|
+
)}
|
|
211
|
+
</span>
|
|
212
|
+
</div>
|
|
213
|
+
<div className="mt-1.5 flex min-w-0 items-center gap-2">
|
|
214
|
+
<p className="min-w-0 flex-1 truncate text-[12px] leading-relaxed text-gray-500">
|
|
215
|
+
{readSceneSummary(scene, visual, language)}
|
|
216
|
+
</p>
|
|
217
|
+
</div>
|
|
218
|
+
</div>
|
|
219
|
+
</div>
|
|
220
|
+
|
|
221
|
+
{isLoading ? (
|
|
222
|
+
<div
|
|
223
|
+
data-testid="marketplace-scene-skeleton"
|
|
224
|
+
className={cn(
|
|
225
|
+
SCENE_CARD_GRID_CLASS,
|
|
226
|
+
"min-h-0 flex-1 auto-rows-[166px] content-start",
|
|
227
|
+
)}
|
|
228
|
+
>
|
|
229
|
+
<MarketplaceListSkeleton count={SCENE_SKELETON_CARD_COUNT} />
|
|
230
|
+
</div>
|
|
231
|
+
) : entries.length > 0 ? (
|
|
232
|
+
<div className={SCENE_CARD_GRID_CLASS}>
|
|
233
|
+
{entries.map((entry) => (
|
|
234
|
+
<SkillShelfCard
|
|
235
|
+
key={entry.item.id}
|
|
236
|
+
entry={entry}
|
|
237
|
+
language={language}
|
|
238
|
+
localeFallbacks={localeFallbacks}
|
|
239
|
+
installState={installState}
|
|
240
|
+
layout="grid"
|
|
241
|
+
onOpen={onOpen}
|
|
242
|
+
onInstall={onInstall}
|
|
243
|
+
/>
|
|
244
|
+
))}
|
|
245
|
+
</div>
|
|
246
|
+
) : (
|
|
247
|
+
<div className="rounded-lg border border-dashed border-gray-200 py-8 text-center text-[12px] text-gray-500">
|
|
248
|
+
{readLocalized({ zh: "这个模块暂无技能。", en: "No skills in this module yet." }, language)}
|
|
249
|
+
</div>
|
|
250
|
+
)}
|
|
251
|
+
</section>
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function ShelfItemRow(props: {
|
|
256
|
+
icon: ComponentType<{ className?: string }>;
|
|
257
|
+
title: string;
|
|
258
|
+
description: string;
|
|
259
|
+
entries: MarketplaceShelfEntry[];
|
|
260
|
+
language: string;
|
|
261
|
+
localeFallbacks: string[];
|
|
262
|
+
installState: InstallState;
|
|
263
|
+
onOpen: (entry: MarketplaceShelfEntry) => void;
|
|
264
|
+
onInstall: (item: MarketplaceItemSummary) => void;
|
|
265
|
+
}) {
|
|
266
|
+
const {
|
|
267
|
+
icon: Icon,
|
|
268
|
+
title,
|
|
269
|
+
description,
|
|
270
|
+
entries,
|
|
271
|
+
language,
|
|
272
|
+
localeFallbacks,
|
|
273
|
+
installState,
|
|
274
|
+
onOpen,
|
|
275
|
+
onInstall,
|
|
276
|
+
} = props;
|
|
277
|
+
|
|
278
|
+
return (
|
|
279
|
+
<section className="space-y-2.5">
|
|
280
|
+
<ShelfHeader icon={Icon} title={title} description={description} />
|
|
281
|
+
<div className="-mx-1 flex gap-2.5 overflow-x-auto px-1 pb-1.5 custom-scrollbar">
|
|
282
|
+
{entries.map((entry) => (
|
|
283
|
+
<SkillShelfCard
|
|
284
|
+
key={entry.item.id}
|
|
285
|
+
entry={entry}
|
|
286
|
+
language={language}
|
|
287
|
+
localeFallbacks={localeFallbacks}
|
|
288
|
+
installState={installState}
|
|
289
|
+
onOpen={onOpen}
|
|
290
|
+
onInstall={onInstall}
|
|
291
|
+
/>
|
|
292
|
+
))}
|
|
293
|
+
</div>
|
|
294
|
+
</section>
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function ShelfHeader({
|
|
299
|
+
icon: Icon,
|
|
300
|
+
title,
|
|
301
|
+
description,
|
|
302
|
+
}: {
|
|
303
|
+
icon: ComponentType<{ className?: string }>;
|
|
304
|
+
title: string;
|
|
305
|
+
description: string;
|
|
306
|
+
}) {
|
|
307
|
+
return (
|
|
308
|
+
<div className="flex items-end justify-between gap-4">
|
|
309
|
+
<div>
|
|
310
|
+
<h3 className="flex items-center gap-2 text-[14px] font-semibold text-gray-950">
|
|
311
|
+
<Icon className="h-4 w-4 text-primary" />
|
|
312
|
+
{title}
|
|
313
|
+
</h3>
|
|
314
|
+
<p className="mt-0.5 text-[12px] leading-relaxed text-gray-500">
|
|
315
|
+
{description}
|
|
316
|
+
</p>
|
|
317
|
+
</div>
|
|
318
|
+
</div>
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function compareUpdatedAt(left: MarketplaceItemSummary, right: MarketplaceItemSummary) {
|
|
323
|
+
const leftTs = Date.parse(left.updatedAt);
|
|
324
|
+
const rightTs = Date.parse(right.updatedAt);
|
|
325
|
+
if (Number.isNaN(leftTs) || Number.isNaN(rightTs)) {
|
|
326
|
+
return right.updatedAt.localeCompare(left.updatedAt);
|
|
327
|
+
}
|
|
328
|
+
return rightTs - leftTs;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function readLocalized(text: MarketplaceShelfLocalizedText, language: string) {
|
|
332
|
+
return language.startsWith("zh") ? text.zh : text.en;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function resolveSceneVisual(scene: string): MarketplaceShelfSceneVisual {
|
|
336
|
+
return MARKETPLACE_SHELF_SCENE_VISUALS.find((visual) => visual.scene === scene) ?? {
|
|
337
|
+
scene,
|
|
338
|
+
...MARKETPLACE_SHELF_FALLBACK_VISUAL,
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function readSceneTitle(scene: MarketplaceSceneView, visual: MarketplaceShelfSceneVisual, language: string) {
|
|
343
|
+
return visual.title ? readLocalized(visual.title, language) : scene.title;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function readSceneSummary(scene: MarketplaceSceneView, visual: MarketplaceShelfSceneVisual, language: string) {
|
|
347
|
+
if (visual.summary) {
|
|
348
|
+
return readLocalized(visual.summary, language);
|
|
349
|
+
}
|
|
350
|
+
return scene.description ?? scene.scene;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function readSceneCount(count: number, language: string) {
|
|
354
|
+
return language.startsWith("zh") ? `${count} 个技能` : `${count} skills`;
|
|
355
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
MarketplaceInstalledRecord,
|
|
3
|
+
MarketplaceItemSummary,
|
|
4
|
+
} from "@/shared/lib/api";
|
|
5
|
+
import type { InstallState } from "@/features/marketplace/components/marketplace-list-card";
|
|
6
|
+
import { MarketplaceItemIcon } from "@/features/marketplace/components/marketplace-item-icon";
|
|
7
|
+
import { pickLocalizedText } from "@/features/marketplace/components/marketplace-localization";
|
|
8
|
+
import { t } from "@/shared/lib/i18n";
|
|
9
|
+
import { cn } from "@/shared/lib/utils";
|
|
10
|
+
import { CheckCircle2, Download } from "lucide-react";
|
|
11
|
+
|
|
12
|
+
type SkillShelfCardEntry = {
|
|
13
|
+
item: MarketplaceItemSummary;
|
|
14
|
+
record?: MarketplaceInstalledRecord;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export function SkillShelfCard(props: {
|
|
18
|
+
entry: SkillShelfCardEntry;
|
|
19
|
+
language: string;
|
|
20
|
+
localeFallbacks: string[];
|
|
21
|
+
installState: InstallState;
|
|
22
|
+
layout?: "rail" | "grid";
|
|
23
|
+
onOpen: (entry: SkillShelfCardEntry) => void;
|
|
24
|
+
onInstall: (item: MarketplaceItemSummary) => void;
|
|
25
|
+
}) {
|
|
26
|
+
const {
|
|
27
|
+
entry,
|
|
28
|
+
language,
|
|
29
|
+
localeFallbacks,
|
|
30
|
+
installState,
|
|
31
|
+
layout = "rail",
|
|
32
|
+
onOpen,
|
|
33
|
+
onInstall,
|
|
34
|
+
} = props;
|
|
35
|
+
const { item, record } = entry;
|
|
36
|
+
const summary = pickLocalizedText(item.summaryI18n, item.summary, localeFallbacks);
|
|
37
|
+
const installSpec = item.install.spec;
|
|
38
|
+
const isInstalling = installState.installingSpecs.has(installSpec);
|
|
39
|
+
const isInstalled = Boolean(record);
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<article
|
|
43
|
+
onClick={() => onOpen(entry)}
|
|
44
|
+
className={cn(
|
|
45
|
+
"group flex min-h-[166px] cursor-pointer flex-col justify-between rounded-xl border border-gray-200/70 bg-white p-3 shadow-sm transition-colors hover:border-gray-300 hover:bg-gray-50/60",
|
|
46
|
+
layout === "rail" ? "w-[260px] shrink-0" : "w-full min-w-0",
|
|
47
|
+
)}
|
|
48
|
+
>
|
|
49
|
+
<div>
|
|
50
|
+
<div className="mb-2.5 flex min-w-0 items-start gap-2.5">
|
|
51
|
+
<MarketplaceItemIcon name={item.name} fallback={item.install.spec} />
|
|
52
|
+
<div className="min-w-0 flex-1 pt-0.5">
|
|
53
|
+
<div className="truncate text-[13px] font-semibold leading-tight text-gray-950">
|
|
54
|
+
{item.name}
|
|
55
|
+
</div>
|
|
56
|
+
<div className="mt-0.5 truncate text-[11px] font-mono leading-tight text-gray-400">
|
|
57
|
+
{formatShelfMeta(item)}
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
<p className="line-clamp-2 text-[12px] leading-relaxed text-gray-500">
|
|
62
|
+
{summary}
|
|
63
|
+
</p>
|
|
64
|
+
<TagLine tags={item.tags} />
|
|
65
|
+
</div>
|
|
66
|
+
|
|
67
|
+
<div className="mt-3 flex items-center justify-between gap-3 border-t border-gray-100 pt-2.5">
|
|
68
|
+
<span className="min-w-0 truncate text-[11px] text-gray-400">
|
|
69
|
+
{formatUpdatedAt(item.updatedAt)}
|
|
70
|
+
</span>
|
|
71
|
+
{isInstalled ? (
|
|
72
|
+
<span className="inline-flex h-7 items-center gap-1.5 text-[11px] font-semibold text-emerald-700">
|
|
73
|
+
<CheckCircle2 className="h-3.5 w-3.5" />
|
|
74
|
+
{readLocalized({ zh: "已安装", en: "Installed" }, language)}
|
|
75
|
+
</span>
|
|
76
|
+
) : (
|
|
77
|
+
<button
|
|
78
|
+
type="button"
|
|
79
|
+
disabled={isInstalling}
|
|
80
|
+
onClick={(event) => {
|
|
81
|
+
event.stopPropagation();
|
|
82
|
+
onInstall(item);
|
|
83
|
+
}}
|
|
84
|
+
className="inline-flex h-7 items-center gap-1.5 rounded-md border border-gray-200 bg-white px-2 text-[11px] font-semibold text-gray-700 transition-colors hover:border-gray-300 hover:bg-gray-50 disabled:opacity-50"
|
|
85
|
+
>
|
|
86
|
+
<Download className="h-3.5 w-3.5" />
|
|
87
|
+
{isInstalling ? t("marketplaceInstalling") : t("marketplaceInstall")}
|
|
88
|
+
</button>
|
|
89
|
+
)}
|
|
90
|
+
</div>
|
|
91
|
+
</article>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function formatShelfMeta(item: MarketplaceItemSummary) {
|
|
96
|
+
return item.slug || item.install.spec;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function TagLine({ tags }: { tags: string[] }) {
|
|
100
|
+
const visibleTags = tags.slice(0, 2);
|
|
101
|
+
if (visibleTags.length === 0) {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
return (
|
|
105
|
+
<div className="mt-2 truncate text-[11px] font-medium text-gray-400">
|
|
106
|
+
{visibleTags.join(" / ")}
|
|
107
|
+
</div>
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function formatUpdatedAt(value: string) {
|
|
112
|
+
const date = value.slice(0, 10);
|
|
113
|
+
return date || value;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function readLocalized(text: { zh: string; en: string }, language: string) {
|
|
117
|
+
return language.startsWith("zh") ? text.zh : text.en;
|
|
118
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
export function escapeHtml(text: string): string {
|
|
2
|
+
return text
|
|
3
|
+
.replace(/&/g, "&")
|
|
4
|
+
.replace(/</g, "<")
|
|
5
|
+
.replace(/>/g, ">")
|
|
6
|
+
.replace(/"/g, """)
|
|
7
|
+
.replace(/'/g, "'");
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function stripYamlFrontmatter(text: string): string {
|
|
11
|
+
const lines = text.replace(/\r\n/g, "\n").split("\n");
|
|
12
|
+
if (lines[0]?.trim() !== "---") {
|
|
13
|
+
return text;
|
|
14
|
+
}
|
|
15
|
+
const closingIndex = lines.findIndex((line, index) => index > 0 && line.trim() === "---");
|
|
16
|
+
return closingIndex > 0 ? lines.slice(closingIndex + 1).join("\n").trim() : text;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function renderInlineMarkdown(text: string): string {
|
|
20
|
+
return escapeHtml(text)
|
|
21
|
+
.replace(/`([^`]+)`/g, "<code>$1</code>")
|
|
22
|
+
.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>")
|
|
23
|
+
.replace(/\[([^\]]+)\]\((https?:\/\/[^)\s]+)\)/g, '<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function isMarkdownBlockStart(line: string): boolean {
|
|
27
|
+
return /^(#{1,4})\s+/.test(line)
|
|
28
|
+
|| /^([-*])\s+/.test(line)
|
|
29
|
+
|| /^\d+\.\s+/.test(line)
|
|
30
|
+
|| /^>\s?/.test(line)
|
|
31
|
+
|| /^```/.test(line);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
type MarkdownRenderResult = {
|
|
35
|
+
html: string;
|
|
36
|
+
nextIndex: number;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
function renderCodeBlock(lines: string[], index: number, fenceMatch: RegExpMatchArray): MarkdownRenderResult {
|
|
40
|
+
const codeLines: string[] = [];
|
|
41
|
+
let nextIndex = index + 1;
|
|
42
|
+
while (nextIndex < lines.length && !lines[nextIndex]?.trim().startsWith("```")) {
|
|
43
|
+
codeLines.push(lines[nextIndex] ?? "");
|
|
44
|
+
nextIndex += 1;
|
|
45
|
+
}
|
|
46
|
+
const language = fenceMatch[1] ? `<span class="code-language">${escapeHtml(fenceMatch[1])}</span>` : "";
|
|
47
|
+
return {
|
|
48
|
+
html: `<div class="code-block">${language}<pre><code>${escapeHtml(codeLines.join("\n"))}</code></pre></div>`,
|
|
49
|
+
nextIndex: nextIndex + 1,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function renderHeadingBlock(headingMatch: RegExpMatchArray, index: number): MarkdownRenderResult {
|
|
54
|
+
const level = Math.min(4, headingMatch[1]?.length ?? 2);
|
|
55
|
+
return {
|
|
56
|
+
html: `<h${level}>${renderInlineMarkdown(headingMatch[2] ?? "")}</h${level}>`,
|
|
57
|
+
nextIndex: index + 1,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function renderListBlock(lines: string[], index: number, ordered: boolean): MarkdownRenderResult {
|
|
62
|
+
const items: string[] = [];
|
|
63
|
+
let nextIndex = index;
|
|
64
|
+
const matcher = ordered ? /^\d+\.\s+/ : /^[-*]\s+/;
|
|
65
|
+
while (nextIndex < lines.length && matcher.test(lines[nextIndex]?.trim() ?? "")) {
|
|
66
|
+
items.push(`<li>${renderInlineMarkdown((lines[nextIndex] ?? "").trim().replace(matcher, ""))}</li>`);
|
|
67
|
+
nextIndex += 1;
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
html: ordered ? `<ol>${items.join("")}</ol>` : `<ul>${items.join("")}</ul>`,
|
|
71
|
+
nextIndex,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function renderQuoteBlock(lines: string[], index: number): MarkdownRenderResult {
|
|
76
|
+
const quotes: string[] = [];
|
|
77
|
+
let nextIndex = index;
|
|
78
|
+
while (nextIndex < lines.length && /^>\s?/.test(lines[nextIndex]?.trim() ?? "")) {
|
|
79
|
+
quotes.push((lines[nextIndex] ?? "").trim().replace(/^>\s?/, ""));
|
|
80
|
+
nextIndex += 1;
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
html: `<blockquote>${renderInlineMarkdown(quotes.join(" "))}</blockquote>`,
|
|
84
|
+
nextIndex,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function renderParagraphBlock(lines: string[], index: number): MarkdownRenderResult {
|
|
89
|
+
const paragraphLines: string[] = [];
|
|
90
|
+
let nextIndex = index;
|
|
91
|
+
while (nextIndex < lines.length) {
|
|
92
|
+
const paragraphLine = lines[nextIndex]?.trim() ?? "";
|
|
93
|
+
if (!paragraphLine || isMarkdownBlockStart(paragraphLine)) {
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
paragraphLines.push(paragraphLine);
|
|
97
|
+
nextIndex += 1;
|
|
98
|
+
}
|
|
99
|
+
return {
|
|
100
|
+
html: `<p>${renderInlineMarkdown(paragraphLines.join(" "))}</p>`,
|
|
101
|
+
nextIndex,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function renderMarkdownBlock(lines: string[], index: number): MarkdownRenderResult {
|
|
106
|
+
const trimmed = lines[index]?.trim() ?? "";
|
|
107
|
+
const fenceMatch = trimmed.match(/^```(\S*)/);
|
|
108
|
+
if (fenceMatch) {
|
|
109
|
+
return renderCodeBlock(lines, index, fenceMatch);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const headingMatch = trimmed.match(/^(#{1,4})\s+(.+)$/);
|
|
113
|
+
if (headingMatch) {
|
|
114
|
+
return renderHeadingBlock(headingMatch, index);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (/^[-*]\s+/.test(trimmed)) {
|
|
118
|
+
return renderListBlock(lines, index, false);
|
|
119
|
+
}
|
|
120
|
+
if (/^\d+\.\s+/.test(trimmed)) {
|
|
121
|
+
return renderListBlock(lines, index, true);
|
|
122
|
+
}
|
|
123
|
+
if (/^>\s?/.test(trimmed)) {
|
|
124
|
+
return renderQuoteBlock(lines, index);
|
|
125
|
+
}
|
|
126
|
+
return renderParagraphBlock(lines, index);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function renderDetailMarkdown(markdown: string): string {
|
|
130
|
+
const lines = stripYamlFrontmatter(markdown).replace(/\r\n/g, "\n").split("\n");
|
|
131
|
+
const blocks: string[] = [];
|
|
132
|
+
let index = 0;
|
|
133
|
+
|
|
134
|
+
while (index < lines.length) {
|
|
135
|
+
if (!(lines[index] ?? "").trim()) {
|
|
136
|
+
index += 1;
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const result = renderMarkdownBlock(lines, index);
|
|
141
|
+
blocks.push(result.html);
|
|
142
|
+
index = result.nextIndex;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return blocks.join("");
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
type MetadataEntry = {
|
|
149
|
+
key: string;
|
|
150
|
+
value: string;
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
function stringifyMetadataValue(value: unknown): string {
|
|
154
|
+
if (Array.isArray(value)) {
|
|
155
|
+
return value.map((entry) => stringifyMetadataValue(entry)).join(", ");
|
|
156
|
+
}
|
|
157
|
+
if (value && typeof value === "object") {
|
|
158
|
+
return JSON.stringify(value);
|
|
159
|
+
}
|
|
160
|
+
return String(value ?? "");
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function readJsonMetadata(raw: string): MetadataEntry[] {
|
|
164
|
+
try {
|
|
165
|
+
const parsed = JSON.parse(raw) as unknown;
|
|
166
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
167
|
+
return [];
|
|
168
|
+
}
|
|
169
|
+
return Object.entries(parsed)
|
|
170
|
+
.map(([key, value]) => ({ key, value: stringifyMetadataValue(value) }))
|
|
171
|
+
.filter((entry) => entry.value.trim().length > 0);
|
|
172
|
+
} catch {
|
|
173
|
+
return [];
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function readYamlLikeMetadata(raw: string): MetadataEntry[] {
|
|
178
|
+
return raw
|
|
179
|
+
.replace(/\r\n/g, "\n")
|
|
180
|
+
.split("\n")
|
|
181
|
+
.map((line) => line.match(/^([A-Za-z0-9_.-]+):\s*(.+)$/))
|
|
182
|
+
.filter((match): match is RegExpMatchArray => Boolean(match))
|
|
183
|
+
.map((match) => ({
|
|
184
|
+
key: match[1] ?? "",
|
|
185
|
+
value: match[2]?.replace(/^["']|["']$/g, "") ?? "",
|
|
186
|
+
}))
|
|
187
|
+
.filter((entry) => entry.key && entry.value.trim().length > 0);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function renderDetailMetadata(raw: string): string {
|
|
191
|
+
const entries = readJsonMetadata(raw);
|
|
192
|
+
const metadataEntries = entries.length > 0 ? entries : readYamlLikeMetadata(raw);
|
|
193
|
+
if (metadataEntries.length === 0) {
|
|
194
|
+
return `<pre class="code">${escapeHtml(raw)}</pre>`;
|
|
195
|
+
}
|
|
196
|
+
return `<dl class="metadata-list">${metadataEntries
|
|
197
|
+
.map(
|
|
198
|
+
(entry) => `<div><dt>${escapeHtml(entry.key)}</dt><dd>${escapeHtml(entry.value)}</dd></div>`,
|
|
199
|
+
)
|
|
200
|
+
.join("")}</dl>`;
|
|
201
|
+
}
|