@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,8 +1,6 @@
1
1
  'use client';
2
2
 
3
3
  import {
4
- Handle,
5
- Position,
6
4
  type NodeProps,
7
5
  } from '@terrafirmagreg-team/ui/lib/canvas/xyflow';
8
6
  import { ListFilter } from 'lucide-react';
@@ -20,40 +18,19 @@ import { recipeTitle } from '@terrafirmagreg-team/recipe/lib/data/parse';
20
18
  import { cn } from '@terrafirmagreg-team/ui/lib/utils/cn';
21
19
  import type { CalcFlowNode } from '@terrafirmagreg-team/recipe/lib/calc/flow';
22
20
  import { CORNER_BUTTON } from './actions';
21
+ import { CardHandles } from './ports';
23
22
 
24
23
  export function CalcHiddenCard({ data }: NodeProps<CalcFlowNode>) {
25
- const { direction } = data;
26
- const srcPos = direction === 'horizontal' ? Position.Left : Position.Top;
27
- const tgtPos =
28
- direction === 'horizontal' ? Position.Right : Position.Bottom;
29
24
  return (
30
25
  <div data-calc-card className="relative h-full w-full">
31
- <Handle
32
- type="source"
33
- position={srcPos}
34
- isConnectable={false}
35
- className="calc-port"
36
- />
37
- <Handle
38
- type="target"
39
- position={tgtPos}
40
- isConnectable={false}
41
- className="calc-port"
42
- />
26
+ <CardHandles node={data.node} interactive={false} />
43
27
  <Skeleton className="size-full rounded-md" />
44
28
  </div>
45
29
  );
46
30
  }
47
31
 
48
32
  export function CalcFarCard({ data, selected }: NodeProps<CalcFlowNode>) {
49
- const {
50
- node,
51
- dataset,
52
- mode,
53
- direction,
54
- recipeBody,
55
- onRequestPicker,
56
- } = data;
33
+ const { node, dataset, mode, recipeBody, onRequestPicker } = data;
57
34
  const t = useTranslations();
58
35
  const session = useCalcSession();
59
36
  const lang = session?.lang ?? 'en_us';
@@ -69,14 +46,10 @@ export function CalcFarCard({ data, selected }: NodeProps<CalcFlowNode>) {
69
46
  node.recipe != null || node.availableRecipes.length > 0;
70
47
  const stateCls = cn(
71
48
  node.cycle && 'border-red-500/70',
72
- (node.catalyst || node.collapsed) &&
73
- 'border-dashed border-muted-foreground/50',
74
- !node.cycle && !node.catalyst && node.recipe && 'border-primary/60',
49
+ !node.cycle && node.recipe && 'border-primary/60',
50
+ !node.recipe && 'border-dashed border-muted-foreground/50',
75
51
  selected && 'ring-2 ring-primary/70',
76
52
  );
77
- const srcPos = direction === 'horizontal' ? Position.Left : Position.Top;
78
- const tgtPos =
79
- direction === 'horizontal' ? Position.Right : Position.Bottom;
80
53
  return (
81
54
  <div
82
55
  data-calc-card
@@ -85,22 +58,11 @@ export function CalcFarCard({ data, selected }: NodeProps<CalcFlowNode>) {
85
58
  stateCls,
86
59
  )}
87
60
  >
88
- <Handle
89
- type="source"
90
- position={srcPos}
91
- isConnectable={false}
92
- className="calc-port"
93
- />
94
- <Handle
95
- type="target"
96
- position={tgtPos}
97
- isConnectable={false}
98
- className="calc-port"
99
- />
61
+ <CardHandles node={node} interactive={false} />
100
62
  <div className="flex min-w-0 items-center gap-1.5">
101
63
  <span className="size-4 shrink-0 rounded-sm border border-border bg-muted/40" />
102
64
  <span className="min-w-0 flex-1 truncate">
103
- <StackCount count={node.amount} /> {name}
65
+ <StackCount count={node.produced} /> {name}
104
66
  </span>
105
67
  </div>
106
68
  {mode === 'detailed' && body && src ? (
@@ -124,7 +86,7 @@ export function CalcFarCard({ data, selected }: NodeProps<CalcFlowNode>) {
124
86
  {pickable ? (
125
87
  <button
126
88
  type="button"
127
- aria-label={t('recipe.calc.select')}
89
+ aria-label={t('Choose recipe', { note: 'recipe.calc.select' })}
128
90
  onClick={(event) => {
129
91
  event.stopPropagation();
130
92
  onRequestPicker(node);
@@ -139,17 +101,13 @@ export function CalcFarCard({ data, selected }: NodeProps<CalcFlowNode>) {
139
101
  }
140
102
 
141
103
  export function CalcDotCard({ data, selected }: NodeProps<CalcFlowNode>) {
142
- const { node, direction } = data;
104
+ const { node } = data;
143
105
  const stateCls = cn(
144
106
  node.cycle && 'border-red-500/70',
145
- (node.catalyst || node.collapsed) &&
146
- 'border-dashed border-muted-foreground/50',
147
- !node.cycle && !node.catalyst && node.recipe && 'border-primary/60',
107
+ !node.cycle && node.recipe && 'border-primary/60',
108
+ !node.recipe && 'border-dashed border-muted-foreground/50',
148
109
  selected && 'ring-2 ring-primary/70',
149
110
  );
150
- const srcPos = direction === 'horizontal' ? Position.Left : Position.Top;
151
- const tgtPos =
152
- direction === 'horizontal' ? Position.Right : Position.Bottom;
153
111
  return (
154
112
  <div
155
113
  data-calc-card
@@ -158,18 +116,8 @@ export function CalcDotCard({ data, selected }: NodeProps<CalcFlowNode>) {
158
116
  stateCls,
159
117
  )}
160
118
  >
161
- <Handle
162
- type="source"
163
- position={srcPos}
164
- isConnectable={false}
165
- className="calc-port"
166
- />
167
- <Handle
168
- type="target"
169
- position={tgtPos}
170
- isConnectable={false}
171
- className="calc-port"
172
- />
119
+ <CardHandles node={node} interactive={false} />
173
120
  </div>
174
121
  );
175
122
  }
123
+
@@ -1,9 +1,9 @@
1
1
  'use client';
2
2
 
3
3
  import { memo, useEffect, useState } from 'react';
4
+ import { Minus, Plus } from 'lucide-react';
4
5
  import {
5
- Handle,
6
- Position,
6
+ useUpdateNodeInternals,
7
7
  type NodeProps,
8
8
  } from '@terrafirmagreg-team/ui/lib/canvas/xyflow';
9
9
  import { useTranslations } from '@terrafirmagreg-team/translate/hooks/use-translations';
@@ -30,10 +30,12 @@ import {
30
30
  import { recipeTitle } from '@terrafirmagreg-team/recipe/lib/data/parse';
31
31
  import { cn } from '@terrafirmagreg-team/ui/lib/utils/cn';
32
32
  import { CALC_NODE_W } from '@terrafirmagreg-team/recipe/lib/calc/flow';
33
+ import { railSpans } from '@terrafirmagreg-team/recipe/lib/calc/ports';
33
34
  import type { CalcFlowNode } from '@terrafirmagreg-team/recipe/lib/calc/flow';
34
35
  import { NodeBadges } from './badges';
35
- import { CycleChip, NodeChips } from './chips';
36
+ import { NodeChips } from './chips';
36
37
  import { NodeActions } from './actions';
38
+ import { PortRailIn, PortRailOut } from './port-rails';
37
39
  import { NodeContextMenu, NodeHoverContent } from './menu';
38
40
 
39
41
  export const CalcNodeCardFull = memo(function CalcNodeCardFull({
@@ -45,10 +47,10 @@ export const CalcNodeCardFull = memo(function CalcNodeCardFull({
45
47
  node,
46
48
  dataset,
47
49
  mode,
48
- direction,
49
50
  recipeBody,
50
51
  onRequestPicker,
51
52
  onRequestMemberPicker,
53
+ onCrafts,
52
54
  } = data;
53
55
  const t = useTranslations();
54
56
  const session = useCalcSession();
@@ -103,72 +105,56 @@ export const CalcNodeCardFull = memo(function CalcNodeCardFull({
103
105
  }, [staged, bodyReady]);
104
106
  const cardOn = card && (!staged || bodyReady);
105
107
 
108
+ useEffect(
109
+ () => () => {
110
+ railSpans.delete(id);
111
+ },
112
+ [id],
113
+ );
114
+
106
115
  const skeleton =
107
116
  mode === 'detailed' && node.recipe != null && !cardOn;
108
117
  const stateCls = cn(
109
118
  node.cycle && 'border-red-500/70',
110
- (node.catalyst || node.collapsed) &&
111
- 'border-dashed border-muted-foreground/50',
112
- !node.cycle && !node.catalyst && node.recipe && 'border-primary/60',
119
+ !node.cycle && node.recipe && 'border-primary/60',
120
+ !node.recipe && 'border-dashed border-muted-foreground/50',
113
121
  selected && 'ring-2 ring-primary/70',
114
122
  );
115
123
 
116
- const srcPos = direction === 'horizontal' ? Position.Left : Position.Top;
117
- const tgtPos = direction === 'horizontal' ? Position.Right : Position.Bottom;
118
-
119
124
  const interactive = tier === 'near';
120
- const cardBody = (
125
+ const updateNodeInternals = useUpdateNodeInternals();
126
+ useEffect(() => {
127
+ updateNodeInternals(id);
128
+ }, [
129
+ id,
130
+ updateNodeInternals,
131
+ mode,
132
+ interactive,
133
+ node.recipe?.recipeId,
134
+ node.inputs.length,
135
+ node.outputs.length,
136
+ ]);
137
+ const innerContent = (
121
138
  <div
122
- data-calc-card
123
139
  className={cn(
124
- 'relative flex h-full w-full flex-col justify-center gap-0.5 text-xs outline-none',
125
- 'rounded-md border bg-card',
126
- mode === 'mini' ? 'items-center px-1.5 py-1' : 'px-2 py-1',
127
- stateCls,
140
+ 'flex min-w-0 flex-1 flex-col justify-center gap-0.5 px-2 py-1',
141
+ mode === 'mini' ? 'items-center px-1.5' : '',
128
142
  )}
129
143
  >
130
- <Handle
131
- type="source"
132
- position={srcPos}
133
- isConnectable={false}
134
- className="calc-port"
135
- />
136
- <Handle
137
- type="target"
138
- position={tgtPos}
139
- isConnectable={false}
140
- className="calc-port"
141
- />
142
144
  {mode === 'mini' ? (
143
- <div className="flex w-full flex-col items-center gap-0.5">
144
- <CycleChip
145
- className="w-full justify-center"
146
- title={
147
- node.recipe || recipes.length
148
- ? t('recipe.calc.select')
149
- : name
150
- }
151
- openLabel={t('recipe.calc.select')}
152
- onOpen={
153
- node.recipe || recipes.length
154
- ? () => onRequestPicker(node)
155
- : undefined
156
- }
157
- centerClassName="py-0.5"
158
- >
159
- <Entry
160
- id={node.materialId}
161
- kind={node.kind}
162
- dataset={dataset}
163
- lang={lang}
164
- langMap={langMap}
165
- name={name}
166
- size="lg"
167
- href={false}
168
- tooltip={false}
169
- badge={<StackCount count={node.amount} />}
170
- />
171
- </CycleChip>
145
+ <div className="mx-3 flex w-auto flex-col items-center gap-0.5">
146
+ <Entry
147
+ id={node.materialId}
148
+ kind={node.kind}
149
+ dataset={dataset}
150
+ lang={lang}
151
+ langMap={langMap}
152
+ name={name}
153
+ size="lg"
154
+ href={false}
155
+ tooltip="adaptive"
156
+ badge={<StackCount count={node.produced} />}
157
+ />
172
158
  <span className="w-full truncate px-0.5 text-center text-[9px] leading-tight text-muted-foreground">
173
159
  {name}
174
160
  </span>
@@ -187,7 +173,7 @@ export const CalcNodeCardFull = memo(function CalcNodeCardFull({
187
173
  </div>
188
174
  ) : (
189
175
  <>
190
- <div className="flex min-w-0 items-center gap-1.5">
176
+ <div className="mx-1 flex min-w-0 items-center gap-1.5">
191
177
  <Entry
192
178
  id={node.materialId}
193
179
  kind={node.kind}
@@ -197,23 +183,57 @@ export const CalcNodeCardFull = memo(function CalcNodeCardFull({
197
183
  size="lg"
198
184
  labeled
199
185
  href={false}
200
- tooltip={false}
201
- badge={<StackCount count={node.amount} />}
186
+ tooltip="adaptive"
187
+ badge={<StackCount count={node.produced} />}
202
188
  className="min-w-0 flex-1 justify-start truncate"
203
189
  />
190
+ <NodeChips
191
+ node={node}
192
+ mini={false}
193
+ name={name}
194
+ category={category}
195
+ hasAlternatives={hasAlternatives}
196
+ dataset={dataset}
197
+ lang={lang}
198
+ langMap={langMap}
199
+ onRequestPicker={onRequestPicker}
200
+ onRequestMemberPicker={onRequestMemberPicker}
201
+ />
202
+ </div>
203
+ <div className="mx-1 flex min-w-0 items-center gap-1">
204
+ {interactive ? (
205
+ <span className="nodrag ml-auto flex shrink-0 items-center gap-0.5">
206
+ <button
207
+ type="button"
208
+ aria-label={t('Fewer crafts', { note: 'recipe.calc.crafts.down' })}
209
+ className="grid size-4 place-items-center rounded border border-border bg-background text-muted-foreground hover:text-foreground"
210
+ onClick={(e) => {
211
+ e.stopPropagation();
212
+ onCrafts(id, Math.max(1, node.crafts - 1));
213
+ }}
214
+ >
215
+ <Minus className="size-2.5" />
216
+ </button>
217
+ <span
218
+ className="min-w-6 text-center tabular-nums"
219
+ title={t('Crafts', { note: 'recipe.calc.crafts.title' })}
220
+ >
221
+ {node.crafts}
222
+ </span>
223
+ <button
224
+ type="button"
225
+ aria-label={t('More crafts', { note: 'recipe.calc.crafts.up' })}
226
+ className="grid size-4 place-items-center rounded border border-border bg-background text-muted-foreground hover:text-foreground"
227
+ onClick={(e) => {
228
+ e.stopPropagation();
229
+ onCrafts(id, node.crafts + 1);
230
+ }}
231
+ >
232
+ <Plus className="size-2.5" />
233
+ </button>
234
+ </span>
235
+ ) : null}
204
236
  </div>
205
- <NodeChips
206
- node={node}
207
- mini={false}
208
- name={name}
209
- category={category}
210
- hasAlternatives={hasAlternatives}
211
- dataset={dataset}
212
- lang={lang}
213
- langMap={langMap}
214
- onRequestPicker={onRequestPicker}
215
- onRequestMemberPicker={onRequestMemberPicker}
216
- />
217
237
  </>
218
238
  )}
219
239
  {cardOn && body && src ? (
@@ -231,7 +251,7 @@ export const CalcNodeCardFull = memo(function CalcNodeCardFull({
231
251
  naturalW={body.w}
232
252
  naturalH={body.h}
233
253
  interactive={false}
234
- slotTooltip={interactive ? 'compact' : 'none'}
254
+ slotTooltip={interactive ? 'adaptive' : 'none'}
235
255
  eager
236
256
  onFail={() => setImgFail(true)}
237
257
  />
@@ -246,13 +266,44 @@ export const CalcNodeCardFull = memo(function CalcNodeCardFull({
246
266
  }
247
267
  />
248
268
  ) : null}
249
- {interactive ? (
250
- <NodeActions
251
- data={data}
252
- nodeId={id}
253
- onPickMissing={onRequestPicker}
254
- />
255
- ) : null}
269
+ </div>
270
+ );
271
+ const hoverBody =
272
+ interactive && hoverCard ? (
273
+ <HoverCardTrigger render={innerContent} />
274
+ ) : (
275
+ innerContent
276
+ );
277
+ const cardBody = (
278
+ <div
279
+ data-calc-card
280
+ className={cn(
281
+ 'relative flex h-full w-full select-none flex-col text-xs outline-none',
282
+ 'rounded-md border bg-card',
283
+ stateCls,
284
+ )}
285
+ >
286
+ <PortRailIn
287
+ node={node}
288
+ data={data}
289
+ dataset={dataset}
290
+ lang={lang}
291
+ langMap={langMap}
292
+ names={names}
293
+ connectable={interactive}
294
+ />
295
+ {hoverBody}
296
+ <PortRailOut
297
+ node={node}
298
+ data={data}
299
+ dataset={dataset}
300
+ lang={lang}
301
+ langMap={langMap}
302
+ names={names}
303
+ connectable={interactive}
304
+ onOpen={() => onRequestPicker(node)}
305
+ />
306
+ {interactive ? <NodeActions data={data} nodeId={id} /> : null}
256
307
  <NodeBadges node={node} />
257
308
  </div>
258
309
  );
@@ -260,9 +311,7 @@ export const CalcNodeCardFull = memo(function CalcNodeCardFull({
260
311
  return (
261
312
  <HoverCard>
262
313
  <ContextMenu>
263
- <HoverCardTrigger
264
- render={<ContextMenuTrigger render={cardBody} />}
265
- />
314
+ <ContextMenuTrigger render={cardBody} />
266
315
  <NodeContextMenu data={data} lang={lang} langMap={langMap} />
267
316
  </ContextMenu>
268
317
  {hoverCard ? (
@@ -280,3 +329,4 @@ export const CalcNodeCardFull = memo(function CalcNodeCardFull({
280
329
  </HoverCard>
281
330
  );
282
331
  });
332
+
@@ -24,3 +24,4 @@ export const CalcNodeCard = memo(function CalcNodeCard(
24
24
  if (tier === 'dot') return <CalcDotCard {...props} />;
25
25
  return <CalcFarCard {...props} />;
26
26
  });
27
+
@@ -1,9 +1,9 @@
1
1
  'use client';
2
+ import type { DatasetRef } from '@terrafirmagreg-team/sites/location';
2
3
 
3
4
  import {
4
5
  ExternalLink,
5
6
  ListFilter,
6
- Package,
7
7
  Pin,
8
8
  PinOff,
9
9
  Plus,
@@ -24,6 +24,7 @@ import { Entry } from '@terrafirmagreg-team/game/components/entry/entry';
24
24
  import { PagedCard } from '@terrafirmagreg-team/recipe/components/cards/paged-card';
25
25
  import { recipeTitle } from '@terrafirmagreg-team/recipe/lib/data/parse';
26
26
  import type { CalcFlowData } from '@terrafirmagreg-team/recipe/lib/calc/flow';
27
+ import type { CalcKind } from '@terrafirmagreg-team/recipe/types/calc';
27
28
  import type { Recipe } from '@terrafirmagreg-team/recipe/lib/data/dto';
28
29
  import type { RecipeCategory } from '@terrafirmagreg-team/recipe/types';
29
30
 
@@ -39,10 +40,9 @@ export function NodeContextMenu({
39
40
  const {
40
41
  node,
41
42
  dataset,
42
- onSetTarget,
43
- onAddStock,
43
+ onAddCard,
44
44
  onOpenItem,
45
- onCollapseNode,
45
+ onSelectRecipe,
46
46
  onRequestPicker,
47
47
  onRequestMemberPicker,
48
48
  recipeDefaults,
@@ -70,24 +70,17 @@ export function NodeContextMenu({
70
70
  <ContextMenuSeparator />
71
71
  <ContextMenuItem
72
72
  className="whitespace-nowrap"
73
- onClick={() => onSetTarget(node.materialId, node.kind)}
73
+ onClick={() => onAddCard(node.materialId, node.kind)}
74
74
  >
75
75
  <Target />
76
- {t('recipe.calc.set_target')}
77
- </ContextMenuItem>
78
- <ContextMenuItem
79
- className="whitespace-nowrap"
80
- onClick={() => onAddStock(node.materialId, node.kind)}
81
- >
82
- <Package />
83
- {t('recipe.calc.add.stock')}
76
+ {t('Add card', { note: 'recipe.calc.add.goal' })}
84
77
  </ContextMenuItem>
85
78
  <ContextMenuItem
86
79
  className="whitespace-nowrap"
87
80
  onClick={() => onOpenItem(node.materialId, node.kind)}
88
81
  >
89
82
  <ExternalLink />
90
- {t('recipe.calc.open.item')}
83
+ {t('Open item page', { note: 'recipe.calc.open.item' })}
91
84
  </ContextMenuItem>
92
85
  {node.recipe ? (
93
86
  <ContextMenuItem
@@ -104,32 +97,32 @@ export function NodeContextMenu({
104
97
  {recipeDefaults[node.materialId] === node.recipe.recipeId ? (
105
98
  <>
106
99
  <PinOff />
107
- {t('recipe.calc.clear.default')}
100
+ {t('Unpin default recipe', { note: 'recipe.calc.clear.default' })}
108
101
  </>
109
102
  ) : (
110
103
  <>
111
104
  <Pin />
112
- {t('recipe.calc.set_default')}
105
+ {t('Pin as default recipe', { note: 'recipe.calc.set_default' })}
113
106
  </>
114
107
  )}
115
108
  </ContextMenuItem>
116
109
  ) : null}
117
- {node.recipe && node.path ? (
110
+ {node.recipe ? (
118
111
  <ContextMenuItem
119
112
  className="whitespace-nowrap"
120
- onClick={() => onCollapseNode(node.path!)}
113
+ onClick={() => onSelectRecipe(node.id, null)}
121
114
  >
122
115
  <Undo2 />
123
- {t('recipe.calc.remove.recipe')}
116
+ {t('Remove recipe', { note: 'recipe.calc.remove.recipe' })}
124
117
  </ContextMenuItem>
125
118
  ) : null}
126
- {node.expandable && !node.recipe ? (
119
+ {!node.recipe && node.availableRecipes.length > 0 ? (
127
120
  <ContextMenuItem
128
121
  className="whitespace-nowrap"
129
122
  onClick={() => onRequestPicker(node)}
130
123
  >
131
124
  <Plus />
132
- {t('recipe.calc.pick.title')}
125
+ {t('Pick a recipe', { note: 'recipe.calc.pick.title' })}
133
126
  </ContextMenuItem>
134
127
  ) : null}
135
128
  {node.tagId && members && members.length > 1 ? (
@@ -138,9 +131,86 @@ export function NodeContextMenu({
138
131
  onClick={() => onRequestMemberPicker(node)}
139
132
  >
140
133
  <ListFilter />
141
- {t('recipe.calc.member.title')}
134
+ {t('Choose tag member', { note: 'recipe.calc.member.title' })}
135
+ </ContextMenuItem>
136
+ ) : null}
137
+ </ContextMenuContent>
138
+ );
139
+ }
140
+
141
+ export function PortContextMenu({
142
+ data,
143
+ node,
144
+ input,
145
+ out,
146
+ id,
147
+ kind,
148
+ dataset,
149
+ lang,
150
+ langMap,
151
+ }: {
152
+ data: CalcFlowData;
153
+ node: CalcFlowData['node'];
154
+ input?: number;
155
+ out?: number;
156
+ id: string;
157
+ kind: CalcKind;
158
+ dataset: DatasetRef;
159
+ lang: string;
160
+ langMap?: Record<string, string>;
161
+ }) {
162
+ const { onAddCard, onOpenItem, onRequestPicker, onRequestSupply } = data;
163
+ const t = useTranslations();
164
+ return (
165
+ <ContextMenuContent>
166
+ <ContextMenuGroup>
167
+ <ContextMenuLabel className="flex items-center gap-1.5">
168
+ <Entry
169
+ id={id}
170
+ kind={kind}
171
+ dataset={dataset}
172
+ lang={lang}
173
+ langMap={langMap}
174
+ size="sm"
175
+ labeled
176
+ href={false}
177
+ tooltip={false}
178
+ />
179
+ </ContextMenuLabel>
180
+ </ContextMenuGroup>
181
+ <ContextMenuSeparator />
182
+ {input != null ? (
183
+ <ContextMenuItem
184
+ className="whitespace-nowrap"
185
+ onClick={() => onRequestSupply(node, input)}
186
+ >
187
+ <Plus />
188
+ {t('Keep as external source', { note: 'recipe.calc.supply.source' })}
189
+ </ContextMenuItem>
190
+ ) : null}
191
+ {out === 0 && !node.recipe && node.availableRecipes.length > 0 ? (
192
+ <ContextMenuItem
193
+ className="whitespace-nowrap"
194
+ onClick={() => onRequestPicker(node)}
195
+ >
196
+ <ListFilter />
197
+ {t('Pick a recipe', { note: 'recipe.calc.pick.title' })}
142
198
  </ContextMenuItem>
143
199
  ) : null}
200
+ <ContextMenuItem
201
+ className="whitespace-nowrap"
202
+ onClick={() => onAddCard(id, kind)}
203
+ >
204
+ <Target />
205
+ {t('Add card', { note: 'recipe.calc.add.goal' })}
206
+ </ContextMenuItem>
207
+ <ContextMenuItem
208
+ className="whitespace-nowrap"
209
+ onClick={() => onOpenItem(id, kind)}
210
+ >
211
+ <ExternalLink />
212
+ {t('Open item page', { note: 'recipe.calc.open.item' })}
213
+ </ContextMenuItem>
144
214
  </ContextMenuContent>
145
215
  );
146
216
  }
@@ -209,3 +279,4 @@ export function NodeHoverContent({
209
279
  </HoverCardContent>
210
280
  );
211
281
  }
282
+