{"id":"biotoxin495/notificationkit","name":"notificationkit","scope":"biotoxin495","platform":"roblox","description":"A client-side Roblox UI module for displaying themed, animated notifications.","version":"1.0.1","latest":"1.0.1","versions":["1.0.0","1.0.1"],"license":"MIT","licenseRating":"safe","licenseCaveats":[],"licenseVerified":true,"dependencies":{},"integrity":"066d404bbca0068687d1c33f4fbf43e80087fd5d3d048b30d23b3ee4ec4b5edc","likes":0,"downloads":0,"install":"forest install biotoxin495/notificationkit","url":"https://forest.dev/p/roblox/biotoxin495/notificationkit","files":"https://api.forest.dev/ai/package/roblox/biotoxin495/notificationkit/files","readme":"# NotificationKit - a typed, framework-independent notification engine for Roblox\n\n**NotificationKit** is a notification orchestration system: it decides when notifications appear, where they appear, how multiple notifications interact, and how their UI is constructed, while remaining independent of Fusion, React, Knit, or any other framework.\n\nOne lifecycle engine powers everything from a one-line toast to a fully custom, structurally-defined notification card:\n\n```lua\nnotifications:Toast(\"Saved successfully\")\n```\n\n```lua\nlocal handle = notifications:Show({\n\tChannel = \"Downloads\",\n\tVariant = \"Progress\",\n\tTitle = \"Loading inventory\",\n\tText = \"Fetching item data...\",\n\tProgress = 0,\n\tAutoDismiss = false,\n})\n\nhandle:Update({\n\tProgress = 0.65,\n\tText = \"Loading item thumbnails...\",\n})\n```\n\nBoth calls share the same queue, channel, timing, rendering, and cleanup systems.\n\n## Quick example\n\n```lua\nlocal ReplicatedStorage = game:GetService(\"ReplicatedStorage\")\nlocal Players = game:GetService(\"Players\")\n\nlocal NotificationKit = require(ReplicatedStorage.NotificationKit)\n\nlocal notifications = NotificationKit.new({\n\tParent = Players.LocalPlayer.PlayerGui,\n})\n\nnotifications:Toast(\"Checkpoint reached\")\n```\n\n## 🚀 Features\n\n### Simple helpers\n\n```lua\nnotifications:Toast(\"Saved successfully\")\nnotifications:Info(\"New area discovered\")\nnotifications:Success(\"Purchase complete\")\nnotifications:Warning(\"Inventory nearly full\")\nnotifications:Error(\"Failed to load profile\")\n```\n\nThese are thin wrappers around `Show` - `Success`/`Warning`/`Error` apply a semantic accent color, `Info` tags the notification's `Metadata.Semantic`.\n\n### Structured notifications\n\n```lua\nlocal handle = notifications:Show({\n\tVariant = \"Toast\",\n\tText = \"You received 250 coins\",\n\tIcon = \"rbxassetid://123\",\n\tIconColor = Color3.fromRGB(255, 220, 80),\n\tDuration = 4,\n\tPriority = NotificationKit.Priority.High,\n})\n```\n\n### Action notifications\n\n```lua\nnotifications:Show({\n\tVariant = \"Action\",\n\tTitle = \"Daily reward\",\n\tText = \"Your daily reward is ready to claim.\",\n\tIcon = \"rbxassetid://123\",\n\tAction = {\n\t\tText = \"Claim\",\n\t\tStyle = \"Primary\",\n\t\tCallback = function()\n\t\t\tclaimReward()\n\t\tend,\n\t},\n\tSecondaryAction = {\n\t\tText = \"Dismiss\",\n\t\tStyle = \"Secondary\",\n\t},\n})\n```\n\nAction notifications default to a 10-second duration and always render a close button. If a persistent (`AutoDismiss = false`) action notification has no `Action` or `SecondaryAction`, debug mode warns that it has no exit path.\n\n### Progress notifications\n\n```lua\nlocal handle = notifications:Show({\n\tVariant = \"Progress\",\n\tTitle = \"Downloading assets\",\n\tProgress = 0,\n})\n\nhandle:Update({ Progress = 0.4, Text = \"Downloading textures...\" })\nhandle:Update({ Progress = 1, Text = \"Complete\" })\nhandle:Dismiss(\"Programmatic\")\n```\n\nProgress notifications default to `AutoDismiss = false` and auto-generate a `\"NN%\"` progress label unless `ProgressText` is supplied. Progress is always clamped to `0..1`.\n\n### Announcements\n\n```lua\nnotifications:Show({\n\tVariant = \"Announcement\",\n\tTitle = \"Round starting\",\n\tText = \"The next round begins in 10 seconds.\",\n})\n```\n\nNotifications with `Variant = \"Announcement\"` are routed to a dedicated `Announcements` channel (top-center, stacked, up to 2 visible) unless a `Channel` is explicitly given.\n\n### Channels\n\nChannels are independent notification lanes so gameplay toasts, prompts, and system warnings never block each other.\n\n```lua\nnotifications:CreateChannel(\"Prompts\", {\n\tPosition = \"TopCenter\",\n\tDefaultPolicy = \"Queue\",\n\tMaxVisible = 1,\n})\n\nnotifications:CreateChannel(\"System\", {\n\tPosition = \"BottomRight\",\n\tMaxVisible = 2,\n})\n\nnotifications:GetChannel(\"Prompts\"):Show({ Text = \"Server restarting soon\" })\n```\n\nThe `Default` channel is created automatically. Position presets: `TopLeft`, `TopCenter`, `TopRight`, `Center`, `BottomLeft`, `BottomCenter`, `BottomRight`.\n\n### Delivery policies\n\n```lua\nPolicy = \"Queue\"     -- wait for room in the channel\nPolicy = \"Stack\"     -- display alongside other active notifications (channel default)\nPolicy = \"Replace\"   -- dismiss a matching or active notification, then show this one\nPolicy = \"Drop\"      -- discard this notification if the channel is busy\nPolicy = \"Coalesce\"  -- merge into an existing notification sharing its Key\n```\n\n```lua\nnotifications:Show({\n\tKey = \"CoinsReceived\",\n\tPolicy = \"Coalesce\",\n\tText = \"+5 coins\",\n\tMetadata = { Amount = 5 },\n\tMerge = function(existing, incoming)\n\t\tlocal total = existing.Metadata.Amount + incoming.Metadata.Amount\n\t\texisting.Metadata.Amount = total\n\t\texisting.Text = `+{total} coins`\n\t\treturn existing\n\tend,\n})\n```\n\nHigher `Priority` notifications are shown first; equal-priority notifications preserve FIFO order.\n\n```lua\nPriority = NotificationKit.Priority.Low      -- -100\nPriority = NotificationKit.Priority.Normal   -- 0\nPriority = NotificationKit.Priority.High     -- 100\nPriority = NotificationKit.Priority.Critical -- 1000\n```\n\n### Themes\n\n```lua\nnotifications:RegisterTheme(\"Neon\", {\n\tBackgroundColor = Color3.fromRGB(8, 8, 12),\n\tAccentColor = Color3.fromRGB(80, 255, 230),\n\tStrokeColor = Color3.fromRGB(80, 255, 230),\n\tCornerRadius = UDim.new(0, 8),\n})\n\nnotifications:Show({ Theme = \"Neon\", Text = \"Power restored\" })\n```\n\nThemes can extend another registered theme with `Extends`. Built-in themes: `Default`, `Minimal`, and `HighContrast`. Theme resolution order is: controller default theme → channel default theme → notification's `Theme` field.\n\n### Custom UI construction\n\nEvery default notification is built with `Instance.new` - no `.rbxm` template is required. Three levels of customization are supported:\n\n**Declarative instance trees**, built per node with semantic `Role`s (`Icon`, `Title`, `Text`, `ActionButton`, `SecondaryActionButton`, `CloseButton`, `ProgressFill`, `ProgressText`, `VisualContainer`, ...):\n\n```lua\nnotifications:RegisterBuildConfig(\"RewardCard\", {\n\tTree = {\n\t\tClassName = \"Frame\",\n\t\tName = \"Root\",\n\t\tRole = \"Root\",\n\t\tChildren = {\n\t\t\t{ ClassName = \"UICorner\", Properties = { CornerRadius = UDim.new(0, 16) } },\n\t\t\t{ ClassName = \"TextLabel\", Name = \"RewardTitle\", Role = \"Title\" },\n\t\t\t{\n\t\t\t\tClassName = \"TextButton\",\n\t\t\t\tName = \"ClaimButton\",\n\t\t\t\tRole = \"ActionButton\",\n\t\t\t\tWhen = function(data)\n\t\t\t\t\treturn data.Action ~= nil\n\t\t\t\tend,\n\t\t\t},\n\t\t},\n\t},\n})\n\nnotifications:Show({ BuildConfig = \"RewardCard\", Title = \"Reward!\", Action = { Text = \"Claim\" } })\n```\n\n**Layout-only configuration**, changing padding, spacing, and minimum height without touching structure:\n\n```lua\nnotifications:RegisterBuildConfig(\"CompactToast\", {\n\tLayout = { Padding = 10, Spacing = 8, MinimumHeight = 42 },\n})\n```\n\n**Fully programmatic builders**, for complete control:\n\n```lua\nnotifications:RegisterBuilder(\"MyBuilder\", function(context)\n\tlocal root = context.Create(\"Frame\", { BackgroundColor3 = context.Theme.BackgroundColor })\n\tlocal title = context.Create(\"TextLabel\", { Parent = root })\n\treturn { Root = root, Elements = { Title = title } }\nend)\n```\n\nRenderers themselves are swappable per notification, per channel, or globally via `RegisterRenderer`.\n\n### Sounds\n\n```lua\nlocal notifications = NotificationKit.new({\n\tSound = { Enabled = true, Default = \"rbxassetid://123\", Volume = 0.5 },\n})\n\nnotifications:Show({ Text = \"Purchase complete\", Sound = \"rbxassetid://456\" })\n```\n\nSounds play once the notification becomes active, are parented to `SoundService`, and are destroyed automatically when playback ends or the notification is cleaned up. Set `SoundEnabled = false` per notification, or `Sound.Enabled = false` on the controller, to suppress playback.\n\n### Optional history\n\n```lua\nlocal notifications = NotificationKit.new({\n\tHistory = { Enabled = true, MaxEntries = 100 },\n})\n\nnotifications:GetActive(\"Rewards\")\nnotifications:GetHistory(\"Rewards\", 20)\n```\n\nHistory is disabled by default. Only notifications that were actually shown are retained, newest first, bounded by `MaxEntries`.\n\n### Live handles\n\nEvery `Show` call returns a handle for inspecting and controlling that notification:\n\n```lua\nlocal handle = notifications:Show({ Text = \"Uploading...\" })\n\nhandle.Shown:Connect(function() print(\"visible now\") end)\nhandle.Dismissed:Connect(function(reason) print(\"dismissed:\", reason) end)\n\nhandle:Update({ Text = \"Almost done...\" })\nhandle:GetState()      -- \"Created\" | \"Queued\" | \"Entering\" | \"Active\" | \"Exiting\" | \"Dismissed\" | \"Dropped\"\nhandle:AwaitDismissed() -- yields until dismissed, returns the reason\nhandle:Dismiss(\"Programmatic\")\n```\n\n### Server-triggered notifications\n\n```lua\n-- Server\nremote:FireClient(player, {\n\tVariant = \"Action\",\n\tTitle = \"Trade request\",\n\tText = \"PlayerName wants to trade.\",\n\tAction = { Id = \"AcceptTrade\", Text = \"Accept\" },\n})\n\n-- Client\nnotifications:BindRemote(remote, function(payload)\n\treturn payload.Variant ~= \"Announcement\" -- optional payload validation\nend)\n```\n\n`BindRemote` rewires any `Action`/`SecondaryAction` on the incoming payload to fire the same `RemoteEvent` back to the server with the notification's ID and action ID, so the server can independently validate the underlying game action.\n\n### Safety and cleanup\n\nAll user callbacks (`Action.Callback`, `OnShow`, `OnUpdate`, `OnDismiss`, `Merge`, declarative `When`/`Transform`, custom builders) run through a protected `xpcall` runner - a callback error is warned and never breaks queue processing, timeouts, or the next notification's delivery. Every notification owns a cleanup container that tracks connections, tweens, threads, sounds, and cloned visuals, guaranteeing no leaks on dismissal or controller destruction. Enable `Strict = true` to turn consumer mistakes (unknown policies, invalid durations) into hard errors during development; enable `Debug = true` for descriptive warnings instead.\n\n### Accessibility and responsiveness\n\nNotifications pause their auto-dismiss timer on hover and gamepad/keyboard focus by default (`PauseOnHover`, `PauseOnFocus`), and can optionally pause while the game window is unfocused (`PauseWhenGameUnfocused`). `ReducedMotion = true` shortens and simplifies entrance/exit animations. Text uses `UITextSizeConstraint` and wraps within a max-width container instead of relying on unconstrained `TextScaled`.\n\n## 📖 Basic usage\n\nNotificationKit is intended for client-side UI and must be required from a `LocalScript`.\n\n```lua\nlocal ReplicatedStorage = game:GetService(\"ReplicatedStorage\")\nlocal Players = game:GetService(\"Players\")\n\nlocal NotificationKit = require(ReplicatedStorage.NotificationKit)\n\nlocal notifications = NotificationKit.new({\n\tParent = Players.LocalPlayer.PlayerGui,\n\tDisplayOrder = 100,\n\tDebug = game:GetService(\"RunService\"):IsStudio(),\n})\n```\n\nIf `Parent` is a `LayerCollector` or `GuiObject`, it's used directly as the root; otherwise NotificationKit creates and owns its own `ScreenGui`. Pass `Root` instead of `Parent` to reuse an existing `ScreenGui`/`GuiObject` you manage yourself.\n\n## ⚙️ API\n\n### Controller\n\n```lua\nNotificationKit.new(config?)\n```\n\nCreates a controller and its `Default` channel. See [Controller configuration](#controller-configuration) below.\n\n```lua\nnotifications:Show(data)                          -- show a structured notification, returns a handle\nnotifications:Toast(text, options?)\nnotifications:Info(text, options?)\nnotifications:Success(text, options?)\nnotifications:Warning(text, options?)\nnotifications:Error(text, options?)\n\nnotifications:CreateChannel(name, config?)\nnotifications:GetChannel(name)\nnotifications:DestroyChannel(name)\n\nnotifications:RegisterTheme(name, theme)\nnotifications:RegisterRenderer(name, renderer)\nnotifications:RegisterBuilder(name, builder)\nnotifications:RegisterBuildConfig(name, config)\n\nnotifications:GetHandle(id)\nnotifications:GetActive(channel?)\nnotifications:GetQueued(channel?)\nnotifications:GetHistory(channel?, limit?)\n\nnotifications:Dismiss(id, reason?)\nnotifications:DismissByKey(key, reason?)\nnotifications:Clear(channel?)\nnotifications:Pause(channel?)\nnotifications:Resume(channel?)\n\nnotifications:BindRemote(remote, validator?)       -- connect a RemoteEvent to Show()\nnotifications:Destroy()                            -- tears down every channel, notification, and the GUI root\n```\n\n### Channel\n\n```lua\nlocal channel = notifications:GetChannel(\"Prompts\")\n\nchannel:Show(data)\nchannel:GetActive()\nchannel:GetQueued()\nchannel:GetHistory(limit?)\nchannel:GetCount()\nchannel:Dismiss(id, reason?)\nchannel:DismissByKey(key, reason?)\nchannel:Clear(reason?)\nchannel:Pause()\nchannel:Resume()\nchannel:SetConfig(patch)\nchannel:Destroy()\n```\n\n### Handle\n\n```lua\nhandle:Update(patch)      -- partial patch; returns false if already dismissed\nhandle:Dismiss(reason?)\nhandle:IsActive()\nhandle:IsQueued()\nhandle:IsDismissed()\nhandle:GetState()\nhandle:GetData()\nhandle:AwaitShown()\nhandle:AwaitDismissed()\n```\n\n```lua\nhandle.Shown           -- fired when the notification becomes Active\nhandle.Updated         -- fired on every Update(), with (handle, patch)\nhandle.Dismissed       -- fired with (reason, handle)\nhandle.ActionTriggered -- fired with (actionId, handle) when Action/SecondaryAction activates\n```\n\n## Complete options reference\n\nAny field you omit falls back to the channel or controller default.\n\n```lua\n{\n\tId = nil,               -- auto-generated if omitted\n\tKey = nil,               -- required for Coalesce, used by DismissByKey\n\tChannel = \"Default\",\n\tVariant = \"Toast\",       -- \"Toast\" | \"Action\" | \"Progress\" | \"Announcement\" | custom string\n\n\tTitle = nil,\n\tText = nil,\n\tRichText = false,\n\n\tIcon = nil,\n\tIconColor = nil,\n\tIconTransparency = 0,\n\n\tVisual = nil,            -- a GuiObject to embed\n\tCloneVisual = true,\n\n\tAction = nil,            -- { Text, Style, Icon, Callback, AutoDismiss }\n\tSecondaryAction = nil,\n\tOnActivated = nil,       -- fires when the notification body itself is clicked\n\n\tProgress = nil,          -- 0..1\n\tProgressText = nil,\n\n\tPriority = 0,\n\tPolicy = nil,            -- channel's DefaultPolicy, usually \"Stack\"\n\tDuration = 4,            -- 10 for Action\n\tAutoDismiss = true,      -- false for Progress\n\n\tTheme = nil,             -- theme name or inline ThemeConfig\n\tRenderer = nil,          -- renderer name or NotificationRenderer table\n\tBuildConfig = nil,       -- build config name or inline NotificationBuildConfig\n\n\tSound = nil,\n\tSoundEnabled = true,\n\tSoundVolume = nil,       -- clamped 0..10, controller default 0.5\n\tSoundPlaybackSpeed = nil,-- clamped 0.05..4, controller default 1\n\n\tMetadata = nil,\n\tMerge = nil,             -- (existing, incoming) -> NotificationData, for Coalesce\n\n\tOnShow = nil,\n\tOnUpdate = nil,\n\tOnDismiss = nil,         -- (reason, handle)\n}\n```\n\n## Controller configuration\n\n```lua\nNotificationKit.new({\n\tParent = playerGui,           -- or Root = existingScreenGui\n\tName = \"NotificationKit\",\n\tDisplayOrder = 100,\n\tDefaultTheme = \"Default\",\n\tDebug = false,\n\tStrict = false,\n\tReducedMotion = false,\n\tPauseOnHover = true,\n\tPauseOnFocus = true,\n\tPauseWhenGameUnfocused = false,\n\tReadableDuration = { Enabled = false, BaseSeconds = 2, CharactersPerSecond = 18, MaximumSeconds = 12 },\n\tHistory = { Enabled = false, MaxEntries = 100 },\n\tSound = { Enabled = true, Default = nil, Volume = 0.5, PlaybackSpeed = 1 },\n})\n```\n\n## Channel configuration\n\n```lua\nnotifications:CreateChannel(\"Gameplay\", {\n\tPosition = \"TopCenter\",        -- TopLeft | TopCenter | TopRight | Center | BottomLeft | BottomCenter | BottomRight\n\tParent = nil,                  -- supply a GuiObject to bypass the built-in container/positioning entirely\n\tLayoutDirection = \"Vertical\",  -- \"Vertical\" | \"Horizontal\"\n\tSpacing = 8,\n\tMaxVisible = 5,\n\tMaxQueued = 50,\n\tDefaultVariant = \"Toast\",\n\tDefaultPolicy = \"Stack\",\n\tDefaultDuration = nil,\n\tDefaultTheme = nil,\n\tDefaultRenderer = nil,\n\tOverflowPolicy = \"DropNewest\", -- \"DropNewest\" | \"DropOldest\" | \"ReplaceOldest\", applied when the queue is full\n})\n```\n\n## Behavior\n\nA `Default` channel exists as soon as the controller is created; an `Announcements` channel is created automatically the first time an `Announcement`-variant notificatio","readmeTruncated":true,"readmeFull":"https://api.forest.dev/v1/package/biotoxin495/roblox/notificationkit/1.0.1/readme"}