@terrafirmagreg-team/recipe 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 (231) hide show
  1. package/package.json +62 -3
  2. package/src/components/button-copy.tsx +42 -0
  3. package/src/components/cards/card/index.tsx +212 -0
  4. package/src/components/cards/card/model.ts +64 -0
  5. package/src/components/cards/card/scene.tsx +105 -0
  6. package/src/components/cards/card/strip.tsx +196 -0
  7. package/src/components/cards/card/text.tsx +45 -0
  8. package/src/components/cards/card/tips.tsx +62 -0
  9. package/src/components/cards/default-item.tsx +43 -0
  10. package/src/components/cards/fit-card.tsx +26 -0
  11. package/src/components/cards/glare-filter.tsx +28 -0
  12. package/src/components/cards/paged-card.tsx +221 -0
  13. package/src/components/cards/slot-hitbox.tsx +211 -0
  14. package/src/components/cards/strip-card/buttons.tsx +101 -0
  15. package/src/components/cards/strip-card/index.tsx +212 -0
  16. package/src/components/cards/strip-card/member-cell.tsx +165 -0
  17. package/src/components/cards/strip-card/sprite.tsx +35 -0
  18. package/src/components/mdx/recipe.tsx +111 -0
  19. package/src/components/tool-list.tsx +48 -0
  20. package/src/components/views/alloy/index.tsx +105 -0
  21. package/src/components/views/alloy/list.tsx +84 -0
  22. package/src/components/views/alloy/mix/index.tsx +34 -0
  23. package/src/components/views/alloy/mix/ratio.tsx +93 -0
  24. package/src/components/views/alloy/mix/rows.tsx +146 -0
  25. package/src/components/views/alloy/mix/status.tsx +121 -0
  26. package/src/components/views/alloy/mix/total.tsx +131 -0
  27. package/src/components/views/alloy/range-bar.tsx +64 -0
  28. package/src/components/views/alloy/stack-bar.tsx +86 -0
  29. package/src/components/views/browser/axis-switcher.tsx +53 -0
  30. package/src/components/views/browser/card-grid.tsx +274 -0
  31. package/src/components/views/browser/favorite-button.tsx +45 -0
  32. package/src/components/views/browser/frame.tsx +111 -0
  33. package/src/components/views/browser/index.tsx +96 -0
  34. package/src/components/views/browser/item-panel/index.tsx +227 -0
  35. package/src/components/views/browser/item-panel/skeleton.tsx +25 -0
  36. package/src/components/views/browser/item-panel-grid/cell.tsx +176 -0
  37. package/src/components/views/browser/item-panel-grid/favorite-cell.tsx +127 -0
  38. package/src/components/views/browser/item-panel-grid/index.tsx +103 -0
  39. package/src/components/views/browser/item-view/index.tsx +191 -0
  40. package/src/components/views/browser/item-view/tags.tsx +83 -0
  41. package/src/components/views/browser/item-view-header.tsx +130 -0
  42. package/src/components/views/browser/pane-tabs.tsx +60 -0
  43. package/src/components/views/browser/place-header.tsx +71 -0
  44. package/src/components/views/browser/query-controls.tsx +103 -0
  45. package/src/components/views/browser/recipe-item-header.tsx +59 -0
  46. package/src/components/views/browser/settings.tsx +51 -0
  47. package/src/components/views/browser/toolbar.tsx +53 -0
  48. package/src/components/views/browser/type-tabs.tsx +69 -0
  49. package/src/components/views/browser/type-view.tsx +189 -0
  50. package/src/components/views/browser/workstation-rail.tsx +42 -0
  51. package/src/components/views/calc/calculator/actions.tsx +57 -0
  52. package/src/components/views/calc/calculator/help.tsx +148 -0
  53. package/src/components/views/calc/calculator/index.tsx +220 -0
  54. package/src/components/views/calc/dot-overlay.tsx +79 -0
  55. package/src/components/views/calc/edge.tsx +50 -0
  56. package/src/components/views/calc/flow/index.tsx +267 -0
  57. package/src/components/views/calc/flow/picker-dialogs.tsx +68 -0
  58. package/src/components/views/calc/flow/toolbar.tsx +72 -0
  59. package/src/components/views/calc/flow/view-menu.tsx +221 -0
  60. package/src/components/views/calc/member-picker.tsx +118 -0
  61. package/src/components/views/calc/node-card/actions.tsx +168 -0
  62. package/src/components/views/calc/node-card/badges.tsx +77 -0
  63. package/src/components/views/calc/node-card/chips.tsx +219 -0
  64. package/src/components/views/calc/node-card/far.tsx +175 -0
  65. package/src/components/views/calc/node-card/full.tsx +282 -0
  66. package/src/components/views/calc/node-card/index.tsx +26 -0
  67. package/src/components/views/calc/node-card/menu.tsx +211 -0
  68. package/src/components/views/calc/recipe-picker/body.tsx +204 -0
  69. package/src/components/views/calc/recipe-picker/footer.tsx +129 -0
  70. package/src/components/views/calc/recipe-picker/index.tsx +47 -0
  71. package/src/components/views/calc/sidebar.tsx +162 -0
  72. package/src/components/views/calc/target.tsx +271 -0
  73. package/src/components/views/calc/trees/icon-picker.tsx +96 -0
  74. package/src/components/views/calc/trees/index.tsx +133 -0
  75. package/src/components/views/calc/trees/label.tsx +65 -0
  76. package/src/components/views/calc/trees/rows.tsx +139 -0
  77. package/src/components/views/forge/gui.tsx +130 -0
  78. package/src/components/views/forge/index.tsx +105 -0
  79. package/src/components/views/forge/list.tsx +112 -0
  80. package/src/components/views/forge/workbench/controls.tsx +96 -0
  81. package/src/components/views/forge/workbench/index.tsx +84 -0
  82. package/src/components/views/forge/workbench/panel.tsx +227 -0
  83. package/src/components/views/forge/workbench/ruler.tsx +132 -0
  84. package/src/components/views/forge/workbench/status.tsx +54 -0
  85. package/src/components/views/forge/workbench/tape.tsx +69 -0
  86. package/src/hooks/alloy/use-fluid-colors.ts +122 -0
  87. package/src/hooks/alloy/use-mix-state.ts +239 -0
  88. package/src/hooks/calc/flow/engage.ts +89 -0
  89. package/src/hooks/calc/flow/handlers.ts +115 -0
  90. package/src/hooks/calc/flow/layout.ts +149 -0
  91. package/src/hooks/calc/flow/mount.ts +96 -0
  92. package/src/hooks/calc/flow/nodes.ts +251 -0
  93. package/src/hooks/calc/flow/pickers.ts +50 -0
  94. package/src/hooks/calc/flow/props.ts +40 -0
  95. package/src/hooks/calc/flow/viewport.ts +154 -0
  96. package/src/hooks/calc/flow/window.ts +86 -0
  97. package/src/hooks/calc/use-actions.ts +376 -0
  98. package/src/hooks/calc/use-amounts.ts +101 -0
  99. package/src/hooks/calc/use-calc-fill.ts +91 -0
  100. package/src/hooks/calc/use-data.ts +289 -0
  101. package/src/hooks/calc/use-defaults.ts +38 -0
  102. package/src/hooks/calc/use-density.ts +28 -0
  103. package/src/hooks/calc/use-derived.ts +258 -0
  104. package/src/hooks/calc/use-direction.ts +23 -0
  105. package/src/hooks/calc/use-edge-style.ts +40 -0
  106. package/src/hooks/calc/use-far-mode.ts +36 -0
  107. package/src/hooks/calc/use-fill.ts +213 -0
  108. package/src/hooks/calc/use-focus.ts +30 -0
  109. package/src/hooks/calc/use-maps.ts +75 -0
  110. package/src/hooks/calc/use-reset.ts +14 -0
  111. package/src/hooks/calc/use-resolver.ts +234 -0
  112. package/src/hooks/calc/use-session.ts +12 -0
  113. package/src/hooks/calc/use-share.ts +61 -0
  114. package/src/hooks/calc/use-storage.ts +125 -0
  115. package/src/hooks/calc/use-tier.ts +12 -0
  116. package/src/hooks/calc/use-view-mode.ts +23 -0
  117. package/src/hooks/calc/use-worker.ts +269 -0
  118. package/src/hooks/forge/use-anvil-gui.ts +14 -0
  119. package/src/hooks/forge/use-forge-state.ts +134 -0
  120. package/src/hooks/forge/use-ruler.ts +99 -0
  121. package/src/hooks/use-add-to-calc.ts +47 -0
  122. package/src/hooks/use-favorite-hotkey.ts +38 -0
  123. package/src/hooks/use-favorites.ts +134 -0
  124. package/src/hooks/use-group-outlines.ts +64 -0
  125. package/src/hooks/use-hide-unused.ts +11 -0
  126. package/src/hooks/use-index.ts +68 -0
  127. package/src/hooks/use-item-feeds.ts +44 -0
  128. package/src/hooks/use-item-pane.ts +104 -0
  129. package/src/hooks/use-item-press.ts +85 -0
  130. package/src/hooks/use-item-tag-groups.ts +52 -0
  131. package/src/hooks/use-item-tooltips.ts +16 -0
  132. package/src/hooks/use-left-sidebar-tree.ts +26 -0
  133. package/src/hooks/use-mod-names.ts +15 -0
  134. package/src/hooks/use-mount-limit.ts +21 -0
  135. package/src/hooks/use-panel-cells.ts +115 -0
  136. package/src/hooks/use-panel-page.ts +51 -0
  137. package/src/hooks/use-panel-tabs.ts +42 -0
  138. package/src/hooks/use-query.ts +184 -0
  139. package/src/hooks/use-session.ts +84 -0
  140. package/src/hooks/use-sidebar-group.ts +20 -0
  141. package/src/hooks/use-tag-members.ts +14 -0
  142. package/src/hooks/use-tool-recipes.ts +25 -0
  143. package/src/hooks/use-type-filter.ts +72 -0
  144. package/src/lib/alloy/alloy.test.ts +186 -0
  145. package/src/lib/alloy/mix.ts +209 -0
  146. package/src/lib/alloy/parse.ts +36 -0
  147. package/src/lib/calc/defaults.ts +45 -0
  148. package/src/lib/calc/flow.ts +279 -0
  149. package/src/lib/calc/focus.ts +14 -0
  150. package/src/lib/calc/meta.ts +15 -0
  151. package/src/lib/calc/pack.ts +184 -0
  152. package/src/lib/calc/resolve.ts +230 -0
  153. package/src/lib/calc/route/buckets.ts +48 -0
  154. package/src/lib/calc/route/geometry.ts +62 -0
  155. package/src/lib/calc/route/index.ts +2 -0
  156. package/src/lib/calc/route/polyline.ts +103 -0
  157. package/src/lib/calc/route/router.ts +270 -0
  158. package/src/lib/calc/storage/entry.ts +89 -0
  159. package/src/lib/calc/storage/index.ts +18 -0
  160. package/src/lib/calc/storage/keys.ts +42 -0
  161. package/src/lib/calc/storage/store.ts +223 -0
  162. package/src/lib/calc/tree/build.ts +196 -0
  163. package/src/lib/calc/tree/context.ts +42 -0
  164. package/src/lib/calc/tree/goal.ts +64 -0
  165. package/src/lib/calc/tree/ideal.ts +36 -0
  166. package/src/lib/calc/tree/index.ts +18 -0
  167. package/src/lib/calc/tree/materials.ts +28 -0
  168. package/src/lib/calc/tree/pick.ts +68 -0
  169. package/src/lib/calc/tree/recipe.ts +103 -0
  170. package/src/lib/calc/tree/share.ts +198 -0
  171. package/src/lib/calc/tree/tree.test.ts +169 -0
  172. package/src/lib/calc/worker/api.ts +33 -0
  173. package/src/lib/calc/worker/fetch.ts +65 -0
  174. package/src/lib/calc/worker/fill.ts +123 -0
  175. package/src/lib/calc/worker/flush.ts +52 -0
  176. package/src/lib/calc/worker/index.ts +8 -0
  177. package/src/lib/calc/worker/protocol.ts +32 -0
  178. package/src/lib/calc/worker/resolve.ts +101 -0
  179. package/src/lib/calc/worker/state.ts +49 -0
  180. package/src/lib/client/recipe-categories.ts +96 -0
  181. package/src/lib/client/recipe.ts +83 -0
  182. package/src/lib/client-index/cache.ts +13 -0
  183. package/src/lib/client-index/catalog.ts +85 -0
  184. package/src/lib/client-index/chrome.ts +180 -0
  185. package/src/lib/client-index/index.ts +19 -0
  186. package/src/lib/client-index/registry.ts +110 -0
  187. package/src/lib/client-index/rows.ts +254 -0
  188. package/src/lib/client-index/tooltips.ts +80 -0
  189. package/src/lib/config.ts +17 -0
  190. package/src/lib/data/category/dto.ts +24 -0
  191. package/src/lib/data/category/load.ts +46 -0
  192. package/src/lib/data/category/parse.ts +70 -0
  193. package/src/lib/data/dto.ts +151 -0
  194. package/src/lib/data/grid.ts +39 -0
  195. package/src/lib/data/parse/entry.ts +84 -0
  196. package/src/lib/data/parse/index.ts +2 -0
  197. package/src/lib/data/parse/layout.ts +135 -0
  198. package/src/lib/data/parse/slots.ts +84 -0
  199. package/src/lib/data/parse/widgets.ts +80 -0
  200. package/src/lib/data/scene.ts +103 -0
  201. package/src/lib/data/slot-io.ts +43 -0
  202. package/src/lib/dump.ts +5 -0
  203. package/src/lib/favorite-hover.ts +12 -0
  204. package/src/lib/fluid-outputs.ts +72 -0
  205. package/src/lib/forge/actions.ts +58 -0
  206. package/src/lib/forge/data.ts +50 -0
  207. package/src/lib/forge/forge.test.ts +123 -0
  208. package/src/lib/forge/solve.ts +79 -0
  209. package/src/lib/group.ts +22 -0
  210. package/src/lib/index-store.ts +64 -0
  211. package/src/lib/links.ts +125 -0
  212. package/src/lib/lookup.ts +117 -0
  213. package/src/lib/panel-outline.ts +85 -0
  214. package/src/lib/panel.ts +104 -0
  215. package/src/lib/preview.ts +28 -0
  216. package/src/lib/recipe-slots.ts +7 -0
  217. package/src/lib/search.ts +138 -0
  218. package/src/lib/sidebar.ts +46 -0
  219. package/src/lib/tree.ts +90 -0
  220. package/src/lib/used-items.ts +24 -0
  221. package/src/locales/en_us.json +228 -0
  222. package/src/locales/ru_ru.json +228 -0
  223. package/src/locales.ts +5 -0
  224. package/src/types/alloy.ts +19 -0
  225. package/src/types/calc.ts +95 -0
  226. package/src/types/forge.ts +20 -0
  227. package/src/types/index.ts +134 -0
  228. package/styles/calc.css +47 -0
  229. package/styles/strip.css +7 -0
  230. package/styles.css +2 -0
  231. package/README.md +0 -3
@@ -0,0 +1,101 @@
1
+ 'use client';
2
+
3
+ import { useState } from 'react';
4
+ import { Check, Copy } from 'lucide-react';
5
+ import { useCopyButton } from '@terrafirmagreg-team/ui/hooks/use-copy-button';
6
+ import { useTranslations } from '@terrafirmagreg-team/translate/hooks/use-translations';
7
+
8
+ export function StripCardTagPager({
9
+ left,
10
+ top,
11
+ dir,
12
+ scale,
13
+ disabled,
14
+ onClick,
15
+ }: {
16
+ left: number;
17
+ top: number;
18
+ dir: -1 | 1;
19
+ scale: number;
20
+ disabled: boolean;
21
+ onClick: () => void;
22
+ }) {
23
+ const t = useTranslations();
24
+ const [hover, setHover] = useState(false);
25
+ const arrow = `${3.5 * scale}px solid ${disabled ? 'var(--color-mc-inset)' : hover ? 'var(--color-mc-arrow-dark)' : 'var(--color-mc-arrow)'}`;
26
+ return (
27
+ <button
28
+ type="button"
29
+ aria-label={t(dir < 0 ? 'ui.prev_page' : 'ui.next_page')}
30
+ disabled={disabled}
31
+ onClick={onClick}
32
+ onMouseEnter={() => setHover(true)}
33
+ onMouseLeave={() => setHover(false)}
34
+ onFocus={() => setHover(true)}
35
+ onBlur={() => setHover(false)}
36
+ className="absolute flex cursor-pointer items-center justify-center border-0 p-0 outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-default"
37
+ style={{
38
+ left,
39
+ top,
40
+ width: 12 * scale,
41
+ height: 12 * scale,
42
+ background: disabled ? 'var(--color-mc-dim)' : hover ? 'var(--color-mc-bright)' : 'var(--color-mc-panel)',
43
+ boxShadow: `inset ${scale}px ${scale}px 0 rgba(255,255,255,0.65), inset -${scale}px -${scale}px 0 rgba(0,0,0,0.45)`,
44
+ }}
45
+ >
46
+ <span
47
+ aria-hidden
48
+ className="block"
49
+ style={{
50
+ width: 0,
51
+ height: 0,
52
+ borderTop: `${2.5 * scale}px solid transparent`,
53
+ borderBottom: `${2.5 * scale}px solid transparent`,
54
+ ...(dir < 0 ? { borderRight: arrow } : { borderLeft: arrow }),
55
+ }}
56
+ />
57
+ </button>
58
+ );
59
+ }
60
+
61
+ export function StripCardTagCopy({
62
+ left,
63
+ top,
64
+ scale,
65
+ value,
66
+ }: {
67
+ left: number;
68
+ top: number;
69
+ scale: number;
70
+ value: string;
71
+ }) {
72
+ const t = useTranslations();
73
+ const [hover, setHover] = useState(false);
74
+ const { checked: done, onClick } = useCopyButton(() =>
75
+ navigator.clipboard.writeText(value),
76
+ );
77
+ const Icon = done ? Check : Copy;
78
+ return (
79
+ <button
80
+ type="button"
81
+ aria-label={t('recipe.copy_recipe_id')}
82
+ onClick={onClick}
83
+ onMouseEnter={() => setHover(true)}
84
+ onMouseLeave={() => setHover(false)}
85
+ onFocus={() => setHover(true)}
86
+ onBlur={() => setHover(false)}
87
+ className="absolute flex cursor-pointer items-center justify-center border-0 p-0 outline-none focus-visible:ring-1 focus-visible:ring-ring"
88
+ style={{
89
+ left,
90
+ top,
91
+ width: 12 * scale,
92
+ height: 12 * scale,
93
+ background: done ? 'var(--color-mc-bright)' : hover ? 'var(--color-mc-bright)' : 'var(--color-mc-panel)',
94
+ boxShadow: `inset ${scale}px ${scale}px 0 rgba(255,255,255,0.65), inset -${scale}px -${scale}px 0 rgba(0,0,0,0.45)`,
95
+ color: done ? 'var(--color-mc-done)' : hover ? 'var(--color-mc-arrow-dark)' : 'var(--color-mc-arrow)',
96
+ }}
97
+ >
98
+ <Icon width={7 * scale} height={7 * scale} strokeWidth={2.5} />
99
+ </button>
100
+ );
101
+ }
@@ -0,0 +1,212 @@
1
+ 'use client';
2
+
3
+ import { Fragment, useRef, useState } from 'react';
4
+ import { SlotHitbox } from '@terrafirmagreg-team/recipe/components/cards/slot-hitbox';
5
+ import { recipeGlareFilter } from '@terrafirmagreg-team/recipe/components/cards/glare-filter';
6
+ import { StripCardSprite } from '@terrafirmagreg-team/recipe/components/cards/strip-card/sprite';
7
+ import {
8
+ StripCardTagCopy,
9
+ StripCardTagPager,
10
+ } from '@terrafirmagreg-team/recipe/components/cards/strip-card/buttons';
11
+ import { StripCardMember } from '@terrafirmagreg-team/recipe/components/cards/strip-card/member-cell';
12
+ import { recipeGrid } from '@terrafirmagreg-team/recipe/lib/data/grid';
13
+ import { recipeSlots } from '@terrafirmagreg-team/recipe/lib/recipe-slots';
14
+ import { cn } from '@terrafirmagreg-team/ui/lib/utils/cn';
15
+ import { useInView } from '@terrafirmagreg-team/ui/hooks/use-in-view';
16
+ import { Hint } from '@terrafirmagreg-team/ui/components/blocks/tooltip/hint';
17
+ import type { EntryTooltipLevel } from '@terrafirmagreg-team/game/components/entry/tooltip';
18
+ import type { Recipe } from '@terrafirmagreg-team/recipe/lib/data/dto';
19
+ import type { RecipeHrefOpts } from '@terrafirmagreg-team/recipe/types';
20
+
21
+ const SCALE = 2;
22
+
23
+ export function StripCard({
24
+ recipe,
25
+ dataset,
26
+ lang,
27
+ langMap,
28
+ nav,
29
+ title,
30
+ src,
31
+ onFail,
32
+ naturalW,
33
+ interactive = true,
34
+ slotTooltip,
35
+ eager,
36
+ frameOnly,
37
+ }: {
38
+ recipe: Recipe;
39
+ dataset: string;
40
+ lang: string;
41
+ langMap?: Record<string, string>;
42
+ nav?: Pick<RecipeHrefOpts, 'tab' | 'search'>;
43
+ title: string;
44
+ src: string;
45
+ onFail: () => void;
46
+ naturalW?: number;
47
+ naturalH?: number;
48
+ interactive?: boolean;
49
+ slotTooltip?: EntryTooltipLevel;
50
+ eager?: boolean;
51
+
52
+ frameOnly?: boolean;
53
+ }) {
54
+ const scale = recipe.scale ?? SCALE;
55
+ const margin = recipe.margin ?? 0;
56
+ const info = recipeGrid(recipe.properties);
57
+ const rootRef = useRef<HTMLDivElement>(null);
58
+ const observedInView = useInView(rootRef);
59
+ const inView = eager || observedInView;
60
+ const [page, setPage] = useState(0);
61
+ const [measured, setMeasured] = useState<{ src: string; h: number }>();
62
+ if (!info) return null;
63
+ const { grid, members } = info;
64
+ const pageSize = grid.cols * grid.rows;
65
+ const pages = Math.max(1, Math.ceil(members.length / pageSize));
66
+ const clamped = Math.min(page, pages - 1);
67
+ const shown = members.slice(clamped * pageSize, (clamped + 1) * pageSize);
68
+ const shownRows = Math.max(1, Math.ceil(shown.length / grid.cols));
69
+
70
+ const imgW = recipe.w ?? naturalW ?? 0;
71
+ const imgH = measured?.src === src ? measured.h : 0;
72
+ const topH = (grid.y + margin) * scale;
73
+ const rowH = grid.row * scale;
74
+ const bottomH = (grid.tail + margin) * scale;
75
+ const rowSrc = (margin + grid.y) * scale;
76
+ const bottomSrc = Math.max(0, imgH - bottomH);
77
+ const edgeW = margin * scale;
78
+ const guiW = imgW / scale - margin * 2;
79
+ const buttonTop = margin * scale;
80
+
81
+ return (
82
+ <div
83
+ ref={rootRef}
84
+ className={cn(
85
+ 'relative w-fit overflow-hidden rounded-sm',
86
+
87
+ measured?.src === src && 'bg-mc-panel',
88
+ )}
89
+ style={{
90
+ width: imgW,
91
+ height: topH + shownRows * rowH + bottomH,
92
+ }}
93
+ >
94
+ {/* eslint-disable-next-line @next/next/no-img-element -- dump asset */}
95
+ <img
96
+ src={src}
97
+ alt=""
98
+ aria-hidden
99
+ className="absolute h-0 w-0 opacity-0"
100
+ onError={onFail}
101
+ onLoad={(event) =>
102
+ setMeasured({ src, h: event.currentTarget.naturalHeight })
103
+ }
104
+ />
105
+ <div className="absolute inset-0" style={recipeGlareFilter}>
106
+ <Hint label={title}>
107
+ <StripCardSprite
108
+ src={src}
109
+ pos="0 0"
110
+ width={imgW}
111
+ height={topH}
112
+ className="absolute left-0 top-0"
113
+ />
114
+ </Hint>
115
+ {Array.from({ length: shownRows }, (_, rowIndex) => {
116
+ const cells = Math.min(
117
+ grid.cols,
118
+ shown.length - rowIndex * grid.cols,
119
+ );
120
+ return (
121
+ <Fragment key={rowIndex}>
122
+ <StripCardSprite
123
+ src={src}
124
+ pos={`0 -${rowSrc}px`}
125
+ width={edgeW + cells * rowH}
126
+ height={rowH}
127
+ className="absolute left-0"
128
+ style={{ top: topH + rowIndex * rowH }}
129
+ />
130
+ <StripCardSprite
131
+ src={src}
132
+ pos={`${edgeW - imgW}px -${rowSrc}px`}
133
+ width={edgeW}
134
+ height={rowH}
135
+ className="absolute right-0"
136
+ style={{ top: topH + rowIndex * rowH }}
137
+ />
138
+ </Fragment>
139
+ );
140
+ })}
141
+ {imgH > 0 && (
142
+ <StripCardSprite
143
+ src={src}
144
+ pos={`0 -${bottomSrc}px`}
145
+ width={imgW}
146
+ height={bottomH}
147
+ className="absolute bottom-0 left-0"
148
+ />
149
+ )}
150
+ </div>
151
+ {inView && !frameOnly
152
+ ? recipeSlots(recipe).map((widget, index) => (
153
+ <SlotHitbox
154
+ key={`${widget.id}:${index}`}
155
+ widget={widget}
156
+ dataset={dataset}
157
+ lang={lang}
158
+ langMap={langMap}
159
+ nav={nav}
160
+ scale={scale}
161
+ margin={margin}
162
+ interactive={interactive}
163
+ tooltipLevel={slotTooltip}
164
+ />
165
+ ))
166
+ : null}
167
+ {inView && !frameOnly
168
+ ? shown.map((member, index) => (
169
+ <StripCardMember
170
+ key={`${member.id}:${index}`}
171
+ member={member}
172
+ dataset={dataset}
173
+ lang={lang}
174
+ langMap={langMap}
175
+ nav={nav}
176
+ x={edgeW + (index % grid.cols) * rowH}
177
+ y={topH + Math.floor(index / grid.cols) * rowH}
178
+ pitch={rowH}
179
+ interactive={interactive}
180
+ tooltipLevel={slotTooltip}
181
+ />
182
+ ))
183
+ : null}
184
+ {pages > 1 && (
185
+ <>
186
+ <StripCardTagPager
187
+ left={(margin + 2) * scale}
188
+ top={buttonTop}
189
+ dir={-1}
190
+ scale={scale}
191
+ disabled={clamped <= 0}
192
+ onClick={() => setPage(clamped - 1)}
193
+ />
194
+ <StripCardTagPager
195
+ left={(margin + guiW - 14) * scale}
196
+ top={buttonTop}
197
+ dir={1}
198
+ scale={scale}
199
+ disabled={clamped + 1 >= pages}
200
+ onClick={() => setPage(clamped + 1)}
201
+ />
202
+ </>
203
+ )}
204
+ <StripCardTagCopy
205
+ left={(margin + 24) * scale}
206
+ top={margin * scale}
207
+ scale={scale}
208
+ value={recipe.id}
209
+ />
210
+ </div>
211
+ );
212
+ }
@@ -0,0 +1,165 @@
1
+ 'use client';
2
+
3
+ import Link from 'next/link';
4
+ import { EntryIcon } from '@terrafirmagreg-team/game/components/entry/icon';
5
+ import {
6
+ EntryTooltip,
7
+ type EntryTooltipLevel,
8
+ } from '@terrafirmagreg-team/game/components/entry/tooltip';
9
+ import { stackDisplayName } from '@terrafirmagreg-team/game/lib/entry/display';
10
+ import { entryHref } from '@terrafirmagreg-team/game/lib/entry/href';
11
+ import { loadStackDisplay } from '@terrafirmagreg-team/game/lib/entry/tooltip';
12
+ import { favoriteHover } from '@terrafirmagreg-team/recipe/lib/favorite-hover';
13
+ import { useAsyncResult } from '@terrafirmagreg-team/ui/hooks/use-async-value';
14
+ import type { ReferenceKind } from '@terrafirmagreg-team/game/lib/dump/contract/ref';
15
+ import type { RecipeGridMember } from '@terrafirmagreg-team/recipe/lib/data/dto';
16
+ import type { RecipeHrefOpts } from '@terrafirmagreg-team/recipe/types';
17
+
18
+ const MEMBER_KIND: Record<string, ReferenceKind> = {
19
+ fluid: 'fluid',
20
+ entity: 'entity',
21
+ block: 'block',
22
+ };
23
+
24
+ export function StripCardMember({
25
+ member,
26
+ dataset,
27
+ lang,
28
+ langMap,
29
+ nav,
30
+ x,
31
+ y,
32
+ pitch,
33
+ interactive = true,
34
+ tooltipLevel,
35
+ }: {
36
+ member: RecipeGridMember;
37
+ dataset: string;
38
+ lang: string;
39
+ langMap?: Record<string, string>;
40
+ nav?: Pick<RecipeHrefOpts, 'tab' | 'search'>;
41
+ x: number;
42
+ y: number;
43
+ pitch: number;
44
+ interactive?: boolean;
45
+ tooltipLevel?: EntryTooltipLevel;
46
+ }) {
47
+ const kind = MEMBER_KIND[member.kind ?? 'item'] ?? 'item';
48
+ const name = stackDisplayName(langMap, member.id);
49
+ const checkKind = kind === 'item' || kind === 'fluid' ? kind : undefined;
50
+ const stackPeek =
51
+ dataset && checkKind
52
+ ? loadStackDisplay.peek(dataset, member.id, checkKind)
53
+ : undefined;
54
+ const stackRes = useAsyncResult(
55
+ dataset && checkKind && stackPeek === undefined
56
+ ? async () => ({
57
+ key: member.id,
58
+ display: await loadStackDisplay(dataset, member.id, checkKind),
59
+ })
60
+ : false,
61
+ [dataset, member.id, checkKind, stackPeek],
62
+ );
63
+ const missingItem =
64
+ checkKind !== undefined &&
65
+ stackPeek === undefined &&
66
+ !stackRes.pending &&
67
+ stackRes.value?.key === member.id &&
68
+ stackRes.value.display === undefined;
69
+ const badges: string[] = [];
70
+ if (member.amount !== undefined && member.amount > 1) {
71
+ badges.push(`×${member.amount}`);
72
+ }
73
+ if (member.chance !== undefined && member.chance < 1) {
74
+ const percent = member.chance <= 1 ? member.chance * 100 : member.chance;
75
+ badges.push(`${Number(percent.toFixed(1))}%`);
76
+ }
77
+ const inner = (
78
+ <>
79
+ <span className="pointer-events-none flex size-full items-center justify-center">
80
+ <EntryIcon
81
+ id={member.id}
82
+ kind={kind}
83
+ dataset={dataset}
84
+ size={Math.max(8, pitch - 4)}
85
+ missing="placeholder"
86
+ />
87
+ </span>
88
+ {badges.length > 0 && (
89
+ <span
90
+ className="font-mc pointer-events-none absolute right-0 bottom-0 leading-none text-white [text-shadow:1px_1px_0_var(--color-mc-arrow)]"
91
+ style={{ fontSize: pitch / 4 }}
92
+ >
93
+ {badges.join(' ')}
94
+ </span>
95
+ )}
96
+ </>
97
+ );
98
+ const faceStyle = { left: x, top: y, width: pitch, height: pitch };
99
+ const hover = {
100
+ onMouseEnter: () => {
101
+ favoriteHover.slot =
102
+ kind === 'item' || kind === 'fluid'
103
+ ? {
104
+ kind: 'stack' as const,
105
+ id: member.id,
106
+ ...(kind === 'fluid' ? { ref: 'fluid' as const } : {}),
107
+ }
108
+ : undefined;
109
+ },
110
+ onMouseLeave: () => {
111
+ favoriteHover.slot = undefined;
112
+ },
113
+ };
114
+ if (!interactive) {
115
+ return (
116
+ <EntryTooltip
117
+ dataset={dataset}
118
+ id={member.id}
119
+ name={name}
120
+ kind={kind === 'fluid' ? 'fluid' : kind === 'entity' ? 'entity' : 'item'}
121
+ lang={lang}
122
+ langMap={langMap}
123
+ level={tooltipLevel}
124
+ >
125
+ <span
126
+ {...hover}
127
+ aria-label={name}
128
+ className="absolute block"
129
+ style={faceStyle}
130
+ >
131
+ {inner}
132
+ </span>
133
+ </EntryTooltip>
134
+ );
135
+ }
136
+ return (
137
+ <EntryTooltip
138
+ dataset={dataset}
139
+ id={member.id}
140
+ name={name}
141
+ kind={kind === 'fluid' ? 'fluid' : kind === 'entity' ? 'entity' : 'item'}
142
+ lang={lang}
143
+ langMap={langMap}
144
+ level={tooltipLevel}
145
+ >
146
+ <span
147
+ {...hover}
148
+ className="absolute block"
149
+ style={faceStyle}
150
+ aria-label={name}
151
+ >
152
+ {!missingItem ? (
153
+ <Link
154
+ href={entryHref(dataset, lang, { kind, id: member.id }, nav)}
155
+ className="absolute inset-0 block"
156
+ >
157
+ {inner}
158
+ </Link>
159
+ ) : (
160
+ <span className="absolute inset-0 block">{inner}</span>
161
+ )}
162
+ </span>
163
+ </EntryTooltip>
164
+ );
165
+ }
@@ -0,0 +1,35 @@
1
+ 'use client';
2
+
3
+ import type { CSSProperties } from 'react';
4
+
5
+ export function StripCardSprite({
6
+ src,
7
+ pos,
8
+ width,
9
+ height,
10
+ className,
11
+ style,
12
+ }: {
13
+ src: string;
14
+
15
+ pos: string;
16
+ width?: number;
17
+ height?: number;
18
+ className?: string;
19
+ style?: CSSProperties;
20
+ }) {
21
+ return (
22
+ <div
23
+ className={className}
24
+ style={{
25
+ ...(width !== undefined ? { width } : {}),
26
+ ...(height !== undefined ? { height } : {}),
27
+ backgroundImage: `url(${src})`,
28
+ backgroundPosition: pos,
29
+ backgroundRepeat: 'no-repeat',
30
+ imageRendering: 'pixelated',
31
+ ...style,
32
+ }}
33
+ />
34
+ );
35
+ }
@@ -0,0 +1,111 @@
1
+ 'use client';
2
+
3
+ import Link from 'next/link';
4
+ import { useState } from 'react';
5
+ import { PagedCard } from '@terrafirmagreg-team/recipe/components/cards/paged-card';
6
+ import { FitCard } from '@terrafirmagreg-team/recipe/components/cards/fit-card';
7
+ import { EmptyState } from '@terrafirmagreg-team/ui/components/blocks/page/empty-state';
8
+ import { Skeleton } from '@terrafirmagreg-team/ui/components/ui/skeleton';
9
+ import { useAsyncResult } from '@terrafirmagreg-team/ui/hooks/use-async-value';
10
+ import { useSectionCatalog } from '@terrafirmagreg-team/kit/hooks/use-catalog';
11
+ import { useCurrentDataset } from '@terrafirmagreg-team/kit/hooks/use-current-dataset';
12
+ import { useResolvedLang } from '@terrafirmagreg-team/translate/hooks/use-resolved-lang';
13
+ import { useRecipeIndex } from '@terrafirmagreg-team/recipe/hooks/use-index';
14
+ import {
15
+ loadRecipeChromeClient,
16
+ recipeItemName,
17
+ resolveRecipeRows,
18
+ } from '@terrafirmagreg-team/recipe/lib/client-index';
19
+ import { loadClientRecipe } from '@terrafirmagreg-team/recipe/lib/client/recipe';
20
+ import { clientDumpLang } from '@terrafirmagreg-team/game/lib/dump/client/dump';
21
+ import { recipeTitle } from '@terrafirmagreg-team/recipe/lib/data/parse';
22
+ import { recipeImageUrl } from '@terrafirmagreg-team/game/lib/dump/assets/urls';
23
+ import { recipeCardImageUrl, recipeHref } from '@terrafirmagreg-team/recipe/lib/links';
24
+
25
+ export function Recipe({
26
+ id,
27
+ dataset: datasetProp,
28
+ maxH = 360,
29
+ }: {
30
+ id?: string;
31
+ dataset?: string;
32
+ maxH?: number;
33
+ }) {
34
+ const catalog = useSectionCatalog();
35
+ const current = useCurrentDataset(catalog.recipe);
36
+ const dataset = datasetProp ?? current ?? catalog.recipe[0]?.id;
37
+ const lang = useResolvedLang();
38
+ const { index, indexLoading } = useRecipeIndex(dataset ?? '', lang);
39
+ const [failed, setFailed] = useState(false);
40
+
41
+ const { value, pending } = useAsyncResult(
42
+ index && dataset && id
43
+ ? async () => {
44
+ const [row] = await resolveRecipeRows(dataset, index, [id]);
45
+ if (!row) return { missing: true as const };
46
+ const [recipe, chrome, langMap] = await Promise.all([
47
+ loadClientRecipe(dataset, row.id, row.bucket),
48
+ loadRecipeChromeClient(dataset, lang),
49
+ clientDumpLang(dataset, lang),
50
+ ]);
51
+ const title = recipe
52
+ ? recipeTitle(recipe, langMap)
53
+ : recipeItemName(chrome.names, lang, row.out[0] ?? row.id);
54
+ const src =
55
+ (recipe ? recipeImageUrl(dataset, recipe) : undefined) ??
56
+ (row.image
57
+ ? recipeImageUrl(dataset, {
58
+ modId: row.id.split(':', 1)[0],
59
+ image: row.image,
60
+ })
61
+ : undefined) ??
62
+ recipeCardImageUrl(dataset, row.id);
63
+ return { row, recipe, langMap, title, src };
64
+ }
65
+ : undefined,
66
+ [dataset, index, id, lang],
67
+ );
68
+
69
+ if (!dataset || !id) return null;
70
+ const resolvedRow = value && 'row' in value ? value.row : undefined;
71
+ const type = resolvedRow ? index?.types[resolvedRow.type] : undefined;
72
+ const href = recipeHref(dataset, lang, { id, type });
73
+ if (!value && (pending || indexLoading)) {
74
+ return <Skeleton className="not-prose h-40 w-full max-w-112 rounded-md" />;
75
+ }
76
+ if (!value || 'missing' in value || !value.recipe) {
77
+ return (
78
+ <Link href={href} className="not-prose text-primary underline">
79
+ {id}
80
+ </Link>
81
+ );
82
+ }
83
+ const { row, recipe, langMap, title, src } = value;
84
+ return (
85
+ <Link
86
+ href={href}
87
+ className="not-prose mb-4 block w-fit max-w-full rounded-md"
88
+ title={title}
89
+ >
90
+ {src && !failed ? (
91
+ <FitCard w={recipe.w ?? row.d?.w ?? 0}>
92
+ <PagedCard
93
+ recipe={recipe}
94
+ dataset={dataset}
95
+ lang={lang}
96
+ langMap={langMap}
97
+ title={title}
98
+ src={src}
99
+ onFail={() => setFailed(true)}
100
+ naturalW={row.d?.w}
101
+ naturalH={row.d?.h}
102
+ maxH={Number(maxH) || 360}
103
+ interactive
104
+ />
105
+ </FitCard>
106
+ ) : (
107
+ <EmptyState variant="missing">{title}</EmptyState>
108
+ )}
109
+ </Link>
110
+ );
111
+ }
@@ -0,0 +1,48 @@
1
+ 'use client';
2
+
3
+ import type * as PageTree from 'fumadocs-core/page-tree';
4
+ import { TreeContextProvider } from '@terrafirmagreg-team/ui/components/layouts/tree-context';
5
+ import {
6
+ LeftSidebarPortal,
7
+ LeftSidebarTree,
8
+ } from '@terrafirmagreg-team/ui/components/layouts/left-sidebar';
9
+ import { Input } from '@terrafirmagreg-team/ui/components/ui/input';
10
+ import { EmptyState } from '@terrafirmagreg-team/ui/components/blocks/page/empty-state';
11
+
12
+ export function ToolList({
13
+ tree,
14
+ loaded,
15
+ query,
16
+ onQuery,
17
+ searchLabel,
18
+ emptyLabel,
19
+ }: {
20
+ tree: PageTree.Root;
21
+ loaded: boolean;
22
+ query: string;
23
+ onQuery: (query: string) => void;
24
+ searchLabel: string;
25
+ emptyLabel: string;
26
+ }) {
27
+ return (
28
+ <LeftSidebarPortal>
29
+ <div className="space-y-2 p-2">
30
+ <Input
31
+ type="search"
32
+ name="tool-search"
33
+ autoComplete="off"
34
+ aria-label={searchLabel}
35
+ placeholder={searchLabel}
36
+ value={query}
37
+ onChange={(event) => onQuery(event.target.value)}
38
+ />
39
+ <TreeContextProvider tree={tree}>
40
+ <LeftSidebarTree />
41
+ </TreeContextProvider>
42
+ {loaded && !tree.children.length ? (
43
+ <EmptyState>{emptyLabel}</EmptyState>
44
+ ) : null}
45
+ </div>
46
+ </LeftSidebarPortal>
47
+ );
48
+ }