local plugin = require("@self/plugin")
if not plugin then
	error("Rocket must be run as a plugin!")
end

local CoreGui = game:GetService("CoreGui")
local LogService = game:GetService("LogService")

local Charm = require("@pkgs/Charm")
local Command = require("@src/core/commands/Command")
local CommandRuntime = require("@src/core/commands/CommandRuntime")
local CommandStore = require("@src/core/commands/CommandStore")
local Extension = require("@src/core/Extension")
local Gizmos = require("@src/core/Gizmos")
local Iris = require("@self/Iris")
local LauncherStore = require("@src/launcher/LauncherStore")
local LauncherWindow = require("@src/launcher/LauncherWindow")
local Log = require("@src/core/Log")
local SettingsStore = require("@src/settings/SettingsStore")
local SettingsWindow = require("@src/settings/SettingsWindow")
local assets = require("@include/assets")
local errors = require("@src/utils/errors")
local ids = require("@src/utils/ids")
local pluginTrove = require("@self/pluginTrove")
local runCommand = require("@src/core/commands/runCommand")
local theme = require("@self/theme")
local types = require("@self/types")
local useKeys = require("@src/core/hooks/useKeys")

local commandActions: { [Command.Command]: PluginAction } = {}

local function requireInstances(instances: { Instance })
	for _, instance in instances do
		if instance:IsA("ModuleScript") then
			task.spawn(pcall, require, instance)
		end
	end
end

local function renderCommandOutput(
	command: Command.Command,
	output: types.CommandOutput,
	runtimes: { [Command.Command]: CommandRuntime.CommandRuntime }
)
	if output.renderViewport then
		output.renderViewport()
	end

	if output.renderWindow then
		if output.windowConfig then
			Iris.PushConfig(table.clone(output.windowConfig))
		end

		local isOpened = Iris.State(true)
		Iris.SetNextWidgetID(`{ids.rocket(command:getFullId())}/window`)
		local window = Iris.Window(output.windowArgs :: any or { command.interface.title }, { isOpened = isOpened })

		if output.windowConfig then
			Iris.PopConfig()
		end

		if isOpened.value then
			output.renderWindow(window)
			Iris.End()
			return
		end

		isOpened:set(true)
		runtimes[command]:cleanup()
		Iris.End()
	end
end

local function mountUi()
	pluginTrove:Add(Charm.subscribe(LauncherStore.isVisible, function()
		LauncherStore.isFocused(false)
		LauncherStore.search("")
		LauncherStore.selectedIndex(1)
	end) :: any)

	pluginTrove:Add(Charm.subscribe(SettingsStore.isVisible, function(isVisible)
		if not isVisible then
			SettingsStore.reset()
		end
	end) :: any)

	local container = Instance.new("ScreenGui")
	container.Name = "Rocket"
	container.Parent = CoreGui

	Iris.Init(container :: any)
	Iris.UpdateGlobalConfig(Iris.TemplateConfig.sizeClear :: any)
	Iris.UpdateGlobalConfig(theme.config())
	pluginTrove:Add((Charm.subscribe :: any)(theme.config, Iris.UpdateGlobalConfig))
	theme.observeStudio(pluginTrove)

	pluginTrove:Add(Iris:Connect(function()
		Charm.startBatch()
		useKeys.store.previous = useKeys.store.now
		useKeys.store.now = useKeys.getKeysDown()

		if LauncherStore.isVisible() then
			LauncherWindow()
		end

		if SettingsStore.isVisible() then
			SettingsWindow()
		end

		local renderResults = CommandStore.uiOutputs()
		local contexts = CommandStore.runtimes()
		for _, command in CommandStore.commandsWithUiOutputs() do
			renderCommandOutput(command, renderResults[command], contexts)
		end

		Gizmos.step()
		Charm.endBatch()
	end) :: any)

	pluginTrove:Add(Iris.Shutdown :: any)
	pluginTrove:Add(container)
	pluginTrove:Add(Gizmos.destroyPool)
end

local function bindVisibleToButton(isVisibleAtom: Charm.Atom<boolean>, button: PluginToolbarButton)
	pluginTrove:Add(button.Click:Connect(function()
		isVisibleAtom(not isVisibleAtom())
	end))

	pluginTrove:Add(Charm.subscribe(isVisibleAtom, function(isVisble)
		button:SetActive(isVisble)
	end) :: any)
end

local function mountToolbar()
	local toolbar = plugin:CreateToolbar("Rocket")

	local launchButton = (
		toolbar:CreateButton(ids.rocket("launch"), "Launches the Rocket command palette", assets.brand.rocket, "Launch")
	)

	local settingsButton = (
		toolbar:CreateButton(ids.rocket("settings"), "Configure Rocket", assets.brand.settings, "Settings")
	)

	bindVisibleToButton(LauncherStore.isVisible, launchButton)
	bindVisibleToButton(SettingsStore.isVisible, settingsButton)
end

local function createPluginAction(id: string, title: string, description: string, icon: string)
	id = ids.rocket(id)
	local action = plugin:CreatePluginAction(id, title, description, icon)
	action.Name = id
	return action
end

local function updateCommandPluginActions(commands: { Command.Command })
	for _, command in commands do
		if commandActions[command] then
			continue
		end

		local action = createPluginAction(
			`command/{command:getFullId()}`,
			command.interface.title,
			command.interface.description,
			command:getIcon()
		)

		pluginTrove:Add(action.Triggered:Connect(function()
			runCommand(command)
		end))

		commandActions[command] = action
	end
end

local function mountPluginActions()
	local enableAction = createPluginAction(
		"launcher/enable",
		"Enable Rocket",
		"Enables the Rocket command launcher",
		assets.brand.rocket
	)

	local disableAction = createPluginAction(
		"launcher/disable",
		"Disable Rocket",
		"Disables the Rocket command launcher",
		assets.brand.rocket
	)

	local toggleAction = createPluginAction(
		"launcher/toggle",
		"Enable/Disable Rocket",
		"Toggles the Rocket command launcher",
		assets.brand.rocket
	)

	pluginTrove:Add(enableAction.Triggered:Connect(function()
		LauncherStore.isVisible(true)
	end))

	pluginTrove:Add(disableAction.Triggered:Connect(function()
		LauncherStore.isVisible(false)
	end))

	pluginTrove:Add(toggleAction.Triggered:Connect(function()
		LauncherStore.isVisible(not LauncherStore.isVisible())
	end))

	-- TODO: command removals?
	pluginTrove:Add(task.spawn(updateCommandPluginActions, Extension._allCommands()))
	pluginTrove:Add(Charm.subscribe(Extension._allCommands, updateCommandPluginActions) :: any)
end

local function start()
	requireInstances(script:WaitForChild("extensions"):GetDescendants())
	mountUi()
	mountToolbar()
	mountPluginActions()
end

do
	LogService:ClearOutput()

	local log = Log.new(script)
	pluginTrove:Add(function()
		log:trace("Rocket shutting down!")
	end)

	local startSuccess, startError: types.Error = xpcall(start, errors.parseError, log)
	if startSuccess then
		log:info("Rocket started!")
	else
		pluginTrove:Destroy()

		local errorTrace = "\n\t" .. string.gsub(startError.trace, "\n", "\n\t")
		local errorMessage = `\nFailed to start Rocket!\nMessage: {startError.message}\n\nStack Trace:{errorTrace}`
		log:error(errorMessage, 0)
	end
end


local Charm = require("@pkgs/Charm")
local Command = require("@src/core/commands/Command")
local Extension = require("@src/core/Extension")
local fzy = require("@include/fzy")

local LauncherStore = {}
LauncherStore.isVisible = Charm.atom(false)
LauncherStore.isFocused = Charm.atom(false)
LauncherStore.selectedIndex = Charm.atom(1)
LauncherStore.search = Charm.atom("")

LauncherStore.searchResults = Charm.computed(function(): { Command.Command }
	local search = LauncherStore.search()
	local allCommands = Extension._allCommands()
	local commandToIndex = Extension._commandToIndex()

	local haystack = table.clone(allCommands)
	if search ~= "" then
		local outputs = fzy.filter(search, Extension._commandTitles(), false)

		local extensions = Extension._extensionsSortedByTitle()
		local extensionResults = fzy.filter(search, Extension._extensionTitles(), false)

		local combined: { [number]: { number } } = {}
		for _, result in outputs do
			-- command titles has priority over extension titles
			result[3] *= 1000
			combined[result[1]] = result
		end

		for _, result in extensionResults do
			for _, command in extensions[result[1]]._commands do
				local commandIndex = commandToIndex[command]
				local output = combined[commandIndex]
				if output then
					output[3] += result[3]
				else
					combined[commandIndex] = { commandToIndex[command], 0, result[3] }
				end
			end
		end

		table.sort(combined, function(lhs: { number }, rhs: { number })
			-- There are going to be gaps because some commands will be
			-- filtered.
			return if lhs and rhs then lhs[3] < rhs[3] else not not rhs
		end)

		haystack = table.create(#combined) :: { Command.Command }
		for _, result in combined do
			table.insert(haystack, allCommands[result[1]])
		end
	end

	return haystack
end)

return LauncherStore


local Workspace = game:GetService("Workspace")

local Command = require("@src/core/commands/Command")
local Iris = require("@src/Iris")
local LauncherStore = require("./LauncherStore")
local runCommand = require("@src/core/commands/runCommand")
local useKeys = require("@src/core/hooks/useKeys")
local widgets = require("@src/widgets")

local WINDOW_SIZE = Vector2.new(640, 284)
local WINDOW_ARGS = {
	"Rocket Launcher",
	[Iris.Args.Window.NoTitleBar] = true,
	[Iris.Args.Window.NoBackground] = true,
	[Iris.Args.Window.NoCollapse] = true,
	[Iris.Args.Window.NoClose] = true,
	[Iris.Args.Window.NoMove] = true,
	[Iris.Args.Window.NoResize] = true,
	[Iris.Args.Window.NoNav] = true,
	[Iris.Args.Window.NoMenu] = true,
}

local function LauncherWindow()
	Iris.PushConfig {
		WindowPadding = Vector2.zero,
		ContentWidth = UDim.new(1, 0),
		ItemSpacing = Vector2.new(8, 0),
	}

	local viewportSize = Workspace.CurrentCamera.ViewportSize
	local windowPosition = viewportSize / 2 - WINDOW_SIZE / 2 - Vector2.new(0, 100)
	local window = Iris.Window(WINDOW_ARGS, { position = windowPosition, size = WINDOW_SIZE } :: any)
	window.state.position:set(viewportSize / 2 - WINDOW_SIZE / 2 - Vector2.new(0, 100))

	if window.closed() or useKeys.andConsumePress(Enum.KeyCode.Escape) then
		LauncherStore.isVisible(false)
		Iris.End()
		Iris.PopConfig()
		return
	end

	widgets.List({
		onSearchChange = LauncherStore.search,
		captureFocus = LauncherStore.isFocused(),
		entries = LauncherStore.searchResults(),
		getEntryTitle = function(command: Command.Command)
			return command.interface.title
		end,
		getEntryTooltip = function(command: Command.Command)
			return command.interface.description
		end,
		renderEntry = function(command: Command.Command)
			Iris.Image({ command:getIcon(), UDim2.fromOffset(18, 18) })
			Iris.Text({ command.interface.title })
			widgets.Subtext({ command._extension.interface.title })
			widgets.FlexSpacer(Enum.UIFlexMode.Fill)
			widgets.Subtext({ "Command" })
		end,
		onEntrySelected = function(command: Command.Command)
			LauncherStore.isVisible(false)
			task.defer(runCommand, command)
		end,
	} :: widgets.ListProps<Command.Command>)

	if not LauncherStore.isFocused() then
		LauncherStore.isFocused(true)
	end

	Iris.End()
	Iris.PopConfig()
end

return LauncherWindow


local plugin: Plugin = plugin or script:FindFirstAncestorWhichIsA("Plugin") :: Plugin

return plugin


local Trove = require("@pkgs/Trove")
local plugin = require("@src/plugin")

local pluginTrove = Trove.new()
pluginTrove:Add(plugin.Unloading:Connect(pluginTrove:WrapClean()))

return pluginTrove


local Command = require("@src/core/commands/Command")
local Iris = require("@src/Iris")
local SettingsStore = require("@src/settings/SettingsStore")
local widgets = require("@src/widgets")

local function CommandListing(command: Command.Command)
	local group = widgets.ButtonGroup({ size = UDim2.fromScale(1, 0) })
	if group.clicked() then
		SettingsStore.extensions.selected(if SettingsStore.extensions.selected() == command then nil else command)
	end
	Iris.Image({ command:getIcon(), UDim2.fromOffset(18, 18) })
	Iris.Text({ command.interface.title })
	Iris.End()
	return group
end

return CommandListing


local Command = require("@src/core/commands/Command")
local CommandListing = require("./CommandListing")
local Extension = require("@src/core/Extension")
local ExtensionListing = require("./ExtensionListing")
local Iris = require("@src/Iris")
local SettingsStore = require("@src/settings/SettingsStore")
local theme = require("@src/theme")

local function getRowThemePreset(row: number, selectable: Extension.Extension | Command.Command): Iris.PartialConfig
	if SettingsStore.extensions.selected() == selectable then
		return theme.presets.button.selected
	end

	return if row % 2 == 0 then theme.presets.button.unhighlighted else theme.presets.button.highlighted
end

local function ExtensionList()
	local extensionsCollapsed = table.clone(SettingsStore.extensions.extensionsCollapsed())
	local row = 1

	for i, extension in Extension._extensionsSortedByTitle() do
		-- local isCoreExtension = extension:isCore()
		-- if not (SettingsStore.extensions.showCore() and isCoreExtension) then
		-- 	continue
		-- elseif SettingsStore.extensions.showThirdParty() and not isCoreExtension then
		-- 	continue
		-- end

		row += 1

		local massCollapse = SettingsStore.extensions.massCollapse()
		if massCollapse ~= nil then
			extensionsCollapsed[extension] = massCollapse or nil
		end

		local layoutOrder = i * 100
		theme.push(getRowThemePreset(row, extension))
		ExtensionListing(extension, extensionsCollapsed, layoutOrder)
		theme.pop()

		if extensionsCollapsed[extension] then
			continue
		end

		Iris.PushId(extension.interface.id .. "/commands")
		Iris.Indent().Instance.LayoutOrder = layoutOrder + 1
		if next(extension._commands) == nil then
			Iris.Text({ "<i>This extension has no commands.</i>" })
		else
			for cmdIndex, command in extension._commands do
				row += 1
				Iris.PushId(command:getFullId())
				Iris.PushConfig(getRowThemePreset(row, command))
				CommandListing(command).Instance.LayoutOrder = cmdIndex
				theme.pop()
				Iris.PopId()
			end
		end
		Iris.End()
		Iris.PopId()
	end
	SettingsStore.extensions.extensionsCollapsed(extensionsCollapsed)
end

return ExtensionList


local Iris = require("@src/Iris")
local SettingsStore = require("@src/settings/SettingsStore")
local Theme = require("@src/theme")
local widgets = require("@src/widgets")

local function ExtensionListToolbar()
	local config = Theme.config()
	Theme.push({
		CellPadding = config.CellPadding,
		WindowPadding = config.WindowPadding,
		ItemSpacing = config.WindowPadding,
	})

	Iris.SameLine()

	Iris.Menu({ "Toggle All" })
	if Iris.MenuItem({ "Uncollasped" }).clicked() then
		SettingsStore.extensions.massCollapse(false)
	end
	if Iris.MenuItem({ "Collasped" }).clicked() then
		SettingsStore.extensions.massCollapse(true)
	end
	Iris.End()

	Iris.Menu({ "Filters" })
	Iris.MenuToggle({ "Show Enabled Extensions" })
	Iris.MenuToggle({ "Show Disabled Extensions" })
	Iris.Separator()
	Iris.MenuToggle({ "Show Core Extensions" })
	Iris.MenuToggle({ "Show Third-Party Extensions" })
	Iris.End()
	widgets.InstantInputText({ hint = "Search commands..." })

	Iris.End()
	Iris.PopConfig()
end

return ExtensionListToolbar


local Extension = require("@src/core/Extension")
local Iris = require("@src/Iris")
local SettingsStore = require("@src/settings/SettingsStore")
local assets = require("@include/assets")
local widgets = require("@src/widgets")

local function ExtensionListing(
	extension: Extension.Extension,
	extensionsCollapsed: { [Extension.Extension]: true },
	layoutOrder: number
)
	local extensionIcon = extension:getIcon()

	Iris.PushId(extension.interface.id)
	local header = widgets.ButtonGroup({ size = UDim2.fromScale(1, 0) })
	header.Instance.LayoutOrder = layoutOrder
	if header.clicked() then
		SettingsStore.extensions.selected(if SettingsStore.extensions.selected() == extension then nil else extension)
	end

	Iris.PushConfig({ FrameBgTransparency = 1 })
	local expand = Iris.ImageButton({
		extensionsCollapsed[extension] and assets.icons.caretright or assets.icons.caretdown,
		UDim2.fromOffset(9, 9),
	})
	Iris.PopConfig()

	if expand.clicked() then
		extensionsCollapsed[extension] = if extensionsCollapsed[extension] then nil else true
	end

	Iris.Image({ extensionIcon, UDim2.fromOffset(18, 18) })
	Iris.Text({ extension.interface.title })

	Iris.PopId()
	Iris.End()
end

return ExtensionListing


local Command = require("@src/core/commands/Command")
local Iris = require("@src/Iris")
local SettingsStore = require("@src/settings/SettingsStore")
local ids = require("@src/utils/ids")
local types = require("@src/types")
local widgets = require("@src/widgets")

local function Info()
	local selected = SettingsStore.extensions.selected()
	if not selected then
		return
	end

	widgets.Padding({ right = UDim.new(0, Iris._config.ScrollbarSize * 2 + Iris._config.FramePadding.X) })

	local asCmd = selected :: Command.Command
	local isCommand = Command.is(asCmd)

	widgets.Spacer(8)
	Iris.SameLine({ 8, Enum.VerticalAlignment.Top })
	Iris.Image({ (selected :: any):getIcon(), UDim2.fromOffset(42, 42) })
	Iris.Group()
	widgets.Flex(Enum.UIFlexMode.Fill)
	Iris.Text({
		`<b>{selected.interface.title}</b><br/>{selected.interface.description}`,
		[Iris.Args.Text.Wrapped] = true,
	})
	Iris.End()
	Iris.End()

	Iris.SeparatorText({ "Preferences" })
	Iris.Checkbox({ "Enabled" })
	if isCommand then
		Iris.Checkbox({ "Run on Startup" })
	end

	Iris.SeparatorText({ "Contributors" })
	Iris.SameLine()
	for _, author: types.Author in (selected :: any):getcontributors() do
		local authorImage, authorName: string = "rbxassetid://-1", "Unknown"
		if typeof(author) == "string" then
			authorName = `{author} (Custom)`
		else
			author = author :: types.AuthorUser | types.AuthorCustom
			if author.type == "user" then
				authorImage = ids.getUserThumbnail(author.id, Enum.ThumbnailType.HeadShot, Enum.ThumbnailSize.Size48x48)
				authorName = `{ids.getUsername(author.id)} (User, ID: {author.id})`
			elseif author.type == "custom" then
				authorImage = author.image
				authorName = `{author.name} (Custom)`
			end
		end

		if Iris.Image({ authorImage, UDim2.fromOffset(32, 32) }).hovered() then
			Iris.Tooltip({ authorName })
		end
	end
	Iris.End()

	Iris.SeparatorText({ "Debug" })
	widgets.Subtext({ `ID: {selected.interface.id}` })
	if isCommand then
		widgets.Subtext({ `Full ID: {asCmd:getFullId()}` })
	end
end

return Info


local ExtensionList = require("@self/ExtensionList")
local ExtensionListToolbar = require("@self/ExtensionListToolbar")
local Info = require("@self/Info")
local Iris = require("@src/Iris")
local assets = require("@include/assets")
local types = require("../types")
local widgets = require("@src/widgets")

local extensions = {} :: types.SettingsPage
extensions.id = "extensions"
extensions.title = "Extensions"
extensions.tabIcon = assets.icons.extension
extensions.extensionIcon = assets.extensions.settings.extensions

function extensions.render()
	ExtensionListToolbar()
	Iris.Separator()

	Iris.SameLine()
	widgets.Flex(Enum.UIFlexMode.Fill)

	widgets.ScrollingFrame({ size = UDim2.fromScale(0.5, 1), automaticSize = Enum.AutomaticSize.None })
	ExtensionList()
	Iris.End()

	Iris.Separator()

	widgets.Frame({ size = UDim2.fromScale(0.5, 0), automaticSize = Enum.AutomaticSize.Y })
	Info()
	Iris.End()

	Iris.End()
end

return extensions


local types = require("./types")

local SettingPages: { types.SettingsPage } = {
	require("@self/extensions"),
	require("@self/about"),
}

return SettingPages


local Iris = require("@src/Iris")
local SettingsPages = require("./SettingsPages")
local SettingsStore = require("./SettingsStore")
local widgets = require("@src/widgets")

local function SettingsSidebar()
	for _, page in SettingsPages do
		if widgets.ButtonGroup({ size = UDim2.fromScale(1, 0) }).clicked() then
			SettingsStore.currentPage(page)
		end

		Iris.Image({ page.tabIcon, UDim2.fromOffset(18, 18) })
		Iris.Text({ page.title })

		Iris.End()
	end
end

return SettingsSidebar


local Charm = require("@pkgs/Charm")
local Command = require("@src/core/commands/Command")
local Extension = require("@src/core/Extension")
local atoms = require("@src/utils/atoms")
local types = require("./types")

local SettingsStore = {
	isVisible = Charm.atom(false),
	currentPage = Charm.atom(nil :: types.SettingsPage?),
}

SettingsStore.resetBase = atoms.wrapSetDefaults(SettingsStore)

SettingsStore.extensions = {
	showEnabled = Charm.atom(true),
	showDisabled = Charm.atom(true),
	showCore = Charm.atom(true),
	showThirdParty = Charm.atom(true),

	massCollapse = Charm.atom(nil :: boolean?),
	extensionsCollapsed = Charm.atom({} :: { [Extension.Extension]: true }),

	selected = Charm.atom(nil :: nil | Extension.Extension | Command.Command),
}

SettingsStore.resetExtensions = atoms.wrapSetDefaults(SettingsStore.extensions)

function SettingsStore.reset()
	SettingsStore.resetBase()
	SettingsStore.resetExtensions()
end

return SettingsStore


local Workspace = game:GetService("Workspace")

local Iris = require("@pkgs/Iris")
local SettingsPages = require("./SettingsPages")
local SettingsSidebar = require("./SettingsSidebar")
local SettingsStore = require("./SettingsStore")

local SIDEBAR_WIDTH = 256
local WINDOW_ARGS = {
	"Rocket Settings",
	[Iris.Args.Window.NoCollapse] = true,
	[Iris.Args.Window.NoMove] = true,
	[Iris.Args.Window.NoResize] = true,
}

local function SettingsWindow()
	Iris.PushConfig({ WindowPadding = Vector2.zero })
	local viewportSize = Workspace.CurrentCamera.ViewportSize
	local windowSize = viewportSize * 0.8
	local windowState: any = { position = (viewportSize - windowSize) / 2, size = windowSize }
	local window = Iris.Window(WINDOW_ARGS, windowState)
	if window.closed() then
		SettingsStore.isVisible(false)
		window.state.isOpened:set(true)
		Iris.End()
		Iris.PopConfig()
		return
	end

	local layout = Iris.Table({ 2, false, false, false, true, true, true })
	layout.state.widths:set({ SIDEBAR_WIDTH, windowSize.X - SIDEBAR_WIDTH })
	layout.Instance.Size = UDim2.new(layout.Instance.Size.X.Scale, layout.Instance.Size.X.Offset, 1, 0)
		- UDim2.fromOffset(0, Iris._config.CellPadding.Y)
	do
		SettingsSidebar()
		Iris.NextColumn()
		local currentPage = SettingsStore.currentPage() or SettingsPages[1]
		currentPage.render()
	end
	Iris.End()

	Iris.End()
	Iris.PopConfig()
end

return SettingsWindow


export type SettingsPage = {
	id: string,
	title: string,
	tabIcon: string,
	extensionIcon: string,
	render: () -> (),
}

return nil


local Charm = require("@pkgs/Charm")
local Iris = require("@pkgs/Iris")
local Trove = require("@pkgs/Trove")
local types = require("@self/types")

export type Config = types.Config
export type PartialConfig = types.PartialConfig
export type Theme = types.Theme
export type StudioTheme = types.StudioTheme

local theme = {}
theme.themes = table.freeze({
	iris = require("@self/themes/iris"),
})

theme.presets = table.freeze({
	base = require("@self/presets/base"),
	button = table.freeze({
		highlighted = require("@self/presets/button/highlighted"),
		unhighlighted = require("@self/presets/button/unhighlighted"),
		selected = require("@self/presets/button/selected"),
	}),
})

function theme.push(config: PartialConfig)
	Iris.PushConfig(config :: any)
end

function theme.pop()
	Iris.PopConfig()
end

function theme.getStudio(): "dark" | "light"
	local studioBG = settings().Studio.Theme:GetColor(Enum.StudioStyleGuideColor.MainBackground)
	local isDark = studioBG.R + studioBG.G + studioBG.B < 1.5
	return if isDark then "dark" else "light"
end

theme.studio = Charm.atom(theme.getStudio())
function theme.observeStudio(trove: Trove.Trove)
	trove:Add(settings().Studio.ThemeChanged:Connect(function()
		theme.studio(theme.getStudio())
	end))
end

theme.userPreferredTheme = Charm.atom(theme.themes.iris) :: Charm.Atom<Theme | StudioTheme>

theme.selectedUserTheme = Charm.computed(function(): Theme
	local userPreferredTheme = theme.userPreferredTheme()
	local asTheme, asStudio = userPreferredTheme :: Theme, userPreferredTheme :: StudioTheme
	return if asTheme.name then asTheme else (asStudio :: any)[theme.studio()]
end)

theme.config = Charm.computed(function(): PartialConfig
	local config = table.clone(theme.presets.base)
	for key, value in theme.selectedUserTheme() :: { [string]: any } do
		(config :: any)[key] = value
	end
	return config
end)

return table.freeze(theme)


local types = require("../types")

local base: types.PartialConfig = {
	TextTransparency = 0,
	TextDisabledTransparency = 0,

	BorderTransparency = 0.5,
	BorderActiveTransparency = 0.3,

	WindowBgTransparency = 0.06,
	PopupBgTransparency = 0.06,

	ScrollbarGrabTransparency = 0,

	TitleBgTransparency = 0,
	TitleBgActiveTransparency = 0,
	TitleBgCollapsedTransparency = 0.5,

	MenubarBgTransparency = 0,

	FrameBgTransparency = 0.46,
	FrameBgHoveredTransparency = 0.46,
	FrameBgActiveTransparency = 0.33,

	ButtonTransparency = 0.6,
	ButtonHoveredTransparency = 0,
	ButtonActiveTransparency = 0,

	ImageTransparency = 0,

	SliderGrabTransparency = 0,
	SliderGrabActiveTransparency = 0,

	HeaderTransparency = 0.69,
	HeaderHoveredTransparency = 0.2,
	HeaderActiveTransparency = 0,

	TabTransparency = 0.14,
	TabHoveredTransparency = 0.2,
	TabActiveTransparency = 0,

	SelectionImageObjectTransparency = 0.8,
	SelectionImageObjectBorderTransparency = 0,

	TableBorderStrongTransparency = 0,
	TableBorderLightTransparency = 0,
	TableRowBgTransparency = 1,
	TableRowBgAltTransparency = 0.94,
	TableHeaderTransparency = 0,
	NavWindowingHighlightTransparency = 0.3,
	NavWindowingDimBgTransparency = 0.65,

	SeparatorTransparency = 0.5,

	CheckMarkTransparency = 0,

	PlotLinesTransparency = 0,
	PlotLinesHoveredTransparency = 0,
	PlotHistogramTransparency = 0,
	PlotHistogramHoveredTransparency = 0,

	ResizeGripTransparency = 0.8,
	ResizeGripHoveredTransparency = 0.33,
	ResizeGripActiveTransparency = 0.05,

	ItemWidth = UDim.new(1, 0),
	-- ContentWidth = UDim.new(1, 0),
	ContentHeight = UDim.new(0, 0),

	WindowPadding = Vector2.new(4, 4),
	WindowResizePadding = Vector2.new(4, 4),
	FramePadding = Vector2.new(6, 4),
	ItemSpacing = Vector2.new(8, 0),
	ItemInnerSpacing = Vector2.new(8, 8),
	CellPadding = Vector2.new(4, 4),
	DisplaySafeAreaPadding = Vector2.new(8, 8),
	SeparatorTextPadding = Vector2.new(24, 6),
	IndentSpacing = 25,

	TextFont = Font.fromEnum(Enum.Font.SourceSans),
	TextSize = 18,
	FrameBorderSize = 0,
	FrameRounding = 4,
	GrabRounding = 4,
	WindowRounding = 4,
	WindowBorderSize = 1,
	WindowTitleAlign = Enum.LeftRight.Center,
	PopupBorderSize = 1,
	PopupRounding = 4,
	ScrollbarSize = 9,
	GrabMinSize = 14,
	SeparatorTextBorderSize = 4,
	ImageBorderSize = 4,

	UseScreenGUIs = true,
	IgnoreGuiInset = true,
	ScreenInsets = Enum.ScreenInsets.None,
	Parent = nil,
	RichText = true,
	TextWrapped = false,
	DisplayOrderOffset = 127,
	ZIndexOffset = 0,

	MouseDoubleClickTime = 0.30,
	MouseDoubleClickMaxDist = 6.0,

	HoverColor = Color3.fromRGB(255, 255, 0),
	HoverTransparency = 0.1,
}

return base


local types = require("../../types")

local buttonHighlighted: types.PartialConfig = {
	ButtonTransparency = 0.75,
	ButtonHoveredTransparency = 0.75,
	ButtonActiveTransparency = 0.825,
}

return buttonHighlighted


local types = require("../../types")

local buttonSelected: types.PartialConfig = {
	ButtonTransparency = 0.5,
	ButtonHoveredTransparency = 0.5,
	ButtonActiveTransparency = 0.75,
}

return buttonSelected


local types = require("../../types")

local buttonUnhighlighted: types.PartialConfig = {
	ButtonTransparency = 1,
	ButtonHoveredTransparency = 0.75,
	ButtonActiveTransparency = 0.825,
}

return buttonUnhighlighted


local types = require("../types")

local iris = {} :: types.Theme
iris.name = "Iris"

iris.colors = {
	TextColor = Color3.fromRGB(255, 255, 255),
	TextDisabledColor = Color3.fromRGB(128, 128, 128),

	BorderColor = Color3.fromRGB(110, 110, 125),
	BorderActiveColor = Color3.fromRGB(160, 160, 175),

	WindowBgColor = Color3.fromRGB(15, 15, 15),
	PopupBgColor = Color3.fromRGB(20, 20, 20),

	ScrollbarGrabColor = Color3.fromRGB(79, 79, 79),

	TitleBgColor = Color3.fromRGB(10, 10, 10),
	TitleBgActiveColor = Color3.fromRGB(41, 74, 122),
	TitleBgCollapsedColor = Color3.fromRGB(0, 0, 0),

	MenubarBgColor = Color3.fromRGB(36, 36, 36),

	FrameBgColor = Color3.fromRGB(41, 74, 122),
	FrameBgHoveredColor = Color3.fromRGB(66, 150, 250),
	FrameBgActiveColor = Color3.fromRGB(66, 150, 250),

	ButtonColor = Color3.fromRGB(66, 150, 250),
	ButtonHoveredColor = Color3.fromRGB(66, 150, 250),
	ButtonActiveColor = Color3.fromRGB(15, 135, 250),

	ImageColor = Color3.fromRGB(255, 255, 255),

	SliderGrabColor = Color3.fromRGB(66, 150, 250),
	SliderGrabActiveColor = Color3.fromRGB(66, 150, 250),

	HeaderColor = Color3.fromRGB(66, 150, 250),

	HeaderHoveredColor = Color3.fromRGB(66, 150, 250),
	HeaderActiveColor = Color3.fromRGB(66, 150, 250),

	TabColor = Color3.fromRGB(46, 89, 148),
	TabHoveredColor = Color3.fromRGB(66, 150, 250),
	TabActiveColor = Color3.fromRGB(51, 105, 173),

	SelectionImageObjectColor = Color3.fromRGB(255, 255, 255),
	SelectionImageObjectBorderColor = Color3.fromRGB(255, 255, 255),

	TableBorderStrongColor = Color3.fromRGB(79, 79, 89),
	TableBorderLightColor = Color3.fromRGB(59, 59, 64),
	TableRowBgColor = Color3.fromRGB(0, 0, 0),
	TableRowBgAltColor = Color3.fromRGB(255, 255, 255),
	TableHeaderColor = Color3.fromRGB(48, 48, 51),

	NavWindowingHighlightColor = Color3.fromRGB(255, 255, 255),
	NavWindowingDimBgColor = Color3.fromRGB(204, 204, 204),

	SeparatorColor = Color3.fromRGB(110, 110, 128),

	CheckMarkColor = Color3.fromRGB(66, 150, 250),

	PlotLinesColor = Color3.fromRGB(156, 156, 156),
	PlotLinesHoveredColor = Color3.fromRGB(255, 110, 89),
	PlotHistogramColor = Color3.fromRGB(230, 179, 0),
	PlotHistogramHoveredColor = Color3.fromRGB(255, 153, 0),

	ResizeGripColor = Color3.fromRGB(66, 150, 250),
	ResizeGripHoveredColor = Color3.fromRGB(66, 150, 250),
	ResizeGripActiveColor = Color3.fromRGB(66, 150, 250),
}

return table.freeze(iris)


-- selene: allow(unused_variable)
local Iris = require("@pkgs/Iris")
local typeforge = require("@include/typeforge")

export type Config = typeof(Iris._config)
export type PartialConfig = typeforge.Partial<Config>

export type Theme = {
	name: string,
	colors: PartialConfig,
}

export type StudioTheme = {
	dark: Theme,
	light: Theme,
}

return nil


local Boba = require("@include/boba")
local Iris = require("@src/Iris")

local AuthorUser = Boba.Struct {
	type = Boba.Literal("user").inner :: "user",
	id = Boba.Number.inner,
}:Nickname("AuthorUser")

local AuthorCustom = Boba.Struct {
	type = Boba.Literal("custom").inner :: "custom",
	image = Boba.String.inner,
	name = Boba.String.inner,
}:Nickname("AuthorCustom")

local Author = Boba
	.String
	:Or(AuthorUser)
	-- :Or(Boba.Struct {
	-- 	type = Boba.Literal("group").inner :: "group",
	-- 	id = Boba.Number.inner,
	-- })
	:Or(
		AuthorCustom
	)
	:Nickname("Author")

local ExtensionInterface = Boba.Struct {
	id = Boba.String.inner,
	title = Boba.String.inner,
	description = Boba.String:Optional().inner,
	contributors = Author:Array().inner,
	icon = Boba.String.inner,
}:Nickname("ExtensionInterface")

-- TODO: is there a better way to do this...?
local CommandArgument = Boba.Struct {
	type = Boba.Literal("text").inner :: "text",

	name = Boba.String.inner,
	placeholder = Boba.String.inner,
	required = Boba.Boolean:Optional().inner,
}
	:Or(Boba.Struct {
		type = Boba.Literal("instance-class").inner :: "instance-class",

		name = Boba.String.inner,
		placeholder = Boba.String.inner,
		required = Boba.Boolean:Optional().inner,
	}:Or(Boba.Struct {
		type = Boba.Literal("password").inner :: "password",

		name = Boba.String.inner,
		placeholder = Boba.String.inner,
		required = Boba.Boolean:Optional().inner,
	}:Or(Boba.Struct {
		type = Boba.Literal("select").inner :: "select",

		name = Boba.String.inner,
		placeholder = Boba.String.inner,
		required = Boba.Boolean:Optional().inner,

		options = Boba.Array(Boba.Struct {
			title = Boba.String.inner,
			value = Boba.String.inner,
		}).inner,
	})))
	:Nickname("CommandArgument")

local CommandInterface = Boba.Struct {
	id = Boba.String.inner,
	title = Boba.String.inner,
	description = Boba.String.inner,
	contributors = Author:Array():Optional().inner,
	icon = Boba.String:Optional().inner,

	arguments = CommandArgument:Array():Optional().inner,
}:Nickname("CommandInterface")

local CommandOutput = Boba.Struct {
	renderViewport = Boba.Function:Optional():Nickname("() -> ()?").inner :: () -> ()?,

	windowArgs = Boba.Unknown:Array():Optional().inner,
	windowConfig = Boba.String:Map(Boba.Any):Optional().inner,
	renderWindow = Boba.Function:Optional():Nickname("(Iris.Window) -> ()?").inner :: (window: Iris.Window) -> ()?,
}:Nickname("output")

export type Author = typeof(Author.inner)
export type AuthorUser = typeof(AuthorUser.inner)
export type AuthorCustom = typeof(AuthorCustom.inner)
export type ExtensionInterface = typeof(ExtensionInterface.inner)
export type CommandArgument = typeof(CommandArgument.inner)
export type CommandInterface = typeof(CommandInterface.inner)
export type CommandOutput = typeof(CommandOutput.inner)

export type Error = {
	raw: string,
	message: string,
	trace: string,
}

return table.freeze {
	ExtensionInterface = ExtensionInterface,
	CommandArgument = CommandArgument,
	CommandInterface = CommandInterface,
	CommandOutput = CommandOutput,
	Author = Author,
}


local function map<I, O>(array: { I }, mapper: (I) -> O): { O }
	local result = {}
	for i, value in array do
		result[i] = mapper(value)
	end
	return result
end

local function filter<T>(array: { T }, predicate: (T) -> boolean): { T }
	local result = {}
	for _, value in array do
		if predicate(value) then
			table.insert(result, value)
		end
	end
	return result
end

local function slice<T>(array: { T }, min: number, max: number): { T }
	return table.move(array, min, max, 1, {})
end

local function concat<T>(destination: { T }, src: { T }): { T }
	return table.move(src, 1, #src, #destination + 1, destination)
end

local function concatAll<T>(destination: { T }, ...: { T }): { T }
	for i = 1, select("#", ...) do
		destination = concat(destination, select(i, ...))
	end
	return destination
end

return table.freeze({
	map = map,
	filter = filter,
	slice = slice,
	concat = concat,
	concatAll = concatAll,
})


local Charm = require("@pkgs/Charm")
local pluginTrove = require("@src/pluginTrove")

local function set<K, V>(atom: Charm.Atom<{ [K]: V }>, key: K, value: V)
	local new = table.clone(atom())
	new[key] = value
	atom(new)
end

local function push<T>(atom: Charm.Atom<{ T }>, value: T)
	local new = table.clone(atom())
	table.insert(new, value)
	atom(new)
end

local function wrapSetDefaults(atoms: { [string]: Charm.Atom<any> })
	local atomMap = atoms
	local originalValues: { [string]: Charm.Atom<any> } = {}

	for key, atom in atomMap do
		originalValues[key] = atom()
	end

	pluginTrove:Add(function()
		table.clear(originalValues)
		atomMap = nil
	end)

	return function()
		Charm.batch(function(atomMapNow: typeof(atomMap), originalValuesNow: typeof(originalValues))
			for key, value in originalValuesNow do
				atomMapNow[key](value)
			end
		end, atomMap, originalValues)
	end
end

return table.freeze({
	set = set,
	push = push,
	wrapSetDefaults = wrapSetDefaults,
})


local Boba = require("@include/boba")

local Error = Boba.Struct {
	raw = Boba.String.inner,
	message = Boba.String.inner,
	trace = Boba.String.inner,
}

export type Error = typeof(Error.inner)

local function parseError(err: string): Error
	return {
		raw = err,
		message = err:gsub("^.+:%d+:%s*", ""),
		trace = debug.traceback(nil, 2),
	}
end

return table.freeze({
	Error = Error,
	parseError = parseError,
})


local Players = game:GetService("Players")

local THUMBNAIL_NAMES: { [EnumItem]: string } = {
	[Enum.ThumbnailType.HeadShot] = "AvatarHeadShot",
	[Enum.ThumbnailType.AvatarThumbnail] = "Avatar",
	[Enum.ThumbnailType.AvatarBust] = "AvatarBust",
	[Enum.ThumbnailSize.Size48x48] = "48",
	[Enum.ThumbnailSize.Size60x60] = "60",
	[Enum.ThumbnailSize.Size100x100] = "100",
	[Enum.ThumbnailSize.Size150x150] = "150",
	[Enum.ThumbnailSize.Size180x180] = "180",
	[Enum.ThumbnailSize.Size352x352] = "352",
	[Enum.ThumbnailSize.Size420x420] = "420",
}

local function rocket(id: string)
	return `@rocket/{id}`
end

local usernamesById: { [number]: string } = {}
local function fetchUsernameFromId(userId: number)
	usernamesById[userId] = Players:GetNameFromUserIdAsync(userId)
end

local function getUsername(userId: number)
	local existing = usernamesById[userId]
	if existing then
		return existing
	end
	task.spawn(fetchUsernameFromId, userId)
	return tostring(userId)
end

local function getUserThumbnail(userId: number, type: Enum.ThumbnailType, size: Enum.ThumbnailSize): string
	return `rbxthumb://type={THUMBNAIL_NAMES[type]}&id={userId}&w={THUMBNAIL_NAMES[size]}&h={THUMBNAIL_NAMES[size]}`
end
return table.freeze({
	rocket = rocket,
	getUsername = getUsername,
	getUserThumbnail = getUserThumbnail,
})


-- selene: allow(unused_variable)
local function noop(...: any) end

return noop


local ButtonGroup = require("@self/interactions/ButtonGroup")
local InstantInputText = require("@self/interactions/InstantInputText")

local Frame = require("@self/layouts/Frame")
local List = require("@self/layouts/List")
local ScrollingFrame = require("@self/layouts/ScrollingFrame")

local AspectRatio = require("@self/styles/AspectRatio")
local Flex = require("@self/styles/Flex")
local FlexSpacer = require("@self/styles/FlexSpacer")
local Padding = require("@self/styles/Padding")
local Spacer = require("@self/styles/Spacer")

local Header = require("@self/typography/Header")
local Subtext = require("@self/typography/Subtext")

export type ButtonGroupProps = ButtonGroup.ButtonGroupProps
export type InstantInputTextProps = InstantInputText.InstantInputTextProps

export type FrameProps = Frame.FrameProps
export type ListProps<T> = List.ListProps<T>
export type ScrollingFrameProps = ScrollingFrame.ScrollingFrameProps

export type PaddingProps = Padding.PaddingProps

export type HeaderProps = Header.HeaderProps

return table.freeze({
	ButtonGroup = ButtonGroup,
	InstantInputText = InstantInputText,

	Frame = Frame,
	List = List,
	ScrollingFrame = ScrollingFrame,

	AspectRatio = AspectRatio,
	Flex = Flex,
	FlexSpacer = FlexSpacer,
	Padding = Padding,
	Spacer = Spacer,

	Subtext = Subtext,
	Header = Header,
})


local Iris = require("@src/Iris")
local Internal = Iris.Internal
local widgets = Internal._utility

export type ButtonGroupProps = {
	size: UDim2,
}

Internal.WidgetConstructor(
	"ButtonGroup",
	{
		hasState = false,
		hasChildren = true,
		Args = {
			size = 1,
		},
		Events = {
			clicked = widgets.EVENTS.click(function(thisWidget: any)
				return thisWidget.Instance
			end),
			rightClicked = widgets.EVENTS.rightClick(function(thisWidget: any)
				return thisWidget.Instance
			end),
			doubleClicked = widgets.EVENTS.doubleClick(function(thisWidget: any)
				return thisWidget.Instance
			end),
			ctrlClicked = widgets.EVENTS.ctrlClick(function(thisWidget: any)
				return thisWidget.Instance
			end),
			hovered = widgets.EVENTS.hover(function(thisWidget: any)
				return thisWidget.Instance
			end),
		},
		Generate = function(thisWidget)
			local ButtonGroup = Instance.new("TextButton")
			ButtonGroup.Name = "Iris_ButtonGroup"
			ButtonGroup.AutomaticSize = Enum.AutomaticSize.XY
			ButtonGroup.Size = UDim2.fromOffset(0, 0)
			ButtonGroup.BackgroundColor3 = Internal._config.ButtonColor
			ButtonGroup.BackgroundTransparency = Internal._config.ButtonTransparency
			ButtonGroup.AutoButtonColor = false
			ButtonGroup.LayoutOrder = thisWidget.ZIndex
			ButtonGroup.Text = ""

			local list = widgets.UIListLayout(
				ButtonGroup,
				Enum.FillDirection.Horizontal,
				UDim.new(0, Internal._config.ItemSpacing.X)
			)

			list.VerticalAlignment = Enum.VerticalAlignment.Center

			widgets.applyFrameStyle(ButtonGroup)

			widgets.applyInteractionHighlights("Background", ButtonGroup, ButtonGroup, {
				Color = Internal._config.ButtonColor,
				Transparency = Internal._config.ButtonTransparency,
				HoveredColor = Internal._config.ButtonHoveredColor,
				HoveredTransparency = Internal._config.ButtonHoveredTransparency,
				ActiveColor = Internal._config.ButtonActiveColor,
				ActiveTransparency = Internal._config.ButtonActiveTransparency,
			})

			return ButtonGroup
		end,
		Update = function(thisWidget)
			local ButtonGroup = thisWidget.Instance :: TextButton
			ButtonGroup.Size = thisWidget.arguments.size or UDim2.fromOffset(0, 0)
		end,
		ChildAdded = function(thisWidget)
			return thisWidget.Instance
		end,
		Discard = function(thisWidget)
			thisWidget.Instance:Destroy()
		end,
	} :: any
)

local function ButtonGroup(props: ButtonGroupProps): typeof(Iris.Button({})) & typeof(Iris.Group())
	return Internal._Insert("ButtonGroup", { props.size }) :: any
end

return ButtonGroup


local Iris = require("@src/Iris")

export type InstantInputTextProps = {
	label: string?,
	hint: string?,
	captureFocus: boolean?,
	width: UDim?,
}

local function InstantInputText(
	props: InstantInputTextProps,
	initialState: { text: any }?
): (string, typeof(Iris.InputText(nil :: any)), TextBox)
	local hasStyles = props.width ~= nil
	if hasStyles then
		Iris.PushConfig({ ContentWidth = props.width })
	end

	local input = Iris.InputText({
		props.label or "",
		props.hint or "",
	}, initialState :: any)

	if hasStyles then
		Iris.PopConfig()
	end

	local inputInstance = input.Instance:FindFirstChildWhichIsA("TextBox") :: TextBox

	if props.captureFocus then
		inputInstance:CaptureFocus()
	end

	return inputInstance.Text, input, inputInstance
end

return InstantInputText


-- https://discord.com/channels/385151591524597761/1097898141396766771/1455680322279571516

local Iris = require("@src/Iris")
local Internal = Iris.Internal
local widgets = Internal._utility

export type FrameProps = {
	size: UDim2,
	automaticSize: Enum.AutomaticSize,
	verticalAlignment: Enum.VerticalAlignment?,
	horizontalAlignment: Enum.HorizontalAlignment?,
}

Internal.WidgetConstructor(
	"Frame",
	{
		hasState = false,
		hasChildren = true,
		Args = {
			Size = 1,
			AutomaticSize = 2,
			VerticalAlignment = 3,
			HorizontalAlignment = 4,
		},
		Events = {},
		Generate = function(thisWidget)
			local frame = Instance.new("Frame")
			frame.Name = "Iris_Frame"
			frame.AutomaticSize = Enum.AutomaticSize.XY
			frame.Size = UDim2.fromOffset(0, 0)
			frame.BackgroundTransparency = 1
			frame.BorderSizePixel = 0
			frame.LayoutOrder = thisWidget.ZIndex
			frame.ClipsDescendants = true

			widgets.UIListLayout(frame, Enum.FillDirection.Vertical, UDim.new(0, Iris._config.ItemSpacing.Y))

			return frame
		end,
		Update = function(thisWidget: any)
			local frame: Frame = thisWidget.Instance
			local list: UIListLayout = (frame :: any).UIListLayout

			list.VerticalAlignment = thisWidget.arguments.VerticalAlignment or Enum.VerticalAlignment.Top
			list.HorizontalAlignment = thisWidget.arguments.HorizontalAlignment or Enum.HorizontalAlignment.Left
			frame.Size = thisWidget.arguments.Size or UDim2.fromOffset(0, 0)
			frame.AutomaticSize = thisWidget.arguments.AutomaticSize or Enum.AutomaticSize.XY
		end,
		Discard = function(thisWidget)
			thisWidget.Instance:Destroy()
		end,
		ChildAdded = function(thisWidget)
			return thisWidget.Instance
		end,
	} :: any
)

local function Frame(props: FrameProps)
	return Internal._Insert(
		"Frame",
		{ props.size, props.automaticSize, props.verticalAlignment, props.horizontalAlignment }
	)
end

return Frame


-- TODO: virtual lists
-- TODO: pagination
-- FIXME: this leaks

local ButtonGroup = require("../interactions/ButtonGroup")
local Flex = require("../styles/Flex")
local Iris = require("@src/Iris")
local Padding = require("../styles/Padding")
local ScrollingFrame = require("./ScrollingFrame")
local fzy = require("@include/fzy")
local theme = require("@src/theme")
local useKeys = require("@src/core/hooks/useKeys")

export type ListProps<T> = {
	searchPlaceholder: string?,
	onSearchChange: (search: string) -> ()?,
	caseSensitive: boolean?,
	captureFocus: boolean?,
	filterEntries: ((search: string, props: ListProps<T>) -> { T })?,

	entries: { T },
	renderEntry: (entry: T, props: ListProps<T>) -> (),
	getEntryTitle: (entry: T, props: ListProps<T>) -> string,
	getEntryTooltip: ((entry: T, props: ListProps<T>) -> string)?,

	renderNoEntries: (props: ListProps<T>) -> ()?,
	renderNoEntriesFromSearch: (search: string, props: ListProps<T>) -> ()?,

	onEntrySelected: (T) -> ()?,
}

local PREVIOUS_KEYCODE = Enum.KeyCode.Up
local NEXT_KEYCODE = Enum.KeyCode.Down
local SELECT_KEYCODE = Enum.KeyCode.Return

local function defaultFilterEntries(search: string, props: ListProps<any>)
	local entryTitles: { string } = {}
	for i, entry in props.entries do
		entryTitles[i] = props.getEntryTitle(entry, props)
	end

	local filtered = fzy.filter(search, entryTitles, props.caseSensitive or false)
	local result = table.create(#filtered) :: { any }

	local entryScores: { [any]: number } = {}
	for i, entry in filtered do
		local realEntry = props.entries[entry[1]]
		result[i] = realEntry
		entryScores[realEntry] = entry[3]
	end

	table.sort(result, function(lhs, rhs)
		return entryScores[lhs] < entryScores[rhs]
	end)

	return result
end

local function defaultRenderNoEntries(_: ListProps<any>)
	Iris.Text({ "Try searching..." })
end

local function defaultRenderNoEntriesFromSearch(search: string, _props: ListProps<any>)
	Iris.Text({ `No entries for "{search}".` })
end

--[=[
	@function List

	Creates a list layout.
]=]
local function List<T>(props: ListProps<T>)
	Iris.PushConfig({ FrameRounding = 0, FramePadding = Iris._config.FramePadding * 2, ContentWidth = UDim.new(1, 0) })
	local searchInput = Iris.InputText({ "", props.searchPlaceholder or "Search..." })
	local searchInstance = searchInput.Instance:FindFirstChildWhichIsA("TextBox") :: TextBox
	local search = searchInstance.Text
	if props.captureFocus then
		searchInstance:CaptureFocus()
	end
	Iris.PopConfig()

	if props.onSearchChange then
		props.onSearchChange(search)
	end

	local filtered = if search == ""
		then props.entries
		else (props.filterEntries or defaultFilterEntries)(search, props)

	local lenFiltered = #filtered
	local selectedIndex = Iris.State(1)

	local oldSelectedIndex = selectedIndex:get()
	local newSelectedIndex = oldSelectedIndex

	if lenFiltered == 0 then
		newSelectedIndex = 1
	else
		if useKeys.andConsumePress(PREVIOUS_KEYCODE) then
			newSelectedIndex -= 1
		end

		if useKeys.andConsumePress(NEXT_KEYCODE) then
			newSelectedIndex += 1
		end

		newSelectedIndex = math.clamp(newSelectedIndex, 1, lenFiltered)

		if useKeys.andConsumePress(SELECT_KEYCODE) then
			if lenFiltered > 0 and props.onEntrySelected then
				props.onEntrySelected(filtered[newSelectedIndex] or filtered[lenFiltered])
			end
		end
	end

	Iris.Separator()

	if #filtered == 0 then
		if search == nil :: any then
			(props.renderNoEntries or defaultRenderNoEntries)(props)
		else
			(props.renderNoEntriesFromSearch or defaultRenderNoEntriesFromSearch)(search, props)
		end
	else
		Iris.PushConfig({ ItemSpacing = Vector2.new(Iris._config.ItemSpacing.X, 0) })
		local scroller = ScrollingFrame({ size = UDim2.fromScale(1, 0), automaticSize = Enum.AutomaticSize.Y })
		local selectedPosition, selectedSize = Vector2.zero, Vector2.zero
		Flex(Enum.UIFlexMode.Fill)
		Padding({ x = UDim.new(0, Iris._config.CellPadding.X), y = UDim.new(0, Iris._config.CellPadding.Y) })
		local tooltipVisible = false
		for i, entry in filtered do
			local isSelected = i == newSelectedIndex
			Iris.PushConfig(if isSelected then theme.presets.button.highlighted else theme.presets.button.unhighlighted)
			local button = ButtonGroup({ size = UDim2.fromScale(1, 0) })
			local buttonInstance = button.Instance
			buttonInstance.LayoutOrder = i + 100
			if isSelected then
				selectedPosition = buttonInstance.AbsolutePosition
				selectedSize = buttonInstance.AbsoluteSize
			end

			props.renderEntry(entry, props)
			Iris.End()
			Iris.PopConfig()

			if not tooltipVisible and props.getEntryTooltip and button.hovered() then
				tooltipVisible = true
				Iris.PushConfig({ WindowPadding = Iris.Internal._rootConfig.WindowPadding })
				Iris.Tooltip({ props.getEntryTooltip(entry, props) })
				Iris.PopConfig()
			end

			if props.onEntrySelected and button.clicked() then
				props.onEntrySelected(entry)
			end
		end
		Iris.End()
		Iris.PopConfig()

		-- TODO: scrolling down is a bit janky
		local sign = math.sign(newSelectedIndex - oldSelectedIndex)
		if sign ~= 0 then
			local scrollingFrame = scroller.Instance :: ScrollingFrame

			local canvasPosition = scrollingFrame.CanvasPosition
			local frameTop = scrollingFrame.AbsolutePosition.Y + Iris._config.FramePadding.Y
			local frameBottom = frameTop + scrollingFrame.AbsoluteWindowSize.Y + Iris._config.FramePadding.Y

			local itemTop = selectedPosition.Y
			local itemBottom = itemTop + selectedSize.Y

			if itemTop < frameTop then
				scrollingFrame.CanvasPosition = Vector2.new(canvasPosition.X, canvasPosition.Y - (frameTop - itemTop))
			end

			if itemBottom > frameBottom then
				scrollingFrame.CanvasPosition =
					Vector2.new(canvasPosition.X, canvasPosition.Y + (itemBottom - frameBottom) + selectedSize.Y)
			end
		end
	end

	selectedIndex:set(newSelectedIndex)
end

return List


-- https://discord.com/channels/385151591524597761/1097898141396766771/1455680322279571516

local Iris = require("@src/Iris")
local Internal = Iris.Internal
local widgets = Internal._utility

export type ScrollingFrameProps = {
	size: UDim2,
	automaticSize: Enum.AutomaticSize,
	verticalAlignment: Enum.VerticalAlignment?,
	horizontalAlignment: Enum.HorizontalAlignment?,
}

Internal.WidgetConstructor(
	"ScrollingFrame",
	{
		hasState = false,
		hasChildren = true,
		Args = {
			Size = 1,
			AutomaticSize = 2,
			VerticalAlignment = 3,
			HorizontalAlignment = 4,
		},
		Events = {},
		Generate = function(thisWidget)
			local frame = Instance.new("ScrollingFrame")
			frame.Name = "Iris_ScrollingFrame"
			frame.AutomaticSize = Enum.AutomaticSize.XY
			frame.AutomaticCanvasSize = Enum.AutomaticSize.Y
			frame.CanvasSize = UDim2.new(0, 0)
			frame.Size = UDim2.new(0, 0)
			frame.BackgroundTransparency = 1
			frame.BorderSizePixel = 0
			frame.LayoutOrder = thisWidget.ZIndex
			frame.ClipsDescendants = true

			frame.VerticalScrollBarInset = Enum.ScrollBarInset.ScrollBar
			frame.TopImage = widgets.ICONS.BLANK_SQUARE
			frame.MidImage = widgets.ICONS.BLANK_SQUARE
			frame.BottomImage = widgets.ICONS.BLANK_SQUARE
			frame.ScrollBarImageColor3 = Iris._config.ScrollbarGrabColor
			frame.ScrollBarThickness = Iris._config.ScrollbarSize
			frame.ScrollBarImageTransparency = Iris._config.ScrollbarGrabTransparency

			widgets.UIListLayout(frame, Enum.FillDirection.Vertical, UDim.new(0, Internal._config.ItemSpacing.Y))

			return frame
		end,
		Update = function(thisWidget)
			local frame: ScrollingFrame = thisWidget.Instance
			local list: UIListLayout = (frame :: any).UIListLayout

			list.VerticalAlignment = thisWidget.arguments.VerticalAlignment or Enum.VerticalAlignment.Top
			list.HorizontalAlignment = thisWidget.arguments.HorizontalAlignment or Enum.HorizontalAlignment.Left
			frame.Size = thisWidget.arguments.Size or UDim2.fromOffset(0, 0)
			frame.AutomaticSize = thisWidget.arguments.AutomaticSize or Enum.AutomaticSize.XY
		end,
		GenerateState = function(thisWidget)
			if thisWidget.state.scrollPosition == nil then
				thisWidget.state.scrollPosition = Internal._widgetState(thisWidget, "scrollPosition", Vector2.zero)
			end
		end,
		UpdateState = function(thisWidget)
			local frame: ScrollingFrame = thisWidget.Instance
			thisWidget.state.scrollPosition:set(frame.CanvasPosition)
		end,
		Discard = function(thisWidget)
			thisWidget.Instance:Destroy()
		end,
		ChildAdded = function(thisWidget)
			return thisWidget.Instance
		end,
	} :: any
)

local function ScrollingFrame(props: ScrollingFrameProps)
	return Internal._Insert(
		"ScrollingFrame",
		{ props.size, props.automaticSize, props.verticalAlignment, props.horizontalAlignment }
	)
end

return ScrollingFrame


local Iris = require("@src/Iris")

local function AspectRatio(ratio: number?, type: Enum.AspectType?)
	local instance = Instance.new("UIAspectRatioConstraint")
	instance.Name = "AspectRatio"
	instance.AspectRatio = ratio or 1
	instance.AspectType = type or Enum.AspectType.FitWithinMaxSize
	Iris.Append(instance)
end

return AspectRatio


local Iris = require("@src/Iris")

local function Flex(mode: Enum.UIFlexMode?)
	local instance = Instance.new("UIFlexItem")
	instance.Name = "Flex"
	instance.FlexMode = mode or Enum.UIFlexMode.None

	Iris.Append(instance)
end

return Flex


local Flex = require("./Flex")
local Iris = require("@src/Iris")

local function FlexSpacer(mode: Enum.UIFlexMode?)
	Iris.Group()
	Flex(mode)
	Iris.End()
end

return FlexSpacer


local Iris = require("@src/Iris")

export type PaddingProps = {
	x: UDim?,
	y: UDim?,
	top: UDim?,
	bottom: UDim?,
	left: UDim?,
	right: UDim?,
	all: UDim?,
}

local ZERO_UDIM = UDim.new()

local function Padding(props: PaddingProps)
	local instance = Instance.new("UIPadding")
	instance.Name = "Padding"

	instance.PaddingTop = props.top or props.y or props.all or ZERO_UDIM
	instance.PaddingBottom = props.bottom or props.y or props.all or ZERO_UDIM
	instance.PaddingLeft = props.left or props.x or props.all or ZERO_UDIM
	instance.PaddingRight = props.right or props.x or props.all or ZERO_UDIM

	Iris.Append(instance)
end

return Padding


local Iris = require("@src/Iris")

local function Spacer(spacing: Vector2 | number | nil)
	Iris.Image({
		"rbxassetid://-1",
		UDim2.fromOffset(
			(if typeof(spacing) == "Vector2" then spacing.X else spacing) or 8,
			(if typeof(spacing) == "Vector2" then spacing.Y else spacing) or 8
		),
	})
end

return Spacer


local Iris = require("@src/Iris")

export type HeaderProps = {
	text: string,
	level: number?,
}

local function Header(props: HeaderProps)
	local minSize = Iris._config.TextSize
	local decrementSize = ((props.level or 1) - 1) * 2
	local maxSize = minSize * 3

	Iris.PushConfig({
		TextSize = math.max(maxSize - decrementSize, minSize),
		TextFont = Font.fromName("SourceSansPro", Enum.FontWeight.Bold),
	})
	Iris.Text({ props.text })
	Iris.PopConfig()
end

return Header


local Iris = require("@src/Iris")

local function Subtext(props: { string })
	Iris.PushConfig({ TextTransparency = 0.5 })
	Iris.Text({ table.concat(props, " ") })
	Iris.PopConfig()
end

return Subtext


local process = require("@lune/process")
local fs = require("@lune/fs")
local task = require("@lune/task")

local logger = require("./lib/logger").new(4)
local executeCommand = require("./lib/executeCommand")

local didError = false

local function execCommandWrapper(...)
	local data = executeCommand(...)

	if data.stderr ~= "" then
		didError = true
	end

	return data
end

local function recursiveCopy(src, dest)
	local files = fs.readDir(src)

	for _, file in files do
		local path = src .. "/" .. file
		local meta = fs.metadata(path)

		if meta.kind == "file" then
			fs.writeDir(dest)
			fs.writeFile(`{dest}/{file}`, fs.readFile(path))
		elseif meta.kind == "dir" then
			recursiveCopy(path, `{dest}/{file}`)
		end
	end
end

if not fs.isDir("packages") and not fs.isDir("Packages") and fs.isFile("wally.toml") then
	execCommandWrapper("lune", { "run", "install-packages" })
end

local hasDarkLua = fs.isFile(".darklua.json")

execCommandWrapper("rojo", { "sourcemap", "sourcemap.project.json", "-o", "sourcemap.json" }, {
	stdio = "inherit",
})

if hasDarkLua then
	execCommandWrapper("darklua", { "process", "src", "temp", "-c", ".darklua.json" })
end

-- get rid of stories; no need for those to be in the actual build
recursiveCopy("temp", "dist")

if fs.isDir("dist/stories") then
	fs.removeDir("dist/stories")
end

task.delay(1, function()
	if not didError then
		execCommandWrapper("rojo", { "build", "default.project.json", "-o", "build.rbxm" })
		logger:success("Build complete!")
	else
		logger:error("Build failed due to errors.")
	end
end)


local process = require("@lune/process")
local fs = require("@lune/fs")
local task = require("@lune/task")

local logger = require("./lib/logger").new(4)
local fileWatcher = require("./lib/fileWatcher")
local executeCommand = require("./lib/executeCommand")

local function recursiveCopy(src, dest)
	local files = fs.readDir(src)

	for _, file in files do
		local path = src .. "/" .. file

		if fs.isFile(path) then
			fs.writeDir(dest)
			fs.writeFile(`{dest}/{file}`, fs.readFile(path))
		elseif fs.isDir(path) then
			recursiveCopy(path, `{dest}/{file}`)
		end
	end
end

local hasDarkLua = fs.isFile(".darklua.json")

local function processFiles()
	if fs.isFile(".darklua.json") then
		-- clone src as build
		recursiveCopy("src", "temp")
		executeCommand("rojo", { "sourcemap", "sourcemap.project.json", "-o", "sourcemap.json" })

		-- optionally, place custom processing steps here. these steps should modify files in the temp directory

		process.env.RBLX_DEV = "true"
		local status = executeCommand("darklua", { "process", "src", "temp", "-c", ".darklua_dev.json" })

		if status.code == 0 then
			recursiveCopy("temp", "dist")
		else
			logger:error("Did not update files due to Darklua error, check console for more information.")
		end

		fs.removeDir("temp")
	end
end

processFiles()

logger:success("Watching for file changes..")

task.spawn(fileWatcher, "./src", function(files)
	logger:info(`Detected changes in {#files} file(s)..`)

	local success, err = pcall(function()
		processFiles()
	end)

	if not success then
		logger:error(`Failed to process files: {err}`)
	end
end, function(files)
	if not hasDarkLua then
		return
	end

	for _, file in files do
		logger:info(`{file} deleted, removing from build..`)

		if file:sub(1, 6) == "./src/" then
			file = file:sub(7)
		end

		if fs.isFile(`dist/{file}`) then
			fs.removeFile(`dist/{file}`)
		elseif fs.isDir(`dist/{file}`) then
			fs.removeDir(`dist/{file}`)
		end
	end
end)

executeCommand("rojo", { "serve", "dev.project.json" }, {
	stdio = "inherit",
})


local fs = require("@lune/fs")

local logger = require("./lib/logger").new(4)
local executeCommand = require("./lib/executeCommand")

if fs.isDir("packages") then
	fs.removeDir("packages")
end

local status = executeCommand("wally", {
	"install",
}, {
    stdio = "inherit"
}, true)

if not status.ok then
    logger:error("Failed to install packages!")
    return
end

if not fs.isDir("packages") then
    fs.writeDir("packages")
end

-- do this so the project file doesn't refer to files that don't exist and cause rojo to fail
if fs.isDir(".zap") then
	executeCommand("zap", { "src/server/network.zap" })
end

executeCommand("rojo", {
	"sourcemap",
	fs.isFile("sourcemap.project.json") and "sourcemap.project.json" or "default.project.json",
	"-o",
	"sourcemap.json",
})

if fs.isDir("Packages") then
	executeCommand("wally-package-types", {
		"--sourcemap",
		"sourcemap.json",
		"./Packages",
	}, nil, true)

    fs.move("./Packages", "./packages", true)
end

logger:info("Installed packages!")


local process = require("@lune/process")

local logger = require("./logger").new(4)

return function(command, args, options, ignoreErrors)
	local data = process.spawn(command, args, options or {
		stdio = "default",
	})

	if data.stderr ~= "" and (not ignoreErrors or data.code ~= 0) then
		logger:error(`Error occurred when running \"{command}\" (code {data.code}): \n{data.stderr}`)
	end

	return data
end


local fs = require("@lune/fs")
local date = require("@lune/datetime")
local task = require("@lune/task")

type fileMap = {
	path: string,
	children: { fileMap | string },
}

return function(directory: string, callback: (files: { string }) -> any, onDelete: (files: { string }) -> any)
	local lastModified: number = nil :: any
	local lastMap: fileMap = {
		path = directory,
		children = {},
	}

	local function scanFiles(dir: string, map: fileMap)
		local files = fs.readDir(dir)
		local modified = {}

		for _, file in files do
			local path = dir .. "/" .. file
			local meta = fs.metadata(path)

			local childValue = meta.kind == "file" and path or {
				path = path,
				children = {},
			}

			table.insert(map.children, childValue)

			if meta.kind == "file" then
				if lastModified and meta.modifiedAt.unixTimestamp > lastModified then
					table.insert(modified, path)
				end
			elseif meta.kind == "dir" then
				local subModified = scanFiles(path, childValue :: fileMap)

				for _, subPath in subModified do
					table.insert(modified, subPath)
				end
			end
		end

		return modified
	end

	while true do
		local currentMap = {
			path = directory,
			children = {},
		}

		local modified = scanFiles(directory, currentMap)

		if #modified > 0 then
			lastModified = date.now().unixTimestamp -- placing this statement below could result in updates being skipped
			callback(modified)
		elseif lastModified == nil then
			lastModified = date.now().unixTimestamp - 3
		end

		local deleted = {}

		-- recursively iterate over the last map and check if the file is in the current map (so do NOT use fs.exists)
		local function checkDeleted(map: fileMap, comparisonMap: fileMap)
			for _, child in map.children do
				if type(child) == "string" and table.find(comparisonMap.children, child) == nil then
					table.insert(deleted, child)
				elseif type(child) == "table" then
					local found = false

					for _, comparisonChild in comparisonMap.children do
						if type(comparisonChild) == "table" and comparisonChild.path == child.path then
							found = true
							checkDeleted(child, comparisonChild)
							break
						end
					end

					if not found then
						table.insert(deleted, child.path)
					end
				end
			end
		end

		checkDeleted(lastMap, currentMap)

		if #deleted > 0 then
			onDelete(deleted)
		end

		lastMap = currentMap
		task.wait(1)
	end
end


local logger = {}

local terminal = require("./terminal")
local chalk = require("./chalk")
local dateTime = require("@lune/datetime")

local function padToTwo(number: number): string
	return number < 10 and `0{number}` or tostring(number)
end

local function generateTimestamp(): string
	local time = dateTime.now():toLocalTime()
	return chalk.dim(`[{padToTwo(time.hour)}:{padToTwo(time.minute)}:{padToTwo(time.second)}]`)
end

local logger_templates = {
	info = chalk.green("INFO"),
	warn = chalk.yellow("WARN"),
	error = chalk.red("ERROR"),
	success = chalk.green("SUCCESS"),
	debug = chalk.magenta("DEBUG"),
}

-- Types
export type logger = typeof(setmetatable(
	{} :: {
		logLevel: number,
	},
	{ __index = logger }
))

function logger.new(logLevel: number): logger
	return setmetatable({
		logLevel = logLevel,
	}, { __index = logger })
end

function logger:error(message: string, ...: string)
	local formattedMessage = string.format(message, ...)

	if self.logLevel >= 1 then
		terminal.write(`{generateTimestamp()} {logger_templates.error} {chalk.white(formattedMessage)}`)
	end
end

function logger:warn(message: string, ...: string)
	local formattedMessage = string.format(message, ...)

	if self.logLevel >= 2 then
		terminal.write(`{generateTimestamp()} {logger_templates.warn} {chalk.white(formattedMessage)}`)
	end
end

function logger:info(message: string, ...: string)
	local formattedMessage = string.format(message, ...)

	if self.logLevel >= 3 then
		terminal.write(`{generateTimestamp()} {logger_templates.info} {chalk.white(formattedMessage)}`)
	end
end

function logger:success(message: string, ...: string)
	local formattedMessage = string.format(message, ...)

	if self.logLevel >= 3 then
		terminal.write(`{generateTimestamp()} {logger_templates.success} {chalk.white(formattedMessage)}`)
	end
end

function logger:debug(message: string, ...: string)
	local formattedMessage = string.format(message, ...)

	if self.logLevel >= 4 then
		terminal.write(`{generateTimestamp()} {logger_templates.debug} {chalk.white(formattedMessage)}`)
	end
end

return logger


local HttpService = game:GetService("HttpService")
local Selection = game:GetService("Selection")

local fusion = require("@packages/fusion")
local Spring = fusion.Spring

local Children = fusion.Children
local Cleanup = fusion.Cleanup
local New = fusion.New
local Out = fusion.Out
local Ref = fusion.Ref

local Clean = fusion.cleanup
local Computed = fusion.Computed
local ForPairs = fusion.ForPairs
local Observer = fusion.Observer
local Value = fusion.Value

local fusionComponents = require("@packages/fusionComponents")
local button = fusionComponents.common.button
local input = fusionComponents.common.input
local frame = fusionComponents.base.frame
local tooltip = fusionComponents.common.tooltip
local modal = fusionComponents.common.modal
local selectMenu = fusionComponents.common.selectMenu
local canvasFrame = fusionComponents.utility.canvasFrame
local scrollingFrame = fusionComponents.common.scrollingFrame
local baseText = fusionComponents.base.text

local fusionUtils = require("@packages/fusionUtils")
local topLayerProvider = fusionUtils.topLayerProvider

local signal = require("@packages/signal")
local tailwind = require("@packages/tailwind")

local theme = require("@src/theme")

local historyHandler = require("@src/lib/historyHandler")
local settingsManager = require("@src/lib/settingsManager")
local classDataHandler = require("@src/lib/classDataHandler")

local newInstanceModal = require("./newInstanceModal")
local settingsMenu = require("./settings")
local appTopbar = require("@components/appTopbar")
local instanceTreeRoot = require("@components/instanceTreeRoot")
local widget = require("@components/widget")
local texturePicker = require("@components/propertyFields/texturePicker/editor")
local standaloneScroller = require("@components/standaloneScroller")

type props = {
	Items: fusion.Value<{ Instance }>,
}

local ENABLE_VIRTUAL_SCROLLER = settingsManager.getReactiveWithFallback("treeview_virtual_scroller", true)
local LIGHTWEIGHT_MODE = settingsManager.getReactiveWithFallback("lightweight_mode", false)

local function App(props: props)
	local useColor = theme:get("App", "gray", "default", "Base")

	local _, firstValue = next(props.Items:get(false))
	local selectedInstance = Value(firstValue or nil)
	local childrenReplacement = Value({}) -- for lightweight mode

	local searchQuery = Value("")
	local searchMode = Value({ "properties" })

	local newInstanceTarget = nil
	local isPlusModalVisible = Value(false)

	local settingsMenuVisible = Value(false)
	local settingsMenuTransparency = Spring(
		Computed(function()
			return settingsMenuVisible:get() and 0 or 1
		end),
		30,
		1
	)

	local sharedContextMenuState = Value(false)
	local listContentSize = Value()
	local scrollFrame = scrollingFrame({
		Content = {
			New("UIListLayout")({
				FillDirection = Enum.FillDirection.Vertical,
				HorizontalAlignment = Enum.HorizontalAlignment.Left,
				Padding = UDim.new(0, 4),
				SortOrder = Enum.SortOrder.LayoutOrder,
				VerticalAlignment = Enum.VerticalAlignment.Top,
				[Out("AbsoluteContentSize")] = listContentSize,
			}),
			New("UIPadding")({
				PaddingRight = UDim.new(0, 4),
			}),
			ForPairs(props.Items, function(index, value)
				local events = {
					OnItemClicked = signal.new(),
					OnPlusClicked = signal.new(),
					AddTab = signal.new(),
				} :: instanceTreeRoot.events

				-- no need to clean these up; instancetreeroot cleans up all of the signals when destroyed
				events.OnPlusClicked:Connect(function(instance)
					newInstanceTarget = instance
					isPlusModalVisible:set(true)
				end)

				events.AddTab:Connect(function(instance)
					local oldItems = table.clone(props.Items:get())

					if table.find(oldItems, instance) then
						return
					end

					table.insert(oldItems, instance)
					props.Items:set(oldItems)
					selectedInstance:set(instance)
				end)

				local isSelected = Computed(function()
					return selectedInstance:get() == value or LIGHTWEIGHT_MODE:get()
				end)

				local maxDepth = LIGHTWEIGHT_MODE:get() and 0
					or settingsManager.getReactiveWithFallback("max_depth", 3):get()

				local isDestroyed = Value(false)

				return index,
					instanceTreeRoot({
						RootInstance = value,
						Query = searchQuery,
						ChildrenReplacement = LIGHTWEIGHT_MODE:get() and childrenReplacement or nil,
						Events = events,
						IsSelfSelected = isSelected,
						MaxDepth = maxDepth,
						SearchMode = searchMode,
						SharedContextMenuState = sharedContextMenuState,
						Visible = isSelected,
						IsDestroyed = isDestroyed,
						[Cleanup] = {
							Selection.SelectionChanged:Connect(function()
								events.OnItemClicked:Fire(Selection:Get()[1])
							end),
							events,
							function()
								isDestroyed:set(true)
							end,
						},
					})
			end, Clean),
		},
		ScrollingFrameProps = {
			VerticalScrollBarInset = Enum.ScrollBarInset.ScrollBar,
			CanvasSize = Computed(function()
				if listContentSize:get() == nil then
					return UDim2.new(0, 0, 0, 0)
				end

				return UDim2.new(0, 0, 0, listContentSize:get().Y)
			end),
		},
		Size = UDim2.new(1, 0, 1, 0),
	})

	local instanceTreeContainerFrame = topLayerProvider.new(frame({
		Name = "InstanceTreeContainer",
		Size = Computed(function()
			if LIGHTWEIGHT_MODE:get() then
				-- topbar goes away, so we get +32 - 4 px extra
				return UDim2.new(1, -8, 1, -40)
			end

			return UDim2.new(1, -8, 1, -72)
		end),
		AnchorPoint = Vector2.new(0.5, 0),
		Position = UDim2.new(0.5, 0, 0, 0),
		Appearance = useColor("TreeBackground", true),
		Stroke = useColor("Stroke", true),
		Padding = UDim.new(0, 4),
		Content = {
			Computed(function()
				if LIGHTWEIGHT_MODE:get() then
					return nil
				end

				local countedItems = 0

				for _ in props.Items:get() do
					countedItems = countedItems + 1
				end

				if countedItems > 0 then
					return
				end

				-- text + button to automatically create a new instance
				-- this instance is just a part + attachment created near in front of the camera
				-- it's automatically added and selected
				return New("Frame")({
					Name = "NoItemPrompt",
					BackgroundTransparency = 1,
					Size = UDim2.new(1, 0, 1, 0),
					[Children] = {
						New("UIListLayout")({
							FillDirection = Enum.FillDirection.Vertical,
							HorizontalAlignment = Enum.HorizontalAlignment.Center,
							Padding = UDim.new(0, 8),
							SortOrder = Enum.SortOrder.LayoutOrder,
							VerticalAlignment = Enum.VerticalAlignment.Center,
						}),
						baseText({
							Appearance = useColor("Text", true),
							Text = {
								Label = "There are no selected items. Click the plus button in the top right to add your currently selected instance to the editor, or click the button below to quickly create a basic instance.",
								TextSize = 18,
								Font = theme.global.font,
							},
							TextWrapped = true,
							Size = UDim2.new(1, 0, 0, 24),
							AutomaticSize = Enum.AutomaticSize.Y,
							TextXAlignment = Enum.TextXAlignment.Center,
						}),
						button({
							Color = "white",
							Variant = "solid",
							AutomaticSize = Enum.AutomaticSize.XY,
							ButtonText = "Create New Instance",
							OnClick = function()
								local instance

								historyHandler("create base instance", function()
									instance = Instance.new("Part")
									instance.Size = Vector3.new(1, 1, 1)
									instance.Position = (workspace.CurrentCamera.CFrame * CFrame.new(0, 0, -5)).Position
									instance.Anchored = true
									instance.Transparency = 1

									local attachment = Instance.new("Attachment")
									attachment.Parent = instance

									local particleEmitter = Instance.new("ParticleEmitter")
									particleEmitter.Parent = attachment
									instance.Parent = workspace
								end)

								props.Items:set({ instance })
								selectedInstance:set(instance)
							end,
						}),
					},
				})
			end, Clean),
			ENABLE_VIRTUAL_SCROLLER:get() and standaloneScroller.frame(scrollFrame) or scrollFrame,
		},
	}))

	modal(instanceTreeContainerFrame, {
		Visible = isPlusModalVisible,
		Content = newInstanceModal({
			useColor = useColor,
			OnClose = function()
				isPlusModalVisible:set(false)
			end,
			OnCreate = function(name, class)
				isPlusModalVisible:set(false)

				historyHandler("Create Instance", function()
					local new = Instance.new(class)
					new.Name = name
					new.Parent = newInstanceTarget
				end)
			end,
		}),
		OnClickBesides = function()
			isPlusModalVisible:set(false)
		end,
		Padding = UDim.new(0, 8),
	})

	local isWidgetEnabled = Value(false)
	local textureWidget

	local topbarContainer = Value()
	local inputContainer = Value()

	local buttonContainer = New("Frame")({
		Name = "Buttons",
		AutomaticSize = Enum.AutomaticSize.XY,
		BackgroundTransparency = 1,
		Size = UDim2.new(0, 0, 1, 0),
		[Children] = {
			New("UIListLayout")({
				FillDirection = Enum.FillDirection.Horizontal,
				HorizontalAlignment = Enum.HorizontalAlignment.Left,
				Padding = UDim.new(0, 4),
				SortOrder = Enum.SortOrder.LayoutOrder,
				VerticalAlignment = Enum.VerticalAlignment.Center,
			}),
			Computed(function()
				if LIGHTWEIGHT_MODE:get() then
					local meetsCondition = false

					if typeof(childrenReplacement:get()) == "table" then
						for _, instance in childrenReplacement:get() do
							local _, className = classDataHandler.getProperties(instance)

							if className ~= "trail" and className ~= nil then
								meetsCondition = true
								break
							end
						end

						return
					end

					if meetsCondition then
						return tooltip(
							button({
								Color = "white",
								Variant = "ghost",
								Icon = "play",
								LayoutOrder = -1,
								OnClick = function()
									if typeof(childrenReplacement:get()) == "table" then
										for _, instance in selectedInstance:get(false) do
											classDataHandler.emit(instance)
										end
									else
										classDataHandler.emit(selectedInstance:get(false))
									end
								end,
							}),
							{
								Variant = "default",
								Text = "Emit",
								Placement = "bottom",
							}
						)
					end

					return
				end

				return button({
					Color = "white",
					Variant = "ghost",
					Icon = "plus",
					LayoutOrder = -1,
					OnClick = function()
						if Selection:Get()[1] then
							-- create new tab with selected instance
							local oldItems = table.clone(props.Items:get())
							local newItem = Selection:Get()[1]

							if table.find(oldItems, newItem) then
								return
							end

							table.insert(oldItems, newItem)
							props.Items:set(oldItems)
						end
					end,
				})
			end, Clean),
			button({
				Color = "white",
				Variant = "ghost",
				Icon = "images",
				OnClick = function()
					if textureWidget == nil then
						textureWidget = widget({
							Name = "Texture Storage",
							Id = HttpService:GenerateGUID(),
							InitialDockTo = Enum.InitialDockState.Float,
							InitialEnabled = false,
							ForceInitialEnabled = true,
							FloatingSize = Vector2.new(575, 408),
							MinimumSize = Vector2.new(400, 300),
							Enabled = isWidgetEnabled,
							[Children] = {
								Computed(function()
									local widgetEnabled = isWidgetEnabled:get()

									if widgetEnabled then
										return ({
											topLayerProvider.new(texturePicker({
												IsSelectingImage = false,
												Path = Value({ "Home" }),
											})),
										})[1]
									end

									return
								end, Clean),
							},
						})
					end

					isWidgetEnabled:set(true)
				end,
			}),
			button({
				Color = "white",
				Variant = "ghost",
				Icon = "settings",
				OnClick = function()
					settingsMenuVisible:set(true)
				end,
			}),
		},
	})

	local function updateButtonParent()
		task.delay(0.1, function()
			if LIGHTWEIGHT_MODE:get() then
				buttonContainer.Parent = inputContainer:get()
			else
				buttonContainer.Parent = topbarContainer:get()
			end
		end)
	end

	local selectionChangedConnection

	local function updateSelectionMode()
		if selectionChangedConnection then
			selectionChangedConnection:Disconnect()
			selectionChangedConnection = nil
		end

		if LIGHTWEIGHT_MODE:get() then
			childrenReplacement:set(Selection:Get())

			selectionChangedConnection = Selection.SelectionChanged:Connect(function()
				local currentSelection = Selection:Get()

				if #currentSelection == 0 then
					return
				end

				childrenReplacement:set(currentSelection)
			end)
		end
	end

	updateSelectionMode()

	local component = topLayerProvider.new(New("Frame")({
		Name = "App",
		BackgroundTransparency = 1,
		Size = UDim2.new(1, 0, 1, 0),
		[Cleanup] = {
			function()
				if textureWidget then
					textureWidget:Destroy()
				end
			end,
			Observer(LIGHTWEIGHT_MODE):onChange(updateSelectionMode),
			Observer(LIGHTWEIGHT_MODE):onChange(updateButtonParent),
			selectionChangedConnection,
			buttonContainer,
		},
		[Children] = {
			canvasFrame({
				Name = "SettingsContainer",
				BackgroundTransparency = 1,
				Size = UDim2.new(1, 0, 1, 0),
				ZIndex = 2,
				GroupTransparency = settingsMenuTransparency,
				Children = settingsMenu({
					SettingsVisible = settingsMenuVisible,
					useColor = useColor,
				}),
			}),
			New("Frame")({
				Name = "MainAppContainer",
				BackgroundTransparency = 1,
				Size = UDim2.new(1, 0, 1, 0),
				Visible = Computed(function()
					if settingsMenuTransparency:get() < 0.1 then
						return false
					end

					return true
				end),
				[Children] = {
					New("UIListLayout")({
						FillDirection = Enum.FillDirection.Vertical,
						HorizontalAlignment = Enum.HorizontalAlignment.Center,
						Padding = UDim.new(0, 4),
						SortOrder = Enum.SortOrder.LayoutOrder,
						VerticalAlignment = Enum.VerticalAlignment.Top,
					}),
					Computed(function()
						if LIGHTWEIGHT_MODE:get() then
							return New("UIPadding")({
								PaddingTop = UDim.new(0, 4),
							})
						end

						return frame({
							Name = "TopbarContainer",
							Appearance = useColor("TopbarBackground", true),
							Size = UDim2.new(1, 0, 0, 32),
							LayoutOrder = -1,
							Content = {
								New("Frame")({
									AnchorPoint = Vector2.new(0, 1),
									BackgroundColor3 = Computed(function()
										return useColor("Stroke").color
									end),
									Position = UDim2.new(0, 0, 1, 0),
									Size = UDim2.new(1, 0, 0, 1),
								}),
								New("Frame")({
									Name = "Topbar",
									BackgroundTransparency = 1,
									Size = UDim2.new(1, 0, 1, 0),
									[Ref] = topbarContainer,
									[Children] = {
										New("UIPadding")({
											PaddingBottom = UDim.new(0, 2),
											PaddingLeft = UDim.new(0, 2),
											PaddingRight = UDim.new(0, 2),
											PaddingTop = UDim.new(0, 2),
										}),
										New("UIListLayout")({
											FillDirection = Enum.FillDirection.Horizontal,
											HorizontalAlignment = Enum.HorizontalAlignment.Left,
											Padding = UDim.new(0, 4),
											SortOrder = Enum.SortOrder.LayoutOrder,
											VerticalAlignment = Enum.VerticalAlignment.Center,
										}),
										appTopbar({
											Items = props.Items,
											LayoutOrder = -1,
											SelectedInstance = selectedInstance,
											SharedContextMenuState = sharedContextMenuState,
										}),
									},
								}),
							},
						})
					end, function(...)
						if LIGHTWEIGHT_MODE:get() then
							-- to prevent it from being cleaned up
							buttonContainer.Parent = inputContainer:get()
						end

						return Clean(...)
					end),
					New("Frame")({
						Name = "InputContainer",
						BackgroundTransparency = 1,
						Size = UDim2.new(1, 0, 0, 28),
						[Ref] = inputContainer,
						[Children] = {
							New("UIPadding")({
								PaddingBottom = UDim.new(0, 2),
								PaddingLeft = UDim.new(0, 4),
								PaddingRight = UDim.new(0, 4),
								PaddingTop = UDim.new(0, 2),
							}),
							New("UIListLayout")({
								FillDirection = Enum.FillDirection.Horizontal,
								HorizontalAlignment = Enum.HorizontalAlignment.Left,
								HorizontalFlex = Enum.UIFlexAlignment.SpaceBetween,
								Padding = UDim.new(0, 4),
								SortOrder = Enum.SortOrder.LayoutOrder,
								VerticalAlignment = Enum.VerticalAlignment.Center,
							}),
							Computed(function()
								if LIGHTWEIGHT_MODE:get() then
									searchMode:set({ "properties" })
									return nil
								end

								return selectMenu({
									Color = "gray",
									Variant = "default",
									AppearanceOverride = {
										_global = {
											Background = Computed(function()
												return theme.global.isDark:get() and tailwind.neutral[800]
													or Color3.fromRGB(255, 255, 255)
											end),
										},
									},
									Size = UDim2.new(0, 128, 1, 0),
									MaxHeight = 2,
									SelectedOptions = searchMode,
									OnOptionClicked = function(id, isVisible)
										searchMode:set({ id })
										isVisible:set(false)
									end,
									Options = Computed(function()
										return {
											{
												Id = "properties",
												Label = "Properties",
												Icon = "table-properties",
											},
											not LIGHTWEIGHT_MODE:get() and {
												Id = "instance",
												Label = "Instance",
												Icon = "cuboid",
											} or nil,
										}
									end),
									LayoutOrder = -1,
								})
							end, Clean),
							input({
								Color = "gray",
								Variant = "default",
								AppearanceOverride = {
									_global = {
										Background = Computed(function()
											return theme.global.isDark:get() and tailwind.neutral[800]
												or Color3.fromRGB(255, 255, 255)
										end),
									},
								},
								Placeholder = Computed(function()
									return searchMode:get()[1] == "instance" and "Search instances.."
										or "Search properties.."
								end),
								Icon = "search",
								Size = UDim2.new(1, -132, 1, 0),
								Text = searchQuery,
								OnTextChanged = function(text)
									searchQuery:set(text)
								end,
							}),
						},
					}),
					instanceTreeContainerFrame,
				},
			}),
		},
	}))

	updateButtonParent()

	return component
end

return App


local fusion = require("@packages/fusion")
local Children = fusion.Children
local New = fusion.New

local Clean = fusion.cleanup
local Computed = fusion.Computed
local ForPairs = fusion.ForPairs
local Value = fusion.Value

local fusionComponents = require("@packages/fusionComponents")
local text = fusionComponents.base.text
local button = fusionComponents.common.button
local selectMenu = fusionComponents.common.selectMenu

local tailwind = require("@packages/tailwind")

local theme = require("@src/theme")

local inputPropertyField = require("@components/propertyFields/input")
local modalOptionWrapper = require("@components/modalOptionWrapper")

type props = {
	useColor: theme.useColorFunction,
	OnClose: () -> (),
	OnCreate: (name: string, class: string) -> (),
}

local function newInstanceModal(props: props)
	local selectedOptions = Value({ "ParticleEmitter" })
	local inputValue = Value("")

	local debounce = false

	return New("Frame")({
		AutomaticSize = Enum.AutomaticSize.Y,
		BackgroundTransparency = 1,
		Size = UDim2.new(0, 300, 0, 0),
		[Children] = {
			New("UIListLayout")({
				FillDirection = Enum.FillDirection.Vertical,
				HorizontalAlignment = Enum.HorizontalAlignment.Center,
				Padding = UDim.new(0, 8),
				SortOrder = Enum.SortOrder.LayoutOrder,
				VerticalAlignment = Enum.VerticalAlignment.Top,
			}),
			New("Frame")({
				Name = "TopItems",
				AutomaticSize = Enum.AutomaticSize.Y,
				BackgroundTransparency = 1,
				Size = UDim2.new(1, 0, 0, 0),
				[Children] = {
					text({
						Appearance = props.useColor("Text", true),
						Text = {
							Label = "New Instance",
							Font = Font.new(theme.global.font.Family, Enum.FontWeight.SemiBold),
							TextSize = 18,
						},
						AnchorPoint = Vector2.new(0, 0.5),
						Position = UDim2.new(0, 0, 0.5, 0),
						Size = UDim2.new(1, 0, 0, 0),
						AutomaticSize = Enum.AutomaticSize.Y,
						TextXAlignment = Enum.TextXAlignment.Center,
						TextYAlignment = Enum.TextYAlignment.Center,
					}),
					button({
						Color = "gray",
						Variant = "ghost",
						Icon = "x",
						AutomaticSize = Enum.AutomaticSize.XY,
						Position = UDim2.new(1, -24, 0, 0),
						OnClick = props.OnClose,
					}),
				},
			}),
			modalOptionWrapper({
				OptionName = "Name",
				Content = inputPropertyField({
					Value = inputValue,
					LayoutOrder = 1,
				}),
			}),
			modalOptionWrapper({
				OptionName = "Class",
				Content = selectMenu({
					Color = "gray",
					Variant = "default",
					AppearanceOverride = {
						_global = {
							Background = {
								color = Computed(function()
									return theme.global.isDark:get() and tailwind.neutral[850] or tailwind.white
								end),
								shadow = 1,
							},
						},
						Focus = {
							Stroke = Computed(function()
								return theme.global.isDark:get() and tailwind.blue[400] or tailwind.blue[500]
							end),
						},
					},
					Size = UDim2.new(0.75, 0, 0, 20),
					SelectedOptions = selectedOptions,
					Options = ForPairs({
						"ParticleEmitter",
						"Beam",
						"Trail",
						"Attachment",
					}, function(index, value)
						return index,
							{
								Id = value,
								Slots = {
									Left = function()
										local classIcon = theme.getClassIcon(value)

										return New("ImageLabel")({
											Name = "ClassIcon",
											BackgroundTransparency = 1,
											Image = Computed(function()
												return classIcon:get().Image
											end),
											ImageRectOffset = Computed(function()
												return classIcon:get().ImageRectOffset
											end),
											ImageRectSize = Computed(function()
												return classIcon:get().ImageRectSize
											end),
											Size = UDim2.new(0, 16, 0, 16),
										})
									end,
								},
							}
					end),
					OnOptionClicked = function(id, visible)
						selectedOptions:set({ id })
						visible:set(false)
					end,
					Slots = {
						LeftInput = function()
							return Computed(function()
								local selectedOptionValue = selectedOptions:get()

								if #selectedOptionValue ~= 0 then
									-- not isolating this in computeds because an update here is not really relevant - it'll update on option change anyways, so this tiny optimization is irrelevant
									local classIcon = theme.getClassIcon(selectedOptionValue[1]):get()

									return New("ImageLabel")({
										Name = "ClassIcon",
										BackgroundTransparency = 1,
										Image = classIcon.Image,
										ImageRectOffset = classIcon.ImageRectOffset,
										ImageRectSize = classIcon.ImageRectSize,
										Size = UDim2.new(0, 16, 0, 16),
									})
								end

								return nil
							end, Clean)
						end,
					},
				}),
			}),
			button({
				Color = "gray",
				Variant = "solid",
				ButtonText = "Create Instance",
				AutomaticSize = Enum.AutomaticSize.XY,
				Disabled = Computed(function()
					local usedInput = inputValue:get()
					local usedOptions = selectedOptions:get()

					if #usedOptions == 0 or not usedInput:match("^[%w%s-()]+$") or usedInput:match("^%s*$") then
						return true
					end

					return false
				end),
				OnClick = function()
					if debounce then
						return
					end

					local usedInput = inputValue:get()
					local usedOptions = selectedOptions:get()

					if #usedOptions == 0 then
						return
					end

					props.OnCreate(usedInput, usedOptions[1])

					selectedOptions:set({})
					inputValue:set("")

					debounce = true

					task.delay(0.25, function()
						debounce = false
					end)
				end,
			}),
		},
	})
end

return newInstanceModal


local fusion = require("@packages/fusion")
local Children = fusion.Children
local New = fusion.New

local Clean = fusion.cleanup
local Computed = fusion.Computed
local ForPairs = fusion.ForPairs
local Observer = fusion.Observer
local Value = fusion.Value

local fusionComponents = require("@packages/fusionComponents")
local button = fusionComponents.common.button
local baseText = fusionComponents.base.text
local frame = fusionComponents.base.frame
local input = fusionComponents.common.input
local scrollingFrame = fusionComponents.common.scrollingFrame
local contextMenu = fusionComponents.common.contextMenu

local tailwind = require("@packages/tailwind")

local theme = require("@src/theme")
local settingsManager = require("@src/lib/settingsManager")

local settings = require("./settings")
local settingsListOption = require("./settingsListOption")

type props = {
	SettingsVisible: fusion.Value<boolean>,
	useColor: theme.useColorFunction,
}

local function settingsMenu(props: props)
	local searchQuery = Value("")
	local sharedContextState = Value(false)

	return New("Frame")({
		Name = "SettingsMenu",
		BackgroundColor3 = theme.global.background,
		Size = UDim2.new(1, 0, 1, 0),
		[Children] = {
			New("UIListLayout")({
				FillDirection = Enum.FillDirection.Vertical,
				HorizontalAlignment = Enum.HorizontalAlignment.Center,
				Padding = UDim.new(0, 4),
				SortOrder = Enum.SortOrder.LayoutOrder,
				VerticalAlignment = Enum.VerticalAlignment.Top,
			}),
			New("UIPadding")({
				PaddingBottom = UDim.new(0, 4),
				PaddingLeft = UDim.new(0, 4),
				PaddingRight = UDim.new(0, 4),
				PaddingTop = UDim.new(0, 4),
			}),
			New("Frame")({
				Name = "TopContent",
				BackgroundTransparency = 1,
				Size = UDim2.new(1, 0, 0, 24),
				[Children] = {
					New("UIListLayout")({
						FillDirection = Enum.FillDirection.Horizontal,
						HorizontalAlignment = Enum.HorizontalAlignment.Left,
						Padding = UDim.new(0, 4),
						SortOrder = Enum.SortOrder.LayoutOrder,
						VerticalAlignment = Enum.VerticalAlignment.Center,
					}),
					button({
						Variant = "solid",
						Color = "white",
						AppearanceOverride = {
							_global = {
								Background = Computed(function()
									return theme.global.isDark:get() and tailwind.neutral[800]
										or Color3.fromRGB(255, 255, 255)
								end),
							},
						},
						Icon = "chevron-left",
						Position = UDim2.new(0, 0, 0.5, 0),
						AnchorPoint = Vector2.new(0, 0.5),
						OnClick = function()
							props.SettingsVisible:set(false)
						end,
					}),
					input({
						Color = "gray",
						Variant = "default",
						AppearanceOverride = {
							_global = {
								Background = Computed(function()
									return theme.global.isDark:get() and tailwind.neutral[800]
										or Color3.fromRGB(255, 255, 255)
								end),
							},
						},
						Placeholder = "Search settings..",
						Icon = "search",
						AnchorPoint = Vector2.new(1, 0),
						Position = UDim2.new(1, 0, 0, 0),
						Size = UDim2.new(0, 0, 1, 0),
						Text = searchQuery,
						OnTextChanged = function(text)
							searchQuery:set(text)
						end,
						[Children] = {
							New("UIFlexItem")({
								FlexMode = Enum.UIFlexMode.Fill,
							}),
						},
					}),
				},
			}),
			frame({
				Name = "Settings",
				Size = UDim2.new(1, 0, 1, -28),
				Appearance = props.useColor("TreeBackground", true),
				Stroke = props.useColor("Stroke", true),
				Padding = UDim.new(0, 4),
				Content = {
					scrollingFrame({
						Content = {
							New("UIPadding")({
								PaddingLeft = UDim.new(0, 4),
								PaddingRight = UDim.new(0, 4),
							}),
							New("UIListLayout")({
								FillDirection = Enum.FillDirection.Vertical,
								HorizontalAlignment = Enum.HorizontalAlignment.Center,
								Padding = UDim.new(0, 4),
								SortOrder = Enum.SortOrder.LayoutOrder,
								VerticalAlignment = Enum.VerticalAlignment.Top,
							}),
							ForPairs(settings, function(index, category)
								return index,
									New("Frame")({
										Name = category.name,
										AutomaticSize = Enum.AutomaticSize.Y,
										BackgroundTransparency = 1,
										LayoutOrder = index,
										Size = UDim2.new(1, 0, 0, 18),
										Visible = Computed(function()
											if searchQuery:get() == "" then
												return true
											end

											for _, field in category.fields do
												if field.title:lower():find(searchQuery:get():lower()) then
													return true
												end
											end

											return false
										end),
										[Children] = {
											New("UIListLayout")({
												FillDirection = Enum.FillDirection.Vertical,
												HorizontalAlignment = Enum.HorizontalAlignment.Right,
												Padding = UDim.new(0, 6),
												SortOrder = Enum.SortOrder.LayoutOrder,
												VerticalAlignment = Enum.VerticalAlignment.Top,
											}),
											baseText({
												Appearance = props.useColor("SecondaryText", true),
												Text = {
													Label = category.name,
													Font = Font.new(theme.global.font.Family, Enum.FontWeight.Bold),
													TextSize = 18,
												},
												Size = UDim2.new(1, 0, 0, 18),
												TextXAlignment = Enum.TextXAlignment.Left,
												TextYAlignment = Enum.TextYAlignment.Center,
											}),
											ForPairs(category.fields, function(idx, field)
												local fieldValue = Value(
													if settingsManager.get(field.pluginSettingKey) ~= nil
														then settingsManager.get(field.pluginSettingKey)
														else field.default
												)

												local isContextMenuVisible = Value(false)

												return idx,
													contextMenu(
														button({
															Color = "neutral",
															Variant = "ghost",
															DisableHoverLighting = true,
															AutomaticSize = Enum.AutomaticSize.Y,
															Size = UDim2.new(1, -8, 0, 24),
															Content = {
																settingsListOption({
																	Title = field.title,
																	Description = field.description,
																	Content = field.render(fieldValue),
																	Visible = Computed(function()
																		return field.title
																			:lower()
																			:find(searchQuery:get():lower()) ~= nil
																	end),
																}),
															},
														}),
														{
															Color = "gray",
															Variant = "default",
															Visible = isContextMenuVisible,
															VisibleLogic = function(isEnabled)
																local currentState = sharedContextState:get()

																if isEnabled then
																	if typeof(currentState) ~= "boolean" then
																		currentState:set(false)
																	end

																	isContextMenuVisible:set(true)
																	sharedContextState:set(isContextMenuVisible)
																elseif currentState:get() == isContextMenuVisible then
																	isContextMenuVisible:set(false)
																	sharedContextState:set(false)
																end

																return true
															end,
															Options = {
																{
																	{
																		Label = "Reset to default",
																		Icon = "list-restart",
																		Callback = function()
																			fieldValue:set(field.default)
																			-- close self
																			isContextMenuVisible:set(false)
																			sharedContextState:set(false)
																		end,
																	},
																},
															},
														}
													),
													Observer(fieldValue):onChange(function()
														local value = fieldValue:get()

														settingsManager.set(
															field.pluginSettingKey,
															field.set and field.set(value) or value
														)
													end)
											end, Clean),
										},
									})
							end, Clean),
						},
						ScrollingFrameProps = {
							VerticalScrollBarInset = Enum.ScrollBarInset.ScrollBar,
							AutomaticCanvasSize = Enum.AutomaticSize.Y,
						},
						Size = UDim2.new(1, 0, 1, 0),
					}),
				},
			}),
		},
	})
end

return settingsMenu