@magicborn/dialogue-forge 0.1.0 → 0.1.2

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.
Files changed (51) hide show
  1. package/demo/app/page.tsx +26 -12
  2. package/demo/public/logo.svg +14 -1
  3. package/dist/components/CharacterSelector.d.ts +15 -0
  4. package/dist/components/CharacterSelector.js +125 -0
  5. package/dist/components/ConditionalNodeV2.d.ts +2 -0
  6. package/dist/components/ConditionalNodeV2.js +50 -29
  7. package/dist/components/DialogueEditorV2.d.ts +2 -0
  8. package/dist/components/DialogueEditorV2.js +43 -6
  9. package/dist/components/GuidePanel.js +99 -16
  10. package/dist/components/NPCNodeV2.d.ts +2 -0
  11. package/dist/components/NPCNodeV2.js +31 -20
  12. package/dist/components/NodeEditor.d.ts +3 -1
  13. package/dist/components/NodeEditor.js +42 -13
  14. package/dist/components/PlayerNodeV2.d.ts +2 -0
  15. package/dist/components/PlayerNodeV2.js +58 -39
  16. package/dist/components/ReactFlowPOC.js +3 -3
  17. package/dist/esm/components/CharacterSelector.d.ts +15 -0
  18. package/dist/esm/components/CharacterSelector.js +89 -0
  19. package/dist/esm/components/ConditionalNodeV2.d.ts +2 -0
  20. package/dist/esm/components/ConditionalNodeV2.js +51 -30
  21. package/dist/esm/components/DialogueEditorV2.d.ts +2 -0
  22. package/dist/esm/components/DialogueEditorV2.js +43 -6
  23. package/dist/esm/components/GuidePanel.js +99 -16
  24. package/dist/esm/components/NPCNodeV2.d.ts +2 -0
  25. package/dist/esm/components/NPCNodeV2.js +32 -21
  26. package/dist/esm/components/NodeEditor.d.ts +3 -1
  27. package/dist/esm/components/NodeEditor.js +42 -13
  28. package/dist/esm/components/PlayerNodeV2.d.ts +2 -0
  29. package/dist/esm/components/PlayerNodeV2.js +59 -40
  30. package/dist/esm/components/ReactFlowPOC.js +3 -3
  31. package/dist/esm/examples/example-characters.d.ts +19 -0
  32. package/dist/esm/examples/example-characters.js +67 -0
  33. package/dist/esm/examples/index.d.ts +1 -0
  34. package/dist/esm/examples/index.js +2 -0
  35. package/dist/esm/index.d.ts +3 -0
  36. package/dist/esm/index.js +3 -0
  37. package/dist/esm/types/characters.d.ts +15 -0
  38. package/dist/esm/types/characters.js +6 -0
  39. package/dist/esm/types/game-state.d.ts +2 -0
  40. package/dist/esm/types/index.d.ts +2 -0
  41. package/dist/examples/example-characters.d.ts +19 -0
  42. package/dist/examples/example-characters.js +73 -0
  43. package/dist/examples/index.d.ts +1 -0
  44. package/dist/examples/index.js +7 -1
  45. package/dist/index.d.ts +3 -0
  46. package/dist/index.js +9 -1
  47. package/dist/types/characters.d.ts +15 -0
  48. package/dist/types/characters.js +7 -0
  49. package/dist/types/game-state.d.ts +2 -0
  50. package/dist/types/index.d.ts +2 -0
  51. package/package.json +1 -1
@@ -10,7 +10,7 @@
10
10
  */
11
11
  import React from 'react';
12
12
  import { Handle, Position } from 'reactflow';
13
- import { MessageSquare, Play, Flag } from 'lucide-react';
13
+ import { MessageSquare, Play, Flag, Hash } from 'lucide-react';
14
14
  // ============================================================================
15
15
  // Styles
16
16
  // ============================================================================
@@ -30,7 +30,11 @@ function getFlagColorClass(type) {
30
30
  // Component
31
31
  // ============================================================================
32
32
  export function NPCNodeV2({ data, selected }) {
33
- const { node, flagSchema, isDimmed, isInPath, layoutDirection = 'TB', isStartNode, isEndNode } = data;
33
+ const { node, flagSchema, characters = {}, isDimmed, isInPath, layoutDirection = 'TB', isStartNode, isEndNode } = data;
34
+ // Get character if characterId is set
35
+ const character = node.characterId ? characters[node.characterId] : undefined;
36
+ const displayName = character ? character.name : (node.speaker || 'NPC');
37
+ const avatar = character?.avatar || '👤';
34
38
  // Handle positions based on layout direction
35
39
  const isHorizontal = layoutDirection === 'LR';
36
40
  const targetPosition = isHorizontal ? Position.Left : Position.Top;
@@ -53,28 +57,35 @@ export function NPCNodeV2({ data, selected }) {
53
57
  const contentPreview = node.content.length > 60
54
58
  ? node.content.slice(0, 60) + '...'
55
59
  : node.content;
56
- return (React.createElement("div", { className: `rounded-lg border-2 transition-all duration-300 ${borderClass} ${isInPath ? 'border-df-node-selected/70' : ''} bg-df-npc-bg min-w-[200px] relative`, style: isDimmed ? { opacity: 0.35, filter: 'saturate(0.3)' } : undefined },
57
- isStartNode && (React.createElement("div", { className: "absolute -top-2 -left-2 bg-df-start text-df-text-primary text-[8px] font-bold px-1.5 py-0.5 rounded-full flex items-center gap-0.5 shadow-lg z-10" },
58
- React.createElement(Play, { size: 8, fill: "currentColor" }),
59
- " START")),
60
- isEndNode && (React.createElement("div", { className: "absolute -top-2 -right-2 bg-df-end text-df-text-primary text-[8px] font-bold px-1.5 py-0.5 rounded-full flex items-center gap-0.5 shadow-lg z-10" },
61
- React.createElement(Flag, { size: 8 }),
62
- " END")),
60
+ return (React.createElement("div", { className: `rounded-lg border-2 transition-all duration-300 ${borderClass} ${isInPath ? 'border-df-node-selected/70' : ''} bg-df-npc-bg min-w-[320px] max-w-[450px] relative overflow-hidden`, style: isDimmed ? { opacity: 0.35, filter: 'saturate(0.3)' } : undefined },
63
61
  React.createElement(Handle, { type: "target", position: targetPosition, className: "!bg-df-control-bg !border-df-control-border !w-4 !h-4 !rounded-full" }),
64
- React.createElement("div", { className: `px-3 py-1.5 border-b border-df-control-border flex items-center gap-2 rounded-t-lg ${headerBgClass}` },
65
- React.createElement(MessageSquare, { size: 12, className: "text-df-npc-selected" }),
66
- React.createElement("span", { className: "text-[10px] font-mono text-df-text-secondary truncate flex-1" }, node.id),
67
- React.createElement("span", { className: "text-[10px] text-df-text-tertiary" }, "NPC")),
68
- React.createElement("div", { className: "px-3 py-2 min-h-[50px]" },
69
- node.speaker && (React.createElement("div", { className: "text-[10px] text-df-npc-selected font-medium mb-1" }, node.speaker)),
70
- React.createElement("div", { className: "text-xs text-df-text-primary line-clamp-2 bg-df-base border border-df-control-border rounded px-2 py-1" },
71
- "\"",
72
- contentPreview,
73
- "\""),
74
- node.setFlags && node.setFlags.length > 0 && (React.createElement("div", { className: "mt-1.5 flex flex-wrap gap-1" }, node.setFlags.map((flagId) => {
62
+ React.createElement("div", { className: `${headerBgClass} border-b-2 border-df-npc-border px-3 py-2.5 flex items-center gap-3 relative` },
63
+ React.createElement("div", { className: "w-14 h-14 rounded-full bg-df-npc-bg border-[3px] border-df-npc-border flex items-center justify-center text-3xl shadow-lg flex-shrink-0" }, avatar),
64
+ React.createElement("div", { className: "flex-1 min-w-0" },
65
+ React.createElement("h3", { className: "text-base font-bold text-df-text-primary truncate leading-tight" }, displayName)),
66
+ React.createElement("div", { className: "flex items-center gap-2 flex-shrink-0" },
67
+ React.createElement("div", { className: "flex items-center gap-1 px-2 py-1 rounded bg-df-base/50 border border-df-control-border", title: `Node ID: ${node.id}` },
68
+ React.createElement(Hash, { size: 12, className: "text-df-text-secondary" }),
69
+ React.createElement("span", { className: "text-[10px] font-mono text-df-text-secondary" }, node.id)),
70
+ React.createElement("div", { className: "flex items-center gap-1 px-2 py-1 rounded bg-df-npc-selected/20 border border-df-npc-selected/50", title: "NPC Node" },
71
+ React.createElement(MessageSquare, { size: 14, className: "text-df-npc-selected" }),
72
+ React.createElement("span", { className: "text-[10px] font-semibold text-df-npc-selected" }, "NPC"))),
73
+ isStartNode && (React.createElement("div", { className: "absolute top-1 right-1 bg-df-start text-df-text-primary text-[8px] font-bold px-1.5 py-0.5 rounded flex items-center gap-0.5 shadow-lg z-20" },
74
+ React.createElement(Play, { size: 8, fill: "currentColor" }),
75
+ " START")),
76
+ isEndNode && (React.createElement("div", { className: "absolute top-1 right-1 bg-df-end text-df-text-primary text-[8px] font-bold px-1.5 py-0.5 rounded flex items-center gap-0.5 shadow-lg z-20" },
77
+ React.createElement(Flag, { size: 8 }),
78
+ " END"))),
79
+ React.createElement("div", { className: "px-4 py-3" },
80
+ React.createElement("div", { className: "bg-df-elevated border border-df-control-border rounded-lg px-4 py-3 mb-2" },
81
+ React.createElement("p", { className: "text-sm text-df-text-primary leading-relaxed" },
82
+ "\"",
83
+ contentPreview,
84
+ "\"")),
85
+ node.setFlags && node.setFlags.length > 0 && (React.createElement("div", { className: "flex flex-wrap gap-1" }, node.setFlags.map((flagId) => {
75
86
  const flag = flagSchema?.flags.find((f) => f.id === flagId);
76
87
  const flagType = flag?.type || 'dialogue';
77
- return (React.createElement("span", { key: flagId, className: `text-[8px] px-1 py-0.5 rounded border ${getFlagColorClass(flagType)}`, title: flag?.name || flagId }, flagType === 'dialogue' ? 't' : flagType[0]));
88
+ return (React.createElement("span", { key: flagId, className: `text-[8px] px-1.5 py-0.5 rounded-full border ${getFlagColorClass(flagType)}`, title: flag?.name || flagId }, flagType === 'dialogue' ? 't' : flagType[0]));
78
89
  })))),
79
90
  React.createElement(Handle, { type: "source", position: sourcePosition, id: "next", className: "!bg-df-control-bg !border-df-control-border !w-4 !h-4 !rounded-full hover:!border-df-npc-selected hover:!bg-df-npc-selected/20" })));
80
91
  }
@@ -1,6 +1,7 @@
1
1
  import React from 'react';
2
2
  import { DialogueNode, DialogueTree, Choice } from '../types';
3
3
  import { FlagSchema } from '../types/flags';
4
+ import { Character } from '../types/characters';
4
5
  interface NodeEditorProps {
5
6
  node: DialogueNode;
6
7
  dialogue: DialogueTree;
@@ -13,6 +14,7 @@ interface NodeEditorProps {
13
14
  onPlayFromHere?: (nodeId: string) => void;
14
15
  onFocusNode?: (nodeId: string) => void;
15
16
  flagSchema?: FlagSchema;
17
+ characters?: Record<string, Character>;
16
18
  }
17
- export declare function NodeEditor({ node, dialogue, onUpdate, onDelete, onAddChoice, onUpdateChoice, onRemoveChoice, onClose, onPlayFromHere, onFocusNode, flagSchema }: NodeEditorProps): React.JSX.Element;
19
+ export declare function NodeEditor({ node, dialogue, onUpdate, onDelete, onAddChoice, onUpdateChoice, onRemoveChoice, onClose, onPlayFromHere, onFocusNode, flagSchema, characters, }: NodeEditorProps): React.JSX.Element;
18
20
  export {};
@@ -1,11 +1,12 @@
1
1
  import React, { useState, useEffect, useRef, useMemo } from 'react';
2
2
  import { FlagSelector } from './FlagSelector';
3
+ import { CharacterSelector } from './CharacterSelector';
3
4
  import { CONDITION_OPERATOR } from '../types/constants';
4
5
  import { AlertCircle, CheckCircle, Info, GitBranch, X, User, Maximize2 } from 'lucide-react';
5
6
  import { CHOICE_COLORS } from '../utils/reactflow-converter';
6
7
  import { EdgeIcon } from './EdgeIcon';
7
8
  import { ConditionAutocomplete } from './ConditionAutocomplete';
8
- export function NodeEditor({ node, dialogue, onUpdate, onDelete, onAddChoice, onUpdateChoice, onRemoveChoice, onClose, onPlayFromHere, onFocusNode, flagSchema }) {
9
+ export function NodeEditor({ node, dialogue, onUpdate, onDelete, onAddChoice, onUpdateChoice, onRemoveChoice, onClose, onPlayFromHere, onFocusNode, flagSchema, characters = {}, }) {
9
10
  // Local state for condition input values (keyed by block id for conditional blocks, choice id for choices)
10
11
  const [conditionInputs, setConditionInputs] = useState({});
11
12
  const [debouncedConditionInputs, setDebouncedConditionInputs] = useState({});
@@ -311,11 +312,21 @@ export function NodeEditor({ node, dialogue, onUpdate, onDelete, onAddChoice, on
311
312
  React.createElement("input", { value: node.id, disabled: true, className: "w-full bg-[#12121a] border border-[#2a2a3e] rounded px-2 py-1 text-xs text-gray-500 font-mono" })),
312
313
  node.type === 'npc' && (React.createElement(React.Fragment, null,
313
314
  React.createElement("div", null,
314
- React.createElement("label", { className: "text-[10px] text-gray-500 uppercase" }, "Speaker"),
315
+ React.createElement("label", { className: "text-[10px] text-df-text-secondary uppercase" }, "Character"),
316
+ React.createElement(CharacterSelector, { characters: characters, selectedCharacterId: node.characterId, onSelect: (characterId) => {
317
+ const character = characterId ? characters[characterId] : undefined;
318
+ onUpdate({
319
+ characterId,
320
+ speaker: character ? character.name : node.speaker, // Keep speaker as fallback
321
+ });
322
+ }, placeholder: "Select character...", className: "mb-2" }),
323
+ React.createElement("div", { className: "text-[9px] text-df-text-tertiary mt-1" }, "Or enter custom speaker name below")),
324
+ React.createElement("div", null,
325
+ React.createElement("label", { className: "text-[10px] text-df-text-secondary uppercase" }, "Speaker (Custom)"),
315
326
  React.createElement("div", { className: "flex items-center gap-2" },
316
- React.createElement("div", { className: "w-8 h-8 rounded-full bg-[#2a2a3e] border border-[#2a2a3e] flex items-center justify-center flex-shrink-0" },
317
- React.createElement(User, { size: 16, className: "text-gray-500" })),
318
- React.createElement("input", { type: "text", value: node.speaker || '', onChange: (e) => onUpdate({ speaker: e.target.value }), className: "flex-1 bg-df-elevated border border-df-control-border rounded px-2 py-1 text-sm text-df-text-primary focus:border-df-npc-selected outline-none", placeholder: "Character name" }))),
327
+ React.createElement("div", { className: "w-8 h-8 rounded-full bg-df-control-bg border border-df-control-border flex items-center justify-center flex-shrink-0" },
328
+ React.createElement(User, { size: 16, className: "text-df-text-secondary" })),
329
+ React.createElement("input", { type: "text", value: node.speaker || '', onChange: (e) => onUpdate({ speaker: e.target.value }), className: "flex-1 bg-df-elevated border border-df-control-border rounded px-2 py-1 text-sm text-df-text-primary focus:border-df-npc-selected outline-none", placeholder: "Custom speaker name (optional)" }))),
319
330
  React.createElement("div", null,
320
331
  React.createElement("label", { className: "text-[10px] text-gray-500 uppercase" }, "Content"),
321
332
  React.createElement("textarea", { value: node.content, onChange: (e) => onUpdate({ content: e.target.value }), className: "w-full bg-df-elevated border border-df-control-border rounded px-2 py-1 text-sm text-df-text-primary focus:border-df-npc-selected outline-none min-h-[100px] resize-y", placeholder: "What the character says..." })),
@@ -375,14 +386,22 @@ export function NodeEditor({ node, dialogue, onUpdate, onDelete, onAddChoice, on
375
386
  return (React.createElement("div", { key: block.id, className: `rounded p-2 space-y-2 ${styles.bg} ${styles.border} border-2` },
376
387
  React.createElement("div", { className: "flex items-center gap-2" },
377
388
  React.createElement("span", { className: `text-[9px] px-1.5 py-0.5 rounded ${styles.tagBg} ${styles.tagText} font-semibold` }, block.type === 'if' ? 'IF' : block.type === 'elseif' ? 'ELSE IF' : 'ELSE'),
378
- React.createElement("div", { className: "flex items-center gap-2 flex-1" },
379
- React.createElement("div", { className: "w-6 h-6 rounded-full bg-[#2a2a2a] flex items-center justify-center flex-shrink-0" },
380
- React.createElement(User, { size: 12, className: "text-gray-400" })),
389
+ React.createElement("div", { className: "flex items-center gap-1.5 flex-1" },
390
+ React.createElement(CharacterSelector, { characters: characters, selectedCharacterId: block.characterId, onSelect: (characterId) => {
391
+ const newBlocks = [...node.conditionalBlocks];
392
+ const character = characterId ? characters[characterId] : undefined;
393
+ newBlocks[idx] = {
394
+ ...block,
395
+ characterId,
396
+ speaker: character ? character.name : block.speaker, // Keep speaker as fallback
397
+ };
398
+ onUpdate({ conditionalBlocks: newBlocks });
399
+ }, placeholder: "Speaker...", compact: true, className: "flex-1" }),
381
400
  React.createElement("input", { type: "text", value: block.speaker || '', onChange: (e) => {
382
401
  const newBlocks = [...node.conditionalBlocks];
383
402
  newBlocks[idx] = { ...block, speaker: e.target.value || undefined };
384
403
  onUpdate({ conditionalBlocks: newBlocks });
385
- }, className: `flex-1 bg-[#1a1a1a] border border-[#2a2a2a] rounded px-2 py-1 text-xs ${styles.text} focus:border-blue-500 outline-none`, placeholder: "Speaker (optional)" }))),
404
+ }, className: `flex-1 bg-df-elevated border border-df-control-border rounded px-1.5 py-0.5 text-[10px] text-df-text-primary focus:border-df-conditional-selected outline-none`, placeholder: "Custom name" }))),
386
405
  block.type !== 'else' && (() => {
387
406
  const parseCondition = (conditionStr) => {
388
407
  const conditions = [];
@@ -690,11 +709,21 @@ export function NodeEditor({ node, dialogue, onUpdate, onDelete, onAddChoice, on
690
709
  }, className: "text-xs px-2 py-1 bg-[#12121a] border border-[#2a2a3e] rounded text-gray-400 hover:text-gray-200" }, "+ Add Else"))))) : (React.createElement("div", { className: "text-xs text-gray-500 p-4 text-center border border-[#2a2a3e] rounded" }, "No conditional blocks. Add an \"If\" block to start."))))),
691
710
  node.type === 'player' && (React.createElement("div", null,
692
711
  React.createElement("div", null,
693
- React.createElement("label", { className: "text-[10px] text-gray-500 uppercase" }, "Speaker"),
712
+ React.createElement("label", { className: "text-[10px] text-df-text-secondary uppercase" }, "Character"),
713
+ React.createElement(CharacterSelector, { characters: characters, selectedCharacterId: node.characterId, onSelect: (characterId) => {
714
+ const character = characterId ? characters[characterId] : undefined;
715
+ onUpdate({
716
+ characterId,
717
+ speaker: character ? character.name : node.speaker, // Keep speaker as fallback
718
+ });
719
+ }, placeholder: "Select character...", className: "mb-2" }),
720
+ React.createElement("div", { className: "text-[9px] text-df-text-tertiary mt-1" }, "Or enter custom speaker name below")),
721
+ React.createElement("div", null,
722
+ React.createElement("label", { className: "text-[10px] text-df-text-secondary uppercase" }, "Speaker (Custom)"),
694
723
  React.createElement("div", { className: "flex items-center gap-2" },
695
- React.createElement("div", { className: "w-8 h-8 rounded-full bg-[#2a2a3e] border border-[#2a2a3e] flex items-center justify-center flex-shrink-0" },
696
- React.createElement(User, { size: 16, className: "text-gray-500" })),
697
- React.createElement("input", { type: "text", value: node.speaker || '', onChange: (e) => onUpdate({ speaker: e.target.value }), className: "flex-1 bg-[#12121a] border border-[#2a2a3e] rounded px-2 py-1 text-sm text-gray-200 focus:border-[#8b5cf6] outline-none", placeholder: "Character name (optional)" }))),
724
+ React.createElement("div", { className: "w-8 h-8 rounded-full bg-df-control-bg border border-df-control-border flex items-center justify-center flex-shrink-0" },
725
+ React.createElement(User, { size: 16, className: "text-df-text-secondary" })),
726
+ React.createElement("input", { type: "text", value: node.speaker || '', onChange: (e) => onUpdate({ speaker: e.target.value }), className: "flex-1 bg-df-elevated border border-df-control-border rounded px-2 py-1 text-sm text-df-text-primary focus:border-df-player-selected outline-none", placeholder: "Custom speaker name (optional)" }))),
698
727
  React.createElement("div", { className: "flex items-center justify-between mb-2 mt-4" },
699
728
  React.createElement("label", { className: "text-[10px] text-gray-500 uppercase" }, "Choices"),
700
729
  React.createElement("button", { onClick: onAddChoice, className: "text-[10px] text-[#e94560] hover:text-[#ff6b6b]" }, "+ Add")),
@@ -1,11 +1,13 @@
1
1
  import React from 'react';
2
2
  import { NodeProps } from 'reactflow';
3
3
  import { DialogueNode } from '../types';
4
+ import { Character } from '../types/characters';
4
5
  import { FlagSchema } from '../types/flags';
5
6
  import { LayoutDirection } from '../utils/layout';
6
7
  interface PlayerNodeData {
7
8
  node: DialogueNode;
8
9
  flagSchema?: FlagSchema;
10
+ characters?: Record<string, Character>;
9
11
  isDimmed?: boolean;
10
12
  isInPath?: boolean;
11
13
  layoutDirection?: LayoutDirection;
@@ -1,11 +1,15 @@
1
1
  import React, { useEffect, useRef, useState } from 'react';
2
2
  import { Handle, Position, useUpdateNodeInternals } from 'reactflow';
3
- import { GitBranch, Play, Flag } from 'lucide-react';
3
+ import { GitBranch, Play, Flag, Hash } from 'lucide-react';
4
4
  // Color scheme for choice edges (same as current implementation)
5
5
  const CHOICE_COLORS = ['#e94560', '#8b5cf6', '#06b6d4', '#22c55e', '#f59e0b'];
6
6
  export function PlayerNodeV2({ data, selected }) {
7
- const { node, flagSchema, isDimmed, isInPath, layoutDirection = 'TB', isStartNode, isEndNode } = data;
7
+ const { node, flagSchema, characters = {}, isDimmed, isInPath, layoutDirection = 'TB', isStartNode, isEndNode } = data;
8
8
  const choices = node.choices || [];
9
+ // Get character if characterId is set
10
+ const character = node.characterId ? characters[node.characterId] : undefined;
11
+ const displayName = character ? character.name : (node.speaker || 'Player');
12
+ const avatar = character?.avatar || '🎮';
9
13
  const updateNodeInternals = useUpdateNodeInternals();
10
14
  const headerRef = useRef(null);
11
15
  const choiceRefs = useRef([]);
@@ -57,47 +61,62 @@ export function PlayerNodeV2({ data, selected }) {
57
61
  : isEndNode
58
62
  ? 'border-df-end shadow-md'
59
63
  : 'border-df-player-border';
60
- return (React.createElement("div", { className: `rounded-lg border-2 transition-all duration-300 ${borderClass} ${isInPath ? 'border-df-player-selected/70' : ''} bg-df-player-bg min-w-[200px] relative`, style: isDimmed ? { opacity: 0.35, filter: 'saturate(0.3)' } : undefined },
61
- isStartNode && (React.createElement("div", { className: "absolute -top-2 -left-2 bg-df-start text-df-text-primary text-[8px] font-bold px-1.5 py-0.5 rounded-full flex items-center gap-0.5 shadow-lg z-10" },
62
- React.createElement(Play, { size: 8, fill: "currentColor" }),
63
- " START")),
64
- isEndNode && (React.createElement("div", { className: "absolute -top-2 -right-2 bg-df-end text-df-text-primary text-[8px] font-bold px-1.5 py-0.5 rounded-full flex items-center gap-0.5 shadow-lg z-10" },
65
- React.createElement(Flag, { size: 8 }),
66
- " END")),
64
+ // Header background for player nodes
65
+ const headerBgClass = isStartNode
66
+ ? 'bg-df-start-bg'
67
+ : isEndNode
68
+ ? 'bg-df-end-bg'
69
+ : 'bg-df-player-header';
70
+ return (React.createElement("div", { className: `rounded-lg border-2 transition-all duration-300 ${borderClass} ${isInPath ? 'border-df-player-selected/70' : ''} bg-df-player-bg min-w-[320px] max-w-[450px] relative overflow-hidden`, style: isDimmed ? { opacity: 0.35, filter: 'saturate(0.3)' } : undefined },
67
71
  React.createElement(Handle, { type: "target", position: targetPosition, className: "!bg-df-control-bg !border-df-control-border !w-4 !h-4 !rounded-full" }),
68
- React.createElement("div", { ref: headerRef, className: "px-3 py-1.5 border-b border-df-control-border bg-df-player-header flex items-center gap-2 rounded-t-lg" },
69
- React.createElement(GitBranch, { size: 12, className: "text-df-player-selected" }),
70
- React.createElement("span", { className: "text-[10px] font-mono text-df-text-secondary truncate flex-1" }, node.id),
71
- React.createElement("span", { className: "text-[10px] text-df-text-tertiary" }, "PLAYER")),
72
- React.createElement("div", { className: "border-t border-df-control-border" }, choices.map((choice, idx) => {
72
+ React.createElement("div", { ref: headerRef, className: `${headerBgClass} border-b-2 border-df-player-border px-3 py-2.5 flex items-center gap-3 relative` },
73
+ React.createElement("div", { className: "w-14 h-14 rounded-full bg-df-player-bg border-[3px] border-df-player-border flex items-center justify-center text-3xl shadow-lg flex-shrink-0" }, avatar),
74
+ React.createElement("div", { className: "flex-1 min-w-0" },
75
+ React.createElement("h3", { className: "text-base font-bold text-df-text-primary truncate leading-tight" }, displayName)),
76
+ React.createElement("div", { className: "flex items-center gap-2 flex-shrink-0" },
77
+ React.createElement("div", { className: "flex items-center gap-1 px-2 py-1 rounded bg-df-base/50 border border-df-control-border", title: `Node ID: ${node.id}` },
78
+ React.createElement(Hash, { size: 12, className: "text-df-text-secondary" }),
79
+ React.createElement("span", { className: "text-[10px] font-mono text-df-text-secondary" }, node.id)),
80
+ React.createElement("div", { className: "flex items-center gap-1 px-2 py-1 rounded bg-df-player-selected/20 border border-df-player-selected/50", title: "Player Node" },
81
+ React.createElement(GitBranch, { size: 14, className: "text-df-player-selected" }),
82
+ React.createElement("span", { className: "text-[10px] font-semibold text-df-player-selected" }, "PLAYER"))),
83
+ isStartNode && (React.createElement("div", { className: "absolute top-1 right-1 bg-df-start text-df-text-primary text-[8px] font-bold px-1.5 py-0.5 rounded flex items-center gap-0.5 shadow-lg z-20" },
84
+ React.createElement(Play, { size: 8, fill: "currentColor" }),
85
+ " START")),
86
+ isEndNode && (React.createElement("div", { className: "absolute top-1 right-1 bg-df-end text-df-text-primary text-[8px] font-bold px-1.5 py-0.5 rounded flex items-center gap-0.5 shadow-lg z-20" },
87
+ React.createElement(Flag, { size: 8 }),
88
+ " END"))),
89
+ React.createElement("div", { className: "px-4 py-3 space-y-2" }, choices.map((choice, idx) => {
73
90
  // Use calculated position or fallback
74
91
  const choiceColor = CHOICE_COLORS[idx % CHOICE_COLORS.length];
75
92
  return (React.createElement("div", { key: choice.id, ref: el => {
76
93
  choiceRefs.current[idx] = el;
77
- }, className: "px-3 py-1.5 text-[10px] text-df-text-secondary flex items-center gap-2 border-b border-df-control-border last:border-0 relative" },
78
- React.createElement("div", { className: "flex-1 min-w-0" },
79
- React.createElement("span", { className: "truncate block bg-df-base border border-df-control-border rounded px-2 py-1 text-df-text-primary" },
80
- "\"",
81
- choice.text || 'Empty choice',
82
- "\""),
83
- choice.setFlags && choice.setFlags.length > 0 && (React.createElement("div", { className: "mt-0.5 flex flex-wrap gap-0.5" }, choice.setFlags.map(flagId => {
84
- const flag = flagSchema?.flags.find(f => f.id === flagId);
85
- const flagType = flag?.type || 'dialogue';
86
- const colorClass = flagType === 'dialogue' ? 'bg-df-flag-dialogue-bg text-df-flag-dialogue border-df-flag-dialogue' :
87
- flagType === 'quest' ? 'bg-df-flag-quest-bg text-df-flag-quest border-df-flag-quest' :
88
- flagType === 'achievement' ? 'bg-df-flag-achievement-bg text-df-flag-achievement border-df-flag-achievement' :
89
- flagType === 'item' ? 'bg-df-flag-item-bg text-df-flag-item border-df-flag-item' :
90
- flagType === 'stat' ? 'bg-df-flag-stat-bg text-df-flag-stat border-df-flag-stat' :
91
- flagType === 'title' ? 'bg-df-flag-title-bg text-df-flag-title border-df-flag-title' :
92
- flagType === 'global' ? 'bg-df-flag-global-bg text-df-flag-global border-df-flag-global' :
93
- 'bg-df-flag-dialogue-bg text-df-flag-dialogue border-df-flag-dialogue';
94
- return (React.createElement("span", { key: flagId, className: `text-[7px] px-0.5 py-0 rounded border ${colorClass}`, title: flag?.name || flagId }, flagType === 'dialogue' ? 't' : flagType[0]));
95
- })))),
96
- React.createElement(Handle, { type: "source", position: Position.Right, id: `choice-${idx}`, style: {
97
- top: `50%`,
98
- transform: `translateY(-50%)`,
99
- right: '-6px',
100
- borderColor: choiceColor,
101
- }, className: "!bg-df-control-bg !border-2 hover:!border-df-player-selected hover:!bg-df-player-selected/20 !w-3 !h-3 !rounded-full" })));
102
- }))));
94
+ }, className: "relative group" },
95
+ React.createElement("div", { className: "bg-df-elevated border border-df-control-border rounded-lg px-3 py-2 flex items-start gap-2 hover:border-df-player-selected/50 transition-colors" },
96
+ React.createElement("div", { className: "flex-1 min-w-0" },
97
+ React.createElement("p", { className: "text-sm text-df-text-primary leading-relaxed" },
98
+ "\"",
99
+ choice.text || 'Empty choice',
100
+ "\""),
101
+ choice.setFlags && choice.setFlags.length > 0 && (React.createElement("div", { className: "mt-1.5 flex flex-wrap gap-1" }, choice.setFlags.map(flagId => {
102
+ const flag = flagSchema?.flags.find(f => f.id === flagId);
103
+ const flagType = flag?.type || 'dialogue';
104
+ const colorClass = flagType === 'dialogue' ? 'bg-df-flag-dialogue-bg text-df-flag-dialogue border-df-flag-dialogue' :
105
+ flagType === 'quest' ? 'bg-df-flag-quest-bg text-df-flag-quest border-df-flag-quest' :
106
+ flagType === 'achievement' ? 'bg-df-flag-achievement-bg text-df-flag-achievement border-df-flag-achievement' :
107
+ flagType === 'item' ? 'bg-df-flag-item-bg text-df-flag-item border-df-flag-item' :
108
+ flagType === 'stat' ? 'bg-df-flag-stat-bg text-df-flag-stat border-df-flag-stat' :
109
+ flagType === 'title' ? 'bg-df-flag-title-bg text-df-flag-title border-df-flag-title' :
110
+ flagType === 'global' ? 'bg-df-flag-global-bg text-df-flag-global border-df-flag-global' :
111
+ 'bg-df-flag-dialogue-bg text-df-flag-dialogue border-df-flag-dialogue';
112
+ return (React.createElement("span", { key: flagId, className: `text-[8px] px-1 py-0.5 rounded-full border ${colorClass}`, title: flag?.name || flagId }, flagType === 'dialogue' ? 't' : flagType[0]));
113
+ })))),
114
+ React.createElement(Handle, { type: "source", position: Position.Right, id: `choice-${idx}`, style: {
115
+ top: `50%`,
116
+ transform: `translateY(-50%)`,
117
+ right: '-6px',
118
+ borderColor: choiceColor,
119
+ }, className: "!bg-df-control-bg !border-2 hover:!border-df-player-selected hover:!bg-df-player-selected/20 !w-3 !h-3 !rounded-full" }))));
120
+ })),
121
+ React.createElement(Handle, { type: "source", position: sourcePosition, id: "next", className: "!bg-df-control-bg !border-df-control-border !w-4 !h-4 !rounded-full hover:!border-df-player-selected hover:!bg-df-player-selected/20" })));
103
122
  }
@@ -234,17 +234,17 @@ export function ReactFlowPOC({ dialogue }) {
234
234
  const onNodesChange = useCallback((changes) => {
235
235
  // Update dialogue tree positions
236
236
  // This would sync back to our DialogueTree structure
237
- console.log('Nodes changed:', changes);
237
+ // Handle node changes
238
238
  }, []);
239
239
  // Handle edge connections
240
240
  const onConnect = useCallback((connection) => {
241
241
  // Handle new edge connections
242
242
  // This would update our DialogueTree structure
243
- console.log('Connected:', connection);
243
+ // Handle connection
244
244
  }, []);
245
245
  // Handle edge changes (delete, etc.)
246
246
  const onEdgesChange = useCallback((changes) => {
247
- console.log('Edges changed:', changes);
247
+ // Handle edge changes
248
248
  }, []);
249
249
  return (React.createElement("div", { className: "w-full h-full" },
250
250
  React.createElement("div", { className: "p-8 text-gray-400" },
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Example Characters
3
+ *
4
+ * Sample character data for use in examples and demos
5
+ */
6
+ import type { Character } from '../types/characters';
7
+ export declare const exampleCharacters: Record<string, Character>;
8
+ /**
9
+ * Get all example characters
10
+ */
11
+ export declare function getExampleCharacters(): Record<string, Character>;
12
+ /**
13
+ * Get a character by ID
14
+ */
15
+ export declare function getExampleCharacter(id: string): Character | undefined;
16
+ /**
17
+ * List all character IDs
18
+ */
19
+ export declare function listExampleCharacterIds(): string[];
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Example Characters
3
+ *
4
+ * Sample character data for use in examples and demos
5
+ */
6
+ export const exampleCharacters = {
7
+ 'stranger': {
8
+ id: 'stranger',
9
+ name: 'Mysterious Stranger',
10
+ avatar: '👤',
11
+ description: 'A cloaked figure who appears at crossroads',
12
+ },
13
+ 'bartender': {
14
+ id: 'bartender',
15
+ name: 'Bartender',
16
+ avatar: '🍺',
17
+ description: 'The friendly tavern keeper',
18
+ },
19
+ 'merchant': {
20
+ id: 'merchant',
21
+ name: 'Merchant',
22
+ avatar: '💰',
23
+ description: 'A traveling trader',
24
+ },
25
+ 'guard': {
26
+ id: 'guard',
27
+ name: 'City Guard',
28
+ avatar: '🛡️',
29
+ description: 'A vigilant city guard',
30
+ },
31
+ 'wizard': {
32
+ id: 'wizard',
33
+ name: 'Wizard',
34
+ avatar: '🧙',
35
+ description: 'An ancient mage',
36
+ },
37
+ 'player': {
38
+ id: 'player',
39
+ name: 'Player',
40
+ avatar: '🎮',
41
+ description: 'The player character',
42
+ },
43
+ 'narrator': {
44
+ id: 'narrator',
45
+ name: 'Narrator',
46
+ avatar: '📖',
47
+ description: 'The story narrator',
48
+ },
49
+ };
50
+ /**
51
+ * Get all example characters
52
+ */
53
+ export function getExampleCharacters() {
54
+ return exampleCharacters;
55
+ }
56
+ /**
57
+ * Get a character by ID
58
+ */
59
+ export function getExampleCharacter(id) {
60
+ return exampleCharacters[id];
61
+ }
62
+ /**
63
+ * List all character IDs
64
+ */
65
+ export function listExampleCharacterIds() {
66
+ return Object.keys(exampleCharacters);
67
+ }
@@ -11,6 +11,7 @@
11
11
  * - index.ts: Public API for loading examples (this file)
12
12
  */
13
13
  export { examplesRegistry, exampleFlagSchemas, getExampleMetadata, listExampleIds, getExampleFlagSchema, listFlagSchemaIds, type ExampleMetadata } from './examples-registry';
14
+ export { exampleCharacters, getExampleCharacters, getExampleCharacter, listExampleCharacterIds, } from './example-characters';
14
15
  /**
15
16
  * Legacy exports for backward compatibility
16
17
  * These maintain the old API while we migrate examples to Yarn format
@@ -14,6 +14,8 @@
14
14
  export { examplesRegistry, exampleFlagSchemas, getExampleMetadata, listExampleIds, getExampleFlagSchema, listFlagSchemaIds } from './examples-registry';
15
15
  import { getExampleDialogue as getYarnExampleDialogue } from './yarn-examples';
16
16
  import { listExampleIds, listFlagSchemaIds, getExampleFlagSchema } from './examples-registry';
17
+ // Export character examples
18
+ export { exampleCharacters, getExampleCharacters, getExampleCharacter, listExampleCharacterIds, } from './example-characters';
17
19
  import { exampleDialogues as legacyExamples, demoFlagSchemas as legacySchemas } from './legacy-examples';
18
20
  // Export legacy examples - these work alongside the new Yarn examples
19
21
  export const exampleDialogues = legacyExamples;
@@ -5,14 +5,17 @@ export { ScenePlayer as DialogueSimulator } from './components/ScenePlayer';
5
5
  export { GuidePanel } from './components/GuidePanel';
6
6
  export { FlagSelector } from './components/FlagSelector';
7
7
  export { FlagManager } from './components/FlagManager';
8
+ export { CharacterSelector } from './components/CharacterSelector';
8
9
  export { ZoomControls } from './components/ZoomControls';
9
10
  export { ExampleLoader } from './components/ExampleLoader';
10
11
  import './styles/scrollbar.css';
11
12
  import './styles/theme.css';
12
13
  export { exampleDialogues, demoFlagSchemas, getExampleDialogue, getDemoFlagSchema, listExamples, listDemoFlagSchemas } from './examples';
14
+ export { exampleCharacters, getExampleCharacters, getExampleCharacter, listExampleCharacterIds } from './examples';
13
15
  export * from './types';
14
16
  export * from './types/flags';
15
17
  export * from './types/game-state';
18
+ export * from './types/characters';
16
19
  export * from './types/constants';
17
20
  export { flattenGameState, validateGameState, extractFlagsFromGameState, type FlattenConfig, type FlattenedState } from './utils/game-state-flattener';
18
21
  export { exportToYarn, importFromYarn } from './lib/yarn-converter';
package/dist/esm/index.js CHANGED
@@ -5,6 +5,7 @@ export { ScenePlayer as DialogueSimulator } from './components/ScenePlayer';
5
5
  export { GuidePanel } from './components/GuidePanel';
6
6
  export { FlagSelector } from './components/FlagSelector';
7
7
  export { FlagManager } from './components/FlagManager';
8
+ export { CharacterSelector } from './components/CharacterSelector';
8
9
  export { ZoomControls } from './components/ZoomControls';
9
10
  export { ExampleLoader } from './components/ExampleLoader';
10
11
  // Export styles
@@ -12,10 +13,12 @@ import './styles/scrollbar.css';
12
13
  import './styles/theme.css';
13
14
  // Export examples
14
15
  export { exampleDialogues, demoFlagSchemas, getExampleDialogue, getDemoFlagSchema, listExamples, listDemoFlagSchemas } from './examples';
16
+ export { exampleCharacters, getExampleCharacters, getExampleCharacter, listExampleCharacterIds } from './examples';
15
17
  // Export all types
16
18
  export * from './types';
17
19
  export * from './types/flags';
18
20
  export * from './types/game-state';
21
+ export * from './types/characters';
19
22
  export * from './types/constants';
20
23
  // Export game state utilities
21
24
  export { flattenGameState, validateGameState, extractFlagsFromGameState } from './utils/game-state-flattener';
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Character Types
3
+ *
4
+ * Defines character data structure for dialogue nodes
5
+ */
6
+ export interface Character {
7
+ id: string;
8
+ name: string;
9
+ avatar?: string;
10
+ description?: string;
11
+ [key: string]: any;
12
+ }
13
+ export interface CharactersState {
14
+ [characterId: string]: Character;
15
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Character Types
3
+ *
4
+ * Defines character data structure for dialogue nodes
5
+ */
6
+ export {};
@@ -19,12 +19,14 @@ export interface FlagState {
19
19
  * Legacy alias for backward compatibility
20
20
  */
21
21
  export type GameFlagState = FlagState;
22
+ import type { Character } from './characters';
22
23
  /**
23
24
  * Base game state structure that users can extend
24
25
  * Must have a 'flags' property, but can have any other structure
25
26
  */
26
27
  export interface BaseGameState {
27
28
  flags?: FlagState;
29
+ characters?: Record<string, Character>;
28
30
  }
29
31
  /**
30
32
  * Convenience type for extending game state
@@ -20,12 +20,14 @@ export interface ConditionalBlock {
20
20
  condition?: Condition[];
21
21
  content: string;
22
22
  speaker?: string;
23
+ characterId?: string;
23
24
  nextNodeId?: string;
24
25
  }
25
26
  export interface DialogueNode {
26
27
  id: string;
27
28
  type: NodeType;
28
29
  speaker?: string;
30
+ characterId?: string;
29
31
  content: string;
30
32
  choices?: Choice[];
31
33
  nextNodeId?: string;
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Example Characters
3
+ *
4
+ * Sample character data for use in examples and demos
5
+ */
6
+ import type { Character } from '../types/characters';
7
+ export declare const exampleCharacters: Record<string, Character>;
8
+ /**
9
+ * Get all example characters
10
+ */
11
+ export declare function getExampleCharacters(): Record<string, Character>;
12
+ /**
13
+ * Get a character by ID
14
+ */
15
+ export declare function getExampleCharacter(id: string): Character | undefined;
16
+ /**
17
+ * List all character IDs
18
+ */
19
+ export declare function listExampleCharacterIds(): string[];