V2 P5-backend + P7 + P8: conflict-safe sync, revocable invites, feats, multiclass, tests
P5 backend (the deferred items): - conflict-aware cloud sync via optimistic concurrency: the server versions each backup blob (file mtime) and rejects a push whose base version is stale (409), so a second device can't silently overwrite a newer cloud copy. The client tracks its last-synced version; the SyncStatusIndicator surfaces a "Sync conflict" with one-click resolution (Use cloud / Keep this device), and the Settings push offers the same choice. - revocable invites: owner can rotate a campaign's invite code (kills the old one) and remove a member (drops them + deletes their published characters). (Skipped editor/player roles — no enforcement target in the current model.) - image storage: deliberately deferred (live images already stream de-duped on a separate channel; a backup blob-store is infra with no correctness gain). P7 — subclass/feat/multiclass depth: - feats are now first-class tracked records (name + effect text) in a Feats & Features sheet section, not buried in notes. Dexie v15 migration. - 5e multiclass spell-slot calculator: a pure multiclassSlots() (full ×1, half ÷2, third ÷3 → full-caster table) + a sheet control to fill the slot table for multiclass casters. (Honest scope: tracking + slots, not per-subclass feature automation.) P8 — test hardening: v2-surfaces e2e (feats, multiclass calc, signals bell, World nav) + server tests (blob versioning, invite rotation, member removal) + multiclass unit tests. Gate green: 283 unit + 35 e2e + 2 realtime. App + server build clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
import { useState } from 'react';
|
||||
import { newId } from '@/lib/ids';
|
||||
import type { Feat } from '@/lib/schemas';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { Input, Textarea } from '@/components/ui/Input';
|
||||
import { SheetSection, type SectionProps } from './common';
|
||||
|
||||
/** Feats tracked as first-class records (name + effect) rather than buried in notes. */
|
||||
export function FeatsSection({ c, update }: SectionProps) {
|
||||
const [name, setName] = useState('');
|
||||
|
||||
const add = () => {
|
||||
if (name.trim() === '') return;
|
||||
const feat: Feat = { id: newId(), name: name.trim(), source: '', description: '' };
|
||||
update({ feats: [...c.feats, feat] });
|
||||
setName('');
|
||||
};
|
||||
const patch = (id: string, p: Partial<Feat>) =>
|
||||
update({ feats: c.feats.map((f) => (f.id === id ? { ...f, ...p } : f)) });
|
||||
const remove = (id: string) => update({ feats: c.feats.filter((f) => f.id !== id) });
|
||||
|
||||
return (
|
||||
<SheetSection title="Feats & Features">
|
||||
<div className="mb-3 flex items-end gap-2">
|
||||
<label className="flex-1 min-w-40 text-xs text-muted">
|
||||
Add feat / feature
|
||||
<Input value={name} onChange={(e) => setName(e.target.value)} onKeyDown={(e) => e.key === 'Enter' && add()} placeholder="Great Weapon Master, Oath of Devotion…" />
|
||||
</label>
|
||||
<Button variant="primary" onClick={add}>Add</Button>
|
||||
</div>
|
||||
|
||||
{c.feats.length === 0 ? (
|
||||
<p className="text-sm text-muted">No feats or features tracked yet.</p>
|
||||
) : (
|
||||
<ul className="space-y-2">
|
||||
{c.feats.map((f) => (
|
||||
<li key={f.id} className="rounded-md border border-line bg-panel p-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Input className="h-8 flex-1 font-medium" value={f.name} onChange={(e) => patch(f.id, { name: e.target.value })} aria-label="Feat name" />
|
||||
<Button size="icon" variant="ghost" className="text-danger" onClick={() => remove(f.id)} aria-label={`Remove ${f.name}`}>✕</Button>
|
||||
</div>
|
||||
<Textarea className="mt-1 text-xs" rows={2} value={f.description} onChange={(e) => patch(f.id, { description: e.target.value })} placeholder="What it does — kept handy at the table." aria-label={`${f.name} description`} />
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</SheetSection>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user