{"id":"biotoxin495/popupkit","name":"popupkit","scope":"biotoxin495","platform":"roblox","description":"A small, dependency-free Roblox module for contextual UI popups - hover cards, inventory details, pinned item panels, cursor-following tooltips, anchored panels, and directly displayed popups - without depending on a framework or service container.","version":"1.0.4","latest":"1.0.4","versions":["1.0.4"],"license":"MIT","licenseRating":"safe","licenseCaveats":[],"licenseVerified":true,"dependencies":{},"integrity":"00dac4621340fb418def5c8e9edb1ad46917b932d5d0f96c2f5552dd4e88e425","likes":0,"downloads":0,"install":"forest install biotoxin495/popupkit","url":"https://forest.dev/p/roblox/biotoxin495/popupkit","files":"https://api.forest.dev/ai/package/roblox/biotoxin495/popupkit/files","readme":"# PopupKit — A standalone contextual popup manager\n\n**PopupKit** is a small, dependency-free Roblox module for contextual UI popups — hover cards, inventory details, pinned item panels, cursor-following tooltips, anchored panels, and directly displayed popups — without depending on a framework or service container.\n\nPopups are registered as named definitions (template or fully custom) and shown either directly or through hover/click triggers. The manager handles placement, boundary clamping and flipping, show/hide delays, pinning, and outside-click dismissal, and returns disposable handles for controlling individual popups and registrations.\n\n## Quick example\n\n```lua\nlocal ReplicatedStorage = game:GetService(\"ReplicatedStorage\")\n\nlocal PopupKit = require(ReplicatedStorage.PopupKit)\n\nlocal popups = PopupKit.new({\n\tParent = screenGui,\n\tDefinitions = {\n\t\tItemDetails = {\n\t\t\tTemplate = popupTemplates.ItemDetails,\n\t\t\tPopulate = function(popup, data, context)\n\t\t\t\tpopup.Title.Text = data.Name\n\t\t\t\tpopup.Description.Text = data.Description\n\t\t\tend,\n\t\t},\n\t},\n})\n\nlocal registration = popups:RegisterTrigger(itemButton, \"ItemDetails\", function()\n\treturn currentItemData\nend)\n```\n\n`Destroy` closes the active popup and tears down every registration and signal owned by the manager.\n\n## 🚀 Features\n\n* Fully standalone and dependency-free\n* One active popup per `PopupKit` instance\n* Independent manager instances\n* Template cloning or fully custom `Create` factories\n* Static data or dynamic resolver callbacks\n* Cursor, top, bottom, left, right, and custom placement\n* Start, center, and end alignment\n* Boundary clamping and automatic opposite-side flipping\n* Configurable show and hide delays\n* Interactive hover retention\n* Optional click/touch pinning\n* Outside-click dismissal\n* Active popup update and refresh handles\n* Disposable trigger registrations\n* Population and creation cleanup callbacks\n* Definition lifecycle hooks (`OnOpen`, `OnClose`, `OnUpdate`, `OnPinChanged`)\n* Opened, closed, updated, and pin-state signals\n* Global enable, disable, hide, and destruction methods\n* Strict-ish Luau types (`--!nonstrict`, fully annotated exports)\n\n## 📖 Basic usage\n\nCopy `PopupKit` into your project and require it from a client script. PopupKit creates and animates Roblox UI, so it should only run on the client.\n\n### Creating a manager\n\n```lua\nlocal popups = PopupKit.new({\n\tParent = screenGui,\n\tDefinitions = { --[[ ... ]] },\n})\n```\n\n`Parent` may be:\n\n* A `GuiObject`, which PopupKit uses directly as its popup layer\n* A `LayerCollector`, such as a `ScreenGui`, under which PopupKit creates a transparent layer\n* A `BasePlayerGui`, under which PopupKit creates its own `ScreenGui`\n* `nil`, in which case PopupKit creates its own `ScreenGui` under the local player's `PlayerGui`\n\n### Registering a trigger\n\n```lua\nlocal registration = popups:RegisterTrigger(\n\titemButton,\n\t\"ItemDetails\",\n\tfunction()\n\t\treturn currentItemData\n\tend,\n\t{\n\t\tPlacement = \"Right\",\n\t\tAlignment = \"Center\",\n\t\tOffset = Vector2.new(12, 0),\n\t\tFlipWhenClipped = true,\n\t\tShowDelay = 0.08,\n\t\tHideDelay = 0.1,\n\t\tKeepOpenWhilePopupHovered = true,\n\t\tPinOnClick = true,\n\t}\n)\n```\n\nThe third argument may be static data or a function. Resolver functions receive the trigger and registration as optional arguments.\n\n### Popup definitions\n\nA definition must provide exactly one of `Template` or `Create`.\n\n```lua\n-- Template definition\nItemDetails = {\n\tTemplate = itemDetailsTemplate,\n\n\tPopulate = function(popup, data, context)\n\t\tpopup.Title.Text = data.Name\n\tend,\n}\n\n-- Custom creation\nNotice = {\n\tCreate = function(data, creationContext)\n\t\tlocal label = Instance.new(\"TextLabel\")\n\t\tlabel.AutomaticSize = Enum.AutomaticSize.XY\n\t\tlabel.Text = data.Text\n\n\t\treturn label, function()\n\t\t\t-- Optional creation cleanup, called only when the popup closes.\n\t\tend\n\tend,\n\n\tUpdate = function(popup, data, context)\n\t\tpopup.Text = data.Text\n\tend,\n}\n```\n\n`Populate` runs when a popup is first shown. When its handle is updated, PopupKit uses `Update` when present; otherwise, it runs `Populate` again.\n\nBoth `Populate` and `Update` may return a cleanup function. The previous population cleanup runs before the next update and when the popup closes.\n\n### Definition lifecycle hooks\n\n```lua\n{\n\tOnOpen = function(context) end,\n\tOnClose = function(context, reason) end,\n\tOnUpdate = function(context, oldData, newData) end,\n\tOnPinChanged = function(context, isPinned) end,\n}\n```\n\nHook errors are reported through PopupKit's error handler without invalidating an otherwise usable popup. Errors in `Create`, `Populate`, or `Update` prevent or close the affected popup.\n\n### Trigger-relative placement\n\nFor `Top`, `Bottom`, `Left`, and `Right`, PopupKit uses the trigger as the anchor unless another `Anchor` is supplied.\n\nWhen `FlipWhenClipped` is enabled, PopupKit compares the preferred side with its opposite side and uses whichever produces less boundary overflow. The final position is still clamped inside the configured boundary.\n\n### Interactive hover behavior\n\nWith `KeepOpenWhilePopupHovered = true`, leaving the trigger does not close the popup while the pointer is over the popup itself. A nonzero `HideDelay` gives the pointer time to cross the gap between the trigger and popup.\n\nPinned popups ignore hover-based hide requests until unpinned, replaced with permission, dismissed by an outside click, or explicitly closed.\n\n### Direct popups\n\n```lua\nlocal handle = popups:Show(\"Notice\", {\n\tText = \"Saved!\",\n}, {\n\tPlacement = \"Cursor\",\n})\n```\n\nFor a directly displayed trigger-relative popup, provide `Anchor`:\n\n```lua\npopups:Show(\"Notice\", data, {\n\tAnchor = saveButton,\n\tPlacement = \"Top\",\n})\n```\n\n### Migrating existing populators\n\nGame-specific modules and services should remain outside PopupKit and be captured by your definition factory:\n\n```lua\nreturn function(dependencies)\n\treturn {\n\t\tCurrencyPopup = {\n\t\t\tTemplate = dependencies.Templates.CurrencyPopup,\n\t\t\tPopulate = function(popup, data)\n\t\t\t\tpopup.Amount.Text = dependencies.FormatNumber(data.Amount)\n\t\t\tend,\n\t\t},\n\t}\nend\n```\n\nPopupKit itself remains framework-independent; consumer definitions may use whatever game-specific dependencies they require.\n\n## ⚙️ API\n\n### Manager\n\n#### `PopupKit.new(config?)`\n\nCreates a new PopupKit manager. Each manager owns its own definitions, registrations, and active popup.\n\n```lua\nlocal popups = PopupKit.new()\n```\n\n#### `popups:RegisterDefinition(name, definition)` / `popups:RegisterDefinitions(definitions)`\n\nRegisters one or several popup definitions. Re-registering a name that has an open popup closes it with reason `\"DefinitionRemoved\"`.\n\n#### `popups:UnregisterDefinition(name)`\n\nRemoves a definition, closes its active popup (if any), and destroys every registration using it.\n\n#### `popups:GetDefinition(name)`\n\nReturns the stored definition, or `nil`.\n\n#### `popups:RegisterTrigger(trigger, definitionName, dataSource, options?)`\n\nRegisters a `GuiObject` trigger that shows/hides `definitionName`'s popup on hover, returning a [registration handle](#registration).\n\n#### `popups:Show(definitionName, data, options?)`\n\nShows a popup directly, bypassing triggers, and returns a [popup handle](#popup-handle).\n\n#### `popups:Hide(reason?)`\n\nCloses whichever popup is currently active, regardless of what opened it.\n\n#### `popups:GetActivePopup()`\n\nReturns the current popup handle, or `nil` if nothing is open.\n\n#### `popups:Enable()` / `popups:Disable()` / `popups:SetEnabled(enabled)` / `popups:IsEnabled()`\n\nGlobally enables or disables the manager. Disabling closes the active popup with reason `\"Disabled\"` and prevents new popups from opening until re-enabled.\n\n#### `popups:Destroy()`\n\nCloses the active popup, destroys every registration and signal, cancels delayed actions, and removes any GUI layer that PopupKit created itself. Call this when the owning UI/controller is permanently torn down.\n\n### Popup handle\n\n`PopupKit:Show` and `Registration:Show` return a popup handle.\n\n```lua\nhandle:Update(newData)\nhandle:Refresh()\nhandle:Pin()\nhandle:Unpin()\nhandle:TogglePinned()\nhandle:SetPinned(true)\nhandle:SetOptions({ Placement = \"Left\" })\nhandle:Reposition()\nhandle:Close(\"Manual\")\n\nhandle:IsOpen()\nhandle:IsPinned()\nhandle:GetInstance()\nhandle:GetData()\nhandle:GetOptions()\nhandle:GetDefinitionName()\nhandle:GetResolvedPlacement()\nhandle:GetCloseReason()\n```\n\n`Update` uses the supplied data. `Refresh` reruns the registration's data resolver, or reruns the active direct popup with its current data.\n\n### Registration\n\n```lua\nregistration:Show()\nregistration:Show(true) -- Show and pin immediately\nregistration:Hide()\nregistration:Refresh()\nregistration:Pin()\nregistration:Unpin()\nregistration:TogglePinned()\nregistration:SetDataSource(newDataOrResolver)\nregistration:SetOptions({ Placement = \"Left\" })\nregistration:GetTrigger()\nregistration:GetActivePopup()\nregistration:IsRegistered()\nregistration:Destroy()\n```\n\nDestroying a trigger automatically destroys its registration and closes its active popup.\n\n### Signals\n\n```lua\npopups.PopupOpened:Connect(function(handle, context) end)\npopups.PopupClosed:Connect(function(handle, reason, context) end)\npopups.PopupUpdated:Connect(function(handle, oldData, newData, context) end)\npopups.PinnedChanged:Connect(function(handle, isPinned, context) end)\n```\n\nEach signal supports `Connect`, `Once`, and `Wait`.\n\n## Complete configuration reference\n\n### Manager config (`PopupKit.new`)\n\n| Property | Type | Default | Description |\n| --- | --- | --- | --- |\n| `Parent` | `Instance?` | Local player's `PlayerGui` | Where PopupKit mounts its popup layer. See [Creating a manager](#creating-a-manager). |\n| `Boundary` | `GuiObject?` | The popup layer | `GuiObject` used for clamping and flip calculations. |\n| `Definitions` | `{ [string]: PopupDefinition }?` | `nil` | Definitions registered immediately via `RegisterDefinitions`. |\n| `DefaultOptions` | `PopupOptions?` | `nil` | Overrides applied on top of PopupKit's built-in option defaults for every popup. |\n| `ScreenGuiName` | `string` | `\"PopupKitGui\"` | Name of the `ScreenGui` PopupKit creates for itself, if any. |\n| `LayerName` | `string` | `\"PopupKitLayer\"` | Name of the transparent `Frame` layer PopupKit creates under a `LayerCollector` parent. |\n| `DisplayOrder` | `number` | `100` | `DisplayOrder` of the `ScreenGui` PopupKit creates for itself, if any. |\n| `IgnoreGuiInset` | `boolean` | `true` | `IgnoreGuiInset` of the `ScreenGui` PopupKit creates for itself, if any. |\n| `OnError` | `(stage: string, errorMessage: string) -> ()?` | `nil` | Called alongside PopupKit's internal `warn` calls whenever a callback errors. |\n\n### Popup options\n\nResolved in this order: PopupKit built-in defaults → `DefaultOptions` on `PopupKit.new` → the definition's `Options` → registration or direct-show options.\n\n| Property | Type | Default | Description |\n| --- | --- | --- | --- |\n| `Placement` | `\"Cursor\" \\| \"Top\" \\| \"Bottom\" \\| \"Left\" \\| \"Right\" \\| \"Custom\"` | `\"Cursor\"` | Where the popup appears relative to the cursor or an anchor. |\n| `Alignment` | `\"Start\" \\| \"Center\" \\| \"End\"` | `\"Center\"` | Cross-axis alignment for `Top`/`Bottom`/`Left`/`Right` placement. |\n| `Offset` | `Vector2` | `(10, 10)` | Offset applied from the cursor or anchor edge. |\n| `EdgePadding` | `Vector2` | `(8, 8)` | Minimum padding kept from the boundary's edges. |\n| `FlipWhenClipped` | `boolean` | `true` | Flips to the opposite side (or axis, for cursor placement) when that reduces boundary overflow. |\n| `TrackPosition` | `boolean` | `true` | Continuously repositions the popup every frame instead of once on show. |\n| `CustomPosition` | `(context: PositionContext) -> Vector2?` | `nil` | Required when `Placement` is `\"Custom\"`; returns an absolute screen-space top-left position. |\n| `Anchor` | `GuiObject?` | The trigger, if any | `GuiObject` used instead of the trigger for relative placement. |\n| `ShowDelay` | `number` | `0.08` | Seconds a trigger must stay hovered before its popup opens. |\n| `HideDelay` | `number` | `0.08` | Seconds after leaving the trigger/popup before it closes. |\n| `KeepOpenWhilePopupHovered` | `boolean` | `true` | Cancels hide requests while the pointer is over the popup itself. |\n| `PinOnClick` | `boolean` | `false` | Pins (or toggles pin) on click/touch of the trigger or `PinTarget`. |\n| `PinTarget` | `GuiObject?` | The trigger | Element whose click/touch pins the popup, when `PinOnClick` is enabled. |\n| `CloseOnOutsideClick` | `boolean` | `true` | Closes the popup when clicking/touching outside it, its trigger, and its pin target. |\n| `ReplacePinned` | `boolean` | `false` | Allows a new popup to replace a currently pinned one. |\n| `RefreshOnShow` | `boolean` | `true` | Re-resolves and re-populates data when showing a reused popup instance. |\n| `ReuseInstance` | `boolean` | `true` | Reuses the existing instance instead of recreating it when the same definition/registration is shown again. |\n\nNegative delay values are treated as 0.\n\n## 📝 Notes\n\n* PopupKit is intended for client-side UI only.\n* A popup is automatically closed when its instance is destroyed externally, its trigger or registration is destroyed, its definition is unregistered, or its manager is destroyed.\n* All cleanup methods are idempotent, so repeated calls are safe.\n\n## 🛠️ Installation\n\nCopy `PopupKit` into your project — for example under `ReplicatedStorage`:\n\n```text\nReplicatedStorage\n└── PopupKit\n```\n\nThen require it from a client script:\n\n```lua\nlocal ReplicatedStorage = game:GetService(\"ReplicatedStorage\")\n\nlocal PopupKit = require(ReplicatedStorage.PopupKit)\n```\n\n\nmade with ❤️ by biotoxin495\n","readmeTruncated":false}