Initial TTRPG character meta-builder with visual sheet editor.
Offline Electron app seeds 10 system presets, supports create-from-scratch designer with palette/drag/resize/bricks, and exports .ttrpg-system.json for the player. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
import type { CharacterValues, SheetWidget, SystemDefinition } from '../shared/types.ts';
|
||||
import { computeDerived } from '../shared/derived.ts';
|
||||
|
||||
type Props = {
|
||||
widget: SheetWidget;
|
||||
system: SystemDefinition;
|
||||
};
|
||||
|
||||
/** Compact in-widget preview for design mode (no nested canvas). */
|
||||
export function WidgetPreview({ widget, system }: Props) {
|
||||
const values: CharacterValues = {
|
||||
attributes: Object.fromEntries(
|
||||
(system.bricks.attributes ?? []).map((a) => [a.key, typeof a.default === 'number' ? a.default : 10]),
|
||||
),
|
||||
skills: Object.fromEntries(
|
||||
(system.bricks.skills ?? []).map((a) => [a.key, typeof a.default === 'number' ? a.default : 0]),
|
||||
),
|
||||
resources: Object.fromEntries((system.bricks.resources ?? []).map((a) => [a.key, a.default ?? 0])),
|
||||
meta: { notes: '…', title: system.name },
|
||||
};
|
||||
const derived = computeDerived(system.bricks.derived ?? [], values);
|
||||
const bind = widget.bind ?? '';
|
||||
const [, key] = bind.split('.');
|
||||
|
||||
if (widget.type === 'section') {
|
||||
return <strong>{String(widget.props?.title ?? 'Section')}</strong>;
|
||||
}
|
||||
if (widget.type === 'dots') {
|
||||
const max = Number(widget.props?.max ?? 5);
|
||||
const cur = Number(values.attributes?.[key!] ?? 2);
|
||||
return (
|
||||
<div>
|
||||
<div className="muted">{String(widget.props?.label ?? key)}</div>
|
||||
<div className="row">
|
||||
{Array.from({ length: max }, (_, i) => (
|
||||
<span
|
||||
key={i}
|
||||
style={{
|
||||
width: 12,
|
||||
height: 12,
|
||||
borderRadius: '50%',
|
||||
border: '1px solid var(--stroke2)',
|
||||
background: i < cur ? 'var(--accent2)' : 'transparent',
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (widget.type === 'track' || widget.type === 'resourceBar') {
|
||||
const v = Number(values.resources?.[key!] ?? 5);
|
||||
const max = Number(widget.props?.max ?? 10);
|
||||
return (
|
||||
<div>
|
||||
<div className="muted">{String(widget.props?.label ?? key)}</div>
|
||||
<div style={{ height: 8, background: 'var(--stroke)', borderRadius: 4 }}>
|
||||
<div style={{ width: `${(v / max) * 100}%`, height: '100%', background: 'var(--accent)' }} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (widget.type === 'skillList') {
|
||||
return (
|
||||
<div className="muted">
|
||||
{(system.bricks.skills ?? []).slice(0, 4).map((s) => (
|
||||
<div key={s.key}>
|
||||
{s.label}: {String(values.skills?.[s.key] ?? 0)}
|
||||
</div>
|
||||
))}
|
||||
…
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (widget.type === 'notes' || widget.type === 'text') {
|
||||
return <div className="muted">{String(values.meta?.notes ?? 'Notes')}</div>;
|
||||
}
|
||||
if (bind.startsWith('derived.')) {
|
||||
return (
|
||||
<div>
|
||||
<div className="muted">{String(widget.props?.label ?? key)}</div>
|
||||
<div style={{ fontSize: 20, fontWeight: 700 }}>{derived[key!] ?? 0}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (bind.startsWith('attr.')) {
|
||||
return (
|
||||
<div>
|
||||
<div className="muted">{String(widget.props?.label ?? key)}</div>
|
||||
<div style={{ fontSize: 20, fontWeight: 700 }}>{String(values.attributes?.[key!] ?? 10)}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return <div className="muted">{widget.type}</div>;
|
||||
}
|
||||
Reference in New Issue
Block a user