import { type CSSProperties, useMemo } from "react"
import {
DocumentGate,
type ToolRenderProps,
defineStateCodec,
defineTool,
field,
useDocument,
} from "@vvd/sdk"
// src/codec.ts
/** One row in the sheet. */
export type Row = {
item: string
quantity: number
price: number
}
export const codec = defineStateCodec({
rows: field.list<Row>([
{ item: "Rope (50 ft)", quantity: 2, price: 1 },
{ item: "Torch", quantity: 6, price: 0.5 },
{ item: "Rations (1 day)", quantity: 10, price: 0.4 },
]),
})
// src/tool.tsx
const TH: CSSProperties = { textAlign: "left", padding: "6px 8px", fontSize: 11, opacity: 0.5, borderBottom: "1px solid rgba(127,127,127,0.25)" }
const TD: CSSProperties = { padding: "4px 8px", borderBottom: "1px solid rgba(127,127,127,0.12)" }
const INPUT: CSSProperties = { width: "100%", border: "none", outline: "none", background: "transparent", font: "inherit", fontSize: 13, color: "inherit", padding: "4px 6px" }
function Board({ data, actions }: {
data: { rows: readonly Row[] }
actions: {
list(name: "rows"): {
push(...items: Row[]): void
remove(index: number, count?: number): void
replace(index: number, item: Row): void
}
}
}) {
const addRow = () => actions.list("rows").push({ item: "", quantity: 1, price: 0 })
const total = data.rows.reduce((sum, row) => sum + row.quantity * row.price, 0)
return (
<div style={{ maxWidth: 560, margin: "0 auto", padding: 20, font: "inherit" }}>
<div style={{ display: "flex", alignItems: "center", gap: 12 }}>
<h1 style={{ margin: 0, fontSize: 20 }}>armory</h1>
<button type="button" onClick={addRow} style={{ marginLeft: "auto" }}>
Add a row
</button>
</div>
<table style={{ marginTop: 14, width: "100%", borderCollapse: "collapse" }}>
<thead>
<tr>
<th style={TH}>Item</th>
<th style={TH}>Qty</th>
<th style={TH}>Price</th>
<th style={TH}>Total</th>
<th style={TH} />
</tr>
</thead>
<tbody>
{data.rows.map((row, i) => (
<tr key={i}>
<td style={TD}>
<input
value={row.item}
onChange={(e) => actions.list("rows").replace(i, { ...row, item: e.target.value })}
style={INPUT}
/>
</td>
<td style={TD}>
<input
type="number"
value={row.quantity}
onChange={(e) => actions.list("rows").replace(i, { ...row, quantity: Number(e.target.value) || 0 })}
style={INPUT}
/>
</td>
<td style={TD}>
<input
type="number"
value={row.price}
onChange={(e) => actions.list("rows").replace(i, { ...row, price: Number(e.target.value) || 0 })}
style={INPUT}
/>
</td>
<td style={{ ...TD, textAlign: "right", opacity: 0.7 }}>
{(row.quantity * row.price).toFixed(2)}
</td>
<td style={TD}>
<button type="button" onClick={() => actions.list("rows").remove(i)}>
×
</button>
</td>
</tr>
))}
</tbody>
<tfoot>
<tr>
<td style={TD} colSpan={3} />
<td style={{ ...TD, textAlign: "right", fontWeight: 700 }}>{total.toFixed(2)}</td>
<td style={TD} />
</tr>
</tfoot>
</table>
</div>
)
}
export function ArmoryView({ document }: ToolRenderProps) {
const coords = useMemo(
() => ({ worldId: document.worldId, documentId: document.id }),
[document.worldId, document.id],
)
const { data, status, actions, retry } = useDocument(coords, codec)
return (
<DocumentGate status={status} onRetry={retry}>
{data && actions && <Board data={data} actions={actions} />}
</DocumentGate>
)
}
export default defineTool({
id: "armory",
name: "armory",
documentTypes: ["armory"],
needs: [],
render: ArmoryView,
})