@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,134 @@
1
+ 'use client';
2
+
3
+ import { FolderTree, History, Plus } from 'lucide-react';
4
+ import { useTranslations } from '@terrafirmagreg-team/translate/hooks/use-translations';
5
+ import {
6
+ LeftSidebarFolder,
7
+ LeftSidebarFolderContent,
8
+ LeftSidebarFolderTrigger,
9
+ } from '@terrafirmagreg-team/ui/components/layouts/left-sidebar';
10
+ import type { CalcTreeEntry } from '@terrafirmagreg-team/recipe/lib/calc/storage';
11
+ import type { EntryMeta, IconItem } from '@terrafirmagreg-team/recipe/lib/calc/meta';
12
+ import { rowClass, rowIdle } from '@terrafirmagreg-team/recipe/lib/calc/meta';
13
+ import { HistoryRow, TabRow } from './rows';
14
+
15
+ export function CalcHistoryList({
16
+ history,
17
+ onOpen,
18
+ meta,
19
+ }: {
20
+ history: CalcTreeEntry[];
21
+ onOpen: (entry: CalcTreeEntry) => void;
22
+ meta: EntryMeta;
23
+ }) {
24
+ const t = useTranslations();
25
+ if (!history.length) return null;
26
+ return (
27
+ <div className="flex w-64 max-w-full flex-col gap-1">
28
+ <p className="px-2 text-[10px] font-semibold uppercase tracking-wider text-muted-foreground/70">
29
+ {t('History', { note: 'recipe.calc.history' })}
30
+ </p>
31
+ <ul className="flex flex-col">
32
+ {history.slice(0, 5).map((entry) => (
33
+ <HistoryRow
34
+ key={entry.id}
35
+ entry={entry}
36
+ onOpen={onOpen}
37
+ meta={meta}
38
+ />
39
+ ))}
40
+ </ul>
41
+ </div>
42
+ );
43
+ }
44
+
45
+ export function CalcTrees({
46
+ tabs,
47
+ activeId,
48
+ history,
49
+ onOpen,
50
+ onOpenEntry,
51
+ onClose,
52
+ onRename,
53
+ onSetIcon,
54
+ onNew,
55
+ onRemoveHistory,
56
+ meta,
57
+ }: {
58
+ tabs: CalcTreeEntry[];
59
+ activeId: string | null;
60
+ history: CalcTreeEntry[];
61
+ onOpen: (id: string) => void;
62
+ onOpenEntry: (entry: CalcTreeEntry) => void;
63
+ onClose: (id: string) => void;
64
+ onRename: (id: string, name: string) => void;
65
+ onSetIcon?: (id: string, icon: IconItem) => void;
66
+ onNew: () => void;
67
+ onRemoveHistory: (id: string) => void;
68
+ meta: EntryMeta;
69
+ }) {
70
+ const t = useTranslations();
71
+ if (!tabs.length && !history.length) return null;
72
+ return (
73
+ <div className="mb-1 flex flex-col gap-1 border-b border-border pb-2">
74
+ {tabs.length ? (
75
+ <LeftSidebarFolder collapsible defaultOpen>
76
+ <LeftSidebarFolderTrigger className="gap-1.5 text-xs font-semibold uppercase tracking-wider text-muted-foreground/70">
77
+ <FolderTree className="size-3.5" />
78
+ {t('Schemes', { note: 'recipe.calc.trees' })}
79
+ </LeftSidebarFolderTrigger>
80
+ <LeftSidebarFolderContent>
81
+ <ul className="flex flex-col">
82
+ {tabs.map((tab) => (
83
+ <TabRow
84
+ key={tab.id}
85
+ entry={tab}
86
+ active={tab.id === activeId}
87
+ onOpen={onOpen}
88
+ onClose={onClose}
89
+ onRename={onRename}
90
+ onSetIcon={onSetIcon}
91
+ meta={meta}
92
+ />
93
+ ))}
94
+ <li>
95
+ <button
96
+ type="button"
97
+ onClick={onNew}
98
+ className={`${rowClass} ${rowIdle}`}
99
+ >
100
+ <Plus className="size-4 shrink-0" />
101
+ <span className="min-w-0 flex-1 text-start">
102
+ {t('New scheme', { note: 'recipe.calc.new_tab' })}
103
+ </span>
104
+ </button>
105
+ </li>
106
+ </ul>
107
+ </LeftSidebarFolderContent>
108
+ </LeftSidebarFolder>
109
+ ) : null}
110
+ {history.length ? (
111
+ <LeftSidebarFolder collapsible defaultOpen>
112
+ <LeftSidebarFolderTrigger className="gap-1.5 text-xs font-semibold uppercase tracking-wider text-muted-foreground/70">
113
+ <History className="size-3.5" />
114
+ {t('History', { note: 'recipe.calc.history' })}
115
+ </LeftSidebarFolderTrigger>
116
+ <LeftSidebarFolderContent>
117
+ <ul className="flex flex-col">
118
+ {history.map((entry) => (
119
+ <HistoryRow
120
+ key={entry.id}
121
+ entry={entry}
122
+ onOpen={onOpenEntry}
123
+ onRemove={onRemoveHistory}
124
+ meta={meta}
125
+ />
126
+ ))}
127
+ </ul>
128
+ </LeftSidebarFolderContent>
129
+ </LeftSidebarFolder>
130
+ ) : null}
131
+ </div>
132
+ );
133
+ }
134
+
@@ -0,0 +1,67 @@
1
+ 'use client';
2
+
3
+ import { Workflow } from 'lucide-react';
4
+ import { useTranslations } from '@terrafirmagreg-team/translate/hooks/use-translations';
5
+ import { Entry } from '@terrafirmagreg-team/game/components/entry/entry';
6
+ import type { CalcTreeEntry } from '@terrafirmagreg-team/recipe/lib/calc/storage';
7
+ import type { EntryMeta } from '@terrafirmagreg-team/recipe/lib/calc/meta';
8
+
9
+ export function TreeIcon({
10
+ entry,
11
+ dataset,
12
+ lang,
13
+ langMap,
14
+ }: { entry: CalcTreeEntry } & EntryMeta) {
15
+ const card = entry.state.cards?.[0];
16
+ const ref = entry.icon ?? (card && { id: card.materialId, kind: card.kind });
17
+ if (!ref) return <Workflow className="size-4 shrink-0" />;
18
+ return (
19
+ <Entry
20
+ id={ref.id}
21
+ kind={ref.kind}
22
+ dataset={dataset}
23
+ lang={lang}
24
+ langMap={langMap}
25
+ size="sm"
26
+ href={false}
27
+ tooltip={false}
28
+ className="shrink-0"
29
+ />
30
+ );
31
+ }
32
+
33
+ export function TreeLabel({
34
+ entry,
35
+ langMap,
36
+ }: { entry: CalcTreeEntry } & EntryMeta) {
37
+ const t = useTranslations();
38
+ const card = entry.state.cards?.[0];
39
+ const text =
40
+ entry.name ??
41
+ (card ? langMap?.[card.materialId] || card.materialId : undefined) ??
42
+ t('Untitled scheme', { note: 'recipe.calc.tab.untitled' });
43
+ return (
44
+ <span className="min-w-0 flex-1 truncate text-start">
45
+ {text}
46
+ </span>
47
+ );
48
+ }
49
+
50
+ export function TreeDate({
51
+ savedAt,
52
+ lang,
53
+ }: {
54
+ savedAt: number;
55
+ lang: string;
56
+ }) {
57
+ const stamp = new Intl.DateTimeFormat(lang.replace(/_/g, '-'), {
58
+ dateStyle: 'short',
59
+ timeStyle: 'short',
60
+ }).format(savedAt);
61
+ return (
62
+ <span className="shrink-0 text-xs tabular-nums text-muted-foreground/70">
63
+ {stamp}
64
+ </span>
65
+ );
66
+ }
67
+
@@ -0,0 +1,140 @@
1
+ 'use client';
2
+
3
+ import { useState } from 'react';
4
+ import { Check, Pencil, X } from 'lucide-react';
5
+ import { useTranslations } from '@terrafirmagreg-team/translate/hooks/use-translations';
6
+ import { IconButton } from '@terrafirmagreg-team/ui/components/blocks/button/icon-button';
7
+ import { Input } from '@terrafirmagreg-team/ui/components/ui/input';
8
+ import type { CalcTreeEntry } from '@terrafirmagreg-team/recipe/lib/calc/storage';
9
+ import type { EntryMeta, IconItem } from '@terrafirmagreg-team/recipe/lib/calc/meta';
10
+ import { rowClass, rowIdle } from '@terrafirmagreg-team/recipe/lib/calc/meta';
11
+ import { TreeDate, TreeIcon, TreeLabel } from './label';
12
+ import { IconPicker } from './icon-picker';
13
+
14
+ export function TabRow({
15
+ entry,
16
+ active,
17
+ onOpen,
18
+ onClose,
19
+ onRename,
20
+ onSetIcon,
21
+ meta,
22
+ }: {
23
+ entry: CalcTreeEntry;
24
+ active: boolean;
25
+ onOpen: (id: string) => void;
26
+ onClose: (id: string) => void;
27
+ onRename: (id: string, name: string) => void;
28
+ onSetIcon?: (id: string, icon: IconItem) => void;
29
+ meta: EntryMeta;
30
+ }) {
31
+ const t = useTranslations();
32
+ const [editing, setEditing] = useState(false);
33
+ const [name, setName] = useState(entry.name ?? '');
34
+ const commit = () => {
35
+ setEditing(false);
36
+ onRename(entry.id, name);
37
+ };
38
+ return (
39
+ <li className="group flex items-center gap-0.5">
40
+ {editing ? (
41
+ <span className="flex min-w-0 flex-1 items-center gap-1.5 px-2 py-0.5">
42
+ <TreeIcon entry={entry} {...meta} />
43
+ <Input
44
+ autoFocus
45
+ value={name}
46
+ onChange={(event) => setName(event.target.value)}
47
+ onKeyDown={(event) => {
48
+ if (event.key === 'Enter') commit();
49
+ if (event.key === 'Escape') setEditing(false);
50
+ }}
51
+ onBlur={commit}
52
+ className="h-6 px-1.5 text-xs"
53
+ placeholder={t('Rename', { note: 'recipe.calc.rename' })}
54
+ />
55
+ <IconButton label={t('Rename', { note: 'recipe.calc.rename' })} onClick={commit}>
56
+ <Check className="size-3.5" />
57
+ </IconButton>
58
+ </span>
59
+ ) : (
60
+ <button
61
+ type="button"
62
+ onClick={() => onOpen(entry.id)}
63
+ className={`${rowClass} ${
64
+ active
65
+ ? 'bg-accent font-medium text-accent-foreground'
66
+ : rowIdle
67
+ }`}
68
+ >
69
+ <TreeIcon entry={entry} {...meta} />
70
+ <TreeLabel entry={entry} {...meta} />
71
+ </button>
72
+ )}
73
+ {!editing && (
74
+ <>
75
+ {meta.iconItems && onSetIcon ? (
76
+ <IconPicker
77
+ items={meta.iconItems}
78
+ meta={meta}
79
+ onPick={(icon) => onSetIcon(entry.id, icon)}
80
+ />
81
+ ) : null}
82
+ <IconButton
83
+ label={t('Rename', { note: 'recipe.calc.rename' })}
84
+ className="opacity-0 transition-opacity group-hover:opacity-100"
85
+ onClick={() => {
86
+ setName(entry.name ?? '');
87
+ setEditing(true);
88
+ }}
89
+ >
90
+ <Pencil className="size-3.5" />
91
+ </IconButton>
92
+ <IconButton
93
+ label={t('Close scheme', { note: 'recipe.calc.close_tab' })}
94
+ className="opacity-0 transition-opacity group-hover:opacity-100"
95
+ onClick={() => onClose(entry.id)}
96
+ >
97
+ <X className="size-3.5" />
98
+ </IconButton>
99
+ </>
100
+ )}
101
+ </li>
102
+ );
103
+ }
104
+
105
+ export function HistoryRow({
106
+ entry,
107
+ onOpen,
108
+ onRemove,
109
+ meta,
110
+ }: {
111
+ entry: CalcTreeEntry;
112
+ onOpen: (entry: CalcTreeEntry) => void;
113
+ onRemove?: (id: string) => void;
114
+ meta: EntryMeta;
115
+ }) {
116
+ const t = useTranslations();
117
+ return (
118
+ <li className="group flex items-center gap-0.5">
119
+ <button
120
+ type="button"
121
+ onClick={() => onOpen(entry)}
122
+ className={`${rowClass} ${rowIdle}`}
123
+ >
124
+ <TreeIcon entry={entry} {...meta} />
125
+ <TreeLabel entry={entry} {...meta} />
126
+ <TreeDate savedAt={entry.savedAt} lang={meta.lang} />
127
+ </button>
128
+ {onRemove && (
129
+ <IconButton
130
+ label={t('Remove from history', { note: 'recipe.calc.remove.history' })}
131
+ className="opacity-0 transition-opacity group-hover:opacity-100"
132
+ onClick={() => onRemove(entry.id)}
133
+ >
134
+ <X className="size-3.5" />
135
+ </IconButton>
136
+ )}
137
+ </li>
138
+ );
139
+ }
140
+
@@ -0,0 +1,131 @@
1
+ 'use client';
2
+
3
+ import type { CSSProperties } from 'react';
4
+ import { cn } from '@terrafirmagreg-team/ui/lib/utils/cn';
5
+ import type { ForgeRule, ForgeRuleAction } from '@terrafirmagreg-team/recipe/types/forge';
6
+
7
+ const ATLAS_SIZE = 256;
8
+
9
+ export type GuiRect = { x: number; y: number; w: number; h: number };
10
+
11
+ function spriteStyle(
12
+ gui: string,
13
+ { x, y, w, h }: GuiRect,
14
+ scale: number,
15
+ ): CSSProperties {
16
+ return {
17
+ width: w * scale,
18
+ height: h * scale,
19
+ imageRendering: 'pixelated',
20
+ backgroundImage: `url(${gui})`,
21
+ backgroundSize: `${ATLAS_SIZE * scale}px ${ATLAS_SIZE * scale}px`,
22
+ backgroundPosition: `-${x * scale}px -${y * scale}px`,
23
+ };
24
+ }
25
+
26
+ function maskStyle(
27
+ gui: string,
28
+ { x, y, w, h }: GuiRect,
29
+ scale: number,
30
+ ): CSSProperties {
31
+ const size = `${ATLAS_SIZE * scale}px ${ATLAS_SIZE * scale}px`;
32
+ const position = `-${x * scale}px -${y * scale}px`;
33
+ return {
34
+ width: w * scale,
35
+ height: h * scale,
36
+ maskImage: `url(${gui})`,
37
+ maskSize: size,
38
+ maskPosition: position,
39
+ maskRepeat: 'no-repeat',
40
+ WebkitMaskImage: `url(${gui})`,
41
+ WebkitMaskSize: size,
42
+ WebkitMaskPosition: position,
43
+ WebkitMaskRepeat: 'no-repeat',
44
+ };
45
+ }
46
+
47
+ export function GuiSprite({
48
+ gui,
49
+ rect,
50
+ scale = 1,
51
+ className,
52
+ style,
53
+ }: {
54
+ gui: string | undefined;
55
+ rect: GuiRect;
56
+ scale?: number;
57
+ className?: string;
58
+ style?: CSSProperties;
59
+ }) {
60
+ if (!gui) return null;
61
+ return (
62
+ <span
63
+ aria-hidden
64
+ className={cn('inline-block shrink-0', className)}
65
+ style={{ ...spriteStyle(gui, rect, scale), ...style }}
66
+ />
67
+ );
68
+ }
69
+
70
+ export function GuiMask({
71
+ gui,
72
+ rect,
73
+ scale = 1,
74
+ className,
75
+ style,
76
+ }: {
77
+ gui: string | undefined;
78
+ rect: GuiRect;
79
+ scale?: number;
80
+ className?: string;
81
+ style?: CSSProperties;
82
+ }) {
83
+ if (!gui) return null;
84
+ return (
85
+ <span
86
+ aria-hidden
87
+ className={cn('inline-block shrink-0', className)}
88
+ style={{ ...maskStyle(gui, rect, scale), ...style }}
89
+ />
90
+ );
91
+ }
92
+
93
+ export const GUI_PANEL: GuiRect = { x: 0, y: 0, w: 176, h: 110 };
94
+
95
+ export const PLAN_SLOT: GuiRect = { x: 218, y: 0, w: 18, h: 18 };
96
+
97
+ export const RULE_OVERLAY_Y: Record<ForgeRule['order'], number> = {
98
+ last: 0,
99
+ second_last: 22,
100
+ third_last: 44,
101
+ not_last: 66,
102
+ any: 88,
103
+ };
104
+
105
+ const STEP_GLYPH_X: Record<string, number> = {
106
+ punch: 0,
107
+ bend: 32,
108
+ upset: 64,
109
+ shrink: 96,
110
+ light: 128,
111
+ medium: 160,
112
+ hard: 192,
113
+ draw: 224,
114
+ };
115
+
116
+ export const RULE_ICON: Record<ForgeRuleAction, GuiRect> = {
117
+ hit: { x: 218, y: 18, w: 32, h: 32 },
118
+ draw: { x: 224, y: 224, w: 32, h: 32 },
119
+ punch: { x: 0, y: 224, w: 32, h: 32 },
120
+ bend: { x: 32, y: 224, w: 32, h: 32 },
121
+ upset: { x: 64, y: 224, w: 32, h: 32 },
122
+ shrink: { x: 96, y: 224, w: 32, h: 32 },
123
+ };
124
+
125
+ export const MARK_PROGRESS: GuiRect = { x: 176, y: 0, w: 5, h: 5 };
126
+ export const MARK_TARGET: GuiRect = { x: 181, y: 0, w: 5, h: 5 };
127
+
128
+ export function actionGlyphRect(action: string): GuiRect {
129
+ return { x: STEP_GLYPH_X[action] ?? 0, y: 224, w: 32, h: 32 };
130
+ }
131
+
@@ -0,0 +1,112 @@
1
+ 'use client';
2
+
3
+ import { useState } from 'react';
4
+ import { useTranslations } from '@terrafirmagreg-team/translate/hooks/use-translations';
5
+ import { ItemRow } from '@terrafirmagreg-team/ui/components/blocks/list/row';
6
+ import { EmptyState } from '@terrafirmagreg-team/ui/components/blocks/page/empty-state';
7
+ import { RecipeItemHeader } from '@terrafirmagreg-team/recipe/components/views/browser/recipe-item-header';
8
+ import { RecipeFrame } from '@terrafirmagreg-team/recipe/components/views/browser/frame';
9
+ import { useToolRecipes } from '@terrafirmagreg-team/recipe/hooks/use-tool-recipes';
10
+ import { toForgeRecipe } from '@terrafirmagreg-team/recipe/lib/forge/data';
11
+ import { RecipeCard } from '@terrafirmagreg-team/recipe/components/cards/card';
12
+ import { FitBox } from '@terrafirmagreg-team/ui/components/blocks/fit-box';
13
+ import {
14
+ recipeCardImageUrl,
15
+ recipeImageUrl,
16
+ } from '@terrafirmagreg-team/recipe/lib/links';
17
+ import { ForgeList } from './list';
18
+ import { ForgeWorkbench } from './workbench';
19
+ import type { Recipe } from '@terrafirmagreg-team/recipe/lib/data/dto';
20
+ import type { RecipeSession } from '@terrafirmagreg-team/recipe/types';
21
+
22
+ function ForgeRecipeCard({
23
+ recipe,
24
+ session,
25
+ }: {
26
+ recipe: Recipe;
27
+ session: RecipeSession;
28
+ }) {
29
+ const [failed, setFailed] = useState(false);
30
+ const src =
31
+ recipeImageUrl(session.dataset, recipe) ??
32
+ recipeCardImageUrl(session.dataset, recipe.id);
33
+ if (!src || failed) return null;
34
+ return (
35
+ <RecipeCard
36
+ recipe={recipe}
37
+ dataset={session.dataset}
38
+ lang={session.lang}
39
+ langMap={session.langMap}
40
+ title={recipe.id}
41
+ src={src}
42
+ onFail={() => setFailed(true)}
43
+ interactive
44
+ />
45
+ );
46
+ }
47
+
48
+ export function ForgeCalculator({ session }: { session: RecipeSession }) {
49
+ const t = useTranslations();
50
+ const { value, pending } = useToolRecipes(
51
+ session,
52
+ 'tfc:anvil',
53
+ toForgeRecipe,
54
+ );
55
+ const activeId = session.recipeId;
56
+ const active =
57
+ value?.find(({ recipe }) => recipe.id === activeId) ??
58
+ (!activeId ? value?.[0] : undefined);
59
+ const loading = session.indexLoading || pending;
60
+
61
+ return (
62
+ <>
63
+ <ForgeList session={session} />
64
+ <RecipeFrame
65
+ session={session}
66
+ header={
67
+ active ? (
68
+ <RecipeItemHeader recipe={active.recipe} session={session} />
69
+ ) : (
70
+ <ItemRow layout="header" title={t('Forging', { note: 'recipe.forge.title' })} />
71
+ )
72
+ }
73
+ >
74
+ <div className="scrollbar-thin flex-1 space-y-5 overflow-y-auto p-2 sm:p-4">
75
+ {loading ? (
76
+ <EmptyState variant="loading" />
77
+ ) : value === undefined ? (
78
+ <EmptyState variant="missing">
79
+ {t('Could not load recipes. Reload the page to retry.', { note: 'recipe.forge.load_error' })}
80
+ </EmptyState>
81
+ ) : active ? (
82
+ <>
83
+ <ForgeWorkbench
84
+ key={active.recipe.id}
85
+ recipe={active}
86
+ session={session}
87
+ />
88
+ <FitBox w={active.recipe.w ?? 0}>
89
+ <ForgeRecipeCard
90
+ key={active.recipe.id}
91
+ recipe={active.recipe}
92
+ session={session}
93
+ />
94
+ </FitBox>
95
+ </>
96
+ ) : (
97
+ <EmptyState>
98
+ {activeId
99
+ ? t('Recipe not found in this dump. Select another recipe.', {
100
+ note: 'recipe.forge.missing',
101
+ })
102
+ : t('No matching recipes in this dump.', {
103
+ note: 'recipe.forge.empty',
104
+ })}
105
+ </EmptyState>
106
+ )}
107
+ </div>
108
+ </RecipeFrame>
109
+ </>
110
+ );
111
+ }
112
+
@@ -0,0 +1,113 @@
1
+ 'use client';
2
+
3
+ import { useDeferredValue, useMemo, useState } from 'react';
4
+ import type * as PageTree from 'fumadocs-core/page-tree';
5
+ import { useTranslations } from '@terrafirmagreg-team/translate/hooks/use-translations';
6
+ import { Entry } from '@terrafirmagreg-team/game/components/entry/entry';
7
+ import { ToolList } from '@terrafirmagreg-team/recipe/components/tool-list';
8
+ import { useToolRecipes } from '@terrafirmagreg-team/recipe/hooks/use-tool-recipes';
9
+ import { recipeItemName } from '@terrafirmagreg-team/recipe/lib/client-index';
10
+ import {
11
+ anvilGroup,
12
+ anvilGroupRank,
13
+ toForgeRecipe,
14
+ } from '@terrafirmagreg-team/recipe/lib/forge/data';
15
+ import { recipeHref } from '@terrafirmagreg-team/recipe/lib/links';
16
+ import type { ForgeRecipe } from '@terrafirmagreg-team/recipe/types/forge';
17
+ import type { RecipeSession } from '@terrafirmagreg-team/recipe/types';
18
+
19
+ function anvilGroupName(session: RecipeSession, id: string): string {
20
+ if (!id) return '—';
21
+ const resolved = recipeItemName(session.names, session.lang, id);
22
+ return resolved === id
23
+ ? (id.split('/').pop()?.replaceAll('_', ' ') ?? id)
24
+ : resolved;
25
+ }
26
+
27
+ export function ForgeList({ session }: { session: RecipeSession }) {
28
+ const t = useTranslations();
29
+ const { value } = useToolRecipes(session, 'tfc:anvil', toForgeRecipe);
30
+ const [query, setQuery] = useState('');
31
+ const search = useDeferredValue(query.trim().toLowerCase());
32
+ const name = (id: string) => recipeItemName(session.names, session.lang, id);
33
+
34
+ const tree = useMemo((): PageTree.Root => {
35
+ const matches = (recipe: ForgeRecipe['recipe']) =>
36
+ [
37
+ recipe.id,
38
+ ...recipe.inputs.map((slot) => slot.id),
39
+ ...recipe.outputs.map((slot) => slot.id),
40
+ ].some((id) => `${id} ${name(id)}`.toLowerCase().includes(search));
41
+
42
+ const groups = [...Map.groupBy(value ?? [], (item) => anvilGroup(item.recipe).id)]
43
+ .map(([id, rows]) => ({
44
+ id,
45
+ icon: anvilGroup(rows[0]!.recipe).icon,
46
+ rows: search ? rows.filter((r) => matches(r.recipe)) : rows,
47
+ }))
48
+ .filter((group) => group.rows.length > 0)
49
+ .sort(
50
+ (a, b) => anvilGroupRank(a.id) - anvilGroupRank(b.id) || a.id.localeCompare(b.id),
51
+ );
52
+
53
+ const children: PageTree.Folder[] = groups.map((group) => ({
54
+ type: 'folder',
55
+ $id: `forge:${group.id}`,
56
+ name: (
57
+ <span className="-me-3 flex min-w-0 flex-1 items-center gap-1.5">
58
+ <span className="truncate">{anvilGroupName(session, group.id)}</span>
59
+ <span className="shrink-0 tabular-nums text-xs text-muted-foreground/70">
60
+ ×{group.rows.length}
61
+ </span>
62
+ </span>
63
+ ),
64
+ icon: group.icon ? (
65
+ <span className="inline-flex size-4 shrink-0 items-center justify-center">
66
+ <Entry id={group.icon} kind="item" size="sm" href={false} tooltip={false} />
67
+ </span>
68
+ ) : undefined,
69
+ defaultOpen: search !== '',
70
+ children: group.rows.map(
71
+ ({ recipe }): PageTree.Item => ({
72
+ type: 'page',
73
+ $id: recipe.id,
74
+ url: recipeHref(session.dataset, session.lang, {
75
+ tool: 'forge',
76
+ id: recipe.id,
77
+ tab: session.tab,
78
+ }),
79
+ name: (
80
+ <Entry
81
+ id={recipe.outputs[0]?.id ?? recipe.id}
82
+ kind={recipe.outputs[0]?.kind === 'fluid' ? 'fluid' : 'item'}
83
+ size="sm"
84
+ labeled
85
+ href={false}
86
+ tooltip={false}
87
+ className="min-w-0 flex-1 justify-start truncate"
88
+ />
89
+ ),
90
+ }),
91
+ ),
92
+ }));
93
+
94
+ return {
95
+ name: 'forge',
96
+ $id: `forge:${session.lang}:${search}:${(value ?? []).length}`,
97
+ children,
98
+ } as PageTree.Root;
99
+ // eslint-disable-next-line react-hooks/exhaustive-deps
100
+ }, [value, search, session.lang, session.dataset, session.tab]);
101
+
102
+ return (
103
+ <ToolList
104
+ tree={tree}
105
+ loaded={value !== undefined}
106
+ query={query}
107
+ onQuery={setQuery}
108
+ searchLabel={t('Search recipes', { note: 'recipe.forge.search' })}
109
+ emptyLabel={t('No matching recipes in this dump.', { note: 'recipe.forge.empty' })}
110
+ />
111
+ );
112
+ }
113
+