@terrafirmagreg-team/guide 0.0.0-stage → 0.1.0

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 (66) hide show
  1. package/package.json +58 -3
  2. package/src/components/book-switcher.tsx +46 -0
  3. package/src/components/views/browser.tsx +124 -0
  4. package/src/components/views/entry/pages.tsx +186 -0
  5. package/src/components/views/entry/view.tsx +60 -0
  6. package/src/components/views/kind/links.tsx +111 -0
  7. package/src/components/views/kind/media.tsx +167 -0
  8. package/src/components/views/kind/recipe.tsx +134 -0
  9. package/src/components/views/kind/table.tsx +138 -0
  10. package/src/components/views/kind/unknown.tsx +23 -0
  11. package/src/components/views/page/category-body.tsx +56 -0
  12. package/src/components/views/page/entry-body.tsx +79 -0
  13. package/src/components/views/page/overview-body.tsx +97 -0
  14. package/src/components/views/page/text.tsx +57 -0
  15. package/src/components/views/page/view.tsx +279 -0
  16. package/src/components/views/text.tsx +130 -0
  17. package/src/hooks/use-left-sidebar-tree.ts +19 -0
  18. package/src/hooks/use-tree.ts +33 -0
  19. package/src/lib/book/convert.ts +74 -0
  20. package/src/lib/book/index-rows.ts +134 -0
  21. package/src/lib/book/index.ts +2 -0
  22. package/src/lib/book/localize.ts +76 -0
  23. package/src/lib/book/raw.ts +80 -0
  24. package/src/lib/books.ts +29 -0
  25. package/src/lib/breadcrumb.ts +54 -0
  26. package/src/lib/client/guides.ts +189 -0
  27. package/src/lib/client-payload/extras.ts +101 -0
  28. package/src/lib/client-payload/index.ts +112 -0
  29. package/src/lib/client-payload/links.ts +101 -0
  30. package/src/lib/config.ts +46 -0
  31. package/src/lib/data/dto.ts +77 -0
  32. package/src/lib/data/parse.ts +93 -0
  33. package/src/lib/dump.ts +5 -0
  34. package/src/lib/format.ts +231 -0
  35. package/src/lib/links.ts +2 -0
  36. package/src/lib/markdown/book.ts +73 -0
  37. package/src/lib/markdown/ctx.ts +26 -0
  38. package/src/lib/markdown/index.ts +8 -0
  39. package/src/lib/markdown/page.ts +154 -0
  40. package/src/lib/markdown/table.ts +35 -0
  41. package/src/lib/markdown/text.ts +39 -0
  42. package/src/lib/markdown-page.ts +72 -0
  43. package/src/lib/messages.ts +13 -0
  44. package/src/lib/meta.ts +9 -0
  45. package/src/lib/multiblock/index.ts +8 -0
  46. package/src/lib/multiblock/url.ts +61 -0
  47. package/src/lib/names.ts +35 -0
  48. package/src/lib/page-kind.ts +117 -0
  49. package/src/lib/paths.ts +52 -0
  50. package/src/lib/preview.ts +33 -0
  51. package/src/lib/query.ts +28 -0
  52. package/src/lib/recipe.ts +30 -0
  53. package/src/lib/reference/index.ts +40 -0
  54. package/src/lib/reference/resolver.ts +88 -0
  55. package/src/lib/search/filter.ts +17 -0
  56. package/src/lib/search/index.ts +156 -0
  57. package/src/lib/sprites.ts +17 -0
  58. package/src/lib/table.ts +10 -0
  59. package/src/lib/toc.ts +137 -0
  60. package/src/lib/tree/children.ts +70 -0
  61. package/src/lib/tree/index.ts +117 -0
  62. package/src/locales/en_us.json +22 -0
  63. package/src/locales/ru_ru.json +22 -0
  64. package/src/locales.ts +5 -0
  65. package/src/types/index.ts +215 -0
  66. package/README.md +0 -3
package/package.json CHANGED
@@ -1,6 +1,61 @@
1
1
  {
2
2
  "name": "@terrafirmagreg-team/guide",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "exports": {
6
+ "./components/*": [
7
+ "./src/components/*.tsx",
8
+ "./src/components/*.ts",
9
+ "./src/components/*/index.tsx",
10
+ "./src/components/*/index.ts"
11
+ ],
12
+ "./hooks/*": [
13
+ "./src/hooks/*.ts",
14
+ "./src/hooks/*.tsx",
15
+ "./src/hooks/*/index.ts",
16
+ "./src/hooks/*/index.tsx"
17
+ ],
18
+ "./lib/*": [
19
+ "./src/lib/*.ts",
20
+ "./src/lib/*.tsx",
21
+ "./src/lib/*/index.ts",
22
+ "./src/lib/*/index.tsx"
23
+ ],
24
+ "./types/*": "./src/types/*.ts",
25
+ "./types": "./src/types/index.ts",
26
+ "./locales": "./src/locales.ts"
27
+ },
28
+ "dependencies": {
29
+ "fumadocs-core": "16.17.8",
30
+ "lucide-react": "^1.63.0",
31
+ "@terrafirmagreg-team/game": "0.1.0",
32
+ "@terrafirmagreg-team/quest": "0.1.0",
33
+ "@terrafirmagreg-team/recipe": "0.1.0",
34
+ "@terrafirmagreg-team/translate": "0.1.0",
35
+ "@terrafirmagreg-team/ui": "0.1.0",
36
+ "@terrafirmagreg-team/kit": "0.1.0",
37
+ "@terrafirmagreg-team/comments": "0.1.0"
38
+ },
39
+ "peerDependencies": {
40
+ "next": ">=15",
41
+ "react": ">=19"
42
+ },
43
+ "devDependencies": {
44
+ "@types/node": "^26.6.5",
45
+ "@types/react": "^19.3.0",
46
+ "@types/react-dom": "^19.3.0"
47
+ },
48
+ "repository": {
49
+ "type": "git",
50
+ "url": "https://github.com/TerraFirmaGreg-Team/Web-Foundation.git",
51
+ "directory": "packages/guide"
52
+ },
53
+ "publishConfig": {
54
+ "access": "public"
55
+ },
56
+ "files": [
57
+ "src",
58
+ "styles",
59
+ "styles.css"
60
+ ]
6
61
  }
@@ -0,0 +1,46 @@
1
+ 'use client';
2
+
3
+ import { usePathname } from 'next/navigation';
4
+ import { BookOpen } from 'lucide-react';
5
+ import { useAsyncValue } from '@terrafirmagreg-team/ui/hooks/use-async-value';
6
+ import { useTranslations } from '@terrafirmagreg-team/translate/hooks/use-translations';
7
+ import { useResolvedLang } from '@terrafirmagreg-team/translate/hooks/use-resolved-lang';
8
+ import { useLocationSearch } from '@terrafirmagreg-team/ui/hooks/page/use-location-search';
9
+ import { guideQueryFromParams } from '@terrafirmagreg-team/guide/lib/query';
10
+ import { listGuideBooks } from '@terrafirmagreg-team/guide/lib/books';
11
+ import { guideHref } from '@terrafirmagreg-team/guide/lib/links';
12
+ import { parseSectionLocation } from '@terrafirmagreg-team/kit/lib/location';
13
+ import { Switcher } from '@terrafirmagreg-team/ui/components/blocks/nav/switcher';
14
+
15
+ function oneLineLanding(text: string | undefined) {
16
+ if (!text) return undefined;
17
+ const stripped = text.replace(/\$\([^)]*\)/g, ' ').replace(/\s+/g, ' ').trim();
18
+ return stripped || undefined;
19
+ }
20
+
21
+ export function GuideBookSwitcher() {
22
+ const t = useTranslations();
23
+ const { dataset } = parseSectionLocation(usePathname());
24
+ const lang = useResolvedLang();
25
+ const { bookId } = guideQueryFromParams(
26
+ new URLSearchParams(useLocationSearch()),
27
+ );
28
+ const books = useAsyncValue(
29
+ dataset ? () => listGuideBooks(dataset, lang) : undefined,
30
+ [dataset, lang],
31
+ );
32
+ if (!books || books.length < 2 || !dataset) return null;
33
+ return (
34
+ <Switcher
35
+ options={books.map((book) => ({
36
+ value: book.bookId,
37
+ title: book.name,
38
+ description: oneLineLanding(book.landingText),
39
+ icon: <BookOpen className="size-4" />,
40
+ url: guideHref(dataset, lang, book.bookId),
41
+ }))}
42
+ selected={bookId}
43
+ placeholder={t('ui.select_book')}
44
+ />
45
+ );
46
+ }
@@ -0,0 +1,124 @@
1
+ 'use client';
2
+
3
+ import { useEffect, useMemo } from 'react';
4
+ import { useRouter, useSearchParams } from 'next/navigation';
5
+ import { useAsyncValue } from '@terrafirmagreg-team/ui/hooks/use-async-value';
6
+ import { useLangMap } from '@terrafirmagreg-team/game/hooks/use-lang-map';
7
+ import { useTranslations } from '@terrafirmagreg-team/translate/hooks/use-translations';
8
+ import { Skeleton } from '@terrafirmagreg-team/ui/components/ui/skeleton';
9
+ import {
10
+ Workspace,
11
+ WorkspaceBody,
12
+ } from '@terrafirmagreg-team/kit/components/workspace';
13
+ import { GuidePageView } from '@terrafirmagreg-team/guide/components/views/page/view';
14
+ import {
15
+ firstGuideEntryId,
16
+ guideQueryFromParams,
17
+ } from '@terrafirmagreg-team/guide/lib/query';
18
+ import { loadClientGuidePayload } from '@terrafirmagreg-team/guide/lib/client-payload';
19
+ import { listGuideBooks } from '@terrafirmagreg-team/guide/lib/books';
20
+ import { loadClientGuideBook } from '@terrafirmagreg-team/guide/lib/client/guides';
21
+ import { guideHref } from '@terrafirmagreg-team/guide/lib/links';
22
+ import { guideConfig } from '@terrafirmagreg-team/guide/lib/config';
23
+ import type { Lang } from '@terrafirmagreg-team/translate/types/locale';
24
+ import type { GuideMessages } from '@terrafirmagreg-team/guide/types';
25
+
26
+ const MESSAGE_KEYS: readonly string[] = [
27
+ 'guide.related',
28
+ 'guide.objective',
29
+ 'guide.link',
30
+ 'guide.quest',
31
+ 'guide.recipe_empty',
32
+ 'guide.entity_unavailable',
33
+ 'guide.entry_missing',
34
+ 'guide.entry_count',
35
+ 'guide.page_count',
36
+ ];
37
+
38
+ export function GuideBrowser({
39
+ dataset,
40
+ datasetTitle,
41
+ lang,
42
+ }: {
43
+ dataset: string;
44
+ datasetTitle: string;
45
+ lang: Lang;
46
+ }) {
47
+ const t = useTranslations();
48
+ const router = useRouter();
49
+ const params = useSearchParams();
50
+ const { bookId, entryId, categoryId } = useMemo(
51
+ () => guideQueryFromParams(params),
52
+ [params],
53
+ );
54
+ const books = useAsyncValue(
55
+ () => listGuideBooks(dataset, lang),
56
+ [dataset, lang],
57
+ );
58
+ const langMap = useLangMap(dataset, lang);
59
+ const messages = useMemo<GuideMessages>(
60
+ () => Object.fromEntries(MESSAGE_KEYS.map((key) => [key, t(key)])),
61
+ [t],
62
+ );
63
+
64
+ const canonical = useAsyncValue(
65
+ books && !bookId
66
+ ? async () => {
67
+ const book = books[0];
68
+ if (!book) return undefined;
69
+ const index = await loadClientGuideBook(dataset, lang, book.bookId);
70
+ const first = index ? firstGuideEntryId(index) : undefined;
71
+ return guideHref(dataset, lang, book.bookId, first);
72
+ }
73
+ : undefined,
74
+ [books, bookId, dataset, lang],
75
+ );
76
+
77
+ useEffect(() => {
78
+ if (canonical) router.replace(canonical);
79
+ }, [canonical, router]);
80
+
81
+ const payload = useAsyncValue(
82
+ langMap && bookId
83
+ ? () =>
84
+ loadClientGuidePayload(
85
+ dataset,
86
+ lang,
87
+ bookId,
88
+ entryId,
89
+ categoryId,
90
+ langMap,
91
+ messages,
92
+ )
93
+ : undefined,
94
+ [dataset, lang, bookId, entryId, categoryId, langMap, messages],
95
+ );
96
+
97
+ useEffect(() => {
98
+ if (payload?.title) {
99
+ document.title = `${payload.title} · ${datasetTitle} | ${guideConfig().appName}`;
100
+ }
101
+ }, [payload?.title, datasetTitle]);
102
+
103
+ if (!bookId || !payload || !langMap) {
104
+ return (
105
+ <Workspace toc={[]}>
106
+ <WorkspaceBody className="flex flex-col gap-3">
107
+ <Skeleton className="h-3.5 w-44" />
108
+ <Skeleton className="mt-1 h-8 w-2/3 max-w-sm" />
109
+ <Skeleton className="h-4 w-full max-w-md" />
110
+ <Skeleton className="h-4 w-4/5 max-w-sm" />
111
+ <div className="mt-6 flex flex-col gap-2.5">
112
+ <Skeleton className="h-4 w-full" />
113
+ <Skeleton className="h-4 w-[92%]" />
114
+ <Skeleton className="h-4 w-[96%]" />
115
+ <Skeleton className="h-32 w-full" />
116
+ <Skeleton className="h-4 w-[88%]" />
117
+ <Skeleton className="h-4 w-3/4" />
118
+ </div>
119
+ </WorkspaceBody>
120
+ </Workspace>
121
+ );
122
+ }
123
+ return <GuidePageView payload={payload} langMap={langMap} />;
124
+ }
@@ -0,0 +1,186 @@
1
+ import type { ReactNode } from 'react';
2
+ import { PageText } from '@terrafirmagreg-team/guide/components/views/page/text';
3
+ import {
4
+ EntityPage,
5
+ ImagePage,
6
+ MultiMultiblockPage,
7
+ MultiblockPage,
8
+ MultiblockVisual,
9
+ } from '@terrafirmagreg-team/guide/components/views/kind/media';
10
+ import {
11
+ LinkPage,
12
+ QuestPage,
13
+ RelationPage,
14
+ } from '@terrafirmagreg-team/guide/components/views/kind/links';
15
+ import {
16
+ RecipePage,
17
+ SpotlightPage,
18
+ } from '@terrafirmagreg-team/guide/components/views/kind/recipe';
19
+ import { TablePage } from '@terrafirmagreg-team/guide/components/views/kind/table';
20
+ import { UnknownPage } from '@terrafirmagreg-team/guide/components/views/kind/unknown';
21
+ import type { PageRendererProps } from '@terrafirmagreg-team/guide/types';
22
+ import type { AtlasBundle } from '@terrafirmagreg-team/game/lib/dump/assets/atlas/dto';
23
+ import type { Lang } from '@terrafirmagreg-team/translate/types/locale';
24
+ import { guideMessage } from '@terrafirmagreg-team/guide/lib/messages';
25
+ import type { PagePreviewData } from '@terrafirmagreg-team/ui/types/preview';
26
+ import type { GuideBookMeta } from '@terrafirmagreg-team/guide/types';
27
+ import type {
28
+ GuideBook,
29
+ GuideEntryExtras,
30
+ GuideEntryRef,
31
+ GuideMessages,
32
+ GuidePage,
33
+ } from '@terrafirmagreg-team/guide/types';
34
+ import {
35
+ guidePageTitles,
36
+ pageAnchors,
37
+ pageIcons,
38
+ repeatedTitle,
39
+ } from '@terrafirmagreg-team/guide/lib/toc';
40
+ import { classify, isIndexPage, type PageKind, pageTexts } from '@terrafirmagreg-team/guide/lib/page-kind';
41
+ import {
42
+ EntryGroup,
43
+ GROUP_CYCLE_MS,
44
+ } from '@terrafirmagreg-team/game/components/entry/group';
45
+ import { Entry } from '@terrafirmagreg-team/game/components/entry/entry';
46
+
47
+ const renderers: Record<
48
+ Exclude<PageKind, 'empty' | 'text'>,
49
+ (props: PageRendererProps) => ReactNode
50
+ > = {
51
+ image: ImagePage,
52
+ recipe: RecipePage,
53
+ spotlight: SpotlightPage,
54
+ entity: EntityPage,
55
+ multiblock: MultiblockPage,
56
+ multimultiblock: MultiMultiblockPage,
57
+ link: LinkPage,
58
+ relations: RelationPage,
59
+ quest: QuestPage,
60
+ table: TablePage,
61
+ unknown: UnknownPage,
62
+ };
63
+
64
+ export function EntryPages({
65
+ dataset,
66
+ lang,
67
+ bookPath,
68
+ pages,
69
+ atlases,
70
+ meta,
71
+ bookModId,
72
+ categoryByEntry,
73
+ book,
74
+ langMap,
75
+ entryRefs,
76
+ keybinds,
77
+ extras,
78
+ questLinks,
79
+ previews,
80
+ messages,
81
+ }: {
82
+ dataset: string;
83
+ lang: Lang;
84
+ bookPath: string;
85
+ pages: GuidePage[];
86
+ atlases?: Record<string, AtlasBundle>;
87
+ meta?: GuideBookMeta;
88
+ bookModId: string;
89
+ categoryByEntry: Map<string, string>;
90
+ book?: GuideBook | null;
91
+ langMap: Record<string, string>;
92
+ entryRefs: Map<string, GuideEntryRef>;
93
+ keybinds?: Record<string, string>;
94
+ extras?: GuideEntryExtras;
95
+ questLinks?: Record<string, { filename: string; questId: string }>;
96
+ previews?: Record<string, PagePreviewData>;
97
+ messages: GuideMessages;
98
+ }) {
99
+ const titles = guidePageTitles(
100
+ pages,
101
+ langMap,
102
+ {
103
+ related: guideMessage(messages, 'guide.related'),
104
+ objective: guideMessage(messages, 'guide.objective'),
105
+ },
106
+ extras?.entityNames,
107
+ );
108
+ const anchors = pageAnchors(pages);
109
+ return (
110
+ <div className="space-y-6">
111
+ {pages.map((page, index) => {
112
+ if (isIndexPage(page, langMap)) return null;
113
+ const kind = classify(page.type);
114
+ const attachedValue =
115
+ kind === 'multiblock' || kind === 'multimultiblock'
116
+ ? undefined
117
+ : (page.model ?? page.multiblock);
118
+ if (kind === 'empty' && !attachedValue && !pageTexts(page).length) {
119
+ return null;
120
+ }
121
+ const rendererProps: PageRendererProps = {
122
+ dataset,
123
+ lang,
124
+ bookPath,
125
+ page,
126
+ atlases,
127
+ meta: meta ?? {},
128
+ bookModId,
129
+ categoryByEntry,
130
+ langMap,
131
+ entryRefs,
132
+ keybinds,
133
+ book,
134
+ extras,
135
+ questLinks,
136
+ previews,
137
+ messages,
138
+ };
139
+ const Renderer =
140
+ kind === 'text' || kind === 'empty' ? undefined : renderers[kind];
141
+ const heading =
142
+ kind === 'image' || repeatedTitle(titles, index)
143
+ ? undefined
144
+ : titles[index];
145
+ const icons = pageIcons(page);
146
+ return (
147
+ <section
148
+ key={index}
149
+ id={anchors[index]}
150
+ className="scroll-mt-20 space-y-3"
151
+ >
152
+ {heading ? (
153
+ <h2 className="flex items-center gap-2 text-xl font-semibold">
154
+ {icons.length > 1 ? (
155
+ <EntryGroup members={icons} size="lg" cycle={GROUP_CYCLE_MS} />
156
+ ) : icons[0] ? (
157
+ <Entry
158
+ id={icons[0].id}
159
+ kind={icons[0].kind}
160
+ dataset={dataset}
161
+ name={heading}
162
+ href={false}
163
+ tooltip={false}
164
+ missing="hide"
165
+ />
166
+ ) : null}
167
+ {heading}
168
+ </h2>
169
+ ) : null}
170
+ {attachedValue ? (
171
+ <MultiblockVisual
172
+ value={attachedValue}
173
+ title={
174
+ typeof page.name === 'string' ? page.name : undefined
175
+ }
176
+ glbs={extras?.glbs}
177
+ />
178
+ ) : null}
179
+ {Renderer ? <Renderer {...rendererProps} /> : null}
180
+ <PageText {...rendererProps} />
181
+ </section>
182
+ );
183
+ })}
184
+ </div>
185
+ );
186
+ }
@@ -0,0 +1,60 @@
1
+ import { EntryPages } from '@terrafirmagreg-team/guide/components/views/entry/pages';
2
+ import type { PagePreviewData } from '@terrafirmagreg-team/ui/types/preview';
3
+ import type { Lang } from '@terrafirmagreg-team/translate/types/locale';
4
+ import type {
5
+ GuideBook,
6
+ GuideEntry,
7
+ GuideEntryExtras,
8
+ GuideEntryRef,
9
+ GuideMessages,
10
+ } from '@terrafirmagreg-team/guide/types';
11
+
12
+ export function EntryView({
13
+ dataset,
14
+ lang,
15
+ bookPath,
16
+ entry,
17
+ bookModId,
18
+ categoryByEntry,
19
+ book,
20
+ langMap,
21
+ entryRefs,
22
+ extras,
23
+ questLinks,
24
+ previews,
25
+ messages,
26
+ }: {
27
+ dataset: string;
28
+ lang: Lang;
29
+ bookPath: string;
30
+ entry: GuideEntry;
31
+ bookModId: string;
32
+ categoryByEntry: Map<string, string>;
33
+ book?: GuideBook | null;
34
+ langMap: Record<string, string>;
35
+ entryRefs: Map<string, GuideEntryRef>;
36
+ extras?: GuideEntryExtras;
37
+ questLinks?: Record<string, { filename: string; questId: string }>;
38
+ previews?: Record<string, PagePreviewData>;
39
+ messages: GuideMessages;
40
+ }) {
41
+ return (
42
+ <EntryPages
43
+ dataset={dataset}
44
+ lang={lang}
45
+ bookPath={bookPath}
46
+ pages={entry.pages ?? []}
47
+ atlases={extras?.atlases}
48
+ bookModId={bookModId}
49
+ categoryByEntry={categoryByEntry}
50
+ book={book}
51
+ langMap={langMap}
52
+ entryRefs={entryRefs}
53
+ keybinds={extras?.keybinds}
54
+ extras={extras}
55
+ questLinks={questLinks}
56
+ previews={previews}
57
+ messages={messages}
58
+ />
59
+ );
60
+ }
@@ -0,0 +1,111 @@
1
+ import { LinkPreview } from '@terrafirmagreg-team/ui/components/blocks/link/link-preview';
2
+ import { Entry } from '@terrafirmagreg-team/game/components/entry/entry';
3
+ import { questHref } from '@terrafirmagreg-team/quest/lib/links';
4
+ import { guideMessage } from '@terrafirmagreg-team/guide/lib/messages';
5
+ import {
6
+ entryLabel,
7
+ entryRef,
8
+ spriteName,
9
+ } from '@terrafirmagreg-team/guide/lib/names';
10
+ import { referenceHref } from '@terrafirmagreg-team/guide/lib/reference';
11
+ import { parseGuideReference } from '@terrafirmagreg-team/guide/lib/reference/resolver';
12
+ import type { PageRendererProps } from '@terrafirmagreg-team/guide/types';
13
+
14
+ export function RelationPage(props: PageRendererProps) {
15
+ const ids = (props.page.entries ?? []).filter(
16
+ (value): value is string => typeof value === 'string' && Boolean(value),
17
+ );
18
+ if (!ids.length) return null;
19
+ return (
20
+ <ul className="space-y-1">
21
+ {ids.map((id) => {
22
+ const resolved = referenceHref(props, id);
23
+ const ref = entryRef(props.entryRefs, id);
24
+ const name = entryLabel(props.entryRefs, props.langMap, id);
25
+ return (
26
+ <li key={id} className="flex items-center gap-2">
27
+ {ref?.icon ? (
28
+ <Entry
29
+ id={ref.icon.id}
30
+ kind={ref.icon.kind}
31
+ dataset={props.dataset}
32
+ size="sm"
33
+ href={false}
34
+ tooltip={false}
35
+ />
36
+ ) : null}
37
+ {resolved ? (
38
+ <LinkPreview
39
+ variant="text"
40
+ href={resolved.href}
41
+ preview={props.previews?.[resolved.href]}
42
+ >
43
+ {name}
44
+ </LinkPreview>
45
+ ) : (
46
+ <span className="text-muted-foreground">{name}</span>
47
+ )}
48
+ </li>
49
+ );
50
+ })}
51
+ </ul>
52
+ );
53
+ }
54
+
55
+ export function LinkPage(props: PageRendererProps) {
56
+ const target = props.page.url || props.page.link || props.page.entry || '';
57
+ const resolved = target ? referenceHref(props, target) : null;
58
+ const label =
59
+ (typeof props.page.link_text === 'string' && props.page.link_text) ||
60
+ (typeof props.page.text === 'string' && props.page.text) ||
61
+ (typeof props.page.name === 'string' && props.page.name) ||
62
+ target ||
63
+ guideMessage(props.messages, 'guide.link');
64
+ return (
65
+ <p>
66
+ {resolved ? (
67
+ <LinkPreview
68
+ variant="chip"
69
+ solid
70
+ href={resolved.href}
71
+ preview={props.previews?.[resolved.href]}
72
+ >
73
+ {label}
74
+ </LinkPreview>
75
+ ) : (
76
+ <span className="text-muted-foreground">{label}</span>
77
+ )}
78
+ </p>
79
+ );
80
+ }
81
+
82
+ export function QuestPage({
83
+ dataset,
84
+ page,
85
+ lang,
86
+ langMap,
87
+ atlases,
88
+ questLinks,
89
+ previews,
90
+ messages,
91
+ }: PageRendererProps) {
92
+ const id = page.trigger || page.quest || page.link;
93
+ const hit = id ? questLinks?.[parseGuideReference(id).id] : undefined;
94
+ const href = hit
95
+ ? questHref(dataset, lang, { category: hit.filename, id: hit.questId })
96
+ : undefined;
97
+ const name = id ? spriteName(dataset, langMap, atlases, id) : '—';
98
+ const label = id && name !== id ? name : <code>{name}</code>;
99
+ return (
100
+ <p className="text-sm text-muted-foreground">
101
+ {guideMessage(messages, 'guide.quest')}:{' '}
102
+ {href ? (
103
+ <LinkPreview href={href} preview={previews?.[href]}>
104
+ {label}
105
+ </LinkPreview>
106
+ ) : (
107
+ label
108
+ )}
109
+ </p>
110
+ );
111
+ }