@terrafirmagreg-team/recipe 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.
Files changed (221) hide show
  1. package/package.json +62 -3
  2. package/src/components/button-copy.tsx +43 -0
  3. package/src/components/cards/card/index.tsx +214 -0
  4. package/src/components/cards/card/scene.tsx +107 -0
  5. package/src/components/cards/card/strip.tsx +198 -0
  6. package/src/components/cards/card/text.tsx +46 -0
  7. package/src/components/cards/card/tips.tsx +63 -0
  8. package/src/components/cards/default-item.tsx +47 -0
  9. package/src/components/cards/glare-filter.tsx +29 -0
  10. package/src/components/cards/paged-card.tsx +222 -0
  11. package/src/components/cards/slot-hitbox.tsx +213 -0
  12. package/src/components/cards/strip-card/buttons.tsx +106 -0
  13. package/src/components/cards/strip-card/index.tsx +214 -0
  14. package/src/components/cards/strip-card/member-cell.tsx +167 -0
  15. package/src/components/cards/strip-card/sprite.tsx +36 -0
  16. package/src/components/embed-card.tsx +44 -0
  17. package/src/components/mdx/recipe.tsx +129 -0
  18. package/src/components/tool-list.tsx +49 -0
  19. package/src/components/tree.tsx +98 -0
  20. package/src/components/views/alloy/index.tsx +112 -0
  21. package/src/components/views/alloy/list.tsx +85 -0
  22. package/src/components/views/alloy/mix/index.tsx +35 -0
  23. package/src/components/views/alloy/mix/ratio.tsx +94 -0
  24. package/src/components/views/alloy/mix/rows.tsx +145 -0
  25. package/src/components/views/alloy/mix/status.tsx +122 -0
  26. package/src/components/views/alloy/mix/total.tsx +132 -0
  27. package/src/components/views/alloy/range-bar.tsx +65 -0
  28. package/src/components/views/alloy/stack-bar.tsx +88 -0
  29. package/src/components/views/browser/axis-switcher.tsx +54 -0
  30. package/src/components/views/browser/card-grid.tsx +275 -0
  31. package/src/components/views/browser/favorite-button.tsx +46 -0
  32. package/src/components/views/browser/frame.tsx +112 -0
  33. package/src/components/views/browser/index.tsx +114 -0
  34. package/src/components/views/browser/item-panel/index.tsx +229 -0
  35. package/src/components/views/browser/item-panel/skeleton.tsx +26 -0
  36. package/src/components/views/browser/item-panel-grid/cell.tsx +178 -0
  37. package/src/components/views/browser/item-panel-grid/favorite-cell.tsx +129 -0
  38. package/src/components/views/browser/item-panel-grid/index.tsx +105 -0
  39. package/src/components/views/browser/item-view/index.tsx +186 -0
  40. package/src/components/views/browser/item-view/tags.tsx +85 -0
  41. package/src/components/views/browser/item-view-header.tsx +131 -0
  42. package/src/components/views/browser/pane-tabs.tsx +61 -0
  43. package/src/components/views/browser/place-header.tsx +72 -0
  44. package/src/components/views/browser/query-controls.tsx +105 -0
  45. package/src/components/views/browser/recipe-item-header.tsx +60 -0
  46. package/src/components/views/browser/settings.tsx +52 -0
  47. package/src/components/views/browser/toolbar.tsx +54 -0
  48. package/src/components/views/browser/type-tabs.tsx +71 -0
  49. package/src/components/views/browser/type-view.tsx +184 -0
  50. package/src/components/views/browser/workstation-rail.tsx +43 -0
  51. package/src/components/views/calc/calculator/actions.tsx +47 -0
  52. package/src/components/views/calc/calculator/help.tsx +189 -0
  53. package/src/components/views/calc/calculator/index.tsx +172 -0
  54. package/src/components/views/calc/dot-overlay.tsx +80 -0
  55. package/src/components/views/calc/edge.tsx +152 -0
  56. package/src/components/views/calc/flow/index.tsx +305 -0
  57. package/src/components/views/calc/flow/picker-dialogs.tsx +146 -0
  58. package/src/components/views/calc/flow/toolbar.tsx +60 -0
  59. package/src/components/views/calc/flow/view-menu.tsx +117 -0
  60. package/src/components/views/calc/member-picker.tsx +120 -0
  61. package/src/components/views/calc/node-card/actions.tsx +90 -0
  62. package/src/components/views/calc/node-card/badges.tsx +59 -0
  63. package/src/components/views/calc/node-card/chips.tsx +187 -0
  64. package/src/components/views/calc/node-card/far.tsx +123 -0
  65. package/src/components/views/calc/node-card/full.tsx +332 -0
  66. package/src/components/views/calc/node-card/index.tsx +27 -0
  67. package/src/components/views/calc/node-card/menu.tsx +282 -0
  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 +207 -0
  72. package/src/components/views/calc/recipe-picker/footer.tsx +131 -0
  73. package/src/components/views/calc/recipe-picker/index.tsx +52 -0
  74. package/src/components/views/calc/sidebar.tsx +171 -0
  75. package/src/components/views/calc/trees/icon-picker.tsx +97 -0
  76. package/src/components/views/calc/trees/index.tsx +134 -0
  77. package/src/components/views/calc/trees/label.tsx +67 -0
  78. package/src/components/views/calc/trees/rows.tsx +140 -0
  79. package/src/components/views/forge/gui.tsx +131 -0
  80. package/src/components/views/forge/index.tsx +112 -0
  81. package/src/components/views/forge/list.tsx +113 -0
  82. package/src/components/views/forge/workbench/controls.tsx +103 -0
  83. package/src/components/views/forge/workbench/index.tsx +85 -0
  84. package/src/components/views/forge/workbench/panel.tsx +228 -0
  85. package/src/components/views/forge/workbench/ruler.tsx +133 -0
  86. package/src/components/views/forge/workbench/status.tsx +55 -0
  87. package/src/components/views/forge/workbench/tape.tsx +70 -0
  88. package/src/hooks/alloy/use-fluid-colors.ts +125 -0
  89. package/src/hooks/alloy/use-mix-state.ts +240 -0
  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/use-mount.ts +97 -0
  94. package/src/hooks/calc/flow/use-nodes.ts +260 -0
  95. package/src/hooks/calc/flow/use-pickers.ts +57 -0
  96. package/src/hooks/calc/flow/use-viewport.ts +141 -0
  97. package/src/hooks/calc/flow/use-window.ts +87 -0
  98. package/src/hooks/calc/use-actions.ts +355 -0
  99. package/src/hooks/calc/use-amounts.ts +64 -0
  100. package/src/hooks/calc/use-data.ts +203 -0
  101. package/src/hooks/calc/use-defaults.ts +40 -0
  102. package/src/hooks/calc/use-far-mode.ts +37 -0
  103. package/src/hooks/calc/use-focus.ts +31 -0
  104. package/src/hooks/calc/use-maps.ts +76 -0
  105. package/src/hooks/calc/use-reset.ts +15 -0
  106. package/src/hooks/calc/use-resolver.ts +70 -0
  107. package/src/hooks/calc/use-session.ts +13 -0
  108. package/src/hooks/calc/use-share.ts +64 -0
  109. package/src/hooks/calc/use-storage.ts +126 -0
  110. package/src/hooks/calc/use-tier.ts +13 -0
  111. package/src/hooks/calc/use-view-mode.ts +24 -0
  112. package/src/hooks/calc/use-worker.ts +204 -0
  113. package/src/hooks/forge/use-anvil-gui.ts +16 -0
  114. package/src/hooks/forge/use-forge-state.ts +135 -0
  115. package/src/hooks/forge/use-ruler.ts +100 -0
  116. package/src/hooks/use-add-to-calc.ts +51 -0
  117. package/src/hooks/use-categories-by-id.ts +18 -0
  118. package/src/hooks/use-favorite-hotkey.ts +39 -0
  119. package/src/hooks/use-favorites.ts +135 -0
  120. package/src/hooks/use-group-outlines.ts +65 -0
  121. package/src/hooks/use-hide-unused.ts +12 -0
  122. package/src/hooks/use-index.ts +72 -0
  123. package/src/hooks/use-item-feeds.ts +46 -0
  124. package/src/hooks/use-item-pane.ts +106 -0
  125. package/src/hooks/use-item-press.ts +89 -0
  126. package/src/hooks/use-item-tag-groups.ts +54 -0
  127. package/src/hooks/use-item-tooltips.ts +18 -0
  128. package/src/hooks/use-left-sidebar-tree.ts +34 -0
  129. package/src/hooks/use-mod-names.ts +17 -0
  130. package/src/hooks/use-mount-limit.ts +22 -0
  131. package/src/hooks/use-panel-cells.ts +117 -0
  132. package/src/hooks/use-panel-page.ts +54 -0
  133. package/src/hooks/use-panel-tabs.ts +43 -0
  134. package/src/hooks/use-query.ts +186 -0
  135. package/src/hooks/use-session.ts +88 -0
  136. package/src/hooks/use-sidebar-group.ts +16 -0
  137. package/src/hooks/use-tag-members.ts +16 -0
  138. package/src/hooks/use-tool-recipes.ts +26 -0
  139. package/src/hooks/use-type-filter.ts +73 -0
  140. package/src/lib/alloy/mix.ts +210 -0
  141. package/src/lib/alloy/parse.ts +37 -0
  142. package/src/lib/calc/defaults.ts +47 -0
  143. package/src/lib/calc/flow.ts +315 -0
  144. package/src/lib/calc/focus.ts +15 -0
  145. package/src/lib/calc/meta.ts +17 -0
  146. package/src/lib/calc/pack.ts +77 -0
  147. package/src/lib/calc/ports.ts +43 -0
  148. package/src/lib/calc/resolve.ts +198 -0
  149. package/src/lib/calc/route/buckets.ts +49 -0
  150. package/src/lib/calc/route/geometry.ts +51 -0
  151. package/src/lib/calc/route/index.ts +3 -0
  152. package/src/lib/calc/route/polyline.ts +103 -0
  153. package/src/lib/calc/route/router.ts +394 -0
  154. package/src/lib/calc/scheme.ts +226 -0
  155. package/src/lib/calc/storage/entry.ts +97 -0
  156. package/src/lib/calc/storage/index.ts +20 -0
  157. package/src/lib/calc/storage/keys.ts +38 -0
  158. package/src/lib/calc/storage/store.ts +198 -0
  159. package/src/lib/calc/tree/index.ts +4 -0
  160. package/src/lib/calc/tree/materials.ts +29 -0
  161. package/src/lib/calc/tree/recipe.ts +104 -0
  162. package/src/lib/calc/tree/share.ts +135 -0
  163. package/src/lib/calc/worker/api.ts +25 -0
  164. package/src/lib/calc/worker/fetch.ts +66 -0
  165. package/src/lib/calc/worker/flush.ts +39 -0
  166. package/src/lib/calc/worker/index.ts +8 -0
  167. package/src/lib/calc/worker/protocol.ts +35 -0
  168. package/src/lib/calc/worker/resolve.ts +49 -0
  169. package/src/lib/calc/worker/state.ts +48 -0
  170. package/src/lib/card-model.ts +65 -0
  171. package/src/lib/client/recipe-categories.ts +14 -0
  172. package/src/lib/client/recipe.ts +85 -0
  173. package/src/lib/client-index/catalog.ts +91 -0
  174. package/src/lib/client-index/chrome.ts +180 -0
  175. package/src/lib/client-index/index.ts +20 -0
  176. package/src/lib/client-index/registry.ts +121 -0
  177. package/src/lib/client-index/rows.ts +260 -0
  178. package/src/lib/client-index/tooltips.ts +74 -0
  179. package/src/lib/config.ts +18 -0
  180. package/src/lib/data/category/dto.ts +25 -0
  181. package/src/lib/data/category/load.ts +47 -0
  182. package/src/lib/data/category/parse.ts +71 -0
  183. package/src/lib/data/dto.ts +152 -0
  184. package/src/lib/data/grid.ts +40 -0
  185. package/src/lib/data/parse/entry.ts +85 -0
  186. package/src/lib/data/parse/index.ts +3 -0
  187. package/src/lib/data/parse/layout.ts +138 -0
  188. package/src/lib/data/parse/slots.ts +85 -0
  189. package/src/lib/data/parse/widgets.ts +81 -0
  190. package/src/lib/data/scene.ts +103 -0
  191. package/src/lib/data/slot-io.ts +44 -0
  192. package/src/lib/dump.ts +6 -0
  193. package/src/lib/embed.ts +22 -0
  194. package/src/lib/favorite-hover.ts +13 -0
  195. package/src/lib/fluid-outputs.ts +70 -0
  196. package/src/lib/forge/actions.ts +59 -0
  197. package/src/lib/forge/data.ts +51 -0
  198. package/src/lib/forge/solve.ts +80 -0
  199. package/src/lib/group.ts +23 -0
  200. package/src/lib/index-store.ts +70 -0
  201. package/src/lib/links.ts +131 -0
  202. package/src/lib/lookup.ts +119 -0
  203. package/src/lib/panel-outline.ts +86 -0
  204. package/src/lib/panel.ts +105 -0
  205. package/src/lib/preview.ts +32 -0
  206. package/src/lib/recipe-slots.ts +8 -0
  207. package/src/lib/search.ts +139 -0
  208. package/src/lib/sidebar.ts +47 -0
  209. package/src/lib/used-items.ts +25 -0
  210. package/src/locales/en_us.json +172 -0
  211. package/src/locales/ru_ru.json +172 -0
  212. package/src/locales.ts +6 -0
  213. package/src/types/alloy.ts +20 -0
  214. package/src/types/calc-flow-props.ts +44 -0
  215. package/src/types/calc.ts +92 -0
  216. package/src/types/forge.ts +21 -0
  217. package/src/types/index.ts +136 -0
  218. package/styles/calc.css +308 -0
  219. package/styles/strip.css +7 -0
  220. package/styles.css +2 -0
  221. package/README.md +0 -3
@@ -0,0 +1,119 @@
1
+ import type { DatasetRef } from '@terrafirmagreg-team/sites/location';
2
+ import { asStringList } from '@terrafirmagreg-team/dump/lib/dump/contract/read';
3
+ import type { RecipeCompactIndex, RecipeCompactRow } from '@terrafirmagreg-team/recipe/types';
4
+ import { registryId, splitDumpId } from '@terrafirmagreg-team/dump/lib/dump/paths';
5
+ import { resolveClientTag } from '@terrafirmagreg-team/dump/lib/dump/client/tags';
6
+ import {
7
+ loadRegistryEntrySlice,
8
+ loadTagSlice,
9
+ loadTypeRows,
10
+ resolveRecipeRows,
11
+ } from '@terrafirmagreg-team/recipe/lib/client-index';
12
+ import { stackIdFromRef } from '@terrafirmagreg-team/dump/lib/dump/contract/ref';
13
+ import { loadFluidIoIndex } from '@terrafirmagreg-team/recipe/lib/fluid-outputs';
14
+
15
+ export { registryId, tagPseudoId } from '@terrafirmagreg-team/dump/lib/dump/paths';
16
+
17
+ function uniqueRows(rows: RecipeCompactRow[]): RecipeCompactRow[] {
18
+ const seen = new Set<string>();
19
+ return rows.filter((row) => {
20
+ if (seen.has(row.id)) return false;
21
+ seen.add(row.id);
22
+ return true;
23
+ });
24
+ }
25
+
26
+ const ALL_KINDS = ['item', 'block', 'fluid', 'entity'];
27
+
28
+ async function entryEdges(
29
+ dataset: DatasetRef,
30
+ kinds: string[],
31
+ id: string,
32
+ field: 'usedBy' | 'producedBy',
33
+ ): Promise<string[]> {
34
+ const edges = await Promise.all(
35
+ kinds.map((kind) =>
36
+ loadRegistryEntrySlice(dataset, kind, id).then((entry) =>
37
+ asStringList(entry?.[field]),
38
+ ),
39
+ ),
40
+ );
41
+ return [...new Set(edges.flat())];
42
+ }
43
+
44
+ export async function recipesForOutput(
45
+ dataset: DatasetRef,
46
+ index: RecipeCompactIndex,
47
+ item: string,
48
+ opts?: { includeTagRows?: boolean },
49
+ ): Promise<RecipeCompactRow[]> {
50
+ const base = registryId(item);
51
+ const [refs, io] = await Promise.all([
52
+ entryEdges(dataset, ALL_KINDS, base, 'producedBy'),
53
+ index.fluids?.includes(base)
54
+ ? loadFluidIoIndex(dataset, index).catch(() => undefined)
55
+ : undefined,
56
+ ]);
57
+ const extra = io?.outputs.get(base) ?? [];
58
+ const ids = extra.length ? [...new Set([...refs, ...extra])] : refs;
59
+ return resolveRecipeRows(dataset, index, ids, opts);
60
+ }
61
+
62
+ export async function usesForInput(
63
+ dataset: DatasetRef,
64
+ index: RecipeCompactIndex,
65
+ item: string,
66
+ kind: string = 'item',
67
+ opts?: { includeTagRows?: boolean },
68
+ ): Promise<RecipeCompactRow[]> {
69
+ const base = registryId(item);
70
+ const ns = splitDumpId(base)?.ns;
71
+ if (!ns) return [];
72
+ const tagKinds = [...new Set(kind === 'item' ? ['item', 'block'] : [kind])];
73
+ const stationTypes = (index.categories ?? [])
74
+ .filter((cat) =>
75
+ (cat.workstations ?? []).some(
76
+ (ref) => registryId(stackIdFromRef(ref)) === base,
77
+ ),
78
+ )
79
+ .map((cat) => cat.id);
80
+ const [refs, io, stationRows, ...tagSlices] = await Promise.all([
81
+ entryEdges(dataset, ALL_KINDS, base, 'usedBy'),
82
+ index.fluids?.includes(base)
83
+ ? loadFluidIoIndex(dataset, index).catch(() => undefined)
84
+ : undefined,
85
+ Promise.all(
86
+ stationTypes.map((typeId) => loadTypeRows(dataset, index, typeId)),
87
+ ),
88
+ ...tagKinds.map((tagKind) => loadTagSlice(dataset, tagKind, ns)),
89
+ ]);
90
+ const tagKeys: string[] = [];
91
+ for (const [i, slice] of tagSlices.entries()) {
92
+ const registry = `${tagKinds[i]}s`;
93
+ for (const tag of slice[base] ?? slice[item] ?? []) {
94
+ const raw = tag.startsWith('#') ? tag.slice(1) : tag;
95
+ const parts = splitDumpId(raw);
96
+ if (parts) tagKeys.push(`${parts.ns}:${registry}/${parts.path}`);
97
+ }
98
+ }
99
+ const tagRefs = (
100
+ await Promise.all(
101
+ [...new Set(tagKeys)].map(async (key) => {
102
+ const parts = splitDumpId(key);
103
+ const tag = parts
104
+ ? await resolveClientTag(dataset, `${parts.ns}:${parts.path}`)
105
+ : undefined;
106
+ return tag?.usedBy ?? [];
107
+ }),
108
+ )
109
+ ).flat();
110
+ const ioRefs = io?.inputs.get(base) ?? [];
111
+ const allRefs = ioRefs.length
112
+ ? [...new Set([...refs, ...ioRefs])]
113
+ : refs;
114
+ return uniqueRows([
115
+ ...(await resolveRecipeRows(dataset, index, [...allRefs, ...tagRefs], opts)),
116
+ ...stationRows.flat(),
117
+ ]);
118
+ }
119
+
@@ -0,0 +1,86 @@
1
+ type OutlineRect = { x: number; y: number; w: number; h: number };
2
+
3
+ type GroupOutline = {
4
+ groupId: string;
5
+ d: string;
6
+ };
7
+
8
+ const ptKey = (x: number, y: number) => `${x.toFixed(2)},${y.toFixed(2)}`;
9
+ const edgeKey = (x1: number, y1: number, x2: number, y2: number) =>
10
+ `${ptKey(x1, y1)}>${ptKey(x2, y2)}`;
11
+
12
+ export function groupOutlines(
13
+ indicesByGroup: Map<string, number[]>,
14
+ cols: number,
15
+ cell: OutlineRect,
16
+ gap: number,
17
+ bounds?: { w: number; h: number },
18
+ stroke = 0,
19
+ ): GroupOutline[] {
20
+ const pad = gap / 2;
21
+ const inset = stroke / 2;
22
+ const maxX = (bounds?.w ?? Number.POSITIVE_INFINITY) - inset;
23
+ const maxY = (bounds?.h ?? Number.POSITIVE_INFINITY) - inset;
24
+ const out: GroupOutline[] = [];
25
+ for (const [groupId, indices] of indicesByGroup) {
26
+ const cells = new Set(
27
+ indices.map((i) => `${i % cols},${Math.floor(i / cols)}`),
28
+ );
29
+ const segs: [number, number, number, number][] = [];
30
+ for (const i of indices) {
31
+ const col = i % cols;
32
+ const row = Math.floor(i / cols);
33
+ const x = Math.max(cell.x + col * (cell.w + gap) - pad, inset);
34
+ const y = Math.max(cell.y + row * (cell.h + gap) - pad, inset);
35
+ const w =
36
+ Math.min(cell.x + col * (cell.w + gap) + cell.w + pad, maxX) - x;
37
+ const h =
38
+ Math.min(cell.y + row * (cell.h + gap) + cell.h + pad, maxY) - y;
39
+ if (!cells.has(`${col},${row - 1}`)) segs.push([x, y, x + w, y]);
40
+ if (!cells.has(`${col},${row + 1}`)) segs.push([x, y + h, x + w, y + h]);
41
+ if (!cells.has(`${col - 1},${row}`)) segs.push([x, y, x, y + h]);
42
+ if (!cells.has(`${col + 1},${row}`)) segs.push([x + w, y, x + w, y + h]);
43
+ }
44
+ const adj = new Map<string, { x: number; y: number }[]>();
45
+ for (const [x1, y1, x2, y2] of segs) {
46
+ let a = adj.get(ptKey(x1, y1));
47
+ if (!a) adj.set(ptKey(x1, y1), (a = []));
48
+ a.push({ x: x2, y: y2 });
49
+ let b = adj.get(ptKey(x2, y2));
50
+ if (!b) adj.set(ptKey(x2, y2), (b = []));
51
+ b.push({ x: x1, y: y1 });
52
+ }
53
+ const used = new Set<string>();
54
+ for (const [x1, y1, x2, y2] of segs) {
55
+ const forward = edgeKey(x1, y1, x2, y2);
56
+ if (used.has(forward)) continue;
57
+ used.add(forward);
58
+ used.add(edgeKey(x2, y2, x1, y1));
59
+ const loop: [number, number][] = [
60
+ [x1, y1],
61
+ [x2, y2],
62
+ ];
63
+ let cx = x2;
64
+ let cy = y2;
65
+ while (cx !== x1 || cy !== y1) {
66
+ const next = (adj.get(ptKey(cx, cy)) ?? []).find(
67
+ (p) => !used.has(edgeKey(cx, cy, p.x, p.y)),
68
+ );
69
+ if (!next) break;
70
+ used.add(edgeKey(cx, cy, next.x, next.y));
71
+ used.add(edgeKey(next.x, next.y, cx, cy));
72
+ loop.push([next.x, next.y]);
73
+ cx = next.x;
74
+ cy = next.y;
75
+ }
76
+ if (loop.length > 2) {
77
+ out.push({
78
+ groupId,
79
+ d: `M${loop.map(([x, y]) => `${x.toFixed(2)} ${y.toFixed(2)}`).join(' L')} Z`,
80
+ });
81
+ }
82
+ }
83
+ }
84
+ return out;
85
+ }
86
+
@@ -0,0 +1,105 @@
1
+ import {
2
+ stackIdFromRef,
3
+ type TaggedReference,
4
+ } from '@terrafirmagreg-team/dump/lib/dump/contract/ref';
5
+ import { registryId } from '@terrafirmagreg-team/dump/lib/dump/paths';
6
+ import { recipeItemName } from '@terrafirmagreg-team/recipe/lib/client-index';
7
+ import type { Lang } from '@terrafirmagreg-team/translate/types/locale';
8
+ import type {
9
+ ItemPanelCell,
10
+ RecipeCompactIndex,
11
+ RecipeStackGroup,
12
+ } from '@terrafirmagreg-team/recipe/types';
13
+
14
+ function catalogPool(index: RecipeCompactIndex, tab?: string): string[] {
15
+ if (!tab) return index.items;
16
+ return index.tabItems?.[tab] ?? [];
17
+ }
18
+
19
+ export type ItemPanelMode = 'all' | 'fav';
20
+
21
+ type ItemPanelTabRow = {
22
+ id: string;
23
+ icon: string;
24
+ iconRef?: TaggedReference;
25
+ help: string;
26
+ };
27
+
28
+ export function panelPool(
29
+ mode: ItemPanelMode,
30
+ favoriteIds: string[],
31
+ index: RecipeCompactIndex | null,
32
+ tab?: string,
33
+ ): string[] {
34
+ if (!index) return [];
35
+ return mode === 'fav' ? favoriteIds : catalogPool(index, tab);
36
+ }
37
+
38
+ export function buildPanelCells(
39
+ ids: readonly string[],
40
+ groups: RecipeStackGroup[] | undefined,
41
+ expandedGroups: ReadonlySet<string>,
42
+ ): ItemPanelCell[] {
43
+ if (!groups?.length) return ids.map((id) => ({ kind: 'stack', id }));
44
+ const byMember = new Map<string, RecipeStackGroup>();
45
+ for (const group of groups)
46
+ for (const member of group.members) {
47
+ const base = registryId(member);
48
+ if (!byMember.has(base)) byMember.set(base, group);
49
+ }
50
+ const inPool = new Set(ids.map(registryId));
51
+ const seen = new Set<string>();
52
+ const out: ItemPanelCell[] = [];
53
+ for (const id of ids) {
54
+ const base = registryId(id);
55
+ if (seen.has(base)) continue;
56
+ seen.add(base);
57
+ const group = byMember.get(base);
58
+ if (!group) {
59
+ out.push({ kind: 'stack', id });
60
+ continue;
61
+ }
62
+ const members = group.members.filter((member) =>
63
+ inPool.has(registryId(member)),
64
+ );
65
+ for (const member of members) seen.add(registryId(member));
66
+ if (members.length <= 1) {
67
+ out.push({ kind: 'stack', id: members[0] ?? id });
68
+ continue;
69
+ }
70
+ const expanded = expandedGroups.has(group.id);
71
+ out.push({
72
+ kind: 'group',
73
+ id: group.id,
74
+ label: group.name ?? group.id,
75
+ members,
76
+ expanded,
77
+ });
78
+ if (expanded)
79
+ for (const member of members)
80
+ out.push({ kind: 'stack', id: member, groupId: group.id });
81
+ }
82
+ return out;
83
+ }
84
+
85
+ export function panelTabRows(
86
+ index: RecipeCompactIndex | null,
87
+ names: Record<string, Record<string, string>>,
88
+ lang: Lang,
89
+ ): ItemPanelTabRow[] {
90
+ const rows: ItemPanelTabRow[] = [];
91
+ for (const row of index?.tabs ?? []) {
92
+ const icon =
93
+ (row.icon ? stackIdFromRef(row.icon) : undefined) ??
94
+ index?.tabItems?.[row.id]?.[0];
95
+ if (!icon) continue;
96
+ rows.push({
97
+ id: row.id,
98
+ icon,
99
+ iconRef: row.icon,
100
+ help: recipeItemName(names, lang, row.display.name ?? row.id),
101
+ });
102
+ }
103
+ return rows;
104
+ }
105
+
@@ -0,0 +1,32 @@
1
+ import { registerPreviewResolver } from '@terrafirmagreg-team/preview/lib';
2
+ import { clientDumpLang } from '@terrafirmagreg-team/dump/lib/dump/client/dump';
3
+ import { loadDatasetRecipeCategories } from '@terrafirmagreg-team/recipe/lib/data/category/load';
4
+ import { langString } from '@terrafirmagreg-team/dump/lib/dump/assets/lang/names';
5
+ import { entryPreviewIcon } from '@terrafirmagreg-team/game/lib/preview';
6
+ import type { PreviewResolverCtx } from '@terrafirmagreg-team/preview/lib';
7
+ import type { PagePreviewData } from '@terrafirmagreg-team/ui/types/preview';
8
+
9
+ export async function recipeLinkPreview({
10
+ parts,
11
+ query,
12
+ langMap,
13
+ }: PreviewResolverCtx): Promise<PagePreviewData | undefined> {
14
+ const dataset = {
15
+ id: parts[1] ?? '',
16
+ version: query.get('version') ?? undefined,
17
+ };
18
+ if (!dataset.id) return undefined;
19
+ const type = query.get('type');
20
+ if (!type) return { title: 'Recipes' };
21
+ const categories = await loadDatasetRecipeCategories(dataset);
22
+ const row = categories.find((category) => category.id === type);
23
+ if (!row) return { title: type };
24
+ return {
25
+ title:
26
+ (row.display?.name && langString(langMap, row.display.name)) || row.id,
27
+ icon: entryPreviewIcon(row.icon, dataset),
28
+ };
29
+ }
30
+
31
+ registerPreviewResolver('recipe', recipeLinkPreview, { langMap: clientDumpLang });
32
+
@@ -0,0 +1,8 @@
1
+ import type { Recipe, RecipeSlotWidget } from '@terrafirmagreg-team/recipe/lib/data/dto';
2
+
3
+ export function recipeSlots(recipe: Recipe): RecipeSlotWidget[] {
4
+ return recipe.widgets.filter(
5
+ (widget): widget is RecipeSlotWidget => widget.type === 'slot',
6
+ );
7
+ }
8
+
@@ -0,0 +1,139 @@
1
+ import { recipeItemName } from '@terrafirmagreg-team/recipe/lib/client-index';
2
+
3
+ function matchesResolvedLabel(
4
+ id: string,
5
+ label: string,
6
+ needle: string,
7
+ ): boolean {
8
+ const q = needle.trim().toLowerCase();
9
+ if (!q) return true;
10
+ if (label.toLowerCase().includes(q)) return true;
11
+ const lower = id.toLowerCase();
12
+ return lower === q || lower.endsWith(`:${q}`) || lower.endsWith(`/${q}`);
13
+ }
14
+
15
+ type ItemSearchContext = {
16
+ names: Record<string, Record<string, string>>;
17
+ lang: string;
18
+ tagMembers?: Record<string, string[]>;
19
+ mods?: Record<string, string>;
20
+ tooltips?: Record<string, string[]>;
21
+ };
22
+
23
+ type SearchToken = {
24
+ negated: boolean;
25
+ kind: 'name' | 'mod' | 'tag' | 'tooltip';
26
+ needle: string;
27
+ regex?: RegExp;
28
+ memberSet?: Set<string>;
29
+ };
30
+
31
+ const TOKEN_RE = /-?[@#$]?(?:"[^"]*"|\/(?:\\.|[^\\/])*\/|[^\s]+)/g;
32
+
33
+ function tokenize(query: string): SearchToken[][] {
34
+ return query
35
+ .split('|')
36
+ .map((branch) =>
37
+ (branch.match(TOKEN_RE) ?? [])
38
+ .flatMap((raw): SearchToken[] => {
39
+ if (!raw) return [];
40
+ const negated = raw.startsWith('-');
41
+ let token = negated ? raw.slice(1) : raw;
42
+ let kind: SearchToken['kind'] = 'name';
43
+ const prefix = token.charAt(0);
44
+ if (prefix === '@' || prefix === '#' || prefix === '$') {
45
+ kind = prefix === '@' ? 'mod' : prefix === '#' ? 'tag' : 'tooltip';
46
+ token = token.slice(1);
47
+ }
48
+ if (
49
+ token.length > 1 &&
50
+ token.startsWith('"') &&
51
+ token.endsWith('"')
52
+ ) {
53
+ token = token.slice(1, -1);
54
+ } else if (
55
+ token.length > 1 &&
56
+ token.startsWith('/') &&
57
+ token.endsWith('/')
58
+ ) {
59
+ try {
60
+ return [
61
+ {
62
+ negated,
63
+ kind,
64
+ needle: '',
65
+ regex: new RegExp(token.slice(1, -1), 'i'),
66
+ },
67
+ ];
68
+ } catch {}
69
+ }
70
+ const needle = token.trim().toLowerCase();
71
+ return needle ? [{ negated, kind, needle }] : [];
72
+ }),
73
+ )
74
+ .filter((branch) => branch.length > 0);
75
+ }
76
+
77
+ function tagMemberSet(token: SearchToken, ctx: ItemSearchContext): Set<string> {
78
+ const set = new Set<string>();
79
+ const test = (value: string) =>
80
+ token.regex
81
+ ? token.regex.test(value)
82
+ : value.toLowerCase().includes(token.needle);
83
+ for (const [tag, ids] of Object.entries(ctx.tagMembers ?? {})) {
84
+ if (test(tag) || test(recipeItemName(ctx.names, ctx.lang, tag))) {
85
+ for (const id of ids) set.add(id);
86
+ }
87
+ }
88
+ return set;
89
+ }
90
+
91
+ function matchToken(
92
+ token: SearchToken,
93
+ id: string,
94
+ label: string,
95
+ ctx: ItemSearchContext,
96
+ ): boolean {
97
+ const test = (value: string) =>
98
+ token.regex
99
+ ? token.regex.test(value)
100
+ : value.toLowerCase().includes(token.needle);
101
+ switch (token.kind) {
102
+ case 'mod': {
103
+ const ns = id.split(':')[0] ?? '';
104
+ const modName = (ctx.mods?.[ns] ?? '').toLowerCase().replace(/\s+/g, '');
105
+ return test(modName) || test(ns);
106
+ }
107
+ case 'tag': {
108
+ token.memberSet ??= tagMemberSet(token, ctx);
109
+ return token.memberSet.has(id);
110
+ }
111
+ case 'tooltip':
112
+ return (ctx.tooltips?.[id] ?? []).some((line) => test(line));
113
+ default:
114
+ if (token.regex) return token.regex.test(label) || token.regex.test(id);
115
+ return matchesResolvedLabel(id, label, token.needle);
116
+ }
117
+ }
118
+
119
+ function matchesGameItemSearch(
120
+ id: string,
121
+ ctx: ItemSearchContext,
122
+ groups: SearchToken[][],
123
+ ): boolean {
124
+ const label = recipeItemName(ctx.names, ctx.lang, id);
125
+ return groups.some((branch) =>
126
+ branch.every((token) => matchToken(token, id, label, ctx) !== token.negated),
127
+ );
128
+ }
129
+
130
+ export function filterCatalogItemIds(
131
+ ids: string[],
132
+ query: string,
133
+ ctx: ItemSearchContext,
134
+ ): string[] {
135
+ const groups = tokenize(query);
136
+ if (!groups.length) return ids;
137
+ return ids.filter((id) => matchesGameItemSearch(id, ctx, groups));
138
+ }
139
+
@@ -0,0 +1,47 @@
1
+ import type * as PageTree from 'fumadocs-core/page-tree';
2
+
3
+ function typeParamFromRecipeUrl(url: string | undefined): string | undefined {
4
+ if (!url) return undefined;
5
+ const query = url.split('?')[1];
6
+ if (!query) return undefined;
7
+ return new URLSearchParams(query).get('type') ?? undefined;
8
+ }
9
+
10
+ function collectRecipeLeaves(nodes: PageTree.Node[]): PageTree.Item[] {
11
+ const out: PageTree.Item[] = [];
12
+ for (const node of nodes) {
13
+ if (node.type === 'page') out.push(node);
14
+ else if (node.type === 'folder')
15
+ out.push(...collectRecipeLeaves(node.children));
16
+ }
17
+ return out;
18
+ }
19
+
20
+ export function groupRecipeSidebarByMod(
21
+ tree: PageTree.Root,
22
+ modNames: Record<string, string>,
23
+ ): PageTree.Root {
24
+ const leaves = collectRecipeLeaves(tree.children);
25
+ if (!leaves.length) return tree;
26
+ const byMod: Record<string, PageTree.Item[]> = {};
27
+ for (const leaf of leaves) {
28
+ const mod = typeParamFromRecipeUrl(leaf.url)?.split(':', 1)[0] ?? '_';
29
+ const list = byMod[mod];
30
+ if (list) list.push(leaf);
31
+ else byMod[mod] = [leaf];
32
+ }
33
+ const folders: PageTree.Folder[] = Object.entries(byMod)
34
+ .map(([mod, children]) => ({
35
+ type: 'folder' as const,
36
+ name: modNames[mod] ?? mod,
37
+ children,
38
+ $id: `mod/${mod}`,
39
+ }))
40
+ .sort((a, b) => String(a.name).localeCompare(String(b.name)));
41
+ return {
42
+ name: tree.name,
43
+ children: folders,
44
+ $id: `${tree.$id}:mod`,
45
+ };
46
+ }
47
+
@@ -0,0 +1,25 @@
1
+ import { registryId } from '@terrafirmagreg-team/recipe/lib/lookup';
2
+ import type { RecipeCompactIndex, UsedItems } from '@terrafirmagreg-team/recipe/types';
3
+
4
+ export function recipeUsedItems(index: RecipeCompactIndex | null): UsedItems {
5
+ const ids = new Set<string>();
6
+ const bases = new Set<string>();
7
+ for (const id of index?.usedIds ?? []) {
8
+ ids.add(id);
9
+ bases.add(registryId(id));
10
+ }
11
+ return { ids, bases };
12
+ }
13
+
14
+ function isRecipeUsed(id: string, used: UsedItems): boolean {
15
+ return used.ids.has(id) || used.bases.has(registryId(id));
16
+ }
17
+
18
+ export function filterUsedIds(ids: string[], used: UsedItems): string[] {
19
+ return ids.filter((id) => isRecipeUsed(id, used));
20
+ }
21
+
22
+ export function hasRecipeUsage(used: UsedItems): boolean {
23
+ return used.ids.size > 0;
24
+ }
25
+