@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,120 @@
1
+ 'use client';
2
+ import type { DatasetRef } from '@terrafirmagreg-team/sites/location';
3
+
4
+ import { useTranslations } from '@terrafirmagreg-team/translate/hooks/use-translations';
5
+ import {
6
+ Command,
7
+ CommandDialog,
8
+ CommandEmpty,
9
+ CommandGroup,
10
+ CommandInput,
11
+ CommandItem,
12
+ CommandList,
13
+ } from '@terrafirmagreg-team/ui/components/ui/command';
14
+ import { Entry } from '@terrafirmagreg-team/game/components/entry/entry';
15
+ import { stackDisplayName } from '@terrafirmagreg-team/game/lib/entry/display';
16
+ import type { ReferenceKind } from '@terrafirmagreg-team/dump/lib/dump/contract/ref';
17
+
18
+ export function CalcMemberList({
19
+ tagId,
20
+ members,
21
+ selected,
22
+ memberKind,
23
+ dataset,
24
+ lang,
25
+ langMap,
26
+ onSelect,
27
+ }: {
28
+ tagId: string;
29
+ members: string[];
30
+ selected?: string;
31
+ memberKind?: ReferenceKind;
32
+ dataset: DatasetRef;
33
+ lang: string;
34
+ langMap?: Record<string, string>;
35
+ onSelect: (tagId: string, memberId: string) => void;
36
+ }) {
37
+ const t = useTranslations();
38
+ return (
39
+ <Command label={tagId}>
40
+ <CommandInput placeholder={t('Search member…', { note: 'recipe.calc.member.search' })} />
41
+ <CommandList>
42
+ <CommandEmpty>{t('No results found', { note: 'recipe.search_empty' })}</CommandEmpty>
43
+ <CommandGroup>
44
+ {members.map((id) => (
45
+ <CommandItem
46
+ key={id}
47
+ value={id}
48
+ keywords={[stackDisplayName(langMap, id), id]}
49
+ data-checked={id === selected}
50
+ onSelect={() => onSelect(tagId, id)}
51
+ >
52
+ <Entry
53
+ id={id}
54
+ kind={memberKind}
55
+ dataset={dataset}
56
+ lang={lang}
57
+ langMap={langMap}
58
+ size="sm"
59
+ labeled
60
+ href={false}
61
+ tooltip="adaptive"
62
+ />
63
+ </CommandItem>
64
+ ))}
65
+ </CommandGroup>
66
+ </CommandList>
67
+ </Command>
68
+ );
69
+ }
70
+
71
+ export function CalcMemberPicker({
72
+ open,
73
+ onOpenChange,
74
+ tagId,
75
+ members,
76
+ selected,
77
+ memberKind,
78
+ dataset,
79
+ lang,
80
+ langMap,
81
+ onSelect,
82
+ }: {
83
+ open: boolean;
84
+ onOpenChange: (open: boolean) => void;
85
+ tagId: string;
86
+ members: string[];
87
+ selected?: string;
88
+ memberKind?: ReferenceKind;
89
+ dataset: DatasetRef;
90
+ lang: string;
91
+ langMap?: Record<string, string>;
92
+ onSelect: (tagId: string, memberId: string) => void;
93
+ }) {
94
+ const t = useTranslations();
95
+ return (
96
+ <CommandDialog
97
+ open={open}
98
+ onOpenChange={onOpenChange}
99
+ title={t('Choose tag member', { note: 'recipe.calc.member.title' })}
100
+ description={tagId}
101
+ className="sm:max-w-md"
102
+ commandProps={{ label: tagId }}
103
+ >
104
+ <CalcMemberList
105
+ tagId={tagId}
106
+ members={members}
107
+ selected={selected}
108
+ memberKind={memberKind}
109
+ dataset={dataset}
110
+ lang={lang}
111
+ langMap={langMap}
112
+ onSelect={(id, member) => {
113
+ onSelect(id, member);
114
+ onOpenChange(false);
115
+ }}
116
+ />
117
+ </CommandDialog>
118
+ );
119
+ }
120
+
@@ -0,0 +1,90 @@
1
+ 'use client';
2
+
3
+ import { useState } from 'react';
4
+ import {
5
+ Check,
6
+ Copy,
7
+ ExternalLink,
8
+ Layers,
9
+ X,
10
+ } from 'lucide-react';
11
+ import { useTranslations } from '@terrafirmagreg-team/translate/hooks/use-translations';
12
+ import { Hint } from '@terrafirmagreg-team/ui/components/blocks/tooltip/hint';
13
+ import { cn } from '@terrafirmagreg-team/ui/lib/utils/cn';
14
+ import type { CalcFlowNode } from '@terrafirmagreg-team/recipe/lib/calc/flow';
15
+
16
+ export const CORNER_BUTTON =
17
+ 'nodrag flex size-5 shrink-0 items-center justify-center rounded-sm border border-border bg-background text-muted-foreground shadow-sm hover:text-foreground';
18
+
19
+ export function NodeActions({
20
+ data,
21
+ nodeId,
22
+ }: {
23
+ data: CalcFlowNode['data'];
24
+ nodeId: string;
25
+ }) {
26
+ const { node, onRemoveCard, onOpenItem, onRequestPicker } = data;
27
+ const t = useTranslations();
28
+ const [copied, setCopied] = useState(false);
29
+ const canPick = node.recipe != null || node.availableRecipes.length > 0;
30
+
31
+ return (
32
+ <span className="pointer-events-auto absolute -top-2 right-3 z-30 flex gap-0.5">
33
+ {canPick ? (
34
+ <Hint label={t('Pick a recipe', { note: 'recipe.calc.pick.title' })} side="top">
35
+ <button
36
+ type="button"
37
+ aria-label={t('Pick a recipe', { note: 'recipe.calc.pick.title' })}
38
+ onClick={() => onRequestPicker(node)}
39
+ className={CORNER_BUTTON}
40
+ >
41
+ <Layers className="size-2.5" />
42
+ </button>
43
+ </Hint>
44
+ ) : null}
45
+ <Hint label={t('Copy id', { note: 'recipe.calc.copy_id' })} side="top">
46
+ <button
47
+ type="button"
48
+ aria-label={t('Copy id', { note: 'recipe.calc.copy_id' })}
49
+ onClick={() => {
50
+ void navigator.clipboard.writeText(node.materialId);
51
+ setCopied(true);
52
+ setTimeout(() => setCopied(false), 1500);
53
+ }}
54
+ className={CORNER_BUTTON}
55
+ >
56
+ {copied ? (
57
+ <Check className="size-2.5 text-emerald-500" />
58
+ ) : (
59
+ <Copy className="size-2.5" />
60
+ )}
61
+ </button>
62
+ </Hint>
63
+ <Hint label={t('Open item page', { note: 'recipe.calc.open.item' })} side="top">
64
+ <button
65
+ type="button"
66
+ aria-label={t('Open item page', { note: 'recipe.calc.open.item' })}
67
+ onClick={() => onOpenItem(node.materialId, node.kind)}
68
+ className={CORNER_BUTTON}
69
+ >
70
+ <ExternalLink className="size-2.5" />
71
+ </button>
72
+ </Hint>
73
+ <Hint label={t('Remove card', { note: 'recipe.calc.remove.title' })} side="top">
74
+ <button
75
+ type="button"
76
+ aria-label={t('Remove card', { note: 'recipe.calc.remove.title' })}
77
+ onPointerDown={(event) => event.stopPropagation()}
78
+ onClick={(event) => {
79
+ event.stopPropagation();
80
+ onRemoveCard(nodeId);
81
+ }}
82
+ className={cn(CORNER_BUTTON, 'hover:text-destructive')}
83
+ >
84
+ <X className="size-3" />
85
+ </button>
86
+ </Hint>
87
+ </span>
88
+ );
89
+ }
90
+
@@ -0,0 +1,59 @@
1
+ 'use client';
2
+
3
+ import type { ReactNode } from 'react';
4
+ import { Mountain, Repeat } from 'lucide-react';
5
+ import { useTranslations } from '@terrafirmagreg-team/translate/hooks/use-translations';
6
+ import { Hint } from '@terrafirmagreg-team/ui/components/blocks/tooltip/hint';
7
+ import { cn } from '@terrafirmagreg-team/ui/lib/utils/cn';
8
+ import { formatCount } from '@terrafirmagreg-team/ui/lib/utils/format';
9
+ import type { CalcFlowNode } from '@terrafirmagreg-team/recipe/lib/calc/flow';
10
+
11
+ const CORNER_CHIP =
12
+ 'flex shrink-0 items-center justify-center gap-0.5 rounded-sm border border-border bg-background px-0.5 py-px text-[9px] leading-none text-muted-foreground';
13
+
14
+ export function NodeBadges({
15
+ node,
16
+ }: {
17
+ node: CalcFlowNode['data']['node'];
18
+ }) {
19
+ const t = useTranslations();
20
+ const chips: ReactNode[] = [];
21
+ if (node.cycle) {
22
+ chips.push(
23
+ <Hint key="cycle" label={t('Recipe cycle: the recipe needs itself upstream', { note: 'recipe.calc.hint.cycle' })} side="top">
24
+ <span
25
+ className={cn(CORNER_CHIP, 'border-red-500/40 text-red-500')}
26
+ >
27
+ <Repeat className="size-2.5" />
28
+ </span>
29
+ </Hint>,
30
+ );
31
+ }
32
+ if (!node.recipe && !node.availableRecipes.length && !node.loading) {
33
+ chips.push(
34
+ <Hint key="source" label={t('No recipe: taken from remainders or gathered externally', { note: 'recipe.calc.hint.source' })} side="top">
35
+ <span className={CORNER_CHIP}>
36
+ <Mountain className="size-2.5" />
37
+ </span>
38
+ </Hint>,
39
+ );
40
+ }
41
+ if (node.remaining < 0) {
42
+ chips.push(
43
+ <Hint key="over" label={t('Overdrawn: more is wired out than produced', { note: 'recipe.calc.overdrawn' })} side="top">
44
+ <span
45
+ className={cn(CORNER_CHIP, 'border-red-500/40 text-red-500')}
46
+ >
47
+ {formatCount(node.remaining)}
48
+ </span>
49
+ </Hint>,
50
+ );
51
+ }
52
+ if (!chips.length) return null;
53
+ return (
54
+ <span className="pointer-events-auto absolute right-3 -bottom-1.5 z-30 flex flex-col-reverse items-end gap-0.5">
55
+ {chips}
56
+ </span>
57
+ );
58
+ }
59
+
@@ -0,0 +1,187 @@
1
+ 'use client';
2
+ import type { DatasetRef } from '@terrafirmagreg-team/sites/location';
3
+
4
+ import type { ReactNode } from 'react';
5
+ import {
6
+ ChevronDown,
7
+ ListFilter,
8
+ Shapes,
9
+ } from 'lucide-react';
10
+ import { useTranslations } from '@terrafirmagreg-team/translate/hooks/use-translations';
11
+ import { Hint } from '@terrafirmagreg-team/ui/components/blocks/tooltip/hint';
12
+ import { Entry } from '@terrafirmagreg-team/game/components/entry/entry';
13
+ import { Skeleton } from '@terrafirmagreg-team/ui/components/ui/skeleton';
14
+ import { cn } from '@terrafirmagreg-team/ui/lib/utils/cn';
15
+ import { formatCount } from '@terrafirmagreg-team/ui/lib/utils/format';
16
+ import type {
17
+ CalcFlowData,
18
+ CalcFlowNode,
19
+ } from '@terrafirmagreg-team/recipe/lib/calc/flow';
20
+ import type { RecipeCategory } from '@terrafirmagreg-team/recipe/types';
21
+
22
+ type CalcNode = CalcFlowNode['data']['node'];
23
+
24
+ export function CycleChip({
25
+ onOpen,
26
+ openLabel,
27
+ title,
28
+ accent,
29
+ className,
30
+ centerClassName,
31
+ children,
32
+ }: {
33
+ onOpen?: () => void;
34
+ openLabel?: string;
35
+ title?: string;
36
+ accent?: boolean;
37
+ className?: string;
38
+ centerClassName?: string;
39
+ children: ReactNode;
40
+ }) {
41
+ const centerCls = cn(
42
+ 'flex min-w-0 flex-1 items-center justify-center gap-1 px-0.5 py-0.5 text-[10px] text-muted-foreground outline-none',
43
+ onOpen && 'hover:text-foreground',
44
+ accent && 'text-sky-500 hover:text-sky-400',
45
+ centerClassName,
46
+ );
47
+ return (
48
+ <span
49
+ className={cn(
50
+ 'flex min-w-0 items-center overflow-hidden rounded-md border bg-background',
51
+ accent ? 'border-sky-500/30' : 'border-border',
52
+ className,
53
+ )}
54
+ >
55
+ {onOpen ? (
56
+ <Hint label={title} side="top">
57
+ <button
58
+ type="button"
59
+ aria-label={openLabel}
60
+ onClick={onOpen}
61
+ className={centerCls}
62
+ >
63
+ {children}
64
+ </button>
65
+ </Hint>
66
+ ) : (
67
+ <Hint label={title} side="top">
68
+ <span className={centerCls}>{children}</span>
69
+ </Hint>
70
+ )}
71
+ </span>
72
+ );
73
+ }
74
+
75
+ export function NodeChips({
76
+ node,
77
+ mini,
78
+ name,
79
+ category,
80
+ hasAlternatives,
81
+ dataset,
82
+ lang,
83
+ langMap,
84
+ onRequestPicker,
85
+ onRequestMemberPicker,
86
+ }: {
87
+ node: CalcNode;
88
+ mini: boolean;
89
+ name: string;
90
+ category: RecipeCategory | undefined;
91
+ hasAlternatives: boolean;
92
+ dataset: DatasetRef;
93
+ lang: string;
94
+ langMap?: Record<string, string>;
95
+ onRequestPicker: CalcFlowData['onRequestPicker'];
96
+ onRequestMemberPicker: CalcFlowData['onRequestMemberPicker'];
97
+ }) {
98
+ const t = useTranslations();
99
+ const members = node.members;
100
+ const loadingChip =
101
+ node.loading && !node.recipe ? (
102
+ <Skeleton className="h-5 min-w-0 flex-1 rounded-md" />
103
+ ) : null;
104
+ return (
105
+ <div
106
+ className={cn(
107
+ 'relative z-20 flex min-w-0 items-center gap-1 overflow-hidden',
108
+ mini && 'h-5 w-full',
109
+ )}
110
+ >
111
+ {loadingChip}
112
+ {!node.recipe && !loadingChip ? (
113
+ <CycleChip
114
+ className="min-w-0 flex-1 border-dashed opacity-60"
115
+ title={t('No recipe: taken from remainders or gathered externally', { note: 'recipe.calc.hint.source' })}
116
+ centerClassName="py-0.5"
117
+ >
118
+ <ListFilter className="size-3 shrink-0" />
119
+ </CycleChip>
120
+ ) : null}
121
+ {node.recipe ? (
122
+ <CycleChip
123
+ className="min-w-0 flex-1"
124
+ title={node.recipe.recipeId}
125
+ openLabel={t('Choose recipe', { note: 'recipe.calc.select' })}
126
+ onOpen={() => onRequestPicker(node)}
127
+ >
128
+ {category?.icon ? (
129
+ <Entry
130
+ dataset={dataset}
131
+ lang={lang}
132
+ langMap={langMap}
133
+ reference={category.icon}
134
+ name={node.recipe.category}
135
+ size="sm"
136
+ href={false}
137
+ tooltip={false}
138
+ missing="hide"
139
+ />
140
+ ) : (
141
+ <Shapes
142
+ className={mini ? 'size-3.5 shrink-0' : 'size-4'}
143
+ />
144
+ )}
145
+ {hasAlternatives ? (
146
+ <ChevronDown
147
+ className={mini ? 'size-2.5 shrink-0' : 'size-3 shrink-0'}
148
+ />
149
+ ) : null}
150
+ </CycleChip>
151
+ ) : null}
152
+ {node.tagId && members && members.length > 1 ? (
153
+ <CycleChip
154
+ accent
155
+ className="shrink-0"
156
+ title={
157
+ mini
158
+ ? `${node.tagId} ×${formatCount(members.length)}`
159
+ : node.tagId
160
+ }
161
+ openLabel={t('Choose tag member', { note: 'recipe.calc.member.title' })}
162
+ onOpen={() => onRequestMemberPicker(node)}
163
+ centerClassName="px-0.5"
164
+ >
165
+ <Entry
166
+ id={node.materialId}
167
+ kind={node.kind}
168
+ dataset={dataset}
169
+ lang={lang}
170
+ langMap={langMap}
171
+ name={name}
172
+ size="sm"
173
+ href={false}
174
+ tooltip={false}
175
+ />
176
+ {!mini ? (
177
+ <span className="shrink-0 tabular-nums">
178
+ ×{formatCount(members.length)}
179
+ </span>
180
+ ) : null}
181
+ <ChevronDown className="size-2.5 shrink-0" />
182
+ </CycleChip>
183
+ ) : null}
184
+ </div>
185
+ );
186
+ }
187
+
@@ -0,0 +1,123 @@
1
+ 'use client';
2
+
3
+ import {
4
+ type NodeProps,
5
+ } from '@terrafirmagreg-team/ui/lib/canvas/xyflow';
6
+ import { ListFilter } from 'lucide-react';
7
+ import { useTranslations } from '@terrafirmagreg-team/translate/hooks/use-translations';
8
+ import { StackCount } from '@terrafirmagreg-team/game/components/entry/stack-count';
9
+ import { PagedCard } from '@terrafirmagreg-team/recipe/components/cards/paged-card';
10
+ import { Skeleton } from '@terrafirmagreg-team/ui/components/ui/skeleton';
11
+ import { useCalcSession } from '@terrafirmagreg-team/recipe/hooks/calc/use-session';
12
+ import { recipeItemName } from '@terrafirmagreg-team/recipe/lib/client-index';
13
+ import {
14
+ recipeCardImageUrl,
15
+ recipeImageUrl,
16
+ } from '@terrafirmagreg-team/recipe/lib/links';
17
+ import { recipeTitle } from '@terrafirmagreg-team/recipe/lib/data/parse';
18
+ import { cn } from '@terrafirmagreg-team/ui/lib/utils/cn';
19
+ import type { CalcFlowNode } from '@terrafirmagreg-team/recipe/lib/calc/flow';
20
+ import { CORNER_BUTTON } from './actions';
21
+ import { CardHandles } from './ports';
22
+
23
+ export function CalcHiddenCard({ data }: NodeProps<CalcFlowNode>) {
24
+ return (
25
+ <div data-calc-card className="relative h-full w-full">
26
+ <CardHandles node={data.node} interactive={false} />
27
+ <Skeleton className="size-full rounded-md" />
28
+ </div>
29
+ );
30
+ }
31
+
32
+ export function CalcFarCard({ data, selected }: NodeProps<CalcFlowNode>) {
33
+ const { node, dataset, mode, recipeBody, onRequestPicker } = data;
34
+ const t = useTranslations();
35
+ const session = useCalcSession();
36
+ const lang = session?.lang ?? 'en_us';
37
+ const names = session?.names ?? {};
38
+ const langMap = session?.langMap;
39
+ const name = recipeItemName(names, lang, node.materialId);
40
+ const body = node.recipe ? recipeBody(node.recipe.recipeId) : undefined;
41
+ const src = body
42
+ ? (recipeImageUrl(dataset, body) ??
43
+ recipeCardImageUrl(dataset, body.id))
44
+ : undefined;
45
+ const pickable =
46
+ node.recipe != null || node.availableRecipes.length > 0;
47
+ const stateCls = cn(
48
+ node.cycle && 'border-red-500/70',
49
+ !node.cycle && node.recipe && 'border-primary/60',
50
+ !node.recipe && 'border-dashed border-muted-foreground/50',
51
+ selected && 'ring-2 ring-primary/70',
52
+ );
53
+ return (
54
+ <div
55
+ data-calc-card
56
+ className={cn(
57
+ 'relative flex h-full w-full flex-col justify-center gap-0.5 rounded-md border bg-card px-2 py-1 text-xs',
58
+ stateCls,
59
+ )}
60
+ >
61
+ <CardHandles node={node} interactive={false} />
62
+ <div className="flex min-w-0 items-center gap-1.5">
63
+ <span className="size-4 shrink-0 rounded-sm border border-border bg-muted/40" />
64
+ <span className="min-w-0 flex-1 truncate">
65
+ <StackCount count={node.produced} /> {name}
66
+ </span>
67
+ </div>
68
+ {mode === 'detailed' && body && src ? (
69
+ <div className="nodrag pointer-events-none mx-auto mt-1 select-none">
70
+ <PagedCard
71
+ recipe={body}
72
+ dataset={dataset}
73
+ lang={lang}
74
+ langMap={langMap}
75
+ title={recipeTitle(body, langMap ?? {})}
76
+ src={src}
77
+ naturalW={body.w}
78
+ naturalH={body.h}
79
+ interactive={false}
80
+ slotTooltip="none"
81
+ frameOnly
82
+ onFail={() => {}}
83
+ />
84
+ </div>
85
+ ) : null}
86
+ {pickable ? (
87
+ <button
88
+ type="button"
89
+ aria-label={t('Choose recipe', { note: 'recipe.calc.select' })}
90
+ onClick={(event) => {
91
+ event.stopPropagation();
92
+ onRequestPicker(node);
93
+ }}
94
+ className={cn(CORNER_BUTTON, 'absolute -top-2 right-1')}
95
+ >
96
+ <ListFilter className="size-2.5" />
97
+ </button>
98
+ ) : null}
99
+ </div>
100
+ );
101
+ }
102
+
103
+ export function CalcDotCard({ data, selected }: NodeProps<CalcFlowNode>) {
104
+ const { node } = data;
105
+ const stateCls = cn(
106
+ node.cycle && 'border-red-500/70',
107
+ !node.cycle && node.recipe && 'border-primary/60',
108
+ !node.recipe && 'border-dashed border-muted-foreground/50',
109
+ selected && 'ring-2 ring-primary/70',
110
+ );
111
+ return (
112
+ <div
113
+ data-calc-card
114
+ className={cn(
115
+ 'h-full w-full overflow-hidden rounded-md border bg-card',
116
+ stateCls,
117
+ )}
118
+ >
119
+ <CardHandles node={node} interactive={false} />
120
+ </div>
121
+ );
122
+ }
123
+