@terrafirmagreg-team/recipe 0.1.0 → 1.2.1

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 (240) hide show
  1. package/package.json +16 -16
  2. package/src/components/button-copy.tsx +1 -0
  3. package/src/components/cards/card/index.tsx +5 -3
  4. package/src/components/cards/card/scene.tsx +4 -2
  5. package/src/components/cards/card/strip.tsx +4 -2
  6. package/src/components/cards/card/text.tsx +2 -1
  7. package/src/components/cards/card/tips.tsx +1 -0
  8. package/src/components/cards/default-item.tsx +6 -2
  9. package/src/components/cards/glare-filter.tsx +1 -0
  10. package/src/components/cards/paged-card.tsx +1 -0
  11. package/src/components/cards/slot-hitbox.tsx +4 -2
  12. package/src/components/cards/strip-card/buttons.tsx +7 -2
  13. package/src/components/cards/strip-card/index.tsx +3 -1
  14. package/src/components/cards/strip-card/member-cell.tsx +4 -2
  15. package/src/components/cards/strip-card/sprite.tsx +1 -0
  16. package/src/components/embed-card.tsx +44 -0
  17. package/src/components/mdx/recipe.tsx +34 -16
  18. package/src/components/tool-list.tsx +1 -0
  19. package/src/{lib/tree.ts → components/tree.tsx} +20 -12
  20. package/src/components/views/alloy/index.tsx +14 -7
  21. package/src/components/views/alloy/list.tsx +3 -2
  22. package/src/components/views/alloy/mix/index.tsx +1 -0
  23. package/src/components/views/alloy/mix/ratio.tsx +7 -6
  24. package/src/components/views/alloy/mix/rows.tsx +6 -7
  25. package/src/components/views/alloy/mix/status.tsx +8 -7
  26. package/src/components/views/alloy/mix/total.tsx +13 -12
  27. package/src/components/views/alloy/range-bar.tsx +2 -1
  28. package/src/components/views/alloy/stack-bar.tsx +4 -2
  29. package/src/components/views/browser/axis-switcher.tsx +13 -12
  30. package/src/components/views/browser/card-grid.tsx +2 -1
  31. package/src/components/views/browser/favorite-button.tsx +2 -1
  32. package/src/components/views/browser/frame.tsx +3 -2
  33. package/src/components/views/browser/index.tsx +20 -2
  34. package/src/components/views/browser/item-panel/index.tsx +11 -9
  35. package/src/components/views/browser/item-panel/skeleton.tsx +1 -0
  36. package/src/components/views/browser/item-panel-grid/cell.tsx +5 -3
  37. package/src/components/views/browser/item-panel-grid/favorite-cell.tsx +4 -2
  38. package/src/components/views/browser/item-panel-grid/index.tsx +4 -2
  39. package/src/components/views/browser/item-view/index.tsx +7 -12
  40. package/src/components/views/browser/item-view/tags.tsx +7 -5
  41. package/src/components/views/browser/item-view-header.tsx +10 -9
  42. package/src/components/views/browser/pane-tabs.tsx +1 -0
  43. package/src/components/views/browser/place-header.tsx +4 -3
  44. package/src/components/views/browser/query-controls.tsx +3 -1
  45. package/src/components/views/browser/recipe-item-header.tsx +5 -4
  46. package/src/components/views/browser/settings.tsx +4 -3
  47. package/src/components/views/browser/toolbar.tsx +3 -2
  48. package/src/components/views/browser/type-tabs.tsx +5 -3
  49. package/src/components/views/browser/type-view.tsx +7 -12
  50. package/src/components/views/browser/workstation-rail.tsx +4 -3
  51. package/src/components/views/calc/calculator/actions.tsx +4 -14
  52. package/src/components/views/calc/calculator/help.tsx +69 -28
  53. package/src/components/views/calc/calculator/index.tsx +34 -82
  54. package/src/components/views/calc/dot-overlay.tsx +1 -0
  55. package/src/components/views/calc/edge.tsx +134 -32
  56. package/src/components/views/calc/flow/index.tsx +92 -54
  57. package/src/components/views/calc/flow/picker-dialogs.tsx +84 -6
  58. package/src/components/views/calc/flow/toolbar.tsx +9 -21
  59. package/src/components/views/calc/flow/view-menu.tsx +16 -120
  60. package/src/components/views/calc/member-picker.tsx +8 -6
  61. package/src/components/views/calc/node-card/actions.tsx +28 -106
  62. package/src/components/views/calc/node-card/badges.tsx +10 -28
  63. package/src/components/views/calc/node-card/chips.tsx +8 -40
  64. package/src/components/views/calc/node-card/far.tsx +13 -65
  65. package/src/components/views/calc/node-card/full.tsx +133 -83
  66. package/src/components/views/calc/node-card/index.tsx +1 -0
  67. package/src/components/views/calc/node-card/menu.tsx +93 -22
  68. package/src/components/views/calc/node-card/port-cells.tsx +186 -0
  69. package/src/components/views/calc/node-card/port-rails.tsx +251 -0
  70. package/src/components/views/calc/node-card/ports.tsx +86 -0
  71. package/src/components/views/calc/recipe-picker/body.tsx +17 -14
  72. package/src/components/views/calc/recipe-picker/footer.tsx +5 -3
  73. package/src/components/views/calc/recipe-picker/index.tsx +5 -0
  74. package/src/components/views/calc/sidebar.tsx +19 -10
  75. package/src/components/views/calc/trees/icon-picker.tsx +3 -2
  76. package/src/components/views/calc/trees/index.tsx +5 -4
  77. package/src/components/views/calc/trees/label.tsx +6 -4
  78. package/src/components/views/calc/trees/rows.tsx +6 -5
  79. package/src/components/views/forge/gui.tsx +2 -1
  80. package/src/components/views/forge/index.tsx +14 -7
  81. package/src/components/views/forge/list.tsx +3 -2
  82. package/src/components/views/forge/workbench/controls.tsx +13 -6
  83. package/src/components/views/forge/workbench/index.tsx +3 -2
  84. package/src/components/views/forge/workbench/panel.tsx +1 -0
  85. package/src/components/views/forge/workbench/ruler.tsx +5 -4
  86. package/src/components/views/forge/workbench/status.tsx +6 -5
  87. package/src/components/views/forge/workbench/tape.tsx +5 -4
  88. package/src/hooks/alloy/use-fluid-colors.ts +12 -9
  89. package/src/hooks/alloy/use-mix-state.ts +3 -2
  90. package/src/hooks/calc/flow/use-engage.ts +129 -0
  91. package/src/hooks/calc/flow/use-handlers.ts +192 -0
  92. package/src/hooks/calc/flow/use-layout.ts +105 -0
  93. package/src/hooks/calc/flow/{mount.ts → use-mount.ts} +1 -0
  94. package/src/hooks/calc/flow/{nodes.ts → use-nodes.ts} +46 -37
  95. package/src/hooks/calc/flow/use-pickers.ts +57 -0
  96. package/src/hooks/calc/flow/{viewport.ts → use-viewport.ts} +13 -26
  97. package/src/hooks/calc/flow/{window.ts → use-window.ts} +1 -0
  98. package/src/hooks/calc/use-actions.ts +208 -229
  99. package/src/hooks/calc/use-amounts.ts +23 -60
  100. package/src/hooks/calc/use-data.ts +106 -192
  101. package/src/hooks/calc/use-defaults.ts +4 -2
  102. package/src/hooks/calc/use-far-mode.ts +1 -0
  103. package/src/hooks/calc/use-focus.ts +8 -7
  104. package/src/hooks/calc/use-maps.ts +1 -0
  105. package/src/hooks/calc/use-reset.ts +1 -0
  106. package/src/hooks/calc/use-resolver.ts +32 -196
  107. package/src/hooks/calc/use-session.ts +1 -0
  108. package/src/hooks/calc/use-share.ts +5 -2
  109. package/src/hooks/calc/use-storage.ts +5 -4
  110. package/src/hooks/calc/use-tier.ts +1 -0
  111. package/src/hooks/calc/use-view-mode.ts +1 -0
  112. package/src/hooks/calc/use-worker.ts +56 -121
  113. package/src/hooks/forge/use-anvil-gui.ts +4 -2
  114. package/src/hooks/forge/use-forge-state.ts +1 -0
  115. package/src/hooks/forge/use-ruler.ts +1 -0
  116. package/src/hooks/use-add-to-calc.ts +23 -19
  117. package/src/hooks/use-categories-by-id.ts +18 -0
  118. package/src/hooks/use-favorite-hotkey.ts +1 -0
  119. package/src/hooks/use-favorites.ts +1 -0
  120. package/src/hooks/use-group-outlines.ts +1 -0
  121. package/src/hooks/use-hide-unused.ts +2 -1
  122. package/src/hooks/use-index.ts +6 -2
  123. package/src/hooks/use-item-feeds.ts +3 -1
  124. package/src/hooks/use-item-pane.ts +6 -4
  125. package/src/hooks/use-item-press.ts +27 -23
  126. package/src/hooks/use-item-tag-groups.ts +4 -2
  127. package/src/hooks/use-item-tooltips.ts +3 -1
  128. package/src/hooks/use-left-sidebar-tree.ts +11 -3
  129. package/src/hooks/use-mod-names.ts +4 -2
  130. package/src/hooks/use-mount-limit.ts +1 -0
  131. package/src/hooks/use-panel-cells.ts +3 -1
  132. package/src/hooks/use-panel-page.ts +9 -6
  133. package/src/hooks/use-panel-tabs.ts +2 -1
  134. package/src/hooks/use-query.ts +8 -6
  135. package/src/hooks/use-session.ts +6 -2
  136. package/src/hooks/use-sidebar-group.ts +3 -7
  137. package/src/hooks/use-tag-members.ts +3 -1
  138. package/src/hooks/use-tool-recipes.ts +1 -0
  139. package/src/hooks/use-type-filter.ts +1 -0
  140. package/src/lib/alloy/mix.ts +1 -0
  141. package/src/lib/alloy/parse.ts +1 -0
  142. package/src/lib/calc/defaults.ts +6 -4
  143. package/src/lib/calc/flow.ts +143 -107
  144. package/src/lib/calc/focus.ts +1 -0
  145. package/src/lib/calc/meta.ts +3 -1
  146. package/src/lib/calc/pack.ts +47 -154
  147. package/src/lib/calc/ports.ts +43 -0
  148. package/src/lib/calc/resolve.ts +6 -38
  149. package/src/lib/calc/route/buckets.ts +1 -0
  150. package/src/lib/calc/route/geometry.ts +13 -24
  151. package/src/lib/calc/route/index.ts +1 -0
  152. package/src/lib/calc/route/polyline.ts +3 -3
  153. package/src/lib/calc/route/router.ts +181 -57
  154. package/src/lib/calc/scheme.ts +226 -0
  155. package/src/lib/calc/storage/entry.ts +27 -19
  156. package/src/lib/calc/storage/index.ts +2 -0
  157. package/src/lib/calc/storage/keys.ts +4 -8
  158. package/src/lib/calc/storage/store.ts +11 -36
  159. package/src/lib/calc/tree/index.ts +2 -16
  160. package/src/lib/calc/tree/materials.ts +1 -0
  161. package/src/lib/calc/tree/recipe.ts +1 -0
  162. package/src/lib/calc/tree/share.ts +86 -149
  163. package/src/lib/calc/worker/api.ts +3 -11
  164. package/src/lib/calc/worker/fetch.ts +1 -0
  165. package/src/lib/calc/worker/flush.ts +7 -20
  166. package/src/lib/calc/worker/index.ts +1 -1
  167. package/src/lib/calc/worker/protocol.ts +16 -13
  168. package/src/lib/calc/worker/resolve.ts +32 -84
  169. package/src/lib/calc/worker/state.ts +5 -6
  170. package/src/{components/cards/card/model.ts → lib/card-model.ts} +1 -0
  171. package/src/lib/client/recipe-categories.ts +11 -93
  172. package/src/lib/client/recipe.ts +13 -11
  173. package/src/lib/client-index/catalog.ts +33 -27
  174. package/src/lib/client-index/chrome.ts +30 -30
  175. package/src/lib/client-index/index.ts +1 -0
  176. package/src/lib/client-index/registry.ts +70 -59
  177. package/src/lib/client-index/rows.ts +63 -57
  178. package/src/lib/client-index/tooltips.ts +28 -34
  179. package/src/lib/config.ts +2 -1
  180. package/src/lib/data/category/dto.ts +4 -3
  181. package/src/lib/data/category/load.ts +9 -8
  182. package/src/lib/data/category/parse.ts +4 -3
  183. package/src/lib/data/dto.ts +4 -3
  184. package/src/lib/data/grid.ts +2 -1
  185. package/src/lib/data/parse/entry.ts +3 -2
  186. package/src/lib/data/parse/index.ts +1 -0
  187. package/src/lib/data/parse/layout.ts +5 -2
  188. package/src/lib/data/parse/slots.ts +3 -2
  189. package/src/lib/data/parse/widgets.ts +3 -2
  190. package/src/lib/data/scene.ts +1 -1
  191. package/src/lib/data/slot-io.ts +1 -0
  192. package/src/lib/dump.ts +4 -3
  193. package/src/lib/embed.ts +22 -0
  194. package/src/lib/favorite-hover.ts +1 -0
  195. package/src/lib/fluid-outputs.ts +10 -12
  196. package/src/lib/forge/actions.ts +2 -1
  197. package/src/lib/forge/data.ts +2 -1
  198. package/src/lib/forge/solve.ts +1 -0
  199. package/src/lib/group.ts +1 -0
  200. package/src/lib/index-store.ts +15 -9
  201. package/src/lib/links.ts +15 -9
  202. package/src/lib/lookup.ts +10 -8
  203. package/src/lib/panel-outline.ts +1 -0
  204. package/src/lib/panel.ts +3 -2
  205. package/src/lib/preview.ts +12 -8
  206. package/src/lib/recipe-slots.ts +1 -0
  207. package/src/lib/search.ts +1 -0
  208. package/src/lib/sidebar.ts +1 -0
  209. package/src/lib/used-items.ts +1 -0
  210. package/src/locales/en_us.json +170 -226
  211. package/src/locales/ru_ru.json +170 -226
  212. package/src/locales.ts +1 -0
  213. package/src/types/alloy.ts +1 -0
  214. package/src/{hooks/calc/flow/props.ts → types/calc-flow-props.ts} +18 -14
  215. package/src/types/calc.ts +41 -44
  216. package/src/types/forge.ts +1 -0
  217. package/src/types/index.ts +4 -2
  218. package/styles/calc.css +308 -47
  219. package/src/components/cards/fit-card.tsx +0 -26
  220. package/src/components/views/calc/target.tsx +0 -271
  221. package/src/hooks/calc/flow/engage.ts +0 -89
  222. package/src/hooks/calc/flow/handlers.ts +0 -115
  223. package/src/hooks/calc/flow/layout.ts +0 -149
  224. package/src/hooks/calc/flow/pickers.ts +0 -50
  225. package/src/hooks/calc/use-calc-fill.ts +0 -91
  226. package/src/hooks/calc/use-density.ts +0 -28
  227. package/src/hooks/calc/use-derived.ts +0 -258
  228. package/src/hooks/calc/use-direction.ts +0 -23
  229. package/src/hooks/calc/use-edge-style.ts +0 -40
  230. package/src/hooks/calc/use-fill.ts +0 -213
  231. package/src/lib/alloy/alloy.test.ts +0 -186
  232. package/src/lib/calc/tree/build.ts +0 -196
  233. package/src/lib/calc/tree/context.ts +0 -42
  234. package/src/lib/calc/tree/goal.ts +0 -64
  235. package/src/lib/calc/tree/ideal.ts +0 -36
  236. package/src/lib/calc/tree/pick.ts +0 -68
  237. package/src/lib/calc/tree/tree.test.ts +0 -169
  238. package/src/lib/calc/worker/fill.ts +0 -123
  239. package/src/lib/client-index/cache.ts +0 -13
  240. package/src/lib/forge/forge.test.ts +0 -123
@@ -1,4 +1,5 @@
1
1
  'use client';
2
+ import type { DatasetRef } from '@terrafirmagreg-team/sites/location';
2
3
 
3
4
  import { useEffect, useMemo, useRef } from 'react';
4
5
  import type * as PageTree from 'fumadocs-core/page-tree';
@@ -20,19 +21,18 @@ export function CalcSidebar({
20
21
  lang,
21
22
  langMap,
22
23
  summary,
23
- target = null,
24
24
  onFocus,
25
25
  trees,
26
26
  }: {
27
- dataset: string;
27
+ dataset: DatasetRef;
28
28
  lang: string;
29
29
  langMap?: Record<string, string>;
30
30
  summary: {
31
31
  raw: CalcMaterial[];
32
32
  remainders: CalcMaterial[];
33
33
  catalysts: CalcMaterial[];
34
+ products: CalcMaterial[];
34
35
  };
35
- target?: CalcMaterial | null;
36
36
  onFocus?: (id: string) => void;
37
37
  trees?: {
38
38
  tabs: CalcTreeEntry[];
@@ -53,21 +53,26 @@ export function CalcSidebar({
53
53
  const focusByUrl = new Map<string, string>();
54
54
  const children: PageTree.Node[] = [];
55
55
  const sections = [
56
- ['inputs', summary.raw],
56
+ ['inputs', summary.raw, t('Inputs', { note: 'recipe.calc.inputs' })],
57
57
  [
58
58
  'outputs',
59
- [...(target ? [target] : []), ...summary.remainders],
59
+ [...summary.products, ...summary.remainders],
60
+ t('Outputs', { note: 'recipe.calc.outputs' }),
61
+ ],
62
+ [
63
+ 'catalysts',
64
+ summary.catalysts,
65
+ t('Catalysts', { note: 'recipe.calc.catalysts' }),
60
66
  ],
61
- ['catalysts', summary.catalysts],
62
67
  ] as const;
63
- for (const [key, rows] of sections) {
68
+ for (const [key, rows, label] of sections) {
64
69
  if (!rows.length) continue;
65
70
  children.push({
66
71
  type: 'folder',
67
72
  $id: `calc-${key}`,
68
73
  name: (
69
74
  <span className="-me-3 flex min-w-0 flex-1 items-center gap-1.5">
70
- <span className="truncate">{t(`recipe.calc.${key}`)}</span>
75
+ <span className="truncate">{label}</span>
71
76
  <span className="shrink-0 tabular-nums text-xs text-muted-foreground/70">
72
77
  ×{rows.length}
73
78
  </span>
@@ -97,7 +102,10 @@ export function CalcSidebar({
97
102
  tooltip={false}
98
103
  className="min-w-0 flex-1 justify-start truncate"
99
104
  />
100
- <span className="ms-auto shrink-0 tabular-nums text-xs text-muted-foreground">
105
+ <span
106
+ className="ms-auto shrink-0 tabular-nums text-xs text-muted-foreground"
107
+ title={row.amount.toLocaleString()}
108
+ >
101
109
  ×{formatCount(row.amount)}
102
110
  </span>
103
111
  </>
@@ -115,7 +123,7 @@ export function CalcSidebar({
115
123
  tree: { name: 'calc', $id: `calc:${stamp}`, children } as PageTree.Root,
116
124
  focusByUrl,
117
125
  };
118
- }, [dataset, lang, langMap, summary, target, t]);
126
+ }, [dataset, lang, langMap, summary, t]);
119
127
 
120
128
  const CalcItem = useMemo(() => {
121
129
  const focus = focusByUrl;
@@ -160,3 +168,4 @@ export function CalcSidebar({
160
168
  </div>
161
169
  );
162
170
  }
171
+
@@ -47,7 +47,7 @@ export function IconPicker({
47
47
  <PopoverTrigger
48
48
  render={
49
49
  <IconButton
50
- label={label ?? t('recipe.calc.pick.icon')}
50
+ label={label ?? t('Pick icon', { note: 'recipe.calc.pick.icon' })}
51
51
  className={
52
52
  alwaysVisible
53
53
  ? undefined
@@ -63,7 +63,7 @@ export function IconPicker({
63
63
  autoFocus
64
64
  value={query}
65
65
  onChange={(event) => setQuery(event.target.value)}
66
- placeholder={t('recipe.search')}
66
+ placeholder={t('Search items', { note: 'recipe.search' })}
67
67
  className="h-7 px-2 text-xs"
68
68
  />
69
69
  <div className="scrollbar-thin mt-2 grid max-h-60 grid-cols-6 gap-1 overflow-y-auto">
@@ -94,3 +94,4 @@ export function IconPicker({
94
94
  </Popover>
95
95
  );
96
96
  }
97
+
@@ -26,7 +26,7 @@ export function CalcHistoryList({
26
26
  return (
27
27
  <div className="flex w-64 max-w-full flex-col gap-1">
28
28
  <p className="px-2 text-[10px] font-semibold uppercase tracking-wider text-muted-foreground/70">
29
- {t('recipe.calc.history')}
29
+ {t('History', { note: 'recipe.calc.history' })}
30
30
  </p>
31
31
  <ul className="flex flex-col">
32
32
  {history.slice(0, 5).map((entry) => (
@@ -75,7 +75,7 @@ export function CalcTrees({
75
75
  <LeftSidebarFolder collapsible defaultOpen>
76
76
  <LeftSidebarFolderTrigger className="gap-1.5 text-xs font-semibold uppercase tracking-wider text-muted-foreground/70">
77
77
  <FolderTree className="size-3.5" />
78
- {t('recipe.calc.trees')}
78
+ {t('Schemes', { note: 'recipe.calc.trees' })}
79
79
  </LeftSidebarFolderTrigger>
80
80
  <LeftSidebarFolderContent>
81
81
  <ul className="flex flex-col">
@@ -99,7 +99,7 @@ export function CalcTrees({
99
99
  >
100
100
  <Plus className="size-4 shrink-0" />
101
101
  <span className="min-w-0 flex-1 text-start">
102
- {t('recipe.calc.new_tab')}
102
+ {t('New scheme', { note: 'recipe.calc.new_tab' })}
103
103
  </span>
104
104
  </button>
105
105
  </li>
@@ -111,7 +111,7 @@ export function CalcTrees({
111
111
  <LeftSidebarFolder collapsible defaultOpen>
112
112
  <LeftSidebarFolderTrigger className="gap-1.5 text-xs font-semibold uppercase tracking-wider text-muted-foreground/70">
113
113
  <History className="size-3.5" />
114
- {t('recipe.calc.history')}
114
+ {t('History', { note: 'recipe.calc.history' })}
115
115
  </LeftSidebarFolderTrigger>
116
116
  <LeftSidebarFolderContent>
117
117
  <ul className="flex flex-col">
@@ -131,3 +131,4 @@ export function CalcTrees({
131
131
  </div>
132
132
  );
133
133
  }
134
+
@@ -12,7 +12,8 @@ export function TreeIcon({
12
12
  lang,
13
13
  langMap,
14
14
  }: { entry: CalcTreeEntry } & EntryMeta) {
15
- const ref = entry.icon ?? entry.state.target;
15
+ const card = entry.state.cards?.[0];
16
+ const ref = entry.icon ?? (card && { id: card.materialId, kind: card.kind });
16
17
  if (!ref) return <Workflow className="size-4 shrink-0" />;
17
18
  return (
18
19
  <Entry
@@ -34,11 +35,11 @@ export function TreeLabel({
34
35
  langMap,
35
36
  }: { entry: CalcTreeEntry } & EntryMeta) {
36
37
  const t = useTranslations();
37
- const target = entry.state.target;
38
+ const card = entry.state.cards?.[0];
38
39
  const text =
39
40
  entry.name ??
40
- (target ? langMap?.[target.id] || target.id : undefined) ??
41
- t('recipe.calc.tab.untitled');
41
+ (card ? langMap?.[card.materialId] || card.materialId : undefined) ??
42
+ t('Untitled scheme', { note: 'recipe.calc.tab.untitled' });
42
43
  return (
43
44
  <span className="min-w-0 flex-1 truncate text-start">
44
45
  {text}
@@ -63,3 +64,4 @@ export function TreeDate({
63
64
  </span>
64
65
  );
65
66
  }
67
+
@@ -50,9 +50,9 @@ export function TabRow({
50
50
  }}
51
51
  onBlur={commit}
52
52
  className="h-6 px-1.5 text-xs"
53
- placeholder={t('recipe.calc.rename')}
53
+ placeholder={t('Rename', { note: 'recipe.calc.rename' })}
54
54
  />
55
- <IconButton label={t('recipe.calc.rename')} onClick={commit}>
55
+ <IconButton label={t('Rename', { note: 'recipe.calc.rename' })} onClick={commit}>
56
56
  <Check className="size-3.5" />
57
57
  </IconButton>
58
58
  </span>
@@ -80,7 +80,7 @@ export function TabRow({
80
80
  />
81
81
  ) : null}
82
82
  <IconButton
83
- label={t('recipe.calc.rename')}
83
+ label={t('Rename', { note: 'recipe.calc.rename' })}
84
84
  className="opacity-0 transition-opacity group-hover:opacity-100"
85
85
  onClick={() => {
86
86
  setName(entry.name ?? '');
@@ -90,7 +90,7 @@ export function TabRow({
90
90
  <Pencil className="size-3.5" />
91
91
  </IconButton>
92
92
  <IconButton
93
- label={t('recipe.calc.close_tab')}
93
+ label={t('Close scheme', { note: 'recipe.calc.close_tab' })}
94
94
  className="opacity-0 transition-opacity group-hover:opacity-100"
95
95
  onClick={() => onClose(entry.id)}
96
96
  >
@@ -127,7 +127,7 @@ export function HistoryRow({
127
127
  </button>
128
128
  {onRemove && (
129
129
  <IconButton
130
- label={t('recipe.calc.remove.history')}
130
+ label={t('Remove from history', { note: 'recipe.calc.remove.history' })}
131
131
  className="opacity-0 transition-opacity group-hover:opacity-100"
132
132
  onClick={() => onRemove(entry.id)}
133
133
  >
@@ -137,3 +137,4 @@ export function HistoryRow({
137
137
  </li>
138
138
  );
139
139
  }
140
+
@@ -1,7 +1,7 @@
1
1
  'use client';
2
2
 
3
3
  import type { CSSProperties } from 'react';
4
- import { cn } from 'cn';
4
+ import { cn } from '@terrafirmagreg-team/ui/lib/utils/cn';
5
5
  import type { ForgeRule, ForgeRuleAction } from '@terrafirmagreg-team/recipe/types/forge';
6
6
 
7
7
  const ATLAS_SIZE = 256;
@@ -128,3 +128,4 @@ export const MARK_TARGET: GuiRect = { x: 181, y: 0, w: 5, h: 5 };
128
128
  export function actionGlyphRect(action: string): GuiRect {
129
129
  return { x: STEP_GLYPH_X[action] ?? 0, y: 224, w: 32, h: 32 };
130
130
  }
131
+
@@ -9,7 +9,7 @@ import { RecipeFrame } from '@terrafirmagreg-team/recipe/components/views/browse
9
9
  import { useToolRecipes } from '@terrafirmagreg-team/recipe/hooks/use-tool-recipes';
10
10
  import { toForgeRecipe } from '@terrafirmagreg-team/recipe/lib/forge/data';
11
11
  import { RecipeCard } from '@terrafirmagreg-team/recipe/components/cards/card';
12
- import { FitCard } from '@terrafirmagreg-team/recipe/components/cards/fit-card';
12
+ import { FitBox } from '@terrafirmagreg-team/ui/components/blocks/fit-box';
13
13
  import {
14
14
  recipeCardImageUrl,
15
15
  recipeImageUrl,
@@ -67,16 +67,16 @@ export function ForgeCalculator({ session }: { session: RecipeSession }) {
67
67
  active ? (
68
68
  <RecipeItemHeader recipe={active.recipe} session={session} />
69
69
  ) : (
70
- <ItemRow layout="header" title={t('recipe.forge.title')} />
70
+ <ItemRow layout="header" title={t('Forging', { note: 'recipe.forge.title' })} />
71
71
  )
72
72
  }
73
73
  >
74
- <div className="flex-1 space-y-5 overflow-y-auto p-2 sm:p-4">
74
+ <div className="scrollbar-thin flex-1 space-y-5 overflow-y-auto p-2 sm:p-4">
75
75
  {loading ? (
76
76
  <EmptyState variant="loading" />
77
77
  ) : value === undefined ? (
78
78
  <EmptyState variant="missing">
79
- {t('recipe.forge.load_error')}
79
+ {t('Could not load recipes. Reload the page to retry.', { note: 'recipe.forge.load_error' })}
80
80
  </EmptyState>
81
81
  ) : active ? (
82
82
  <>
@@ -85,17 +85,23 @@ export function ForgeCalculator({ session }: { session: RecipeSession }) {
85
85
  recipe={active}
86
86
  session={session}
87
87
  />
88
- <FitCard w={active.recipe.w ?? 0}>
88
+ <FitBox w={active.recipe.w ?? 0}>
89
89
  <ForgeRecipeCard
90
90
  key={active.recipe.id}
91
91
  recipe={active.recipe}
92
92
  session={session}
93
93
  />
94
- </FitCard>
94
+ </FitBox>
95
95
  </>
96
96
  ) : (
97
97
  <EmptyState>
98
- {t(activeId ? 'recipe.forge.missing' : 'recipe.forge.empty')}
98
+ {activeId
99
+ ? t('Recipe not found in this dump. Select another recipe.', {
100
+ note: 'recipe.forge.missing',
101
+ })
102
+ : t('No matching recipes in this dump.', {
103
+ note: 'recipe.forge.empty',
104
+ })}
99
105
  </EmptyState>
100
106
  )}
101
107
  </div>
@@ -103,3 +109,4 @@ export function ForgeCalculator({ session }: { session: RecipeSession }) {
103
109
  </>
104
110
  );
105
111
  }
112
+
@@ -105,8 +105,9 @@ export function ForgeList({ session }: { session: RecipeSession }) {
105
105
  loaded={value !== undefined}
106
106
  query={query}
107
107
  onQuery={setQuery}
108
- searchLabel={t('recipe.forge.search')}
109
- emptyLabel={t('recipe.forge.empty')}
108
+ searchLabel={t('Search recipes', { note: 'recipe.forge.search' })}
109
+ emptyLabel={t('No matching recipes in this dump.', { note: 'recipe.forge.empty' })}
110
110
  />
111
111
  );
112
112
  }
113
+
@@ -34,7 +34,7 @@ export function WorkbenchControls({
34
34
  return (
35
35
  <div className="flex flex-wrap items-end gap-3">
36
36
  <label className="flex w-24 flex-col gap-1 border-l-2 border-green-600 pl-2 text-xs text-green-700 dark:border-green-500 dark:text-green-400">
37
- {t('recipe.forge.current')}
37
+ {t('Current work', { note: 'recipe.forge.current' })}
38
38
  <Input
39
39
  inputMode="numeric"
40
40
  name="forge-start"
@@ -46,7 +46,7 @@ export function WorkbenchControls({
46
46
  />
47
47
  </label>
48
48
  <label className="flex w-24 flex-col gap-1 border-l-2 border-red-600 pl-2 text-xs text-red-700 dark:border-red-500 dark:text-red-400">
49
- {t('recipe.forge.target')}
49
+ {t('Target', { note: 'recipe.forge.target' })}
50
50
  <Input
51
51
  inputMode="numeric"
52
52
  name="forge-target"
@@ -62,25 +62,31 @@ export function WorkbenchControls({
62
62
  disabled={target === null || !rulesSupported || solving}
63
63
  >
64
64
  <Wand2 className="size-4" />
65
- {t(solving ? 'recipe.forge.solving' : 'recipe.forge.solve')}
65
+ {solving
66
+ ? t('Solving…', { note: 'recipe.forge.solving' })
67
+ : t('Solve', { note: 'recipe.forge.solve' })}
66
68
  </Button>
67
69
  <span className="flex items-center gap-1.5">
68
70
  <IconButton
69
- label={t('recipe.forge.undo')}
71
+ label={t('Undo last action', { note: 'recipe.forge.undo' })}
70
72
  disabled={!applied.length || solving}
71
73
  onClick={() => state.setApplied((prev) => prev.slice(0, -1))}
72
74
  >
73
75
  <Undo2 />
74
76
  </IconButton>
75
77
  <IconButton
76
- label={t('recipe.forge.reset')}
78
+ label={t('Reset the workpiece', { note: 'recipe.forge.reset' })}
77
79
  disabled={!applied.length || solving}
78
80
  onClick={() => state.setApplied([])}
79
81
  >
80
82
  <RotateCcw />
81
83
  </IconButton>
82
84
  <IconButton
83
- label={t(copied ? 'recipe.forge.copied' : 'recipe.forge.copy')}
85
+ label={
86
+ copied
87
+ ? t('Copied', { note: 'recipe.forge.copied' })
88
+ : t('Copy steps', { note: 'recipe.forge.copy' })
89
+ }
84
90
  disabled={!steps.length}
85
91
  onClick={copy}
86
92
  >
@@ -94,3 +100,4 @@ export function WorkbenchControls({
94
100
  </div>
95
101
  );
96
102
  }
103
+
@@ -11,7 +11,7 @@ import { AnvilPanel } from './panel';
11
11
  import { RulerOverlay } from './ruler';
12
12
  import { StatusPlate } from './status';
13
13
  import { StepTape } from './tape';
14
- import { cn } from 'cn';
14
+ import { cn } from '@terrafirmagreg-team/ui/lib/utils/cn';
15
15
  import type { ForgeRecipe } from '@terrafirmagreg-team/recipe/types/forge';
16
16
  import type { RecipeSession } from '@terrafirmagreg-team/recipe/types';
17
17
 
@@ -43,7 +43,7 @@ export function ForgeWorkbench({
43
43
  <WorkbenchControls state={state} rulesSupported={recipe.rulesSupported} langMap={session.langMap} />
44
44
  {!recipe.rulesSupported ? (
45
45
  <p role="alert" className="text-sm text-destructive">
46
- {t('recipe.forge.unsupported')}
46
+ {t('The dump contains missing or unrecognized forging rules. A safe solution cannot be calculated.', { note: 'recipe.forge.unsupported' })}
47
47
  </p>
48
48
  ) : null}
49
49
 
@@ -82,3 +82,4 @@ export function ForgeWorkbench({
82
82
  </section>
83
83
  );
84
84
  }
85
+
@@ -225,3 +225,4 @@ export function AnvilPanel({
225
225
  </>
226
226
  );
227
227
  }
228
+
@@ -3,7 +3,7 @@
3
3
  import { useTranslations } from '@terrafirmagreg-team/translate/hooks/use-translations';
4
4
  import { MARK_PROGRESS, MARK_TARGET, GuiSprite } from '@terrafirmagreg-team/recipe/components/views/forge/gui';
5
5
  import { RULER, useRuler } from '@terrafirmagreg-team/recipe/hooks/forge/use-ruler';
6
- import { cn } from 'cn';
6
+ import { cn } from '@terrafirmagreg-team/ui/lib/utils/cn';
7
7
  import type { ForgeState } from '@terrafirmagreg-team/recipe/hooks/forge/use-forge-state';
8
8
 
9
9
  export function RulerOverlay({
@@ -38,8 +38,8 @@ export function RulerOverlay({
38
38
  });
39
39
 
40
40
  const markerHandles = [
41
- { kind: 'target', value: target ?? 0, label: t('recipe.forge.target') },
42
- { kind: 'work', value: work, label: t('recipe.forge.current') },
41
+ { kind: 'target', value: target ?? 0, label: t('Target', { note: 'recipe.forge.target' }) },
42
+ { kind: 'work', value: work, label: t('Current work', { note: 'recipe.forge.current' }) },
43
43
  ] as const;
44
44
 
45
45
  return (
@@ -66,7 +66,7 @@ export function RulerOverlay({
66
66
  />
67
67
  <div
68
68
  role="slider"
69
- aria-label={t('recipe.forge.ruler')}
69
+ aria-label={t('Work ruler', { note: 'recipe.forge.ruler' })}
70
70
  aria-valuemin={0}
71
71
  aria-valuemax={149}
72
72
  aria-valuenow={target ?? work}
@@ -130,3 +130,4 @@ export function RulerOverlay({
130
130
  </>
131
131
  );
132
132
  }
133
+
@@ -22,20 +22,20 @@ export function StatusPlate({
22
22
  style={{ width: PX(GUI_PANEL.w) }}
23
23
  >
24
24
  <span>
25
- {t('recipe.forge.work')}:{' '}
25
+ {t('Work', { note: 'recipe.forge.work' })}:{' '}
26
26
  <span className="font-medium text-green-700 dark:text-green-400">
27
27
  {work}
28
28
  </span>
29
29
  </span>
30
30
  <span>
31
- {t('recipe.forge.target')}:{' '}
31
+ {t('Target', { note: 'recipe.forge.target' })}:{' '}
32
32
  <span className="font-medium text-red-700 dark:text-red-400">
33
33
  {target ?? 0}
34
34
  </span>
35
35
  </span>
36
36
  {finished ? (
37
37
  <span className="inline-flex items-center gap-1 text-green-600 animate-in fade-in-0">
38
- <Check className="size-3" /> {t('recipe.forge.done')}
38
+ <Check className="size-3" /> {t('Done', { note: 'recipe.forge.done' })}
39
39
  </span>
40
40
  ) : null}
41
41
  {failed ? (
@@ -43,12 +43,13 @@ export function StatusPlate({
43
43
  role="status"
44
44
  className="inline-flex items-center gap-1 text-destructive"
45
45
  >
46
- <X className="size-3" /> {t('recipe.forge.no_solution')}
46
+ <X className="size-3" /> {t('No sequence reaches this target under these rules. Reset the workpiece and try another target.', { note: 'recipe.forge.no_solution' })}
47
47
  </span>
48
48
  ) : null}
49
49
  {target !== null && work > target && !failed ? (
50
- <span>{t('recipe.forge.over_target')}</span>
50
+ <span>{t('Work is above the target, use Draw or lighter hits to come back.', { note: 'recipe.forge.over_target' })}</span>
51
51
  ) : null}
52
52
  </div>
53
53
  );
54
54
  }
55
+
@@ -2,7 +2,7 @@
2
2
 
3
3
  import { useTranslations } from '@terrafirmagreg-team/translate/hooks/use-translations';
4
4
  import { GuiSprite, actionGlyphRect } from '@terrafirmagreg-team/recipe/components/views/forge/gui';
5
- import { cn } from 'cn';
5
+ import { cn } from '@terrafirmagreg-team/ui/lib/utils/cn';
6
6
  import type { ForgeState } from '@terrafirmagreg-team/recipe/hooks/forge/use-forge-state';
7
7
 
8
8
  const TAPE_MAX = 12;
@@ -26,8 +26,8 @@ export function StepTape({
26
26
  <li>
27
27
  <button
28
28
  type="button"
29
- title={t('recipe.forge.expand_steps')}
30
- aria-label={t('recipe.forge.expand_steps')}
29
+ title={t('Show all steps', { note: 'recipe.forge.expand_steps' })}
30
+ aria-label={t('Show all steps', { note: 'recipe.forge.expand_steps' })}
31
31
  onClick={() => setExpanded(true)}
32
32
  className="flex items-center rounded border border-dashed px-1.5 py-0.5 text-xs tabular-nums text-muted-foreground hover:bg-accent"
33
33
  >
@@ -41,7 +41,7 @@ export function StepTape({
41
41
  <li key={i} className="animate-in fade-in-0 zoom-in-95 duration-150">
42
42
  <button
43
43
  type="button"
44
- title={t('recipe.forge.trim_to')}
44
+ title={t('Trim to this step', { note: 'recipe.forge.trim_to' })}
45
45
  onClick={() => setApplied((prev) => prev.slice(0, i + 1))}
46
46
  className={cn(
47
47
  'flex items-center gap-1.5 rounded border px-1.5 py-0.5 text-xs tabular-nums hover:bg-accent',
@@ -67,3 +67,4 @@ export function StepTape({
67
67
  </ol>
68
68
  );
69
69
  }
70
+
@@ -1,11 +1,13 @@
1
1
  'use client';
2
+ import type { DatasetRef } from '@terrafirmagreg-team/sites/location';
2
3
 
3
4
  import { useEffect, useMemo, useReducer } from 'react';
4
- import { loadClientModAtlases } from '@terrafirmagreg-team/game/lib/dump/client/atlases';
5
- import { lookupSpriteInBundle } from '@terrafirmagreg-team/game/lib/dump/assets/atlas/lookup';
6
- import { atlasPagePublicUrl } from '@terrafirmagreg-team/game/lib/dump/assets/urls';
7
- import { splitDumpId } from '@terrafirmagreg-team/game/lib/dump/paths';
8
- import { createAsyncCache } from '@terrafirmagreg-team/ui/lib/utils/async-cache';
5
+ import { loadClientModAtlases } from '@terrafirmagreg-team/dump/lib/dump/client/atlases';
6
+ import { lookupSpriteInBundle } from '@terrafirmagreg-team/dump/lib/dump/assets/atlas/lookup';
7
+ import { atlasPagePublicUrl } from '@terrafirmagreg-team/dump/lib/dump/assets/urls';
8
+ import { splitDumpId } from '@terrafirmagreg-team/dump/lib/dump/paths';
9
+ import { createAsyncCache } from '@terrafirmagreg-team/data/lib/async-cache';
10
+ import { createDatasetCache } from '@terrafirmagreg-team/dump/lib/dump/reader';
9
11
  import type { RecipeSlotFill } from '@terrafirmagreg-team/recipe/lib/data/dto';
10
12
 
11
13
  const loadAtlasPage = createAsyncCache(
@@ -43,8 +45,8 @@ function displayColor(r: number, g: number, b: number): string {
43
45
  return `hsl(${Math.round(h * 360)} ${Math.round(sat * 100)}% ${Math.round(light * 100)}%)`;
44
46
  }
45
47
 
46
- const loadFluidColor = createAsyncCache(
47
- async (dataset: string, id: string): Promise<string | null> => {
48
+ const loadFluidColor = createDatasetCache(
49
+ async (dataset: DatasetRef, id: string): Promise<string | null> => {
48
50
  const parts = splitDumpId(id);
49
51
  if (!parts) return null;
50
52
  const bundle = await loadClientModAtlases(dataset, parts.ns);
@@ -84,11 +86,11 @@ const loadFluidColor = createAsyncCache(
84
86
  }
85
87
  return n ? displayColor(r / n, g / n, b / n) : null;
86
88
  },
87
- (dataset, id) => `${dataset}|${id}`,
89
+ (dataset, id) => `${dataset.id}/${dataset.version ?? ''}|${id}`,
88
90
  );
89
91
 
90
92
  export function useFluidColors(
91
- dataset: string,
93
+ dataset: DatasetRef,
92
94
  slots: readonly (RecipeSlotFill | { id: string; kind?: string })[],
93
95
  ): (string | undefined)[] {
94
96
  const ids = useMemo(
@@ -120,3 +122,4 @@ export function useFluidColors(
120
122
  id ? (loadFluidColor.peek(dataset, id) ?? undefined) : undefined,
121
123
  );
122
124
  }
125
+
@@ -143,7 +143,7 @@ export function useMixState(recipe: AlloyRecipe, session: RecipeSession) {
143
143
  );
144
144
  const output = recipe.recipe.outputs[0];
145
145
  const unitLabel =
146
- unit === 'ingot' ? t('recipe.alloy.unit.ingot') : t('recipe.alloy.unit.mb');
146
+ unit === 'ingot' ? t('ing.', { note: 'recipe.alloy.unit.ingot' }) : t('mB', { note: 'recipe.alloy.unit.mb' });
147
147
  const format = (value: number) =>
148
148
  value.toLocaleString(undefined, { maximumFractionDigits: 2 });
149
149
  const formatPercent = (value: number) =>
@@ -158,7 +158,7 @@ export function useMixState(recipe: AlloyRecipe, session: RecipeSession) {
158
158
  .join(', ');
159
159
  const copyText = `${recipe.components
160
160
  .map(({ slot }, i) => `${name(slot.id)} ${format(amounts[i] || 0)}`)
161
- .join(' + ')} → ${output ? name(output.id) : t('recipe.alloy.result')} ${format(
161
+ .join(' + ')} → ${output ? name(output.id) : t('Alloy', { note: 'recipe.alloy.result' })} ${format(
162
162
  mix.output,
163
163
  )} ${unitLabel}`;
164
164
  const fill = (target: number) => {
@@ -237,3 +237,4 @@ export function useMixState(recipe: AlloyRecipe, session: RecipeSession) {
237
237
  }
238
238
 
239
239
  export type MixState = ReturnType<typeof useMixState>;
240
+