@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,189 @@
1
+ 'use client';
2
+
3
+ import {
4
+ ArrowUp,
5
+ ChevronUp,
6
+ ChevronsUpDown,
7
+ Eye,
8
+ Info,
9
+ Mountain,
10
+ Move,
11
+ Scissors,
12
+ Settings2,
13
+ SquareMousePointer,
14
+ } from 'lucide-react';
15
+ import { IconButton } from '@terrafirmagreg-team/ui/components/blocks/button/icon-button';
16
+ import {
17
+ Popover,
18
+ PopoverContent,
19
+ PopoverTrigger,
20
+ } from '@terrafirmagreg-team/ui/components/ui/popover';
21
+ import { useTranslations } from '@terrafirmagreg-team/translate/hooks/use-translations';
22
+
23
+ export function CalcHelp() {
24
+ const t = useTranslations();
25
+ return (
26
+ <Popover>
27
+ <PopoverTrigger
28
+ render={<IconButton overlay label={t('Calculator help', { note: 'recipe.calc.help.title' })} />}
29
+ >
30
+ <Info className="size-3.5" />
31
+ </PopoverTrigger>
32
+ <PopoverContent side="bottom" align="end" className="w-80 space-y-3 p-3">
33
+ <section className="space-y-1.5">
34
+ <h4 className="text-[10px] font-semibold uppercase tracking-wider text-muted-foreground/70">
35
+ {t('Nodes', { note: 'recipe.calc.help.sec_nodes' })}
36
+ </h4>
37
+ <ul className="space-y-1.5 text-xs text-muted-foreground">
38
+ <li className="flex items-center gap-2">
39
+ <span className="flex w-5 shrink-0 items-center justify-center">
40
+ <span className="size-4 rounded border border-primary/60" />
41
+ </span>
42
+ {t('Resolved with a recipe', { note: 'recipe.calc.help.recipe' })}
43
+ </li>
44
+ <li className="flex items-center gap-2">
45
+ <span className="flex w-5 shrink-0 items-center justify-center">
46
+ <span className="size-4 rounded border border-red-500/70" />
47
+ </span>
48
+ {t('Recipe cycle', { note: 'recipe.calc.help.cycle' })}
49
+ </li>
50
+ <li className="flex items-center gap-2">
51
+ <span className="flex w-5 shrink-0 items-center justify-center">
52
+ <span className="size-3.5 rounded ring-2 ring-primary/70" />
53
+ </span>
54
+ {t('Focused node', { note: 'recipe.calc.help.focus' })}
55
+ </li>
56
+ <li className="flex items-center gap-2">
57
+ <span className="flex w-5 shrink-0 items-center justify-center">
58
+ <span className="rounded border border-emerald-500/40 px-0.5 text-[10px] font-medium leading-3 text-emerald-500">
59
+ +N
60
+ </span>
61
+ </span>
62
+ {t('Covered by another card output', { note: 'recipe.calc.help.satisfied' })}
63
+ </li>
64
+ <li className="flex items-center gap-2">
65
+ <span className="flex w-5 shrink-0 items-center justify-center">
66
+ <Mountain className="size-3.5 text-muted-foreground" />
67
+ </span>
68
+ {t('No recipe; raw material', { note: 'recipe.calc.help.source' })}
69
+ </li>
70
+ </ul>
71
+ </section>
72
+ <section className="space-y-1.5">
73
+ <h4 className="text-[10px] font-semibold uppercase tracking-wider text-muted-foreground/70">
74
+ {t('Ports', { note: 'recipe.calc.help.sec_ports' })}
75
+ </h4>
76
+ <ul className="space-y-1.5 text-xs text-muted-foreground">
77
+ <li className="flex items-center gap-2">
78
+ <span className="flex w-5 shrink-0 items-center justify-center">
79
+ <span className="size-3.5 rounded-sm border border-emerald-500" />
80
+ </span>
81
+ {t('Input port: click to pick a supplier or drag a wire', { note: 'recipe.calc.help.port_in' })}
82
+ </li>
83
+ <li className="flex items-center gap-2">
84
+ <span className="flex w-5 shrink-0 items-center justify-center">
85
+ <span className="size-3.5 rounded-sm border border-red-400" />
86
+ </span>
87
+ {t('Output port: click to pick a recipe or drag a wire', { note: 'recipe.calc.help.port_out' })}
88
+ </li>
89
+ <li className="flex items-center gap-2">
90
+ <span className="flex w-5 shrink-0 items-center justify-center">
91
+ <span className="size-3.5 rounded-sm border border-dotted border-emerald-500" />
92
+ </span>
93
+ {t('Catalyst: needed but not consumed; wire it or click to pick a supplier', { note: 'recipe.calc.help.port_cat' })}
94
+ </li>
95
+ <li className="flex items-center gap-2">
96
+ <span className="flex w-5 shrink-0 items-center justify-center">
97
+ <ChevronUp className="size-3.5 text-muted-foreground" />
98
+ </span>
99
+ {t('Hidden ports - scroll the rail or click the arrow', { note: 'recipe.calc.help.port_more' })}
100
+ </li>
101
+ </ul>
102
+ </section>
103
+ <section className="space-y-1.5">
104
+ <h4 className="text-[10px] font-semibold uppercase tracking-wider text-muted-foreground/70">
105
+ {t('Edges', { note: 'recipe.calc.help.sec_edges' })}
106
+ </h4>
107
+ <ul className="space-y-1.5 text-xs text-muted-foreground">
108
+ <li className="flex items-center gap-2">
109
+ <span className="flex w-5 shrink-0 items-center justify-center">
110
+ <ArrowUp className="size-3.5 text-muted-foreground" />
111
+ </span>
112
+ {t('Arrows flow from ingredient to product', { note: 'recipe.calc.help.edges' })}
113
+ </li>
114
+ <li className="flex items-center gap-2">
115
+ <span className="flex w-5 shrink-0 items-center justify-center">
116
+ <span className="h-0.5 w-4 rounded bg-teal-500" />
117
+ </span>
118
+ {t('Outgoing edges', { note: 'recipe.calc.help.edge_out' })}
119
+ </li>
120
+ <li className="flex items-center gap-2">
121
+ <span className="flex w-5 shrink-0 items-center justify-center">
122
+ <span className="h-0.5 w-4 rounded bg-yellow-500" />
123
+ </span>
124
+ {t('Incoming edges', { note: 'recipe.calc.help.edge_in' })}
125
+ </li>
126
+ <li className="flex items-center gap-2">
127
+ <span className="flex w-5 shrink-0 items-center justify-center">
128
+ <span className="h-0 w-4 border-t border-dashed border-primary/70" />
129
+ </span>
130
+ {t('Drag port to port to link; drag a wire end to retarget', { note: 'recipe.calc.help.edge_link' })}
131
+ </li>
132
+ <li className="flex items-center gap-2">
133
+ <span className="flex w-5 shrink-0 items-center justify-center">
134
+ <span className="rounded border border-border bg-background px-0.5 text-[9px] leading-3 tabular-nums">
135
+ 16
136
+ </span>
137
+ </span>
138
+ {t('Flow on a wire - shows on hover; click to cap the amount', { note: 'recipe.calc.help.edge_flow' })}
139
+ </li>
140
+ <li className="flex items-center gap-2">
141
+ <span className="flex w-5 shrink-0 items-center justify-center">
142
+ <Scissors className="size-3.5 text-muted-foreground" />
143
+ </span>
144
+ {t('Shift + hover a wire shows x - click to delete it', { note: 'recipe.calc.help.edge_cut' })}
145
+ </li>
146
+ </ul>
147
+ </section>
148
+ <section className="space-y-1.5">
149
+ <h4 className="text-[10px] font-semibold uppercase tracking-wider text-muted-foreground/70">
150
+ {t('Actions', { note: 'recipe.calc.help.sec_actions' })}
151
+ </h4>
152
+ <ul className="space-y-1.5 text-xs text-muted-foreground">
153
+ <li className="flex items-center gap-2">
154
+ <span className="flex w-5 shrink-0 items-center justify-center">
155
+ <ChevronsUpDown className="size-3.5 text-muted-foreground" />
156
+ </span>
157
+ {t('Recipe/tag member: open picker', { note: 'recipe.calc.help.chip' })}
158
+ </li>
159
+ <li className="flex items-center gap-2">
160
+ <span className="flex w-5 shrink-0 items-center justify-center">
161
+ <Eye className="size-3.5 text-muted-foreground" />
162
+ </span>
163
+ {t('Recipe preview', { note: 'recipe.calc.help.hover' })}
164
+ </li>
165
+ <li className="flex items-center gap-2">
166
+ <span className="flex w-5 shrink-0 items-center justify-center">
167
+ <SquareMousePointer className="size-3.5 text-muted-foreground" />
168
+ </span>
169
+ {t('Node context menu', { note: 'recipe.calc.help.ctx' })}
170
+ </li>
171
+ <li className="flex items-center gap-2">
172
+ <span className="flex w-5 shrink-0 items-center justify-center">
173
+ <Settings2 className="size-3.5 text-muted-foreground" />
174
+ </span>
175
+ {t('View modes', { note: 'recipe.calc.help.modes' })}
176
+ </li>
177
+ <li className="flex items-center gap-2">
178
+ <span className="flex w-5 shrink-0 items-center justify-center">
179
+ <Move className="size-3.5 text-muted-foreground" />
180
+ </span>
181
+ {t('Pan and zoom', { note: 'recipe.calc.help.nav' })}
182
+ </li>
183
+ </ul>
184
+ </section>
185
+ </PopoverContent>
186
+ </Popover>
187
+ );
188
+ }
189
+
@@ -0,0 +1,172 @@
1
+ 'use client';
2
+
3
+ import { useDeferredValue, useMemo } from 'react';
4
+ import { useTranslations } from '@terrafirmagreg-team/translate/hooks/use-translations';
5
+ import { EmptyState } from '@terrafirmagreg-team/ui/components/blocks/page/empty-state';
6
+ import { LeftSidebarPortal } from '@terrafirmagreg-team/ui/components/layouts/left-sidebar';
7
+ import { CalcFlow } from '@terrafirmagreg-team/recipe/components/views/calc/flow';
8
+ import { CalcSidebar } from '@terrafirmagreg-team/recipe/components/views/calc/sidebar';
9
+ import { CalcHistoryList } from '@terrafirmagreg-team/recipe/components/views/calc/trees';
10
+ import { CalcActions } from '@terrafirmagreg-team/recipe/components/views/calc/calculator/actions';
11
+ import { useCalcFocus } from '@terrafirmagreg-team/recipe/hooks/calc/use-focus';
12
+ import { useCalcReset } from '@terrafirmagreg-team/recipe/hooks/calc/use-reset';
13
+ import { useCalcAmounts } from '@terrafirmagreg-team/recipe/hooks/calc/use-amounts';
14
+ import { useCalcActions } from '@terrafirmagreg-team/recipe/hooks/calc/use-actions';
15
+ import type { RecipeSession } from '@terrafirmagreg-team/recipe/types';
16
+ import { useCalcData } from '@terrafirmagreg-team/recipe/hooks/calc/use-data';
17
+ import { useCalcViewMode } from '@terrafirmagreg-team/recipe/hooks/calc/use-view-mode';
18
+ import { useCalcFarMode } from '@terrafirmagreg-team/recipe/hooks/calc/use-far-mode';
19
+
20
+ export function Calculator({ session }: { session: RecipeSession }) {
21
+ const t = useTranslations();
22
+ const { dataset, lang } = session;
23
+ const [mode, setMode] = useCalcViewMode();
24
+ const [farCards, setFarCards] = useCalcFarMode();
25
+ const {
26
+ cards,
27
+ wires,
28
+ tagSelections,
29
+ tabs,
30
+ activeTabId,
31
+ history,
32
+ openTab,
33
+ openEntry,
34
+ closeTab,
35
+ newTab,
36
+ renameTab,
37
+ setTabIcon,
38
+ removeHistory,
39
+ recipeDefaults,
40
+ setRecipeDefault,
41
+ getRecipesForItem,
42
+ ensureMaterial,
43
+ nodes,
44
+ flows,
45
+ summary,
46
+ persist,
47
+ encodeShare,
48
+ recipeBody,
49
+ settled,
50
+ } = useCalcData(session);
51
+ const deferredNodes = useDeferredValue(nodes);
52
+ const deferredFlows = useDeferredValue(flows);
53
+ const deferredLangMap = useDeferredValue(session.langMap);
54
+ const { focusNodeRef, onFocusNode, focusNodeId, setFocusNodeId } =
55
+ useCalcFocus(cards);
56
+ const { resetGen } = useCalcReset(activeTabId);
57
+ const { displayCards, flushAmounts, onCrafts } = useCalcAmounts({
58
+ cards,
59
+ wires,
60
+ tagSelections,
61
+ persist,
62
+ });
63
+ const {
64
+ shared,
65
+ onShare,
66
+ flowData,
67
+ onMoveCards,
68
+ onClear,
69
+ } = useCalcActions({
70
+ dataset,
71
+ lang,
72
+ cards: displayCards,
73
+ wires,
74
+ tagSelections,
75
+ persist,
76
+ recipeDefaults,
77
+ setRecipeDefault,
78
+ recipeBody,
79
+ encodeShare,
80
+ flushAmounts,
81
+ setFocusNodeId,
82
+ nodes: deferredNodes,
83
+ });
84
+
85
+ const iconItems = useMemo(
86
+ () => [
87
+ ...(session.index?.items ?? []).map(
88
+ (id) => ({ id, kind: 'item' as const }),
89
+ ),
90
+ ...(session.index?.fluids ?? []).map(
91
+ (id) => ({ id, kind: 'fluid' as const }),
92
+ ),
93
+ ],
94
+ [session.index],
95
+ );
96
+
97
+ const data = useMemo(
98
+ () => ({ ...flowData, onCrafts, getRecipesForItem, ensureMaterial }),
99
+ [flowData, onCrafts, getRecipesForItem, ensureMaterial],
100
+ );
101
+
102
+ return (
103
+ <>
104
+ <LeftSidebarPortal>
105
+ <CalcSidebar
106
+ dataset={dataset}
107
+ lang={lang}
108
+ langMap={deferredLangMap}
109
+ summary={summary}
110
+ onFocus={onFocusNode}
111
+ trees={{
112
+ tabs,
113
+ activeId: activeTabId,
114
+ history,
115
+ iconItems,
116
+ onOpen: openTab,
117
+ onOpenEntry: openEntry,
118
+ onClose: closeTab,
119
+ onRename: renameTab,
120
+ onSetIcon: setTabIcon,
121
+ onNew: newTab,
122
+ onRemoveHistory: removeHistory,
123
+ }}
124
+ />
125
+ </LeftSidebarPortal>
126
+ <CalcFlow
127
+ nodes={deferredNodes}
128
+ wires={wires}
129
+ flows={deferredFlows}
130
+ cards={displayCards}
131
+ dataset={dataset}
132
+ session={session}
133
+ data={data}
134
+ mode={mode}
135
+ onModeChange={setMode}
136
+ farCards={farCards}
137
+ onFarCardsChange={setFarCards}
138
+ treeReady={settled}
139
+ resetGen={resetGen}
140
+ canvasId={activeTabId ?? undefined}
141
+ onRelayout={onMoveCards}
142
+ actions={
143
+ <CalcActions
144
+ session={session}
145
+ shared={shared}
146
+ onShare={onShare}
147
+ hasTargets={displayCards.length > 0}
148
+ onClear={onClear}
149
+ />
150
+ }
151
+ focusRef={focusNodeRef}
152
+ focusNodeId={focusNodeId}
153
+ onFocusedNode={() => setFocusNodeId(null)}
154
+ empty={
155
+ <div className="scrollbar-thin absolute inset-0 z-10 grid place-items-center overflow-auto p-4">
156
+ <div className="flex flex-col items-center gap-4">
157
+ <EmptyState variant="empty">
158
+ {t('Click an item in the catalog to add a card.', { note: 'recipe.calc.empty' })}
159
+ </EmptyState>
160
+ <CalcHistoryList
161
+ history={history}
162
+ onOpen={openEntry}
163
+ meta={{ dataset, lang, langMap: session.langMap }}
164
+ />
165
+ </div>
166
+ </div>
167
+ }
168
+ />
169
+ </>
170
+ );
171
+ }
172
+
@@ -0,0 +1,80 @@
1
+ 'use client';
2
+
3
+ import { memo, useEffect, useRef } from 'react';
4
+ import { useStore } from '@terrafirmagreg-team/ui/lib/canvas/xyflow';
5
+ import {
6
+ CALC_NODE_H,
7
+ CALC_NODE_W,
8
+ type AnyCalcFlowNode,
9
+ } from '@terrafirmagreg-team/recipe/lib/calc/flow';
10
+
11
+ const cssVar = (name: string, fallback: string) =>
12
+ getComputedStyle(document.documentElement)
13
+ .getPropertyValue(name)
14
+ .trim() || fallback;
15
+
16
+ export const CalcDotOverlay = memo(function CalcDotOverlay({
17
+ nodes,
18
+ focusId,
19
+ }: {
20
+ nodes: AnyCalcFlowNode[];
21
+ focusId: string | null;
22
+ }) {
23
+ const canvasRef = useRef<HTMLCanvasElement>(null);
24
+ const tx = useStore((s) => s.transform[0]);
25
+ const ty = useStore((s) => s.transform[1]);
26
+ const zoom = useStore((s) => s.transform[2]);
27
+ const w = useStore((s) => s.width);
28
+ const h = useStore((s) => s.height);
29
+
30
+ useEffect(() => {
31
+ const canvas = canvasRef.current;
32
+ if (!canvas || !w || !h || !zoom) return;
33
+ const dpr = window.devicePixelRatio || 1;
34
+ const pw = Math.round(w * dpr);
35
+ const ph = Math.round(h * dpr);
36
+ if (canvas.width !== pw || canvas.height !== ph) {
37
+ canvas.width = pw;
38
+ canvas.height = ph;
39
+ }
40
+ const ctx = canvas.getContext('2d');
41
+ if (!ctx) return;
42
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
43
+ ctx.clearRect(0, 0, w, h);
44
+ const card = cssVar('--color-card', '#17171a');
45
+ const border = cssVar('--color-border', '#3f3f46');
46
+ const primary = cssVar('--color-primary', '#00c8c8');
47
+ const danger = cssVar('--color-red-500', '#ef4444');
48
+ ctx.lineWidth = 1;
49
+ for (const node of nodes) {
50
+ const nw = node.width ?? CALC_NODE_W;
51
+ const nh = node.height ?? CALC_NODE_H;
52
+ const x = (node.position.x - nw / 2) * zoom + tx;
53
+ const y = (node.position.y - nh / 2) * zoom + ty;
54
+ const rw = nw * zoom;
55
+ const rh = nh * zoom;
56
+ if (x + rw < 0 || x > w || y + rh < 0 || y > h) continue;
57
+ const { node: calc } = node.data;
58
+ const stroke =
59
+ node.id === focusId || calc.recipe
60
+ ? primary
61
+ : calc.cycle
62
+ ? danger
63
+ : border;
64
+ ctx.fillStyle = card;
65
+ ctx.fillRect(x, y, rw, rh);
66
+ ctx.strokeStyle = stroke;
67
+ ctx.strokeRect(x + 0.5, y + 0.5, Math.max(rw - 1, 0), Math.max(rh - 1, 0));
68
+ }
69
+ });
70
+
71
+ return (
72
+ <canvas
73
+ ref={canvasRef}
74
+ aria-hidden
75
+ className="pointer-events-none absolute inset-0 z-10"
76
+ style={{ width: w, height: h }}
77
+ />
78
+ );
79
+ });
80
+
@@ -0,0 +1,152 @@
1
+ 'use client';
2
+
3
+ import { useState, type KeyboardEvent } from 'react';
4
+ import {
5
+ createChevronEdge,
6
+ type ChevronEdgePathContext,
7
+ } from '@terrafirmagreg-team/ui/components/blocks/canvas/chevron-edge';
8
+ import { getSmoothStepPath } from '@terrafirmagreg-team/ui/lib/canvas/xyflow';
9
+ import {
10
+ roundedPolylinePath,
11
+ svgPathToPoints,
12
+ } from '@terrafirmagreg-team/ui/lib/canvas/edge';
13
+ import { Entry } from '@terrafirmagreg-team/game/components/entry/entry';
14
+ import type { DatasetRef } from '@terrafirmagreg-team/sites/location';
15
+ import { useCalcSession } from '@terrafirmagreg-team/recipe/hooks/calc/use-session';
16
+ import { recipeItemName } from '@terrafirmagreg-team/recipe/lib/client-index';
17
+ import { formatCount } from '@terrafirmagreg-team/ui/lib/utils/format';
18
+ import type { FlowPoint } from '@terrafirmagreg-team/ui/types/canvas';
19
+ import type { CalcWire } from '@terrafirmagreg-team/recipe/types/calc';
20
+
21
+ function EdgeAmountLabel({ ctx }: { ctx: ChevronEdgePathContext }) {
22
+ const wire = ctx.data?.wire as CalcWire | undefined;
23
+ const flow = ctx.data?.flow as number | undefined;
24
+ const matId = ctx.data?.matId as string | undefined;
25
+ const matKind = ctx.data?.matKind as 'item' | 'fluid' | undefined;
26
+ const dataset = ctx.data?.dataset as DatasetRef | undefined;
27
+ const onWireAmount = ctx.data?.onWireAmount as
28
+ | ((from: string, to: string, input: number, amount: number | null) => void)
29
+ | undefined;
30
+ const onUnwire = ctx.data?.onUnwire as
31
+ | ((from: string, to: string, input: number, out?: number) => void)
32
+ | undefined;
33
+ const session = useCalcSession();
34
+ const [edit, setEdit] = useState<string | null>(null);
35
+ if (!wire || flow == null) return null;
36
+ const cap = wire.amount;
37
+
38
+ const commit = () => {
39
+ if (edit == null) return;
40
+ const raw = edit.trim();
41
+ const value = raw === '' ? null : Math.max(0, Number(raw));
42
+ onWireAmount?.(
43
+ wire.from,
44
+ wire.to,
45
+ wire.input,
46
+ value != null && Number.isFinite(value) ? value : null,
47
+ );
48
+ setEdit(null);
49
+ };
50
+ const onKey = (e: KeyboardEvent<HTMLInputElement>) => {
51
+ if (e.key === 'Enter') commit();
52
+ if (e.key === 'Escape') setEdit(null);
53
+ };
54
+
55
+ if (edit != null) {
56
+ return (
57
+ <input
58
+ autoFocus
59
+ className="calc-edge-label__input"
60
+ value={edit}
61
+ onChange={(e) => setEdit(e.target.value)}
62
+ onBlur={commit}
63
+ onKeyDown={onKey}
64
+ onClick={(e) => e.stopPropagation()}
65
+ />
66
+ );
67
+ }
68
+ const matName =
69
+ matId && session?.names
70
+ ? recipeItemName(session.names, session.lang, matId)
71
+ : matId;
72
+ return (
73
+ <button
74
+ type="button"
75
+ className="calc-edge-label"
76
+ title={`${matName ?? ''} ${flow}${cap != null ? ` / ${cap}` : ''}`}
77
+ onClick={(e) => {
78
+ e.stopPropagation();
79
+ if (e.shiftKey) {
80
+ onUnwire?.(wire.from, wire.to, wire.input, wire.out);
81
+ return;
82
+ }
83
+ setEdit(cap != null ? String(cap) : '');
84
+ }}
85
+ >
86
+ {matId && matKind && dataset ? (
87
+ <Entry
88
+ id={matId}
89
+ kind={matKind}
90
+ dataset={dataset}
91
+ size="sm"
92
+ href={false}
93
+ tooltip={false}
94
+ />
95
+ ) : null}
96
+ {matName ? (
97
+ <span className="calc-edge-label__name">{matName}</span>
98
+ ) : null}
99
+ <span className="calc-edge-label__amount">
100
+ ×{formatCount(flow, 1_000)}
101
+ {cap != null ? `/${formatCount(cap, 1_000)}` : ''}
102
+ </span>
103
+ <span className="calc-edge-label__cut" aria-hidden>
104
+ ×
105
+ </span>
106
+ </button>
107
+ );
108
+ }
109
+
110
+ export const CalcEdge = createChevronEdge({
111
+ renderLabel: (ctx) => <EdgeAmountLabel ctx={ctx} />,
112
+ labelVisibility: 'hover',
113
+ resolvePath: (ctx) => {
114
+ const points = ctx.data?.points;
115
+ if (Array.isArray(points) && points.length >= 2) {
116
+ const pts = (points as FlowPoint[]).slice();
117
+ const oldA = pts[0]!;
118
+ const oldB = pts[pts.length - 1]!;
119
+ pts[0] = { x: ctx.sourceX, y: ctx.sourceY };
120
+ pts[pts.length - 1] = { x: ctx.targetX, y: ctx.targetY };
121
+ if (pts.length > 2) {
122
+ const s2 = pts[1]!;
123
+ const sL = pts[pts.length - 2]!;
124
+ if (s2.x === oldA.x) s2.x = ctx.sourceX;
125
+ if (s2.y === oldA.y) s2.y = ctx.sourceY;
126
+ if (sL.x === oldB.x) sL.x = ctx.targetX;
127
+ if (sL.y === oldB.y) sL.y = ctx.targetY;
128
+ }
129
+ if (
130
+ pts.length === 2 &&
131
+ pts[0].x !== pts[1].x &&
132
+ pts[0].y !== pts[1].y
133
+ ) {
134
+ const mid = (pts[0].x + pts[1].x) / 2;
135
+ pts.splice(1, 0, { x: mid, y: pts[0].y }, { x: mid, y: pts[1].y });
136
+ }
137
+ return svgPathToPoints(roundedPolylinePath(pts, 12), 8);
138
+ }
139
+ return svgPathToPoints(
140
+ getSmoothStepPath({
141
+ sourceX: ctx.sourceX,
142
+ sourceY: ctx.sourceY,
143
+ sourcePosition: ctx.sourcePosition,
144
+ targetX: ctx.targetX,
145
+ targetY: ctx.targetY,
146
+ targetPosition: ctx.targetPosition,
147
+ borderRadius: 10,
148
+ })[0],
149
+ );
150
+ },
151
+ });
152
+