# Popup System Implementation Plan > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. **Goal:** Build `com.jovian.popup-system`, a low-allocation popup/tooltip package with category-based isolation, fluent content builder, grow-only element cache, screen edge clamping, and extensible animations. **Architecture:** Per-game-state `IPopupSystem` instances with registered categories. Each category lazily creates one `PopupReference` MonoBehaviour that reuses cached content elements. `PopupTrigger` MonoBehaviour handles hover detection. `PopupContentBuilder` struct operates directly on cached elements. Float-based timers in `Tick()` — no coroutines. **Tech Stack:** Unity 6 / C# 9, TextMeshPro, Unity Input System, Newtonsoft.Json --- ### Task 1: Package scaffold **Files:** - Create: `Packages/com.jovian.popup-system/package.json` - Create: `Packages/com.jovian.popup-system/Runtime/Jovian.PopupSystem.asmdef` - Create: `Packages/com.jovian.popup-system/Editor/Jovian.PopupSystem.Editor.asmdef` - Create: `Packages/com.jovian.popup-system/Samples~/README.md` **Step 1: Create package.json** ```json { "name": "com.jovian.popup-system", "version": "0.1.0", "displayName": "Jovian Popup System", "description": "A lightweight, low-allocation popup and tooltip system with category-based isolation, fluent content builder, and extensible animations.", "unity": "2022.3", "dependencies": { "com.unity.textmeshpro": "3.0.6", "com.unity.inputsystem": "1.18.0", "com.unity.nuget.newtonsoft-json": "3.2.1" }, "keywords": [ "popup", "tooltip", "ui" ], "author": { "name": "Jovian" } } ``` **Step 2: Create Runtime asmdef** ```json { "name": "Jovian.PopupSystem", "rootNamespace": "Jovian.PopupSystem", "references": [ "Unity.TextMeshPro", "Unity.InputSystem" ], "includePlatforms": [], "excludePlatforms": [], "allowUnsafeCode": false, "overrideReferences": true, "precompiledReferences": [ "Newtonsoft.Json.dll" ], "autoReferenced": true, "defineConstraints": [], "versionDefines": [], "noEngineReferences": false } ``` **Step 3: Create Editor asmdef** ```json { "name": "Jovian.PopupSystem.Editor", "rootNamespace": "Jovian.PopupSystem.Editor", "references": [ "Jovian.PopupSystem" ], "includePlatforms": ["Editor"], "excludePlatforms": [], "allowUnsafeCode": false, "overrideReferences": false, "precompiledReferences": [], "autoReferenced": true, "defineConstraints": [], "versionDefines": [], "noEngineReferences": false } ``` **Step 4: Create Samples~/README.md** ```markdown # Samples This folder is reserved for sample scenes and scripts demonstrating the Popup System. ``` **Step 5: Commit** ```bash git add Packages/com.jovian.popup-system/ git commit -m "feat: scaffold com.jovian.popup-system package" ``` --- ### Task 2: PopupCategory + JSON converter + enums **Files:** - Create: `Packages/com.jovian.popup-system/Runtime/PopupCategory.cs` - Create: `Packages/com.jovian.popup-system/Runtime/PopupCategoryJsonConverter.cs` - Create: `Packages/com.jovian.popup-system/Runtime/PopupEnums.cs` **Step 1: Implement PopupCategory** (same pattern as LogChannel) ```csharp using System; using UnityEngine; namespace Jovian.PopupSystem { [Serializable] public readonly struct PopupCategory : IEquatable { [SerializeField] readonly string id; public string Id => id; public PopupCategory(string id) { this.id = id; } public static readonly PopupCategory Character = new("Character"); public static readonly PopupCategory Item = new("Item"); public static readonly PopupCategory Skill = new("Skill"); public static readonly PopupCategory General = new("General"); public bool Equals(PopupCategory other) { return string.Equals(id, other.id, StringComparison.Ordinal); } public override bool Equals(object obj) { return obj is PopupCategory other && Equals(other); } public override int GetHashCode() { return id != null ? id.GetHashCode() : 0; } public override string ToString() { return id ?? string.Empty; } public static bool operator ==(PopupCategory left, PopupCategory right) { return left.Equals(right); } public static bool operator !=(PopupCategory left, PopupCategory right) { return !left.Equals(right); } } } ``` **Step 2: Implement JSON converter** ```csharp using System; using Newtonsoft.Json; namespace Jovian.PopupSystem { public sealed class PopupCategoryJsonConverter : JsonConverter { public override void WriteJson(JsonWriter writer, PopupCategory value, JsonSerializer serializer) { writer.WriteValue(value.Id); } public override PopupCategory ReadJson(JsonReader reader, Type objectType, PopupCategory existingValue, bool hasExistingValue, JsonSerializer serializer) { var id = reader.Value as string; return new PopupCategory(id); } } } ``` **Step 3: Implement enums** ```csharp namespace Jovian.PopupSystem { public enum AnchorSide { Below, Above, Left, Right } public enum PopupPositionMode { AnchorToElement, FollowMouse } } ``` **Step 4: Commit** ```bash git add Packages/com.jovian.popup-system/Runtime/ git commit -m "feat: add PopupCategory, enums, and JSON converter" ``` --- ### Task 3: PopupSettings ScriptableObject **Files:** - Create: `Packages/com.jovian.popup-system/Runtime/PopupSettings.cs` **Step 1: Implement settings** ```csharp using System; using System.Collections.Generic; using UnityEngine; namespace Jovian.PopupSystem { [CreateAssetMenu(fileName = "PopupSettings", menuName = "Jovian/Popup System/Popup Settings")] public class PopupSettings : ScriptableObject { [Header("General")] public float popupDelay = 0.4f; public float fadeDuration = 0.2f; public AnchorSide defaultAnchorSide = AnchorSide.Below; public float screenEdgePadding = 10f; public float maxPopupWidth = 400f; public int sortingOrder = 100; [Header("Follow Mouse")] public Vector2 followMouseOffset = new(15f, -15f); [Header("Input")] public float touchHoldDuration = 0.6f; public bool gamepadFocusTrigger = true; [Header("Priority")] public List categoryPriorities = new(); [Header("Per-Category Overrides")] public List categoryDelayOverrides = new(); public int GetPriority(PopupCategory category) { foreach(var cp in categoryPriorities) { if(cp.category == category) { return cp.priority; } } return 0; } public float GetDelay(PopupCategory category) { foreach(var cd in categoryDelayOverrides) { if(cd.category == category) { return cd.delay; } } return popupDelay; } } [Serializable] public sealed class CategoryPriority { public PopupCategory category; public int priority; } [Serializable] public sealed class CategoryDelay { public PopupCategory category; public float delay; } } ``` **Step 2: Commit** ```bash git add Packages/com.jovian.popup-system/Runtime/PopupSettings.cs git commit -m "feat: add PopupSettings ScriptableObject with full configuration" ``` --- ### Task 4: IPopupAnimator + FadePopupAnimator **Files:** - Create: `Packages/com.jovian.popup-system/Runtime/IPopupAnimator.cs` - Create: `Packages/com.jovian.popup-system/Runtime/FadePopupAnimator.cs` **Step 1: Implement interface** ```csharp using System; using UnityEngine; namespace Jovian.PopupSystem { public interface IPopupAnimator { void Show(CanvasGroup canvasGroup, float duration, Action onComplete); void Hide(CanvasGroup canvasGroup, float duration, Action onComplete); void Tick(float deltaTime); bool IsAnimating { get; } } } ``` **Step 2: Implement FadePopupAnimator** ```csharp using System; using UnityEngine; namespace Jovian.PopupSystem { public sealed class FadePopupAnimator : IPopupAnimator { private CanvasGroup target; private float duration; private float elapsed; private float startAlpha; private float endAlpha; private Action onComplete; public bool IsAnimating => target != null; public void Show(CanvasGroup canvasGroup, float duration, Action onComplete) { target = canvasGroup; this.duration = Mathf.Max(duration, 0.001f); elapsed = 0f; startAlpha = 0f; endAlpha = 1f; this.onComplete = onComplete; canvasGroup.alpha = 0f; } public void Hide(CanvasGroup canvasGroup, float duration, Action onComplete) { target = canvasGroup; this.duration = Mathf.Max(duration, 0.001f); elapsed = 0f; startAlpha = canvasGroup.alpha; endAlpha = 0f; this.onComplete = onComplete; } public void Tick(float deltaTime) { if(target == null) { return; } elapsed += deltaTime; var t = Mathf.Clamp01(elapsed / duration); target.alpha = Mathf.Lerp(startAlpha, endAlpha, t); if(t >= 1f) { var callback = onComplete; target = null; onComplete = null; callback?.Invoke(); } } } } ``` **Step 3: Commit** ```bash git add Packages/com.jovian.popup-system/Runtime/IPopupAnimator.cs Packages/com.jovian.popup-system/Runtime/FadePopupAnimator.cs git commit -m "feat: add IPopupAnimator interface and FadePopupAnimator" ``` --- ### Task 5: PopupReference MonoBehaviour with element cache **Files:** - Create: `Packages/com.jovian.popup-system/Runtime/UI/PopupReference.cs` **Step 1: Implement PopupReference with grow-only element cache, positioning, and screen clamping** ```csharp using System.Collections.Generic; using TMPro; using UnityEngine; using UnityEngine.InputSystem; using UnityEngine.UI; namespace Jovian.PopupSystem.UI { public class PopupReference : MonoBehaviour { [SerializeField] RectTransform content; [SerializeField] CanvasGroup canvasGroup; [SerializeField] RectTransform background; [Header("Element Prefabs")] [SerializeField] TMP_Text headerPrefab; [SerializeField] TMP_Text textPrefab; [SerializeField] RectTransform statPrefab; // contains label + value TMP_Text [SerializeField] Image imagePrefab; [SerializeField] Image separatorPrefab; // Element caches (grow-only) readonly List headerCache = new(); readonly List textCache = new(); readonly List statCache = new(); readonly List imageCache = new(); readonly List separatorCache = new(); int headerIndex; int textIndex; int statIndex; int imageIndex; int separatorIndex; // State PopupPositionMode positionMode; RectTransform anchorTarget; AnchorSide anchorSide; Vector2 followOffset; float screenEdgePadding; bool isVisible; public CanvasGroup CanvasGroup => canvasGroup; public RectTransform Content => content; public bool IsVisible => isVisible; public void SetVisible(bool visible) { isVisible = visible; gameObject.SetActive(visible); if(!visible) { canvasGroup.alpha = 0f; } } public void ClearContent() { for(int i = 0; i < headerIndex; i++) { headerCache[i].gameObject.SetActive(false); } for(int i = 0; i < textIndex; i++) { textCache[i].gameObject.SetActive(false); } for(int i = 0; i < statIndex; i++) { statCache[i].gameObject.SetActive(false); } for(int i = 0; i < imageIndex; i++) { imageCache[i].gameObject.SetActive(false); } for(int i = 0; i < separatorIndex; i++) { separatorCache[i].gameObject.SetActive(false); } headerIndex = 0; textIndex = 0; statIndex = 0; imageIndex = 0; separatorIndex = 0; } // --- Element access (grow-only) --- public TMP_Text GetHeader() { return GetOrCreate(headerCache, headerPrefab, ref headerIndex); } public TMP_Text GetText() { return GetOrCreate(textCache, textPrefab, ref textIndex); } public (TMP_Text label, TMP_Text value) GetStat() { var rt = GetOrCreate(statCache, statPrefab, ref statIndex); var children = rt.GetComponentsInChildren(true); return (children[0], children[1]); } public Image GetImage() { return GetOrCreate(imageCache, imagePrefab, ref imageIndex); } public Image GetSeparator() { return GetOrCreate(separatorCache, separatorPrefab, ref separatorIndex); } private T GetOrCreate(List cache, T prefab, ref int index) where T : Component { if(index < cache.Count) { var existing = cache[index]; existing.gameObject.SetActive(true); existing.transform.SetAsLastSibling(); index++; return existing; } var created = Instantiate(prefab, content); created.gameObject.SetActive(true); created.transform.SetAsLastSibling(); cache.Add(created); index++; return created; } // --- Positioning --- public void SetAnchorMode(RectTransform target, AnchorSide side, float edgePadding) { positionMode = PopupPositionMode.AnchorToElement; anchorTarget = target; anchorSide = side; screenEdgePadding = edgePadding; } public void SetFollowMouseMode(Vector2 offset, float edgePadding) { positionMode = PopupPositionMode.FollowMouse; followOffset = offset; screenEdgePadding = edgePadding; } public void UpdatePosition() { if(positionMode == PopupPositionMode.FollowMouse) { PositionAtScreenPoint(Mouse.current.position.ReadValue() + followOffset); } else if(anchorTarget != null) { PositionAnchoredTo(anchorTarget, anchorSide); } } private void PositionAnchoredTo(RectTransform target, AnchorSide side) { var targetRect = GetScreenRect(target); var popupRect = GetScreenRect((RectTransform)transform); var popupSize = popupRect.size; var pos = side switch { AnchorSide.Below => new Vector2(targetRect.center.x - popupSize.x * 0.5f, targetRect.yMin - popupSize.y), AnchorSide.Above => new Vector2(targetRect.center.x - popupSize.x * 0.5f, targetRect.yMax), AnchorSide.Left => new Vector2(targetRect.xMin - popupSize.x, targetRect.center.y - popupSize.y * 0.5f), AnchorSide.Right => new Vector2(targetRect.xMax, targetRect.center.y - popupSize.y * 0.5f), _ => targetRect.center }; PositionAtScreenPoint(pos); } private void PositionAtScreenPoint(Vector2 screenPos) { var rt = (RectTransform)transform; LayoutRebuilder.ForceRebuildLayoutImmediate(content); var popupSize = rt.rect.size; // Clamp to screen screenPos.x = Mathf.Clamp(screenPos.x, screenEdgePadding, Screen.width - popupSize.x - screenEdgePadding); screenPos.y = Mathf.Clamp(screenPos.y, screenEdgePadding, Screen.height - popupSize.y - screenEdgePadding); rt.position = screenPos; } private static Rect GetScreenRect(RectTransform rt) { var corners = new Vector3[4]; rt.GetWorldCorners(corners); var min = new Vector2(corners[0].x, corners[0].y); var max = new Vector2(corners[2].x, corners[2].y); return new Rect(min, max - min); } } } ``` **Step 2: Commit** ```bash git add Packages/com.jovian.popup-system/Runtime/UI/PopupReference.cs git commit -m "feat: add PopupReference with grow-only element cache and positioning" ``` --- ### Task 6: PopupContentBuilder struct **Files:** - Create: `Packages/com.jovian.popup-system/Runtime/PopupContentBuilder.cs` **Step 1: Implement fluent builder** ```csharp using Jovian.PopupSystem.UI; using UnityEngine; namespace Jovian.PopupSystem { public struct PopupContentBuilder { readonly PopupReference view; public PopupContentBuilder(PopupReference view) { this.view = view; } public PopupContentBuilder AddHeader(string text) { var header = view.GetHeader(); header.text = text; return this; } public PopupContentBuilder AddText(string text) { var label = view.GetText(); label.text = text; return this; } public PopupContentBuilder AddText(string text, string hexColor) { var label = view.GetText(); var prefix = text.Length > 0 && hexColor[0] == '#' ? "" : "#"; label.text = $"{text}"; return this; } public PopupContentBuilder AddStat(string label, int value) { var (labelText, valueText) = view.GetStat(); labelText.text = label; valueText.text = value.ToString(); return this; } public PopupContentBuilder AddStat(string label, string value) { var (labelText, valueText) = view.GetStat(); labelText.text = label; valueText.text = value; return this; } public PopupContentBuilder AddImage(Sprite sprite, float height = 64f) { var image = view.GetImage(); image.sprite = sprite; var rt = (RectTransform)image.transform; rt.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, height); return this; } public PopupContentBuilder AddSeparator() { view.GetSeparator(); return this; } } } ``` **Step 2: Commit** ```bash git add Packages/com.jovian.popup-system/Runtime/PopupContentBuilder.cs git commit -m "feat: add PopupContentBuilder fluent API" ``` --- ### Task 7: IPopupSystem + PopupSystem implementation **Files:** - Create: `Packages/com.jovian.popup-system/Runtime/IPopupSystem.cs` - Create: `Packages/com.jovian.popup-system/Runtime/PopupSystem.cs` **Step 1: Implement interface** ```csharp using System; using UnityEngine; namespace Jovian.PopupSystem { public interface IPopupSystem { void RegisterCategory(PopupCategory category, int priority = 0); void Show(PopupCategory category, Action buildContent, RectTransform anchor = null, AnchorSide? anchorSide = null); void ShowAtPosition(PopupCategory category, Action buildContent, Vector2 screenPosition); void Hide(PopupCategory category); void HideAll(); void Tick(float deltaTime); void Dispose(); } } ``` **Step 2: Implement PopupSystem** ```csharp using System; using System.Collections.Generic; using Jovian.PopupSystem.UI; using UnityEngine; using Object = UnityEngine.Object; namespace Jovian.PopupSystem { public sealed class PopupSystem : IPopupSystem { readonly PopupSettings settings; readonly PopupReference viewPrefab; readonly IPopupAnimator animator; readonly Dictionary categories = new(); public PopupSystem(PopupSettings settings, PopupReference viewPrefab, IPopupAnimator animator = null) { this.settings = settings; this.viewPrefab = viewPrefab; this.animator = animator ?? new FadePopupAnimator(); } public void RegisterCategory(PopupCategory category, int priority = 0) { if(categories.ContainsKey(category)) { return; } var effectivePriority = settings.GetPriority(category); if(effectivePriority == 0) { effectivePriority = priority; } categories[category] = new ViewState { priority = effectivePriority, delay = settings.GetDelay(category) }; } public void Show(PopupCategory category, Action buildContent, RectTransform anchor = null, AnchorSide? anchorSide = null) { if(!categories.TryGetValue(category, out var state)) { return; } // Dismiss lower-priority visible popups DismissLowerPriority(state.priority); state.pendingBuild = buildContent; state.pendingAnchor = anchor; state.pendingAnchorSide = anchorSide ?? settings.defaultAnchorSide; state.pendingScreenPos = null; state.delayTimer = state.delay; state.isPending = true; } public void ShowAtPosition(PopupCategory category, Action buildContent, Vector2 screenPosition) { if(!categories.TryGetValue(category, out var state)) { return; } DismissLowerPriority(state.priority); state.pendingBuild = buildContent; state.pendingAnchor = null; state.pendingScreenPos = screenPosition; state.delayTimer = state.delay; state.isPending = true; } public void Hide(PopupCategory category) { if(!categories.TryGetValue(category, out var state)) { return; } state.isPending = false; if(state.view != null && state.view.IsVisible) { animator.Hide(state.view.CanvasGroup, settings.fadeDuration, () => { state.view.SetVisible(false); }); } } public void HideAll() { foreach(var kvp in categories) { Hide(kvp.Key); } } public void Tick(float deltaTime) { animator.Tick(deltaTime); foreach(var kvp in categories) { var state = kvp.Value; if(!state.isPending) { continue; } state.delayTimer -= deltaTime; if(state.delayTimer > 0f) { continue; } state.isPending = false; ShowImmediate(state); } // Update follow-mouse positions foreach(var kvp in categories) { var state = kvp.Value; if(state.view != null && state.view.IsVisible && state.isFollowMouse) { state.view.UpdatePosition(); } } } public void Dispose() { foreach(var kvp in categories) { if(kvp.Value.view != null) { Object.Destroy(kvp.Value.view.gameObject); } } categories.Clear(); } private void ShowImmediate(ViewState state) { EnsureView(state); state.view.ClearContent(); var builder = new PopupContentBuilder(state.view); state.pendingBuild?.Invoke(builder); if(state.pendingScreenPos.HasValue) { state.view.SetFollowMouseMode(settings.followMouseOffset, settings.screenEdgePadding); state.isFollowMouse = false; // positioned at fixed screen point } else if(state.pendingAnchor != null) { state.view.SetAnchorMode(state.pendingAnchor, state.pendingAnchorSide, settings.screenEdgePadding); state.isFollowMouse = false; } else { state.view.SetFollowMouseMode(settings.followMouseOffset, settings.screenEdgePadding); state.isFollowMouse = true; } state.view.SetVisible(true); state.view.UpdatePosition(); animator.Show(state.view.CanvasGroup, settings.fadeDuration, null); } private void EnsureView(ViewState state) { if(state.view != null) { return; } state.view = Object.Instantiate(viewPrefab); state.view.SetVisible(false); var canvas = state.view.GetComponent(); if(canvas != null) { canvas.sortingOrder = settings.sortingOrder; } } private void DismissLowerPriority(int showingPriority) { foreach(var kvp in categories) { var state = kvp.Value; if(state.priority < showingPriority && state.view != null && state.view.IsVisible) { state.isPending = false; animator.Hide(state.view.CanvasGroup, settings.fadeDuration, () => { state.view.SetVisible(false); }); } } } private sealed class ViewState { public PopupReference view; public int priority; public float delay; public float delayTimer; public bool isPending; public bool isFollowMouse; public Action pendingBuild; public RectTransform pendingAnchor; public AnchorSide pendingAnchorSide; public Vector2? pendingScreenPos; } } } ``` **Step 3: Commit** ```bash git add Packages/com.jovian.popup-system/Runtime/IPopupSystem.cs Packages/com.jovian.popup-system/Runtime/PopupSystem.cs git commit -m "feat: add IPopupSystem and PopupSystem with priority and delay" ``` --- ### Task 8: PopupTrigger MonoBehaviour **Files:** - Create: `Packages/com.jovian.popup-system/Runtime/UI/PopupTrigger.cs` **Step 1: Implement trigger** ```csharp using System; using UnityEngine; using UnityEngine.EventSystems; namespace Jovian.PopupSystem.UI { public class PopupTrigger : MonoBehaviour, IPointerEnterHandler, IPointerExitHandler { [SerializeField] PopupCategory category; [SerializeField] AnchorSide anchorSide = AnchorSide.Below; [SerializeField] PopupPositionMode positionMode = PopupPositionMode.AnchorToElement; IPopupSystem popupSystem; Action contentCallback; bool initialized; public PopupCategory Category => category; public void Initialize(IPopupSystem popupSystem, Action contentCallback) { this.popupSystem = popupSystem; this.contentCallback = contentCallback; initialized = true; } public void Initialize(IPopupSystem popupSystem, PopupCategory category, Action contentCallback) { this.popupSystem = popupSystem; this.category = category; this.contentCallback = contentCallback; initialized = true; } public void UpdateContent(Action contentCallback) { this.contentCallback = contentCallback; } public void OnPointerEnter(PointerEventData eventData) { if(!initialized || popupSystem == null || contentCallback == null) { return; } if(positionMode == PopupPositionMode.AnchorToElement) { popupSystem.Show(category, contentCallback, (RectTransform)transform, anchorSide); } else { popupSystem.Show(category, contentCallback); } } public void OnPointerExit(PointerEventData eventData) { if(!initialized || popupSystem == null) { return; } popupSystem.Hide(category); } } } ``` **Step 2: Commit** ```bash git add Packages/com.jovian.popup-system/Runtime/UI/PopupTrigger.cs git commit -m "feat: add PopupTrigger MonoBehaviour for hover detection" ``` --- ### Task 9: README.md **Files:** - Create: `Packages/com.jovian.popup-system/README.md` **Step 1: Write README** covering: quick start, PopupCategory, PopupSettings, content builder API, PopupTrigger setup, code-only triggers, prefab setup, IPopupAnimator extensibility. **Step 2: Commit** ```bash git add Packages/com.jovian.popup-system/README.md git commit -m "feat: add README for popup system package" ``` --- ### Task 10: Editor — PopupSettingsProvider **Files:** - Create: `Packages/com.jovian.popup-system/Editor/PopupSettingsProvider.cs` **Step 1: Implement settings provider** for Project Settings > Jovian > Popup System (same pattern as SaveSystemSettingsProvider). **Step 2: Commit** ```bash git add Packages/com.jovian.popup-system/Editor/PopupSettingsProvider.cs git commit -m "feat: add PopupSettingsProvider for Project Settings" ``` --- ## File Summary | File | Type | Purpose | |------|------|---------| | `Packages/com.jovian.popup-system/package.json` | Create | Package manifest | | `Packages/com.jovian.popup-system/Runtime/Jovian.PopupSystem.asmdef` | Create | Runtime assembly | | `Packages/com.jovian.popup-system/Editor/Jovian.PopupSystem.Editor.asmdef` | Create | Editor assembly | | `Packages/com.jovian.popup-system/Samples~/README.md` | Create | Samples placeholder | | `Packages/com.jovian.popup-system/Runtime/PopupCategory.cs` | Create | Category readonly struct | | `Packages/com.jovian.popup-system/Runtime/PopupCategoryJsonConverter.cs` | Create | Newtonsoft converter | | `Packages/com.jovian.popup-system/Runtime/PopupEnums.cs` | Create | AnchorSide, PopupPositionMode | | `Packages/com.jovian.popup-system/Runtime/PopupSettings.cs` | Create | ScriptableObject config | | `Packages/com.jovian.popup-system/Runtime/IPopupAnimator.cs` | Create | Animation interface | | `Packages/com.jovian.popup-system/Runtime/FadePopupAnimator.cs` | Create | Default fade animation | | `Packages/com.jovian.popup-system/Runtime/PopupContentBuilder.cs` | Create | Fluent builder struct | | `Packages/com.jovian.popup-system/Runtime/IPopupSystem.cs` | Create | System interface | | `Packages/com.jovian.popup-system/Runtime/PopupSystem.cs` | Create | System implementation | | `Packages/com.jovian.popup-system/Runtime/UI/PopupReference.cs` | Create | MonoBehaviour with element cache | | `Packages/com.jovian.popup-system/Runtime/UI/PopupTrigger.cs` | Create | Hover trigger MonoBehaviour | | `Packages/com.jovian.popup-system/README.md` | Create | Package documentation | | `Packages/com.jovian.popup-system/Editor/PopupSettingsProvider.cs` | Create | Project Settings UI |