@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
@@ -0,0 +1,186 @@
1
+ 'use client';
2
+
3
+ import type { DatasetRef } from '@terrafirmagreg-team/sites/location';
4
+ import {
5
+ Handle,
6
+ Position,
7
+ } from '@terrafirmagreg-team/ui/lib/canvas/xyflow';
8
+ import { Entry } from '@terrafirmagreg-team/game/components/entry/entry';
9
+ import {
10
+ ContextMenu,
11
+ ContextMenuTrigger,
12
+ } from '@terrafirmagreg-team/ui/components/ui/context-menu';
13
+ import { recipeItemName } from '@terrafirmagreg-team/recipe/lib/client-index';
14
+ import { cn } from '@terrafirmagreg-team/ui/lib/utils/cn';
15
+ import type { CalcFlowData } from '@terrafirmagreg-team/recipe/lib/calc/flow';
16
+ import type {
17
+ CalcNode,
18
+ CalcNodeInput,
19
+ CalcOutput,
20
+ } from '@terrafirmagreg-team/recipe/types/calc';
21
+ import type { Lang } from '@terrafirmagreg-team/translate/types/locale';
22
+ import { PortContextMenu } from './menu';
23
+ import { formatPortCount, PortCount } from './ports';
24
+
25
+ interface CellProps {
26
+ index: number;
27
+ node: CalcNode;
28
+ data: CalcFlowData;
29
+ dataset: DatasetRef;
30
+ lang: Lang;
31
+ langMap?: Record<string, string>;
32
+ names: Record<string, Record<string, string>>;
33
+ connectable: boolean;
34
+ }
35
+
36
+ export function OutputCell({
37
+ out,
38
+ index,
39
+ node,
40
+ data,
41
+ dataset,
42
+ lang,
43
+ langMap,
44
+ names,
45
+ connectable,
46
+ onOpen,
47
+ }: CellProps & {
48
+ out: CalcOutput;
49
+ onOpen?: () => void;
50
+ }) {
51
+ const name = recipeItemName(names, lang, out.id);
52
+ const count = out.remaining;
53
+ const qty = formatPortCount(count, out.kind);
54
+ return (
55
+ <ContextMenu>
56
+ <ContextMenuTrigger
57
+ onContextMenu={(e) => e.stopPropagation()}
58
+ render={
59
+ <Handle
60
+ type="source"
61
+ id={index === 0 ? 'out' : `out-${index}`}
62
+ position={Position.Right}
63
+ isConnectable={connectable}
64
+ isConnectableEnd={connectable}
65
+ className={cn(
66
+ 'calc-port calc-port--out',
67
+ out.remaining < 0 && 'calc-port--over',
68
+ )}
69
+ onClick={(e) => {
70
+ e.stopPropagation();
71
+ if (index === 0) onOpen?.();
72
+ }}
73
+ >
74
+ <span className="calc-port-inner">
75
+ <Entry
76
+ id={out.id}
77
+ kind={out.kind}
78
+ dataset={dataset}
79
+ lang={lang}
80
+ langMap={langMap}
81
+ name={`${name} ×${qty}`}
82
+ size="sm"
83
+ href={false}
84
+ tooltip="adaptive"
85
+ />
86
+ <PortCount value={count} kind={out.kind} />
87
+ </span>
88
+ </Handle>
89
+ }
90
+ />
91
+ <PortContextMenu
92
+ data={data}
93
+ node={node}
94
+ out={index}
95
+ id={out.id}
96
+ kind={out.kind}
97
+ dataset={dataset}
98
+ lang={lang}
99
+ langMap={langMap}
100
+ />
101
+ </ContextMenu>
102
+ );
103
+ }
104
+
105
+ export function InputCell({
106
+ inp,
107
+ index,
108
+ node,
109
+ data,
110
+ dataset,
111
+ lang,
112
+ langMap,
113
+ names,
114
+ connectable,
115
+ }: CellProps & {
116
+ inp: CalcNodeInput;
117
+ }) {
118
+ const id = inp.id;
119
+ const name = id ? recipeItemName(names, lang, id) : (inp.tagId ?? '?');
120
+ const uncovered = Math.max(0, inp.amount - inp.covered);
121
+ const qty = formatPortCount(uncovered, inp.kind);
122
+ const state = inp.wired
123
+ ? uncovered <= 0
124
+ ? 'full'
125
+ : 'partial'
126
+ : 'open';
127
+ return (
128
+ <ContextMenu>
129
+ <ContextMenuTrigger
130
+ onContextMenu={(e) => e.stopPropagation()}
131
+ render={
132
+ <Handle
133
+ type="target"
134
+ id={`in-${index}`}
135
+ position={Position.Left}
136
+ isConnectable={connectable}
137
+ isConnectableEnd={connectable}
138
+ className={cn(
139
+ 'calc-port',
140
+ 'calc-port--in',
141
+ inp.catalyst && 'calc-port--cat',
142
+ `calc-port--${state}`,
143
+ )}
144
+ onClick={(e) => {
145
+ e.stopPropagation();
146
+ if (id) data.ensureMaterial(id);
147
+ data.onRequestSupply(node, index);
148
+ }}
149
+ >
150
+ <span className="calc-port-inner">
151
+ {id ? (
152
+ <Entry
153
+ id={id}
154
+ kind={inp.kind}
155
+ dataset={dataset}
156
+ lang={lang}
157
+ langMap={langMap}
158
+ name={`${name} ×${qty}`}
159
+ size="sm"
160
+ href={false}
161
+ tooltip="adaptive"
162
+ />
163
+ ) : (
164
+ <span className="text-[9px]">?</span>
165
+ )}
166
+ <PortCount value={uncovered} kind={inp.kind} />
167
+ </span>
168
+ </Handle>
169
+ }
170
+ />
171
+ {id ? (
172
+ <PortContextMenu
173
+ data={data}
174
+ node={node}
175
+ input={index}
176
+ id={id}
177
+ kind={inp.kind}
178
+ dataset={dataset}
179
+ lang={lang}
180
+ langMap={langMap}
181
+ />
182
+ ) : null}
183
+ </ContextMenu>
184
+ );
185
+ }
186
+
@@ -0,0 +1,251 @@
1
+ 'use client';
2
+
3
+ import type { DatasetRef } from '@terrafirmagreg-team/sites/location';
4
+ import {
5
+ useCallback,
6
+ useEffect,
7
+ useLayoutEffect,
8
+ useState,
9
+ } from 'react';
10
+ import { ChevronDown, ChevronUp } from 'lucide-react';
11
+ import { useUpdateNodeInternals } from '@terrafirmagreg-team/ui/lib/canvas/xyflow';
12
+ import {
13
+ PORT_STEP,
14
+ railSpans,
15
+ } from '@terrafirmagreg-team/recipe/lib/calc/ports';
16
+ import type { CalcFlowData } from '@terrafirmagreg-team/recipe/lib/calc/flow';
17
+ import type { CalcNode } from '@terrafirmagreg-team/recipe/types/calc';
18
+ import type { Lang } from '@terrafirmagreg-team/translate/types/locale';
19
+ import { GhostHandle } from './ports';
20
+ import { InputCell, OutputCell } from './port-cells';
21
+
22
+ function useRailWindow(total: number, nodeId: string, side: 'in' | 'out') {
23
+ const [el, setEl] = useState<HTMLDivElement | null>(null);
24
+ const [rows, setRows] = useState(0);
25
+ const [offset, setOffset] = useState(0);
26
+ const updateNodeInternals = useUpdateNodeInternals();
27
+ useLayoutEffect(() => {
28
+ if (!el) return;
29
+ const measure = () =>
30
+ setRows(Math.max(1, Math.floor(el.clientHeight / PORT_STEP)));
31
+ measure();
32
+ const ro = new ResizeObserver(measure);
33
+ ro.observe(el);
34
+ return () => ro.disconnect();
35
+ }, [el]);
36
+ const paged = total > rows;
37
+ const start = Math.min(offset, Math.max(0, total - 1));
38
+ let end = total;
39
+ if (paged) {
40
+ end = start + rows;
41
+ for (let i = 0; i < 3; i += 1) {
42
+ const marks = (start > 0 ? 1 : 0) + (end < total ? 1 : 0);
43
+ const next = Math.min(total, start + Math.max(1, rows - marks));
44
+ if (next === end) break;
45
+ end = next;
46
+ }
47
+ }
48
+ const above = paged ? start : 0;
49
+ const below = paged ? total - end : 0;
50
+ const scroll = useCallback(
51
+ (dir: number) =>
52
+ setOffset((cur) => Math.min(Math.max(0, cur + dir), total - 1)),
53
+ [total],
54
+ );
55
+ const onWheel = useCallback(
56
+ (e: React.WheelEvent) => {
57
+ if (!paged) return;
58
+ scroll(e.deltaY > 0 ? 1 : -1);
59
+ },
60
+ [paged, scroll],
61
+ );
62
+ useEffect(() => {
63
+ updateNodeInternals(nodeId);
64
+ }, [nodeId, updateNodeInternals, start, end, rows, total]);
65
+ useEffect(() => {
66
+ railSpans.set(nodeId, {
67
+ ...railSpans.get(nodeId),
68
+ ...(side === 'in'
69
+ ? { inStart: start, inRows: rows }
70
+ : { outStart: start, outRows: rows }),
71
+ });
72
+ }, [nodeId, side, start, rows]);
73
+ return { ref: setEl, start, end, above, below, paged, scroll, onWheel };
74
+ }
75
+
76
+ interface RailProps {
77
+ node: CalcNode;
78
+ data: CalcFlowData;
79
+ dataset: DatasetRef;
80
+ lang: Lang;
81
+ langMap?: Record<string, string>;
82
+ names: Record<string, Record<string, string>>;
83
+ connectable?: boolean;
84
+ }
85
+
86
+ export function PortRailIn({
87
+ node,
88
+ data,
89
+ dataset,
90
+ lang,
91
+ langMap,
92
+ names,
93
+ connectable = true,
94
+ }: RailProps) {
95
+ const {
96
+ ref,
97
+ start,
98
+ end,
99
+ above,
100
+ below,
101
+ scroll,
102
+ onWheel,
103
+ } = useRailWindow(node.inputs.length, node.id, 'in');
104
+ if (!node.inputs.length) return null;
105
+ const slice = node.inputs.slice(start, end);
106
+ return (
107
+ <div
108
+ ref={ref}
109
+ className="calc-rail calc-rail--in nodrag nowheel"
110
+ onWheel={onWheel}
111
+ >
112
+ {above > 0 ? (
113
+ <button
114
+ type="button"
115
+ className="calc-port calc-port--more"
116
+ onClick={(e) => {
117
+ e.stopPropagation();
118
+ scroll(-1);
119
+ }}
120
+ >
121
+ {node.inputs.slice(0, above).map((inp, i) => (
122
+ <GhostHandle key={i} type="target" id={`in-${i}`} />
123
+ ))}
124
+ <ChevronUp className="size-2.5" />
125
+ <span>+{above}</span>
126
+ </button>
127
+ ) : null}
128
+ {slice.map((inp, i) => (
129
+ <InputCell
130
+ key={start + i}
131
+ inp={inp}
132
+ index={start + i}
133
+ node={node}
134
+ data={data}
135
+ dataset={dataset}
136
+ lang={lang}
137
+ langMap={langMap}
138
+ names={names}
139
+ connectable={connectable}
140
+ />
141
+ ))}
142
+ {below > 0 ? (
143
+ <button
144
+ type="button"
145
+ className="calc-port calc-port--more"
146
+ onClick={(e) => {
147
+ e.stopPropagation();
148
+ scroll(1);
149
+ }}
150
+ >
151
+ {node.inputs.slice(end).map((inp, i) => (
152
+ <GhostHandle
153
+ key={end + i}
154
+ type="target"
155
+ id={`in-${end + i}`}
156
+ />
157
+ ))}
158
+ <ChevronDown className="size-2.5" />
159
+ <span>+{below}</span>
160
+ </button>
161
+ ) : null}
162
+ </div>
163
+ );
164
+ }
165
+
166
+ export function PortRailOut({
167
+ node,
168
+ data,
169
+ dataset,
170
+ lang,
171
+ langMap,
172
+ names,
173
+ connectable = true,
174
+ onOpen,
175
+ }: RailProps & { onOpen?: () => void }) {
176
+ const {
177
+ ref,
178
+ start,
179
+ end,
180
+ above,
181
+ below,
182
+ scroll,
183
+ onWheel,
184
+ } = useRailWindow(node.outputs.length, node.id, 'out');
185
+ if (!node.outputs.length) return null;
186
+ const slice = node.outputs.slice(start, end);
187
+ return (
188
+ <div
189
+ ref={ref}
190
+ className="calc-rail calc-rail--out nodrag nowheel"
191
+ onWheel={onWheel}
192
+ >
193
+ {above > 0 ? (
194
+ <button
195
+ type="button"
196
+ className="calc-port calc-port--more"
197
+ onClick={(e) => {
198
+ e.stopPropagation();
199
+ scroll(-1);
200
+ }}
201
+ >
202
+ {node.outputs.slice(0, above).map((out, i) => (
203
+ <GhostHandle
204
+ key={i}
205
+ type="source"
206
+ id={i === 0 ? 'out' : `out-${i}`}
207
+ />
208
+ ))}
209
+ <ChevronUp className="size-2.5" />
210
+ <span>+{above}</span>
211
+ </button>
212
+ ) : null}
213
+ {slice.map((out, i) => (
214
+ <OutputCell
215
+ key={`${out.kind}:${out.id}:${start + i}`}
216
+ out={out}
217
+ index={start + i}
218
+ node={node}
219
+ data={data}
220
+ dataset={dataset}
221
+ lang={lang}
222
+ langMap={langMap}
223
+ names={names}
224
+ connectable={connectable}
225
+ onOpen={onOpen}
226
+ />
227
+ ))}
228
+ {below > 0 ? (
229
+ <button
230
+ type="button"
231
+ className="calc-port calc-port--more"
232
+ onClick={(e) => {
233
+ e.stopPropagation();
234
+ scroll(1);
235
+ }}
236
+ >
237
+ {node.outputs.slice(end).map((out, i) => (
238
+ <GhostHandle
239
+ key={end + i}
240
+ type="source"
241
+ id={`out-${end + i}`}
242
+ />
243
+ ))}
244
+ <ChevronDown className="size-2.5" />
245
+ <span>+{below}</span>
246
+ </button>
247
+ ) : null}
248
+ </div>
249
+ );
250
+ }
251
+
@@ -0,0 +1,86 @@
1
+ 'use client';
2
+
3
+ import { Handle, Position } from '@terrafirmagreg-team/ui/lib/canvas/xyflow';
4
+ import { formatCount } from '@terrafirmagreg-team/ui/lib/utils/format';
5
+ import { PORT_OUT } from '@terrafirmagreg-team/recipe/lib/calc/ports';
6
+ import type {
7
+ CalcKind,
8
+ CalcNode,
9
+ } from '@terrafirmagreg-team/recipe/types/calc';
10
+
11
+ export function formatPortCount(value: number, kind: CalcKind): string {
12
+ if (kind === 'fluid') {
13
+ if (Math.abs(value) >= 1_000)
14
+ return `${Math.round((value / 1000) * 100) / 100}`;
15
+ return `${Math.round(value)}`;
16
+ }
17
+ return formatCount(value, 1_000);
18
+ }
19
+
20
+ export function PortCount({ value, kind }: { value: number; kind: CalcKind }) {
21
+ const text = formatPortCount(value, kind);
22
+ return (
23
+ <span
24
+ className="calc-port-count"
25
+ data-len={text.length >= 5 ? 'xl' : text.length >= 4 ? 'l' : undefined}
26
+ title={String(value)}
27
+ >
28
+ {text}
29
+ </span>
30
+ );
31
+ }
32
+
33
+ export function CardHandles({
34
+ node,
35
+ interactive,
36
+ }: {
37
+ node: CalcNode;
38
+ interactive: boolean;
39
+ }) {
40
+ return (
41
+ <>
42
+ {node.outputs.map((out, index) => (
43
+ <Handle
44
+ key={`out-${index}`}
45
+ type="source"
46
+ id={index === 0 ? 'out' : `out-${index}`}
47
+ position={Position.Right}
48
+ isConnectable={interactive}
49
+ className="calc-port calc-port--ghost"
50
+ style={{ right: -PORT_OUT, top: '50%' }}
51
+ />
52
+ ))}
53
+ {node.inputs.map((inp, index) => (
54
+ <Handle
55
+ key={index}
56
+ type="target"
57
+ id={`in-${index}`}
58
+ position={Position.Left}
59
+ isConnectable={interactive}
60
+ className="calc-port calc-port--ghost"
61
+ style={{ left: -PORT_OUT, top: '50%' }}
62
+ />
63
+ ))}
64
+ </>
65
+ );
66
+ }
67
+
68
+ export function GhostHandle({
69
+ id,
70
+ type,
71
+ }: {
72
+ id: string;
73
+ type: 'source' | 'target';
74
+ }) {
75
+ return (
76
+ <Handle
77
+ type={type}
78
+ id={id}
79
+ position={type === 'source' ? Position.Right : Position.Left}
80
+ isConnectable={false}
81
+ className="calc-port calc-port--ghost"
82
+ style={{ position: 'absolute', inset: 0, width: 'auto', height: 'auto' }}
83
+ />
84
+ );
85
+ }
86
+
@@ -1,8 +1,9 @@
1
1
  'use client';
2
2
 
3
- import { useMemo, useRef, useState } from 'react';
3
+ import { useMemo, useRef, useState, type ReactNode } from 'react';
4
4
  import { Sparkles } from 'lucide-react';
5
5
  import { useTranslations } from '@terrafirmagreg-team/translate/hooks/use-translations';
6
+ import { useCategoriesById } from '@terrafirmagreg-team/recipe/hooks/use-categories-by-id';
6
7
  import { useFitPages } from '@terrafirmagreg-team/ui/hooks/page/use-fit-pages';
7
8
  import {
8
9
  DialogHeader,
@@ -19,7 +20,7 @@ import { RecipeTypeTabs } from '@terrafirmagreg-team/recipe/components/views/bro
19
20
  import { PlaceHeader } from '@terrafirmagreg-team/recipe/components/views/browser/place-header';
20
21
  import { WorkstationRail } from '@terrafirmagreg-team/recipe/components/views/browser/workstation-rail';
21
22
  import type { CalcFlowData } from '@terrafirmagreg-team/recipe/lib/calc/flow';
22
- import type { RecipeCategory, RecipeSession } from '@terrafirmagreg-team/recipe/types';
23
+ import type { RecipeSession } from '@terrafirmagreg-team/recipe/types';
23
24
  import { PickerFooter } from './footer';
24
25
 
25
26
  export function PickerBody({
@@ -29,6 +30,7 @@ export function PickerBody({
29
30
  onSelectTagMember,
30
31
  recipeDefaults,
31
32
  onSetRecipeDefault,
33
+ actions,
32
34
  close,
33
35
  }: {
34
36
  node: CalcFlowData['node'];
@@ -37,6 +39,7 @@ export function PickerBody({
37
39
  onSelectTagMember: CalcFlowData['onSelectTagMember'];
38
40
  recipeDefaults: CalcFlowData['recipeDefaults'];
39
41
  onSetRecipeDefault: CalcFlowData['onSetRecipeDefault'];
42
+ actions?: ReactNode;
40
43
  close: () => void;
41
44
  }) {
42
45
  const t = useTranslations();
@@ -66,13 +69,7 @@ export function PickerBody({
66
69
  ?.category ??
67
70
  recipes.find((row) => row.recipeId === defaultRecipeId)?.category ??
68
71
  categories[0]);
69
- const categoriesById = useMemo(
70
- () =>
71
- new Map<string, RecipeCategory>(
72
- (index?.categories ?? []).map((row) => [row.id, row]),
73
- ),
74
- [index],
75
- );
72
+ const categoriesById = useCategoriesById(index);
76
73
  const rows = useMemo(
77
74
  () =>
78
75
  recipes
@@ -105,7 +102,8 @@ export function PickerBody({
105
102
  setPage(next);
106
103
  };
107
104
  const custom =
108
- !!node.recipe && node.recipe.recipeId !== recipes[0]?.recipeId;
105
+ !!node.recipe &&
106
+ node.recipe.recipeId !== (defaultRecipeId ?? recipes[0]?.recipeId);
109
107
  const members =
110
108
  node.tagId && node.members && node.members.length > 1
111
109
  ? node.members
@@ -137,14 +135,18 @@ export function PickerBody({
137
135
  variant="outline"
138
136
  className="ms-auto shrink-0"
139
137
  onClick={() => {
140
- onSelectRecipe(node.materialId, null);
138
+ onSelectRecipe(
139
+ node.id,
140
+ defaultRecipeId ?? recipes[0]?.recipeId ?? null,
141
+ );
141
142
  close();
142
143
  }}
143
144
  >
144
145
  <Sparkles className="size-3.5" />
145
- {t('recipe.calc.auto.title')}
146
+ {t('Default recipe', { note: 'recipe.calc.auto.title' })}
146
147
  </Button>
147
148
  ) : null}
149
+ {actions}
148
150
  </DialogTitle>
149
151
  </DialogHeader>
150
152
  <RecipeFrame
@@ -178,12 +180,12 @@ export function PickerBody({
178
180
  boxRef={boxRef}
179
181
  maxH={list.box.h}
180
182
  maxW={list.box.w}
181
- empty={t('recipe.empty_type')}
183
+ empty={t('No recipes of this type.', { note: 'recipe.empty_type' })}
182
184
  onSwipe={(dir) =>
183
185
  goPage((list.page + dir + list.pages) % Math.max(list.pages, 1))
184
186
  }
185
187
  onPick={(recipeId) => {
186
- onSelectRecipe(node.materialId, recipeId);
188
+ onSelectRecipe(node.id, recipeId);
187
189
  close();
188
190
  }}
189
191
  defaultMaterial={node.materialId}
@@ -202,3 +204,4 @@ export function PickerBody({
202
204
  </>
203
205
  );
204
206
  }
207
+
@@ -1,4 +1,5 @@
1
1
  'use client';
2
+ import type { DatasetRef } from '@terrafirmagreg-team/sites/location';
2
3
 
3
4
  import { useState } from 'react';
4
5
  import { ListFilter } from 'lucide-react';
@@ -25,7 +26,7 @@ export function PickerFooter({
25
26
  }: {
26
27
  node: CalcFlowData['node'];
27
28
  members: string[] | undefined;
28
- dataset: string;
29
+ dataset: DatasetRef;
29
30
  lang: Lang;
30
31
  langMap?: Record<string, string>;
31
32
  recipeDefaults: CalcFlowData['recipeDefaults'];
@@ -67,7 +68,7 @@ export function PickerFooter({
67
68
  onClick={() => setMemberPickerOpen(true)}
68
69
  >
69
70
  <ListFilter className="size-3.5" />
70
- {t('recipe.calc.member.title')}
71
+ {t('Choose tag member', { note: 'recipe.calc.member.title' })}
71
72
  </Button>
72
73
  </>
73
74
  ) : null}
@@ -89,7 +90,7 @@ export function PickerFooter({
89
90
  )
90
91
  }
91
92
  />
92
- {t('recipe.calc.set_default')}
93
+ {t('Pin as default recipe', { note: 'recipe.calc.set_default' })}
93
94
  </label>
94
95
  ) : null}
95
96
  </div>
@@ -127,3 +128,4 @@ export function PickerFooter({
127
128
  </>
128
129
  );
129
130
  }
131
+
@@ -1,5 +1,6 @@
1
1
  'use client';
2
2
 
3
+ import type { ReactNode } from 'react';
3
4
  import {
4
5
  Dialog,
5
6
  DialogContent,
@@ -17,6 +18,7 @@ export function CalcRecipePicker({
17
18
  onSelectTagMember,
18
19
  recipeDefaults,
19
20
  onSetRecipeDefault,
21
+ actions,
20
22
  }: {
21
23
  open: boolean;
22
24
  onOpenChange: (open: boolean) => void;
@@ -26,6 +28,7 @@ export function CalcRecipePicker({
26
28
  onSelectTagMember: CalcFlowData['onSelectTagMember'];
27
29
  recipeDefaults: CalcFlowData['recipeDefaults'];
28
30
  onSetRecipeDefault: CalcFlowData['onSetRecipeDefault'];
31
+ actions?: ReactNode;
29
32
  }) {
30
33
  return (
31
34
  <Dialog open={open} onOpenChange={onOpenChange}>
@@ -38,6 +41,7 @@ export function CalcRecipePicker({
38
41
  onSelectTagMember={onSelectTagMember}
39
42
  recipeDefaults={recipeDefaults}
40
43
  onSetRecipeDefault={onSetRecipeDefault}
44
+ actions={actions}
41
45
  close={() => onOpenChange(false)}
42
46
  />
43
47
  ) : null}
@@ -45,3 +49,4 @@ export function CalcRecipePicker({
45
49
  </Dialog>
46
50
  );
47
51
  }
52
+