@terrafirmagreg-team/game 0.0.0-stage → 1.2.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.
- package/package.json +59 -3
- package/src/components/entry/atlas-sprite.tsx +44 -0
- package/src/components/entry/copy-id-menu.tsx +48 -0
- package/src/components/entry/entry.tsx +308 -0
- package/src/components/entry/group.tsx +161 -0
- package/src/components/entry/icon.tsx +181 -0
- package/src/components/entry/list.tsx +67 -0
- package/src/components/entry/mc-text.tsx +6 -0
- package/src/components/entry/stack-count.tsx +18 -0
- package/src/components/entry/tooltip.tsx +137 -0
- package/src/components/mdx/item-icon.tsx +77 -0
- package/src/components/preview-icon.tsx +26 -0
- package/src/hooks/use-dump-location.ts +20 -0
- package/src/hooks/use-entry-context.ts +37 -0
- package/src/hooks/use-icon-source.ts +306 -0
- package/src/hooks/use-lang-map.ts +20 -0
- package/src/lib/entry/display.ts +5 -0
- package/src/lib/entry/entry.ts +59 -0
- package/src/lib/entry/formula.ts +227 -0
- package/src/lib/entry/href.ts +156 -0
- package/src/lib/entry/icon-load.ts +231 -0
- package/src/lib/entry/icon.ts +11 -0
- package/src/lib/entry/tooltip.ts +62 -0
- package/src/lib/preview.ts +20 -0
- package/src/lib/search.ts +30 -0
- package/src/locales/en_us.json +5 -0
- package/src/locales/ru_ru.json +5 -0
- package/src/locales.ts +6 -0
- package/src/types/entry.ts +91 -0
- package/styles/fonts.css +31 -0
- package/styles/tokens.css +15 -0
- package/styles.css +2 -0
- package/README.md +0 -3
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import type { DatasetRef } from '@terrafirmagreg-team/sites/location';
|
|
3
|
+
|
|
4
|
+
import { useState } from 'react';
|
|
5
|
+
import { useEntryContext } from '@terrafirmagreg-team/game/hooks/use-entry-context';
|
|
6
|
+
import {
|
|
7
|
+
Sprite,
|
|
8
|
+
SpriteFrame,
|
|
9
|
+
} from '@terrafirmagreg-team/ui/components/blocks/sprite';
|
|
10
|
+
import { AtlasSprite } from '@terrafirmagreg-team/game/components/entry/atlas-sprite';
|
|
11
|
+
import { useIconSource } from '@terrafirmagreg-team/game/hooks/use-icon-source';
|
|
12
|
+
import { Skeleton } from '@terrafirmagreg-team/ui/components/ui/skeleton';
|
|
13
|
+
import type {
|
|
14
|
+
DumpNbt,
|
|
15
|
+
ReferenceKind,
|
|
16
|
+
TaggedReference,
|
|
17
|
+
} from '@terrafirmagreg-team/dump/lib/dump/contract/ref';
|
|
18
|
+
import type { EntrySize } from '@terrafirmagreg-team/game/types/entry';
|
|
19
|
+
|
|
20
|
+
export { ENTRY_CYCLE_MS } from '@terrafirmagreg-team/game/hooks/use-icon-source';
|
|
21
|
+
|
|
22
|
+
const ENTRY_SIZE_PX = { sm: 16, md: 24, lg: 32, xl: 40 } as const;
|
|
23
|
+
|
|
24
|
+
export function entrySizePx(size: EntrySize): number {
|
|
25
|
+
return typeof size === 'number' ? size : ENTRY_SIZE_PX[size];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function EntryIcon({
|
|
29
|
+
reference,
|
|
30
|
+
id,
|
|
31
|
+
kind,
|
|
32
|
+
tag,
|
|
33
|
+
dataset: datasetProp,
|
|
34
|
+
size: sizeProp = 'md',
|
|
35
|
+
labeled = false,
|
|
36
|
+
label,
|
|
37
|
+
missing = 'placeholder',
|
|
38
|
+
chip = false,
|
|
39
|
+
preferVariant = false,
|
|
40
|
+
entityView,
|
|
41
|
+
badge,
|
|
42
|
+
badgeClassName,
|
|
43
|
+
children,
|
|
44
|
+
}: {
|
|
45
|
+
|
|
46
|
+
reference?: TaggedReference;
|
|
47
|
+
id?: string;
|
|
48
|
+
kind?: ReferenceKind;
|
|
49
|
+
tag?: DumpNbt;
|
|
50
|
+
dataset?: DatasetRef;
|
|
51
|
+
size?: EntrySize;
|
|
52
|
+
labeled?: boolean;
|
|
53
|
+
label?: string;
|
|
54
|
+
missing?: 'placeholder' | 'item' | 'hide';
|
|
55
|
+
chip?: boolean;
|
|
56
|
+
|
|
57
|
+
preferVariant?: boolean;
|
|
58
|
+
entityView?: 'auto' | 'texture' | 'egg';
|
|
59
|
+
badge?: React.ReactNode;
|
|
60
|
+
badgeClassName?: string;
|
|
61
|
+
children?: React.ReactNode;
|
|
62
|
+
}) {
|
|
63
|
+
const size = entrySizePx(sizeProp);
|
|
64
|
+
const ctx = useEntryContext();
|
|
65
|
+
const dataset = datasetProp ?? ctx?.dataset;
|
|
66
|
+
const source = useIconSource({
|
|
67
|
+
dataset,
|
|
68
|
+
reference,
|
|
69
|
+
id,
|
|
70
|
+
kind,
|
|
71
|
+
tag,
|
|
72
|
+
missing,
|
|
73
|
+
preferVariant,
|
|
74
|
+
entityView,
|
|
75
|
+
});
|
|
76
|
+
const caption = label || undefined;
|
|
77
|
+
const name = caption ?? source.rawId;
|
|
78
|
+
const [fileFailed, setFileFailed] = useState<{
|
|
79
|
+
key: string;
|
|
80
|
+
failed: boolean;
|
|
81
|
+
}>({ key: '', failed: false });
|
|
82
|
+
const hideMissing = missing === 'hide';
|
|
83
|
+
const imgSrc = source.imgSrc;
|
|
84
|
+
|
|
85
|
+
const failed =
|
|
86
|
+
fileFailed.failed && fileFailed.key === (imgSrc ?? source.sourceKey);
|
|
87
|
+
|
|
88
|
+
const boxStyle = { width: size, height: size };
|
|
89
|
+
const pendingBox = (
|
|
90
|
+
<Skeleton
|
|
91
|
+
className="inline-block shrink-0 rounded-sm"
|
|
92
|
+
style={boxStyle}
|
|
93
|
+
/>
|
|
94
|
+
);
|
|
95
|
+
const missingBox = (
|
|
96
|
+
<span
|
|
97
|
+
className="font-mc inline-flex shrink-0 items-center justify-center rounded-sm border border-border bg-secondary text-muted-foreground"
|
|
98
|
+
style={{ ...boxStyle, fontSize: Math.max(7, Math.round(size * 0.6)) }}
|
|
99
|
+
>
|
|
100
|
+
?
|
|
101
|
+
</span>
|
|
102
|
+
);
|
|
103
|
+
const img = (src: string) => (
|
|
104
|
+
<Sprite
|
|
105
|
+
src={src}
|
|
106
|
+
alt={name}
|
|
107
|
+
size={size}
|
|
108
|
+
onError={() =>
|
|
109
|
+
setFileFailed({ key: imgSrc ?? source.sourceKey, failed: true })
|
|
110
|
+
}
|
|
111
|
+
/>
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
let icon: React.ReactNode = null;
|
|
115
|
+
if (source.cycleRef && dataset) {
|
|
116
|
+
icon = (
|
|
117
|
+
<EntryIcon
|
|
118
|
+
reference={source.cycleRef}
|
|
119
|
+
dataset={dataset}
|
|
120
|
+
size={size}
|
|
121
|
+
missing={missing}
|
|
122
|
+
/>
|
|
123
|
+
);
|
|
124
|
+
} else if (source.isEntity && imgSrc && !failed) {
|
|
125
|
+
icon = img(imgSrc);
|
|
126
|
+
} else if (source.isEntity && source.awaitingTexture) {
|
|
127
|
+
icon = hideMissing ? null : pendingBox;
|
|
128
|
+
} else if (source.lookup && dataset) {
|
|
129
|
+
icon = <AtlasSprite dataset={dataset} lookup={source.lookup} size={size} />;
|
|
130
|
+
} else if (source.pending || source.awaitingAtlas) {
|
|
131
|
+
icon = hideMissing ? null : pendingBox;
|
|
132
|
+
} else if (source.awaitingTexture) {
|
|
133
|
+
icon = hideMissing ? null : pendingBox;
|
|
134
|
+
} else if (source.usesAtlas || !imgSrc || failed) {
|
|
135
|
+
icon = hideMissing
|
|
136
|
+
? null
|
|
137
|
+
: missing === 'item' && source.missingLookup && dataset
|
|
138
|
+
? (
|
|
139
|
+
<AtlasSprite
|
|
140
|
+
dataset={dataset}
|
|
141
|
+
lookup={source.missingLookup}
|
|
142
|
+
size={size}
|
|
143
|
+
/>
|
|
144
|
+
)
|
|
145
|
+
: missing === 'item' && source.missingPending
|
|
146
|
+
? pendingBox
|
|
147
|
+
: missingBox;
|
|
148
|
+
} else {
|
|
149
|
+
icon = img(imgSrc);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (!icon && !labeled && !children) return null;
|
|
153
|
+
|
|
154
|
+
const framed = (
|
|
155
|
+
<SpriteFrame
|
|
156
|
+
size={size}
|
|
157
|
+
chip={chip}
|
|
158
|
+
badge={badge}
|
|
159
|
+
badgeClassName={badgeClassName}
|
|
160
|
+
>
|
|
161
|
+
{icon}
|
|
162
|
+
</SpriteFrame>
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
if (!labeled && !children) return framed;
|
|
166
|
+
|
|
167
|
+
return (
|
|
168
|
+
<span className="inline-flex items-center gap-2 align-middle not-prose">
|
|
169
|
+
{framed}
|
|
170
|
+
{labeled ? (
|
|
171
|
+
caption && caption !== source.rawId ? (
|
|
172
|
+
<span className="text-xs">{caption}</span>
|
|
173
|
+
) : (
|
|
174
|
+
<code className="text-xs">{source.rawId}</code>
|
|
175
|
+
)
|
|
176
|
+
) : null}
|
|
177
|
+
{children}
|
|
178
|
+
</span>
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import type { DatasetRef } from '@terrafirmagreg-team/sites/location';
|
|
3
|
+
|
|
4
|
+
import { Entry } from '@terrafirmagreg-team/game/components/entry/entry';
|
|
5
|
+
import { SpriteList } from '@terrafirmagreg-team/ui/components/blocks/sprite';
|
|
6
|
+
import type { Lang } from '@terrafirmagreg-team/translate/types/locale';
|
|
7
|
+
import type { TaggedReference } from '@terrafirmagreg-team/dump/lib/dump/contract/ref';
|
|
8
|
+
|
|
9
|
+
export function EntryList({
|
|
10
|
+
items,
|
|
11
|
+
dataset,
|
|
12
|
+
lang,
|
|
13
|
+
langMap,
|
|
14
|
+
tab,
|
|
15
|
+
type,
|
|
16
|
+
variant = 'grid',
|
|
17
|
+
labeled,
|
|
18
|
+
href,
|
|
19
|
+
className,
|
|
20
|
+
}: {
|
|
21
|
+
items: TaggedReference[];
|
|
22
|
+
dataset?: DatasetRef;
|
|
23
|
+
lang?: Lang;
|
|
24
|
+
langMap?: Record<string, string>;
|
|
25
|
+
tab?: string;
|
|
26
|
+
type?: string;
|
|
27
|
+
variant?: 'grid' | 'row';
|
|
28
|
+
|
|
29
|
+
labeled?: boolean;
|
|
30
|
+
|
|
31
|
+
href?: string | false;
|
|
32
|
+
className?: string;
|
|
33
|
+
}) {
|
|
34
|
+
return (
|
|
35
|
+
<SpriteList
|
|
36
|
+
variant={variant}
|
|
37
|
+
className={className}
|
|
38
|
+
items={items.map((item, index) => ({
|
|
39
|
+
key: `${item.kind}:${item.id}:${index}`,
|
|
40
|
+
node:
|
|
41
|
+
variant === 'grid' ? (
|
|
42
|
+
<Entry
|
|
43
|
+
reference={item}
|
|
44
|
+
dataset={dataset}
|
|
45
|
+
lang={lang}
|
|
46
|
+
langMap={langMap}
|
|
47
|
+
tab={tab}
|
|
48
|
+
type={type}
|
|
49
|
+
href={href}
|
|
50
|
+
variant="chip"
|
|
51
|
+
size="lg"
|
|
52
|
+
className="size-9"
|
|
53
|
+
/>
|
|
54
|
+
) : (
|
|
55
|
+
<Entry
|
|
56
|
+
reference={item}
|
|
57
|
+
dataset={dataset}
|
|
58
|
+
langMap={langMap}
|
|
59
|
+
labeled={labeled}
|
|
60
|
+
href={href}
|
|
61
|
+
/>
|
|
62
|
+
),
|
|
63
|
+
}))}
|
|
64
|
+
/>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { FormatText } from '@terrafirmagreg-team/ui/components/blocks/text/format-text';
|
|
2
|
+
import { MC_FORMAT } from '@terrafirmagreg-team/dump/lib/dump/contract/text';
|
|
3
|
+
|
|
4
|
+
export function McText({ text }: { text: string }) {
|
|
5
|
+
return <FormatText text={text} format={MC_FORMAT} />;
|
|
6
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { SpriteCount } from '@terrafirmagreg-team/ui/components/blocks/sprite';
|
|
2
|
+
|
|
3
|
+
export function StackCount({
|
|
4
|
+
count,
|
|
5
|
+
limit,
|
|
6
|
+
}: {
|
|
7
|
+
count: number;
|
|
8
|
+
limit?: number;
|
|
9
|
+
}) {
|
|
10
|
+
return (
|
|
11
|
+
<SpriteCount
|
|
12
|
+
count={count}
|
|
13
|
+
limit={limit}
|
|
14
|
+
className="font-mc pointer-events-none leading-none text-white"
|
|
15
|
+
style={{ fontSize: 9, textShadow: '1px 1px 0 var(--color-mc-arrow)' }}
|
|
16
|
+
/>
|
|
17
|
+
);
|
|
18
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import type { DatasetRef } from '@terrafirmagreg-team/sites/location';
|
|
3
|
+
|
|
4
|
+
import { useState, type ReactElement, type ReactNode } from 'react';
|
|
5
|
+
import { useModifierKeys } from '@terrafirmagreg-team/ui/hooks/use-modifier-keys';
|
|
6
|
+
import { LazyTooltip } from '@terrafirmagreg-team/ui/components/blocks/tooltip/lazy';
|
|
7
|
+
import { stackTooltip } from '@terrafirmagreg-team/game/lib/entry/tooltip';
|
|
8
|
+
import { formulaRuns } from '@terrafirmagreg-team/game/lib/entry/formula';
|
|
9
|
+
import { McText } from '@terrafirmagreg-team/game/components/entry/mc-text';
|
|
10
|
+
import { stripMcFormat } from '@terrafirmagreg-team/dump/lib/dump/assets/lang/names';
|
|
11
|
+
|
|
12
|
+
const ENCHANTED_TEXT_COLOR = 'var(--color-mc-enchant)';
|
|
13
|
+
|
|
14
|
+
export type EntryTooltipLevel =
|
|
15
|
+
| 'compact'
|
|
16
|
+
| 'extended'
|
|
17
|
+
| 'full'
|
|
18
|
+
| 'adaptive'
|
|
19
|
+
| 'none';
|
|
20
|
+
|
|
21
|
+
function TooltipLine({ text }: { text: string }) {
|
|
22
|
+
const runs = formulaRuns(stripMcFormat(text));
|
|
23
|
+
if (!runs) return <McText text={text} />;
|
|
24
|
+
return (
|
|
25
|
+
<>
|
|
26
|
+
{runs.map((run, index) =>
|
|
27
|
+
run.color ? (
|
|
28
|
+
<span key={index} style={{ color: run.color }}>
|
|
29
|
+
{run.text}
|
|
30
|
+
</span>
|
|
31
|
+
) : (
|
|
32
|
+
run.text
|
|
33
|
+
),
|
|
34
|
+
)}
|
|
35
|
+
</>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function EntryTooltip({
|
|
40
|
+
dataset,
|
|
41
|
+
id,
|
|
42
|
+
name,
|
|
43
|
+
kind,
|
|
44
|
+
lang,
|
|
45
|
+
langMap,
|
|
46
|
+
level = 'full',
|
|
47
|
+
footer,
|
|
48
|
+
children,
|
|
49
|
+
}: {
|
|
50
|
+
dataset: DatasetRef;
|
|
51
|
+
id: string;
|
|
52
|
+
name: string;
|
|
53
|
+
kind?: 'item' | 'fluid' | 'entity';
|
|
54
|
+
|
|
55
|
+
lang?: string;
|
|
56
|
+
langMap?: Record<string, string>;
|
|
57
|
+
|
|
58
|
+
level?: EntryTooltipLevel;
|
|
59
|
+
footer?: ReactNode;
|
|
60
|
+
children: ReactElement;
|
|
61
|
+
}) {
|
|
62
|
+
const [open, setOpen] = useState(false);
|
|
63
|
+
const held = useModifierKeys(open && (level === 'full' || level === 'adaptive'));
|
|
64
|
+
|
|
65
|
+
const renderLines = (value?: {
|
|
66
|
+
lines: string[];
|
|
67
|
+
mods: Record<string, string[]>;
|
|
68
|
+
}) => {
|
|
69
|
+
const tooltip = value ?? { lines: [], mods: {} };
|
|
70
|
+
const base = tooltip.lines;
|
|
71
|
+
const modLine =
|
|
72
|
+
base.length > 1 && !base[base.length - 1].includes('§')
|
|
73
|
+
? base[base.length - 1]
|
|
74
|
+
: undefined;
|
|
75
|
+
|
|
76
|
+
let extra: string[];
|
|
77
|
+
if (level === 'compact') extra = modLine ? [modLine] : [];
|
|
78
|
+
else if (level === 'extended') extra = base;
|
|
79
|
+
else if (level === 'full') {
|
|
80
|
+
const activeMod = held.shift
|
|
81
|
+
? 'shift'
|
|
82
|
+
: held.ctrl
|
|
83
|
+
? 'ctrl'
|
|
84
|
+
: held.alt
|
|
85
|
+
? 'alt'
|
|
86
|
+
: null;
|
|
87
|
+
extra = (activeMod ? tooltip.mods[activeMod] : undefined) ?? base;
|
|
88
|
+
} else if (held.ctrl || held.alt)
|
|
89
|
+
extra = (held.ctrl ? tooltip.mods.ctrl : tooltip.mods.alt) ?? base;
|
|
90
|
+
else if (held.shift) extra = base;
|
|
91
|
+
else extra = modLine ? [modLine] : [];
|
|
92
|
+
|
|
93
|
+
const lines = extra.length
|
|
94
|
+
? [name, ...extra.filter((line) => line !== name)]
|
|
95
|
+
: [name];
|
|
96
|
+
const lastIsMod =
|
|
97
|
+
lines.length > 1 && !lines[lines.length - 1].includes('§');
|
|
98
|
+
|
|
99
|
+
return (
|
|
100
|
+
<>
|
|
101
|
+
{lines.map((line, index) => (
|
|
102
|
+
<p
|
|
103
|
+
key={`${index}:${line}`}
|
|
104
|
+
className={
|
|
105
|
+
index === 0
|
|
106
|
+
? 'font-medium'
|
|
107
|
+
: lastIsMod && index === lines.length - 1
|
|
108
|
+
? 'italic'
|
|
109
|
+
: 'text-muted-foreground'
|
|
110
|
+
}
|
|
111
|
+
style={
|
|
112
|
+
lastIsMod && index === lines.length - 1
|
|
113
|
+
? { color: ENCHANTED_TEXT_COLOR }
|
|
114
|
+
: undefined
|
|
115
|
+
}
|
|
116
|
+
>
|
|
117
|
+
<TooltipLine text={line} />
|
|
118
|
+
</p>
|
|
119
|
+
))}
|
|
120
|
+
{footer}
|
|
121
|
+
</>
|
|
122
|
+
);
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
return (
|
|
126
|
+
<LazyTooltip
|
|
127
|
+
load={() => stackTooltip(dataset, id, lang, langMap, kind)}
|
|
128
|
+
deps={[dataset, id, lang, langMap, kind]}
|
|
129
|
+
onOpenChange={setOpen}
|
|
130
|
+
trigger={children}
|
|
131
|
+
contentClassName="flex-col items-start gap-0.5 p-3 text-sm break-words whitespace-pre-wrap"
|
|
132
|
+
>
|
|
133
|
+
{renderLines}
|
|
134
|
+
</LazyTooltip>
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { usePathname } from 'next/navigation';
|
|
4
|
+
import type { ComponentProps } from 'react';
|
|
5
|
+
import { Entry } from '@terrafirmagreg-team/game/components/entry/entry';
|
|
6
|
+
import { useEntryContext } from '@terrafirmagreg-team/game/hooks/use-entry-context';
|
|
7
|
+
import { useLangMap } from '@terrafirmagreg-team/game/hooks/use-lang-map';
|
|
8
|
+
import { useResolvedLang } from '@terrafirmagreg-team/translate/hooks/use-resolved-lang';
|
|
9
|
+
import { useAsyncValue } from '@terrafirmagreg-team/ui/hooks/use-async-value';
|
|
10
|
+
import {
|
|
11
|
+
type DatasetRef,
|
|
12
|
+
parseSectionLocation,
|
|
13
|
+
} from '@terrafirmagreg-team/sites/location';
|
|
14
|
+
import { lookupSpriteInBundle } from '@terrafirmagreg-team/dump/lib/dump/assets/atlas/lookup';
|
|
15
|
+
import { stackDisplayName } from '@terrafirmagreg-team/dump/lib/dump/assets/lang/names';
|
|
16
|
+
import { loadClientModAtlases } from '@terrafirmagreg-team/dump/lib/dump/client/atlases';
|
|
17
|
+
import { fetchTagItem } from '@terrafirmagreg-team/game/lib/entry/icon-load';
|
|
18
|
+
import type { Lang } from '@terrafirmagreg-team/translate/types/locale';
|
|
19
|
+
|
|
20
|
+
export type ItemIconProps = {
|
|
21
|
+
id?: string;
|
|
22
|
+
dataset?: DatasetRef;
|
|
23
|
+
lang?: Lang;
|
|
24
|
+
label?: string;
|
|
25
|
+
children?: React.ReactNode;
|
|
26
|
+
} & ComponentProps<'div'>;
|
|
27
|
+
|
|
28
|
+
export function ItemIcon({
|
|
29
|
+
id,
|
|
30
|
+
dataset: datasetProp,
|
|
31
|
+
lang: langProp,
|
|
32
|
+
label,
|
|
33
|
+
children,
|
|
34
|
+
}: ItemIconProps) {
|
|
35
|
+
const ctx = useEntryContext();
|
|
36
|
+
const pathname = usePathname();
|
|
37
|
+
const locId = parseSectionLocation(pathname).dataset;
|
|
38
|
+
const dataset =
|
|
39
|
+
datasetProp ?? ctx?.dataset ?? (locId ? { id: locId } : undefined);
|
|
40
|
+
const resolvedLang = useResolvedLang();
|
|
41
|
+
const lang = langProp ?? ctx?.lang ?? resolvedLang;
|
|
42
|
+
const iconId = useAsyncValue(
|
|
43
|
+
dataset && id?.startsWith('#')
|
|
44
|
+
? () => fetchTagItem(dataset, id, 'item').then((item) => item ?? id)
|
|
45
|
+
: false,
|
|
46
|
+
[dataset, id],
|
|
47
|
+
) ?? id;
|
|
48
|
+
const langKey = useAsyncValue(
|
|
49
|
+
dataset && iconId?.includes(':') && !iconId.startsWith('#')
|
|
50
|
+
? () =>
|
|
51
|
+
loadClientModAtlases(dataset, iconId.split(':', 2)[0]!).then(
|
|
52
|
+
(bundle) => lookupSpriteInBundle(bundle, iconId)?.sprite.langKey,
|
|
53
|
+
)
|
|
54
|
+
: false,
|
|
55
|
+
[dataset, iconId],
|
|
56
|
+
);
|
|
57
|
+
const fetchedMap = useLangMap(dataset, lang, !ctx?.langMap);
|
|
58
|
+
const map = ctx?.langMap ?? fetchedMap ?? {};
|
|
59
|
+
const resolved =
|
|
60
|
+
label ?? (iconId ? stackDisplayName(map, iconId, undefined, langKey) : undefined);
|
|
61
|
+
if (!iconId) return children ? <>{children}</> : null;
|
|
62
|
+
return (
|
|
63
|
+
<Entry
|
|
64
|
+
kind="item"
|
|
65
|
+
id={iconId}
|
|
66
|
+
dataset={dataset}
|
|
67
|
+
lang={lang}
|
|
68
|
+
name={resolved ?? iconId}
|
|
69
|
+
labeled
|
|
70
|
+
label={resolved}
|
|
71
|
+
href={false}
|
|
72
|
+
>
|
|
73
|
+
{children}
|
|
74
|
+
</Entry>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { DynamicIcon, type IconName } from 'lucide-react/dynamic';
|
|
4
|
+
import { EntryIcon } from '@terrafirmagreg-team/game/components/entry/icon';
|
|
5
|
+
import type { ReferenceKind } from '@terrafirmagreg-team/dump/lib/dump/contract/ref';
|
|
6
|
+
import type { PreviewIcon } from '@terrafirmagreg-team/ui/types/preview';
|
|
7
|
+
|
|
8
|
+
export function PagePreviewIcon({ icon }: { icon: PreviewIcon }) {
|
|
9
|
+
if (icon.type === 'entry') {
|
|
10
|
+
return (
|
|
11
|
+
<EntryIcon
|
|
12
|
+
reference={{ kind: icon.kind as ReferenceKind, id: icon.id }}
|
|
13
|
+
dataset={icon.dataset}
|
|
14
|
+
size={16}
|
|
15
|
+
missing="placeholder"
|
|
16
|
+
/>
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
return (
|
|
20
|
+
<DynamicIcon
|
|
21
|
+
name={icon.name as IconName}
|
|
22
|
+
className="size-4 shrink-0"
|
|
23
|
+
/>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import type { DatasetRef } from '@terrafirmagreg-team/sites/location';
|
|
3
|
+
|
|
4
|
+
import { useSyncExternalStore } from 'react';
|
|
5
|
+
import {
|
|
6
|
+
dumpLocation,
|
|
7
|
+
subscribeDumpLocations,
|
|
8
|
+
type DumpLocation,
|
|
9
|
+
} from '@terrafirmagreg-team/dump/lib/dump/paths';
|
|
10
|
+
|
|
11
|
+
export function useDumpLocation(
|
|
12
|
+
dataset: DatasetRef | undefined,
|
|
13
|
+
): DumpLocation | undefined {
|
|
14
|
+
return useSyncExternalStore(
|
|
15
|
+
subscribeDumpLocations,
|
|
16
|
+
() => (dataset ? dumpLocation(dataset) : undefined),
|
|
17
|
+
() => undefined,
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import type { DatasetRef } from '@terrafirmagreg-team/sites/location';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
createContext,
|
|
6
|
+
createElement,
|
|
7
|
+
useContext,
|
|
8
|
+
useMemo,
|
|
9
|
+
type ReactNode,
|
|
10
|
+
} from 'react';
|
|
11
|
+
import type { Lang } from '@terrafirmagreg-team/translate/types/locale';
|
|
12
|
+
|
|
13
|
+
type EntryContextValue = {
|
|
14
|
+
dataset: DatasetRef;
|
|
15
|
+
lang?: Lang;
|
|
16
|
+
langMap?: Record<string, string>;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const EntryContext = createContext<EntryContextValue | null>(null);
|
|
20
|
+
|
|
21
|
+
export function EntryProvider({
|
|
22
|
+
dataset,
|
|
23
|
+
lang,
|
|
24
|
+
langMap,
|
|
25
|
+
children,
|
|
26
|
+
}: EntryContextValue & { children: ReactNode }) {
|
|
27
|
+
const value = useMemo(
|
|
28
|
+
() => ({ dataset, lang, langMap }),
|
|
29
|
+
[dataset, lang, langMap],
|
|
30
|
+
);
|
|
31
|
+
return createElement(EntryContext.Provider, { value }, children);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function useEntryContext(): EntryContextValue | null {
|
|
35
|
+
return useContext(EntryContext);
|
|
36
|
+
}
|
|
37
|
+
|