@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,122 @@
1
+ 'use client';
2
+
3
+ import { useTranslations } from '@terrafirmagreg-team/translate/hooks/use-translations';
4
+ import { useCopyButton } from '@terrafirmagreg-team/ui/hooks/use-copy-button';
5
+ import { Badge } from '@terrafirmagreg-team/ui/components/ui/badge';
6
+ import { Entry } from '@terrafirmagreg-team/game/components/entry/entry';
7
+ import { IconButton } from '@terrafirmagreg-team/ui/components/blocks/button/icon-button';
8
+ import {
9
+ ArrowUpToLine,
10
+ Check,
11
+ Copy,
12
+ Magnet,
13
+ RotateCcw,
14
+ } from 'lucide-react';
15
+ import type { MixState } from '@terrafirmagreg-team/recipe/hooks/alloy/use-mix-state';
16
+
17
+ export function MixStatus({ state }: { state: MixState }) {
18
+ const t = useTranslations();
19
+ const {
20
+ mix,
21
+ fix,
22
+ needsSnap,
23
+ snapped,
24
+ factor,
25
+ totalNumber,
26
+ unit,
27
+ ratioMix,
28
+ output,
29
+ unitLabel,
30
+ format,
31
+ copyText,
32
+ trim,
33
+ setInputs,
34
+ fill,
35
+ } = state;
36
+ const { checked: copied, onClick: onCopy } = useCopyButton(() =>
37
+ navigator.clipboard.writeText(copyText),
38
+ );
39
+
40
+ return (
41
+ <div
42
+ aria-live="polite"
43
+ className="flex flex-wrap items-center gap-3 rounded-md border bg-muted/40 px-3 py-2"
44
+ >
45
+ <Badge variant={mix.valid ? 'default' : 'destructive'}>
46
+ {mix.valid ? t('Valid alloy', { note: 'recipe.alloy.valid' }) : t('Outside allowed ranges', { note: 'recipe.alloy.invalid' })}
47
+ </Badge>
48
+ <span className="flex flex-wrap items-baseline gap-x-3">
49
+ <span className="text-sm text-muted-foreground tabular-nums">
50
+ {t('Total', { note: 'recipe.alloy.total_in' })}: {format(mix.total)} {unitLabel}
51
+ </span>
52
+ <span className="flex items-center gap-1.5 text-sm tabular-nums font-medium">
53
+ <span className="text-muted-foreground">→</span>
54
+ {output ? (
55
+ <Entry
56
+ id={output.id}
57
+ kind={output.kind === 'fluid' ? 'fluid' : 'item'}
58
+ size="sm"
59
+ labeled
60
+ href={false}
61
+ tooltip={false}
62
+ />
63
+ ) : (
64
+ t('Alloy', { note: 'recipe.alloy.result' })
65
+ )}
66
+ <span className="font-normal text-muted-foreground">
67
+ {format(mix.output)} {unitLabel}
68
+ </span>
69
+ </span>
70
+ </span>
71
+ <span className="ms-auto flex flex-wrap items-center gap-2">
72
+ <IconButton
73
+ label={`${t('Top up to minimum', { note: 'recipe.alloy.fix' })}: ${t('Add just enough metal to bring every share into its range', { note: 'recipe.alloy.hint.fix' })}`}
74
+ disabled={!fix || mix.valid}
75
+ onClick={() =>
76
+ fix &&
77
+ setInputs((prev) =>
78
+ prev.map((value, i) => trim((Number(value) || 0) + fix[i])),
79
+ )
80
+ }
81
+ >
82
+ <ArrowUpToLine className="size-3.5" />
83
+ </IconButton>
84
+ {needsSnap ? (
85
+ <IconButton
86
+ label={`${t('Snap to whole ingots', { note: 'recipe.alloy.snap_ingots' })}: ${t('Round each metal to whole ingots, staying inside the ranges', { note: 'recipe.alloy.hint.snap' })}`}
87
+ disabled={!snapped}
88
+ onClick={() =>
89
+ snapped && setInputs(snapped.map((value) => trim(value / factor)))
90
+ }
91
+ >
92
+ <Magnet className="size-3.5" />
93
+ </IconButton>
94
+ ) : (
95
+ <span className="size-7 shrink-0" aria-hidden />
96
+ )}
97
+ <IconButton
98
+ label={`${t('Reset to recipe ratio', { note: 'recipe.alloy.reset' })}: ${t('Restore the recipe-ratio mix', { note: 'recipe.alloy.hint.reset' })}`}
99
+ onClick={() =>
100
+ fill(
101
+ Number.isFinite(totalNumber)
102
+ ? totalNumber
103
+ : unit === 'ingot'
104
+ ? (ratioMix?.total ?? 10)
105
+ : 144,
106
+ )
107
+ }
108
+ >
109
+ <RotateCcw className="size-3.5" />
110
+ </IconButton>
111
+ <IconButton label={t('Copy the mix as text', { note: 'recipe.alloy.copy_mix' })} onClick={onCopy}>
112
+ {copied ? (
113
+ <Check className="size-3.5 text-green-600 dark:text-green-400" />
114
+ ) : (
115
+ <Copy className="size-3.5" />
116
+ )}
117
+ </IconButton>
118
+ </span>
119
+ </div>
120
+ );
121
+ }
122
+
@@ -0,0 +1,132 @@
1
+ 'use client';
2
+
3
+ import { useTranslations } from '@terrafirmagreg-team/translate/hooks/use-translations';
4
+ import { Input } from '@terrafirmagreg-team/ui/components/ui/input';
5
+ import { Button } from '@terrafirmagreg-team/ui/components/ui/button';
6
+ import {
7
+ ToggleGroup,
8
+ ToggleGroupItem,
9
+ } from '@terrafirmagreg-team/ui/components/ui/toggle-group';
10
+ import {
11
+ Tooltip,
12
+ TooltipContent,
13
+ TooltipTrigger,
14
+ } from '@terrafirmagreg-team/ui/components/ui/tooltip';
15
+ import { IconButton } from '@terrafirmagreg-team/ui/components/blocks/button/icon-button';
16
+ import { INGOT_MB, suggestAmounts } from '@terrafirmagreg-team/recipe/lib/alloy/mix';
17
+ import { Percent } from 'lucide-react';
18
+ import type { AlloyComponent } from '@terrafirmagreg-team/recipe/types/alloy';
19
+ import type { MixState } from '@terrafirmagreg-team/recipe/hooks/alloy/use-mix-state';
20
+
21
+ export function MixTotal({
22
+ state,
23
+ components,
24
+ }: {
25
+ state: MixState;
26
+ components: readonly AlloyComponent[];
27
+ }) {
28
+ const t = useTranslations();
29
+ const {
30
+ unit,
31
+ total,
32
+ totalNumber,
33
+ totalInvalid,
34
+ presets,
35
+ ratioMix,
36
+ format,
37
+ unitLabel,
38
+ applyTotal,
39
+ switchUnit,
40
+ fill,
41
+ } = state;
42
+
43
+ return (
44
+ <>
45
+ <div className="flex flex-wrap items-end gap-2">
46
+ <label className="flex w-44 flex-col gap-1 text-xs text-muted-foreground">
47
+ <span className="flex items-baseline justify-between gap-2">
48
+ {t('Target total', { note: 'recipe.alloy.total' })}
49
+ {unit === 'ingot' ? (
50
+ <span className="tabular-nums text-muted-foreground/70">
51
+ {Number.isFinite(totalNumber)
52
+ ? `= ${format(totalNumber * INGOT_MB)} ${t('mB', { note: 'recipe.alloy.unit.mb' })}`
53
+ : `×${INGOT_MB} ${t('mB', { note: 'recipe.alloy.unit.mb' })}`}
54
+ </span>
55
+ ) : null}
56
+ </span>
57
+ <Tooltip>
58
+ <TooltipTrigger
59
+ render={
60
+ <Input
61
+ inputMode="decimal"
62
+ name="alloy-total"
63
+ autoComplete="off"
64
+ value={total}
65
+ onChange={(event) => applyTotal(event.target.value)}
66
+ placeholder={
67
+ unit === 'ingot' ? String(ratioMix?.total ?? 10) : '144'
68
+ }
69
+ />
70
+ }
71
+ />
72
+ <TooltipContent>{t('Scales the current mix to this total as you type', { note: 'recipe.alloy.hint.scale' })}</TooltipContent>
73
+ </Tooltip>
74
+ {totalInvalid ? (
75
+ <span className="text-[11px] text-destructive">
76
+ {t('Enter a positive total amount.', { note: 'recipe.alloy.invalid_total' })}
77
+ </span>
78
+ ) : null}
79
+ </label>
80
+ <ToggleGroup
81
+ value={[unit]}
82
+ onValueChange={(next) => {
83
+ const value = next[0];
84
+ if (value === 'mb' || value === 'ingot') switchUnit(value);
85
+ }}
86
+ variant="outline"
87
+ size="sm"
88
+ spacing={0}
89
+ aria-label={t('units', { note: 'recipe.alloy.units' })}
90
+ >
91
+ <Tooltip>
92
+ <TooltipTrigger render={<ToggleGroupItem value="mb" />}>
93
+ {t('mB', { note: 'recipe.alloy.unit.mb' })}
94
+ </TooltipTrigger>
95
+ <TooltipContent>{t('Millibuckets (mB)', { note: 'recipe.alloy.hint.mb' })}</TooltipContent>
96
+ </Tooltip>
97
+ <Tooltip>
98
+ <TooltipTrigger render={<ToggleGroupItem value="ingot" />}>
99
+ {t('ing.', { note: 'recipe.alloy.unit.ingot' })}
100
+ </TooltipTrigger>
101
+ <TooltipContent>{t('Ingots: 1 ingot = 144 mB', { note: 'recipe.alloy.hint.ingot' })}</TooltipContent>
102
+ </Tooltip>
103
+ </ToggleGroup>
104
+ <IconButton
105
+ label={`${t('Fill by recipe', { note: 'recipe.alloy.fill' })}: ${t('Split the target total between metals by the recipe ratio', { note: 'recipe.alloy.hint.fill' })}`}
106
+ disabled={!suggestAmounts(components, totalNumber)}
107
+ onClick={() => fill(totalNumber)}
108
+ >
109
+ <Percent className="size-3.5" />
110
+ </IconButton>
111
+ </div>
112
+ <div
113
+ role="group"
114
+ aria-label={t('Quick totals', { note: 'recipe.alloy.presets' })}
115
+ className="flex flex-wrap gap-1.5"
116
+ >
117
+ {presets.map((value) => (
118
+ <Button
119
+ key={value}
120
+ variant="outline"
121
+ size="xs"
122
+ className="tabular-nums"
123
+ onClick={() => applyTotal(String(value))}
124
+ >
125
+ {format(value)} {unitLabel}
126
+ </Button>
127
+ ))}
128
+ </div>
129
+ </>
130
+ );
131
+ }
132
+
@@ -0,0 +1,65 @@
1
+ 'use client';
2
+
3
+ import {
4
+ Tooltip,
5
+ TooltipContent,
6
+ TooltipTrigger,
7
+ } from '@terrafirmagreg-team/ui/components/ui/tooltip';
8
+ import { cn } from '@terrafirmagreg-team/ui/lib/utils/cn';
9
+
10
+ export function AlloyRangeBar({
11
+ min,
12
+ max,
13
+ value,
14
+ color,
15
+ label,
16
+ }: {
17
+ min: number;
18
+ max: number;
19
+ value: number;
20
+ color?: string;
21
+ label?: string;
22
+ }) {
23
+ const valid = value >= min - 1e-8 && value <= max + 1e-8;
24
+ const bar = (
25
+ <div className="relative h-2.5 w-full min-w-24 rounded bg-secondary">
26
+ <div
27
+ className={cn(
28
+ 'absolute inset-y-0 rounded transition-[background-color]',
29
+ !color &&
30
+ (valid
31
+ ? 'bg-green-600/30 dark:bg-green-400/30'
32
+ : 'bg-muted-foreground/25'),
33
+ )}
34
+ style={{
35
+ left: `${min}%`,
36
+ width: `${max - min}%`,
37
+ ...(color ? { backgroundColor: color, opacity: 0.28 } : {}),
38
+ }}
39
+ />
40
+ <div
41
+ className={cn(
42
+ 'absolute -inset-y-0.5 w-0.5 rounded transition-[left,background-color]',
43
+ !color && (valid ? 'bg-green-700 dark:bg-green-300' : 'bg-destructive'),
44
+ color && !valid && 'bg-destructive',
45
+ )}
46
+ style={{
47
+ left: `${Math.min(100, Math.max(0, value))}%`,
48
+ ...(color && valid ? { backgroundColor: color } : {}),
49
+ }}
50
+ />
51
+ </div>
52
+ );
53
+ if (!label) return bar;
54
+ return (
55
+ <Tooltip>
56
+ <TooltipTrigger
57
+ render={<div className="w-full" tabIndex={0} aria-label={label} />}
58
+ >
59
+ {bar}
60
+ </TooltipTrigger>
61
+ <TooltipContent>{label}</TooltipContent>
62
+ </Tooltip>
63
+ );
64
+ }
65
+
@@ -0,0 +1,88 @@
1
+ 'use client';
2
+ import type { DatasetRef } from '@terrafirmagreg-team/sites/location';
3
+
4
+ import { useMemo } from 'react';
5
+ import { cn } from '@terrafirmagreg-team/ui/lib/utils/cn';
6
+ import { useFluidColors } from '@terrafirmagreg-team/recipe/hooks/alloy/use-fluid-colors';
7
+ import type { AlloyComponent, AlloyMixRow } from '@terrafirmagreg-team/recipe/types/alloy';
8
+
9
+ const PALETTE = [
10
+ 'bg-amber-500',
11
+ 'bg-sky-500',
12
+ 'bg-emerald-500',
13
+ 'bg-lime-500',
14
+ 'bg-violet-500',
15
+ 'bg-orange-500',
16
+ 'bg-teal-500',
17
+ ];
18
+
19
+ export function AlloyStackBar({
20
+ rows,
21
+ colors,
22
+ className,
23
+ label,
24
+ }: {
25
+ rows: readonly Pick<AlloyMixRow, 'percent' | 'valid'>[];
26
+ colors?: readonly (string | undefined)[];
27
+ className?: string;
28
+
29
+ label?: string;
30
+ }) {
31
+ return (
32
+ <div
33
+ className={cn(
34
+ 'flex h-3 w-full overflow-hidden rounded bg-secondary',
35
+ className,
36
+ )}
37
+ role={label ? 'img' : undefined}
38
+ aria-label={label}
39
+ aria-hidden={label ? undefined : true}
40
+ >
41
+ {rows.map((row, i) => {
42
+ const color = colors?.[i];
43
+ return (
44
+ <div
45
+ key={i}
46
+ className={cn(
47
+ 'h-full transition-[width,background-color]',
48
+ !color && PALETTE[i % PALETTE.length],
49
+ !row.valid && 'opacity-40',
50
+ )}
51
+ style={{
52
+ width: `${Math.max(0, Math.min(100, row.percent))}%`,
53
+ ...(color ? { backgroundColor: color } : {}),
54
+ }}
55
+ />
56
+ );
57
+ })}
58
+ </div>
59
+ );
60
+ }
61
+
62
+ export function AlloyMiniBar({
63
+ dataset,
64
+ components,
65
+ className,
66
+ }: {
67
+ dataset: DatasetRef;
68
+ components: readonly AlloyComponent[];
69
+ className?: string;
70
+ }) {
71
+ const slots = useMemo(() => components.map(({ slot }) => slot), [components]);
72
+ const colors = useFluidColors(dataset, slots);
73
+ const sum = components.reduce(
74
+ (total, { slot }) => total + (slot.amount || 0),
75
+ 0,
76
+ );
77
+ return (
78
+ <AlloyStackBar
79
+ className={className}
80
+ colors={colors}
81
+ rows={components.map(({ slot }) => ({
82
+ percent: sum > 0 ? (slot.amount / sum) * 100 : 0,
83
+ valid: true,
84
+ }))}
85
+ />
86
+ );
87
+ }
88
+
@@ -0,0 +1,54 @@
1
+ 'use client';
2
+
3
+ import { usePathname } from 'next/navigation';
4
+ import { Anvil, Calculator, FlaskConical, Shapes } from 'lucide-react';
5
+ import { useTranslations } from '@terrafirmagreg-team/translate/hooks/use-translations';
6
+ import { useResolvedLang } from '@terrafirmagreg-team/translate/hooks/use-resolved-lang';
7
+ import { useLocationSearchParams } from '@terrafirmagreg-team/ui/hooks/page/use-location-search';
8
+ import { recipePlaceFromParams } from '@terrafirmagreg-team/recipe/lib/links';
9
+ import { recipeCalcHref, recipeHref } from '@terrafirmagreg-team/recipe/lib/links';
10
+ import { parseSectionLocation } from '@terrafirmagreg-team/sites/location';
11
+ import { Switcher } from '@terrafirmagreg-team/ui/components/blocks/nav/switcher';
12
+
13
+ export function RecipeAxisSwitcher() {
14
+ const t = useTranslations();
15
+ const { dataset: datasetId } = parseSectionLocation(usePathname());
16
+ const lang = useResolvedLang();
17
+ const params = useLocationSearchParams();
18
+ const axis = recipePlaceFromParams(params).kind;
19
+ const dataset = { id: datasetId ?? '', version: params.get('version') ?? undefined };
20
+ if (!datasetId) return null;
21
+ return (
22
+ <Switcher
23
+ options={[
24
+ {
25
+ value: 'type',
26
+ title: t('Categories', { note: 'recipe.axis_type' }),
27
+ icon: <Shapes className="size-4" />,
28
+ url: recipeHref(dataset, lang),
29
+ },
30
+ {
31
+ value: 'calc',
32
+ title: t('Calculator', { note: 'recipe.calculator' }),
33
+ icon: <Calculator className="size-4" />,
34
+ url: recipeCalcHref(dataset, lang),
35
+ },
36
+ {
37
+ value: 'forge',
38
+ title: t('Forging', { note: 'recipe.forge.title' }),
39
+ icon: <Anvil className="size-4" />,
40
+ url: recipeHref(dataset, lang, { tool: 'forge' }),
41
+ },
42
+ {
43
+ value: 'alloy',
44
+ title: t('Alloys', { note: 'recipe.alloy.title' }),
45
+ icon: <FlaskConical className="size-4" />,
46
+ url: recipeHref(dataset, lang, { tool: 'alloy' }),
47
+ },
48
+ ]}
49
+ selected={axis ?? 'type'}
50
+ placeholder={t('Categories', { note: 'recipe.axis_type' })}
51
+ />
52
+ );
53
+ }
54
+