@voltro/plugin-inventory 0.0.0-stage → 0.88.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.
@@ -0,0 +1,320 @@
1
+ import { identifier as e, integer as t, list as n } from "./capture.js";
2
+ import { defineRecipe as r } from "./crafting.js";
3
+ import { captureInventoryPresentation as i, inventoryCanEquip as a, inventoryRecipePreview as o } from "./inventoryPresentation.js";
4
+ import { inventoryUiCopy as s } from "./inventoryUiCopy.js";
5
+ import { useId as c, useMemo as l, useRef as u, useState as d } from "react";
6
+ import { Button as f } from "@voltro/ui-shadcn";
7
+ import { Fragment as p, jsx as m, jsxs as h } from "react/jsx-runtime";
8
+ // src/components.tsx
9
+ var g = (e) => e.copy ?? s[e.locale ?? "en"], _ = "min-h-11 min-w-0 rounded-md border border-input bg-background px-3 text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", v = (e) => {
10
+ let t = g(e), n = e.item, r = n.content ? e.title(n.content.display.nameKey) : n.instance.itemId;
11
+ return /* @__PURE__ */ h("article", {
12
+ className: "flex h-full min-w-0 items-center gap-4 border-b border-border px-4 py-3",
13
+ children: [
14
+ e.renderIcon ? /* @__PURE__ */ m("div", {
15
+ className: "size-12 shrink-0 overflow-hidden rounded-xl",
16
+ children: e.renderIcon(n)
17
+ }) : null,
18
+ /* @__PURE__ */ h("div", {
19
+ className: "min-w-0 flex-1",
20
+ children: [
21
+ /* @__PURE__ */ m("h3", {
22
+ className: "line-clamp-2 break-words font-medium",
23
+ title: r,
24
+ children: r
25
+ }),
26
+ /* @__PURE__ */ m("p", {
27
+ className: "text-sm tabular-nums text-muted-foreground",
28
+ children: t.quantity(n.instance.qty)
29
+ }),
30
+ /* @__PURE__ */ m("p", {
31
+ className: "line-clamp-2 text-sm text-muted-foreground",
32
+ children: [
33
+ n.equipped ? t.equipped : null,
34
+ n.instance.locked ? t.locked : null,
35
+ n.expired ? t.expired : null,
36
+ n.content ? null : t.unknown
37
+ ].filter(Boolean).join(" · ")
38
+ })
39
+ ]
40
+ }),
41
+ e.onSelect ? /* @__PURE__ */ m(f, {
42
+ type: "button",
43
+ variant: "outline",
44
+ className: "min-h-11 shrink-0",
45
+ "aria-label": `${t.select}: ${r}`,
46
+ "aria-disabled": e.busy || n.expired || n.instance.locked,
47
+ onClick: () => {
48
+ e.busy || n.expired || n.instance.locked || e.onSelect?.(n.instance.id);
49
+ },
50
+ children: t.select
51
+ }) : null
52
+ ]
53
+ });
54
+ }, y = (e) => {
55
+ let t = g(e), n = c(), r = c(), a = c(), o = u(null), [s, y] = d(""), [b, x] = d(""), [S, C] = d(!1), [w, T] = d(0), [E, D] = d(), O = l(() => {
56
+ if (e.source.error !== void 0 || e.catalog.error !== void 0) return { failed: !0 };
57
+ if (e.source.data === void 0 || e.catalog.data === void 0) return { failed: !1 };
58
+ try {
59
+ return {
60
+ failed: !1,
61
+ view: i(e.source.data, e.catalog.data, [], e.at)
62
+ };
63
+ } catch {
64
+ return { failed: !0 };
65
+ }
66
+ }, [
67
+ e.source.data,
68
+ e.source.error,
69
+ e.catalog.data,
70
+ e.catalog.error,
71
+ e.at
72
+ ]), k = O.view, A = [...new Set(k?.catalog.items.flatMap((e) => e.tags) ?? [])].sort(), j = k?.items.filter((t) => (!b || t.content?.tags.includes(b)) && (t.content ? e.title(t.content.display.nameKey) : t.instance.itemId).toLocaleLowerCase(e.locale ?? "en").includes(s.toLocaleLowerCase(e.locale ?? "en"))) ?? [], M = Math.max(0, Math.min(j.length, Math.floor(w / 144) - 2)), N = Math.min(j.length, M + 10), P = S ? j.map((e, t) => t) : Array.from({ length: N - M }, (e, t) => M + t), F = j.findIndex((e) => e.instance.id === E);
73
+ !S && F >= 0 && !P.includes(F) && P.push(F), P.sort((e, t) => e - t);
74
+ let I = () => {
75
+ T(0), o.current && (o.current.scrollTop = 0);
76
+ };
77
+ return /* @__PURE__ */ h("section", {
78
+ "aria-labelledby": n,
79
+ className: "min-w-0 space-y-5",
80
+ children: [/* @__PURE__ */ m("h2", {
81
+ id: n,
82
+ className: "text-2xl font-semibold",
83
+ children: t.inventory
84
+ }), O.failed ? /* @__PURE__ */ m("p", {
85
+ role: "alert",
86
+ children: t.unavailable
87
+ }) : !k || e.source.loading || e.catalog.loading ? /* @__PURE__ */ m("p", {
88
+ role: "status",
89
+ children: t.loading
90
+ }) : /* @__PURE__ */ h(p, { children: [
91
+ /* @__PURE__ */ h("div", {
92
+ className: "grid grid-cols-1 items-end gap-4 sm:grid-cols-[minmax(0,1fr)_auto_auto]",
93
+ children: [
94
+ /* @__PURE__ */ h("label", {
95
+ htmlFor: r,
96
+ className: "flex min-w-0 flex-col gap-2 text-sm",
97
+ children: [t.search, /* @__PURE__ */ m("input", {
98
+ id: r,
99
+ type: "search",
100
+ maxLength: 120,
101
+ className: _,
102
+ value: s,
103
+ onChange: (e) => {
104
+ y(e.target.value), I();
105
+ }
106
+ })]
107
+ }),
108
+ /* @__PURE__ */ h("label", {
109
+ htmlFor: a,
110
+ className: "flex min-w-0 flex-col gap-2 text-sm",
111
+ children: [t.tag, /* @__PURE__ */ h("select", {
112
+ id: a,
113
+ className: _,
114
+ value: b,
115
+ onChange: (e) => {
116
+ x(e.target.value), I();
117
+ },
118
+ children: [/* @__PURE__ */ m("option", {
119
+ value: "",
120
+ children: t.allTags
121
+ }), A.map((e) => /* @__PURE__ */ m("option", {
122
+ value: e,
123
+ children: e
124
+ }, e))]
125
+ })]
126
+ }),
127
+ /* @__PURE__ */ m(f, {
128
+ type: "button",
129
+ variant: "outline",
130
+ "aria-pressed": S,
131
+ className: "min-h-11",
132
+ onClick: () => {
133
+ C((e) => !e), I();
134
+ },
135
+ children: S ? t.virtualize : t.showAll
136
+ })
137
+ ]
138
+ }),
139
+ /* @__PURE__ */ m("p", {
140
+ role: "status",
141
+ className: "text-sm tabular-nums text-muted-foreground",
142
+ children: t.count(j.length, k.items.length)
143
+ }),
144
+ j.length === 0 ? /* @__PURE__ */ m("p", { children: k.items.length === 0 ? t.empty : t.noMatches }) : /* @__PURE__ */ m("div", {
145
+ ref: o,
146
+ className: "max-h-[36rem] overflow-y-auto rounded-xl border border-border",
147
+ onScroll: (e) => T(e.currentTarget.scrollTop),
148
+ children: /* @__PURE__ */ m("ul", {
149
+ className: S ? "" : "relative",
150
+ style: S ? void 0 : { height: j.length * 144 },
151
+ onBlur: (e) => {
152
+ e.currentTarget.contains(e.relatedTarget) || D(void 0);
153
+ },
154
+ children: P.map((t) => {
155
+ let n = j[t];
156
+ return /* @__PURE__ */ m("li", {
157
+ "aria-setsize": j.length,
158
+ "aria-posinset": t + 1,
159
+ onFocus: () => D(n.instance.id),
160
+ style: S ? void 0 : {
161
+ position: "absolute",
162
+ top: t * 144,
163
+ height: 144,
164
+ width: "100%"
165
+ },
166
+ children: /* @__PURE__ */ m(v, {
167
+ item: n,
168
+ title: e.title,
169
+ renderIcon: e.renderIcon,
170
+ locale: e.locale,
171
+ copy: e.copy,
172
+ onSelect: e.onSelect,
173
+ busy: e.busy
174
+ })
175
+ }, n.instance.id);
176
+ })
177
+ })
178
+ })
179
+ ] })]
180
+ });
181
+ }, b = (t) => {
182
+ let r = g(t), o = c(), s, l = [], u = !1;
183
+ try {
184
+ if (e(t.slotSet), l = n(t.slots, 64).map(e), new Set(l).size !== l.length) throw Error("Repeated slot");
185
+ u = [
186
+ t.source,
187
+ t.catalog,
188
+ t.loadout
189
+ ].some((e) => e.error !== void 0), !u && t.source.data !== void 0 && t.catalog.data !== void 0 && t.loadout.data !== void 0 && (s = i(t.source.data, t.catalog.data, t.loadout.data, t.at));
190
+ } catch {
191
+ u = !0;
192
+ }
193
+ let d = s;
194
+ return /* @__PURE__ */ h("section", {
195
+ "aria-labelledby": o,
196
+ className: "min-w-0 space-y-5",
197
+ children: [/* @__PURE__ */ m("h2", {
198
+ id: o,
199
+ className: "text-2xl font-semibold",
200
+ children: r.loadout
201
+ }), u ? /* @__PURE__ */ m("p", {
202
+ role: "alert",
203
+ children: r.unavailable
204
+ }) : !d || t.source.loading || t.catalog.loading || t.loadout.loading ? /* @__PURE__ */ m("p", {
205
+ role: "status",
206
+ children: r.loading
207
+ }) : /* @__PURE__ */ m("div", {
208
+ className: "grid grid-cols-1 gap-5 sm:grid-cols-2",
209
+ children: l.map((n) => {
210
+ let i = d.loadout.find((e) => e.slotSet === t.slotSet && e.slot === n)?.instanceId ?? "", s = d.items.filter((e) => a(e, n) || e.instance.id === i), c = o + "." + n;
211
+ return /* @__PURE__ */ h("label", {
212
+ htmlFor: c,
213
+ className: "flex min-w-0 flex-col gap-2 text-sm",
214
+ children: [t.title(n), /* @__PURE__ */ h("select", {
215
+ id: c,
216
+ className: _,
217
+ disabled: t.busy,
218
+ value: i,
219
+ onChange: (r) => {
220
+ let i = r.target.value || null;
221
+ !t.busy && (i === null || s.some((e) => e.instance.id === i && a(e, n))) && t.onEquip(Object.freeze({
222
+ slotSet: t.slotSet,
223
+ slot: n,
224
+ instanceId: i,
225
+ requestId: e(t.requestId())
226
+ }));
227
+ },
228
+ children: [/* @__PURE__ */ m("option", {
229
+ value: "",
230
+ children: r.unequipped
231
+ }), s.map((e) => /* @__PURE__ */ m("option", {
232
+ value: e.instance.id,
233
+ disabled: !a(e, n),
234
+ children: e.content ? t.title(e.content.display.nameKey) : e.instance.itemId
235
+ }, e.instance.id))]
236
+ })]
237
+ }, n);
238
+ })
239
+ })]
240
+ });
241
+ }, x = (e) => {
242
+ let n = g(e), r = c(), [i, a] = d(1), s = Math.min(i, e.recipe.maxBatches), l = o(e.view, e.recipe, s, e.at);
243
+ return /* @__PURE__ */ h("article", {
244
+ className: "min-w-0 space-y-4 border-b border-border pb-5",
245
+ children: [
246
+ /* @__PURE__ */ m("h3", {
247
+ className: "break-words text-lg font-medium",
248
+ children: e.title(l.recipe.id)
249
+ }),
250
+ /* @__PURE__ */ h("label", {
251
+ htmlFor: r,
252
+ className: "flex items-center gap-3 text-sm",
253
+ children: [n.batches, /* @__PURE__ */ m("select", {
254
+ id: r,
255
+ className: _,
256
+ value: s,
257
+ onChange: (e) => a(t(Number(e.target.value), 1, l.recipe.maxBatches)),
258
+ children: Array.from({ length: l.recipe.maxBatches }, (e, t) => t + 1).map((e) => /* @__PURE__ */ m("option", {
259
+ value: e,
260
+ children: e
261
+ }, e))
262
+ })]
263
+ }),
264
+ /* @__PURE__ */ h("dl", {
265
+ className: "grid grid-cols-[minmax(0,1fr)_minmax(0,2fr)] gap-x-4 gap-y-2 text-sm",
266
+ children: [
267
+ /* @__PURE__ */ m("dt", {
268
+ className: "text-muted-foreground",
269
+ children: n.materials
270
+ }),
271
+ /* @__PURE__ */ m("dd", {
272
+ className: "break-words",
273
+ children: l.recipe.inputs.map((t) => `${t.qty * s} × ${e.title(t.itemId)}`).join(", ")
274
+ }),
275
+ /* @__PURE__ */ m("dt", {
276
+ className: "text-muted-foreground",
277
+ children: n.currencyCost
278
+ }),
279
+ /* @__PURE__ */ m("dd", {
280
+ className: "break-words tabular-nums",
281
+ children: l.currencyCost ? `${l.currencyCost.amount} ${e.currencyLabel?.(l.currencyCost.currency) ?? l.currencyCost.currency}` : n.freeCost
282
+ })
283
+ ]
284
+ }),
285
+ /* @__PURE__ */ m("p", {
286
+ role: "status",
287
+ className: "text-sm",
288
+ children: l.materialsAvailable ? n.materialsReady : n.materialsMissing
289
+ }),
290
+ e.renderAction?.(l.recipe, s, l.materialsAvailable)
291
+ ]
292
+ });
293
+ }, S = (e) => {
294
+ let t = g(e), i = c(), a = [], s = !1;
295
+ try {
296
+ if (a = n(e.recipes, 256).map((e) => r(e)), new Set(a.map((e) => e.id)).size !== a.length) throw Error("Repeated recipe");
297
+ for (let t of a) o(e.view, t, 1, e.at);
298
+ } catch {
299
+ s = !0;
300
+ }
301
+ return /* @__PURE__ */ h("section", {
302
+ "aria-labelledby": i,
303
+ className: "min-w-0 space-y-5",
304
+ children: [/* @__PURE__ */ m("h2", {
305
+ id: i,
306
+ className: "text-2xl font-semibold",
307
+ children: t.recipes
308
+ }), s ? /* @__PURE__ */ m("p", {
309
+ role: "alert",
310
+ children: t.unavailable
311
+ }) : a.length === 0 ? /* @__PURE__ */ m("p", { children: t.empty }) : /* @__PURE__ */ h(p, { children: [/* @__PURE__ */ m("p", {
312
+ className: "max-w-prose text-sm leading-relaxed text-muted-foreground",
313
+ children: t.serverCraft
314
+ }), a.map((t) => /* @__PURE__ */ m(x, {
315
+ ...e,
316
+ recipe: t
317
+ }, `${t.id}:${t.version}`))] })]
318
+ });
319
+ };
320
+ export { S as CraftingPanel, y as InventoryGrid, v as ItemCard, b as LoadoutEditor, i as captureInventoryPresentation, a as inventoryCanEquip, o as inventoryRecipePreview, s as inventoryUiCopy };
@@ -0,0 +1,211 @@
1
+ import { Context } from 'effect';
2
+ import { Effect } from 'effect';
3
+ import { PluginPrivacyTarget } from '@voltro/protocol';
4
+ import { PreparedSqlMutation } from '@voltro/database/sql';
5
+ import { Schema } from 'effect';
6
+ import { Subject } from '@voltro/protocol';
7
+ import { YieldableError } from 'effect/Cause';
8
+
9
+ /** Admit actual child receipt or retained craft evidence without rerunning a wallet callback. */
10
+ export declare const captureCraftPayment: (input: unknown, recipe: Recipe, batches: number) => CraftPaymentReceipt | null;
11
+
12
+ /** Admit retained intent without calling historical custom policies. */
13
+ export declare const captureCraftPlan: (original: unknown, input: CraftInput) => CraftPlan;
14
+
15
+ export declare interface CraftEconomyParticipant {
16
+ readonly prepareTransfer: (subject: Subject, input: {
17
+ readonly tenantId: string;
18
+ readonly requestId: string;
19
+ readonly currency: string;
20
+ readonly amount: string;
21
+ readonly from: {
22
+ readonly kind: 'wallet';
23
+ readonly ownerKind: 'player' | 'guild';
24
+ readonly ownerId: string;
25
+ };
26
+ readonly to: {
27
+ readonly kind: 'system';
28
+ readonly id: string;
29
+ };
30
+ readonly reason: string;
31
+ readonly ref: {
32
+ readonly kind: string;
33
+ readonly id: string;
34
+ };
35
+ }) => Effect.Effect<PreparedSqlMutation<CraftPaymentReceipt>, {
36
+ readonly code: string;
37
+ }>;
38
+ }
39
+
40
+ export declare interface CraftingOptions {
41
+ readonly recipes: readonly (RecipeDefinition | Recipe)[];
42
+ readonly craftResolver?: CraftResolver | undefined;
43
+ readonly version?: string | undefined;
44
+ readonly service?: CraftingService | undefined;
45
+ readonly economy?: CraftEconomyParticipant | undefined;
46
+ readonly systemAccount?: string | undefined;
47
+ }
48
+
49
+ export declare class CraftingPort extends CraftingPort_base {
50
+ }
51
+
52
+ declare const CraftingPort_base: Context.ServiceClass<CraftingPort, "@voltro/CraftingPort", CraftingService>;
53
+
54
+ /** Small independent seam; InventoryPort retains its eight-method surface. */
55
+ export declare interface CraftingService {
56
+ readonly craft: (subject: Subject, input: CraftRequest) => Effect.Effect<InventoryReceipt, InventoryError>;
57
+ readonly recipes: (subject: Subject) => Effect.Effect<readonly Recipe[], InventoryError>;
58
+ readonly exportSubject: (target: PluginPrivacyTarget) => Effect.Effect<unknown, InventoryError>;
59
+ readonly eraseSubject: (target: PluginPrivacyTarget) => Effect.Effect<void, InventoryError>;
60
+ }
61
+
62
+ export declare interface CraftInput {
63
+ readonly recipe: Recipe;
64
+ readonly batches: number;
65
+ readonly at: number;
66
+ readonly available: readonly InventoryInstance[];
67
+ readonly excludedIds: readonly string[];
68
+ }
69
+
70
+ export declare interface CraftPaymentReceipt {
71
+ readonly txId: string;
72
+ readonly currency: string;
73
+ readonly amount: string;
74
+ readonly at: number;
75
+ }
76
+
77
+ export declare interface CraftPlan {
78
+ readonly consume: readonly {
79
+ readonly instanceId: string;
80
+ readonly qty: number;
81
+ }[];
82
+ readonly outputs: readonly RecipeOutput[];
83
+ }
84
+
85
+ export declare interface CraftRequest {
86
+ readonly tenantId: string;
87
+ readonly owner: InventoryOwner;
88
+ readonly recipeId: string;
89
+ readonly requestId: string;
90
+ readonly batches?: number;
91
+ }
92
+
93
+ export declare type CraftResolver = (input: CraftInput) => CraftPlan;
94
+
95
+ /** Deterministic oldest acquisitions first; equipped, locked and expired items never enter a recipe. */
96
+ export declare const defaultCraftResolver: CraftResolver;
97
+
98
+ /** Immediate recipe; currency cost requires an installed exact-store prepared participant. Duration/prerequisites refuse. */
99
+ export declare const defineRecipe: (input: RecipeDefinition) => Recipe;
100
+
101
+ /** Public errors carry codes, never subjects, catalog values or provider causes. */
102
+ declare class InventoryError extends InventoryError_base {
103
+ }
104
+
105
+ declare const InventoryError_base: Schema.Class<InventoryError, Schema.TaggedStruct<"InventoryError", {
106
+ readonly code: Schema.Literals<readonly ["invalid_input", "invalid_record", "invalid_item", "invalid_attributes", "conflict", "forbidden", "not_found", "unavailable", "limit_exceeded", "erased", "insufficient_items", "insufficient_funds", "payment_refused", "locked", "expired", "invalid_slot"]>;
107
+ }>, YieldableError>;
108
+
109
+ declare interface InventoryInstance {
110
+ readonly id: string;
111
+ readonly itemId: string;
112
+ readonly schemaVersion: number;
113
+ readonly qty: number;
114
+ readonly attributes: JsonValue;
115
+ readonly acquiredAt: number;
116
+ readonly expiresAt: number | null;
117
+ readonly locked: boolean;
118
+ readonly source: string;
119
+ }
120
+
121
+ declare interface InventoryOwner {
122
+ readonly ownerKind: 'player' | 'guild';
123
+ readonly ownerId: string;
124
+ }
125
+
126
+ declare interface InventoryReceipt {
127
+ readonly operationId: string;
128
+ readonly kind: 'grant' | 'consume' | 'equip' | 'expire' | 'craft' | 'reserve' | 'release' | 'transfer-out' | 'transfer-in';
129
+ readonly changes: readonly {
130
+ readonly instanceId: string;
131
+ readonly delta: number;
132
+ }[];
133
+ readonly at: number;
134
+ }
135
+
136
+ declare type JsonValue = null | boolean | number | string | readonly JsonValue[] | {
137
+ readonly [key: string]: JsonValue;
138
+ };
139
+
140
+ export declare interface Recipe {
141
+ readonly id: string;
142
+ readonly version: number;
143
+ readonly inputs: readonly RecipeIngredient[];
144
+ readonly outputs: readonly RecipeOutput[];
145
+ readonly maxBatches: number;
146
+ readonly cost: RecipeCurrencyCost | null;
147
+ readonly timeMs: 0;
148
+ readonly requires: null;
149
+ }
150
+
151
+ export declare interface RecipeCurrencyCost {
152
+ readonly currency: string;
153
+ readonly amount: string;
154
+ }
155
+
156
+ export declare interface RecipeDefinition {
157
+ readonly id: string;
158
+ readonly version?: number;
159
+ readonly inputs: readonly RecipeIngredient[];
160
+ readonly outputs: readonly RecipeOutput[];
161
+ readonly maxBatches?: number;
162
+ readonly cost?: RecipeCurrencyCost | null;
163
+ readonly timeMs?: 0;
164
+ readonly requires?: null;
165
+ }
166
+
167
+ export declare interface RecipeIngredient {
168
+ readonly itemId: string;
169
+ readonly qty: number;
170
+ }
171
+
172
+ export declare interface RecipeOutput extends RecipeIngredient {
173
+ readonly attributes?: JsonValue;
174
+ }
175
+
176
+ export declare const resolveCraftingOptions: (input: CraftingOptions) => Readonly<{
177
+ economy: Readonly<{
178
+ prepareTransfer: (subject: Subject, input: {
179
+ readonly tenantId: string;
180
+ readonly requestId: string;
181
+ readonly currency: string;
182
+ readonly amount: string;
183
+ readonly from: {
184
+ readonly kind: "wallet";
185
+ readonly ownerKind: "player" | "guild";
186
+ readonly ownerId: string;
187
+ };
188
+ readonly to: {
189
+ readonly kind: "system";
190
+ readonly id: string;
191
+ };
192
+ readonly reason: string;
193
+ readonly ref: {
194
+ readonly kind: string;
195
+ readonly id: string;
196
+ };
197
+ }) => Effect.Effect<PreparedSqlMutation<CraftPaymentReceipt>, {
198
+ readonly code: string;
199
+ }>;
200
+ }> | undefined;
201
+ systemAccount: string | undefined;
202
+ recipes: readonly Recipe[];
203
+ craftResolver: CraftResolver;
204
+ version: string;
205
+ service: CraftingService | undefined;
206
+ }>;
207
+
208
+ /** Custom selection may choose instances and valid output attributes, never amounts or additional value. */
209
+ export declare const resolveCraftPlan: (input: CraftInput, resolver?: CraftResolver) => CraftPlan;
210
+
211
+ export { }