Three card families: Collectibles (mint) + Agents (skill-bar back); New-card under cards
CI / policy (push) Has been cancelled
CI / backend (push) Has been cancelled
CI / profile (push) Has been cancelled
CI / mobile (push) Has been cancelled

- localCardsStore: cards gain `kind` (business/collectible/agent) + optional
  provenance (collectibles) and agent meta (agents); cardsOfKind() filter.
- Cards tab: "+ New card" moved to a footer below the cards (no floating FAB);
  filtered to business cards.
- Collectibles + Agents tabs: real grids (CardCollection) with empty states and
  a "+ New" footer that opens the creator pre-set to that kind.
- Creator (demo.tsx) is kind-aware: collectibles mint a provenance record on
  save (utils/provenance stub, mapped to the creator); agents capture flip-side
  details (AgentDetailsEditor: tagline, skill bars, tools, capabilities).
- Card back per kind: AgentBackTemplate (skill bars/tools/capabilities) for
  agents; CardBackTemplate gains a "Minted · #token · chain" footer for
  collectibles. Both reuse the AI image/video front workflow.
- tsc + 28 jest green.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
Omar Sobh
2026-06-06 00:16:06 -05:00
co-authored by Claude Opus 4.8
parent e77e812609
commit d30deaf857
10 changed files with 516 additions and 60 deletions
+62 -5
View File
@@ -18,13 +18,22 @@ import {
TextInput,
View,
} from "react-native";
import { AgentDetailsEditor } from "../src/components/AgentDetailsEditor";
import { AgentBackTemplate } from "../src/components/card/AgentBackTemplate";
import { CardBackTemplate } from "../src/components/card/CardBackTemplate";
import { CardViewer } from "../src/components/card/CardViewer";
import { defaultLinks, LinksEditor } from "../src/components/LinksEditor";
import { defaultLinks } from "../src/components/LinksEditor";
import { newLayerId } from "../src/stores/cardStore";
import { useDraftStore } from "../src/stores/draftStore";
import { CardLink, useLocalCardsStore } from "../src/stores/localCardsStore";
import {
AgentMeta,
CardKind,
CardLink,
Provenance,
useLocalCardsStore,
} from "../src/stores/localCardsStore";
import { useProfileStore } from "../src/stores/profileStore";
import { mintProvenance } from "../src/utils/provenance";
import { CardDefinition, DEFAULT_SETTINGS, TextLayer } from "../src/types/card";
import { deleteImage, persistImage, persistVideo } from "../src/utils/imageStore";
@@ -100,7 +109,7 @@ function buildDemoCard(
export default function CardEditorScreen() {
const router = useRouter();
const { cardId } = useLocalSearchParams<{ cardId?: string }>();
const { cardId, kind: kindParam } = useLocalSearchParams<{ cardId?: string; kind?: string }>();
const upsert = useLocalCardsStore((s) => s.upsert);
const remove = useLocalCardsStore((s) => s.remove);
const getById = useLocalCardsStore((s) => s.getById);
@@ -118,6 +127,14 @@ export default function CardEditorScreen() {
const [links, setLinks] = useState<CardLink[]>(() =>
cardId ? [] : profile.socialLinks.length > 0 ? profile.socialLinks : defaultLinks(),
);
const [cardKind, setCardKind] = useState<CardKind>(() => (kindParam as CardKind) || "business");
const [agentMeta, setAgentMeta] = useState<AgentMeta>(() => ({
tagline: "",
skills: [],
tools: [],
capabilities: [],
}));
const [provenance, setProvenance] = useState<Provenance | undefined>(undefined);
const [showing, setShowing] = useState(false);
const [onBack, setOnBack] = useState(false);
const [saving, setSaving] = useState(false);
@@ -133,6 +150,9 @@ export default function CardEditorScreen() {
setTitle(existing.title);
setUrl(existing.url);
setLinks(existing.links ?? []);
setCardKind(existing.kind ?? "business");
setProvenance(existing.provenance);
if (existing.agent) setAgentMeta(existing.agent);
setOnBack(false);
setShowing(true);
}
@@ -189,17 +209,28 @@ export default function CardEditorScreen() {
const existing = getById(id);
const imagePath = imageUri ? await persistImage(imageUri, id) : (existing?.imagePath ?? "");
const videoPath = videoUri ? await persistVideo(videoUri, id) : undefined;
// Collectibles are "minted" (provenance recorded, mapped to the creator) on
// acceptance — once; agents carry their flip-side metadata.
const prov =
cardKind === "collectible"
? (existing?.provenance ?? mintProvenance(profile.displayName || name))
: undefined;
const agent = cardKind === "agent" ? agentMeta : undefined;
upsert({
id,
name,
title,
url,
kind: cardKind,
imagePath,
videoPath,
links,
publishedUrl: existing?.publishedUrl,
provenance: prov,
agent,
updatedAt: Date.now(),
});
setProvenance(prov);
if (imageUri) setImageUri(imagePath);
if (videoPath) setVideoUri(videoPath);
};
@@ -247,7 +278,17 @@ export default function CardEditorScreen() {
fullScreen
onSideChange={setOnBack}
backContent={
<CardBackTemplate name={name} title={title} profileUrl={url} links={links} />
cardKind === "agent" ? (
<AgentBackTemplate name={name} title={title} agent={agentMeta} />
) : (
<CardBackTemplate
name={name}
title={title}
profileUrl={url}
links={links}
provenance={cardKind === "collectible" ? provenance : undefined}
/>
)
}
/>
{/* Front: a hint to flip. Back (QR side): the Gallery / Edit controls. */}
@@ -270,7 +311,16 @@ export default function CardEditorScreen() {
return (
<ScrollView style={styles.root} contentContainerStyle={styles.content}>
<Stack.Screen
options={{ title: cardId ? "Edit card" : "Make your card", headerTitleAlign: "center" }}
options={{
title: cardId
? "Edit card"
: cardKind === "collectible"
? "New collectible"
: cardKind === "agent"
? "New agent"
: "Make your card",
headerTitleAlign: "center",
}}
/>
{videoUri ? (
@@ -310,6 +360,13 @@ export default function CardEditorScreen() {
</Pressable>
</View>
{cardKind === "agent" && (
<>
<Text style={styles.sectionLabel}>Agent details · shown on the flip side</Text>
<AgentDetailsEditor meta={agentMeta} onChange={setAgentMeta} />
</>
)}
<Pressable
style={[styles.cta, (!media || saving) && styles.ctaDisabled]}
disabled={!media || saving}