--!strict

-- This wraps over TextBox
-- Maybe later, can adjust for other locales.
-- Also could add a calender popup.

-- Use :getValue to get the value (unix timestamp milliseconds)

-- Valid Formats:
--	MM/DD/YY
--	MM-DD-YY
--	Month Day, Year
--	Month Day
--	ISO 8601

local TextBoxStyleState = require(script.Parent.TextBoxStyleState)

local DatePickerStyleState = setmetatable({}, { __index = TextBoxStyleState })
DatePickerStyleState.__index = DatePickerStyleState

-- Date stuff
type Timestamp = number
type Date = Timestamp | DateTime

-- NOTE: Is it ethical to support mispelling for better user experience?
local monthNames = {
	jan = 1,
	january = 1,
	janury = 1,
	feb = 2,
	february = 2,
	febuary = 2,
	feburary = 2,
	mar = 3,
	march = 3,
	apr = 4,
	april = 4,
	may = 5,
	jun = 6,
	june = 6,
	jul = 7,
	july = 7,
	aug = 8,
	august = 8,
	sep = 9,
	september = 9,
	sept = 9,
	oct = 10,
	october = 10,
	octo = 10,
	octob = 10,
	nov = 11,
	november = 11,
	novem = 11,
	nove = 11,
	dec = 12,
	december = 12,
	christmas = 12,
}

local function isValidMonth(month: number?): boolean
	return (typeof(month) == "number") and (math.floor(month) == month) and (1 <= month) and (month <= 12)
end

local function isValidYear(year: number?): boolean
	return (typeof(year) == "number") and (1400 <= year) and (year <= 9999)
end

local function isLeapYear(year: number): boolean
	return ((year % 4 == 0) and (year % 100 ~= 0)) or (year % 400 == 0)
end

local function getDaysInMonth(month: number, year: number): number
	if month == 2 then
		return if isLeapYear(year) then 29 else 28
	elseif month == 4 or month == 6 or month == 9 or month == 11 then
		return 30
	else
		return 31
	end
end

local function isValidDay(day: number?, month: number, year: number): boolean
	return (typeof(day) == "number") and (1 <= day) and (day <= getDaysInMonth(month, year))
end

local function tryGetDate(text: string, upToEndOfDay: boolean): (DateTime?, string?)
	-- Format: month
	do
		local month = monthNames[text:lower()]
		if month then
			local now = DateTime.now():ToUniversalTime()
			if upToEndOfDay then
				return DateTime.fromUniversalTime(now.Year, month, getDaysInMonth(month, now.Year), 23, 59, 59, 999)
			else
				return DateTime.fromUniversalTime(now.Year, month, 1)
			end
		end
	end

	-- Format: MM/DD/YYYY
	local parts = text:split("/")
	if #parts ~= 3 then
		-- Format: MM-DD-YY
		parts = text:split("-")
		if #parts ~= 3 then
			-- Format: Jan 12, 2024
			parts = text:gsub(";", ""):gsub(",", ""):split(" ")
			if #parts == 3 or #parts == 2 then
				local month = monthNames[parts[1]:lower()]
				if not month then
					return nil, "Invalid month. Format: month day, year or MM/DD/YYYY."
				else
					parts[1] = month
				end

				if #parts == 2 then
					-- Auto-fill the year
					parts[3] = tostring(DateTime.now():ToUniversalTime().Year)
				end
			else
				return nil, "Invalid format."
			end
		end
	end

	local month = tonumber(parts[1])
	if not isValidMonth(month) then
		return nil, "Invalid month. Format: month day, year or MM/DD/YYYY."
	end

	local year = tonumber(parts[3])
	if not isValidYear(year) then
		return nil, "Invalid year. Format: month day, year or MM/DD/YYYY."
	end

	local day = tonumber(parts[2])
	if not isValidDay(day, month :: number, year :: number) then
		return nil, "Invalid day. Format: month day, year or MM/DD/YYYY."
	end

	if upToEndOfDay then
		return DateTime.fromUniversalTime(year :: number, month :: number, day :: number, 23, 59, 59, 999)
	else
		return DateTime.fromUniversalTime(year :: number, month :: number, day :: number)
	end
end

local function tryGetTimestamp(text: string): (Timestamp?, string?)
	local timestamp = tonumber(text)
	if not timestamp then
		return nil, "Invalid date. Format: month day, year or MM/DD/YYYY."
	end

	return timestamp
end

local function tryGetTime(text: string, upToEndOfDay: boolean): (Date?, string?)
	local date, err = tryGetDate(text, upToEndOfDay)
	if date == nil and err == "Invalid format." then
		local iso = DateTime.fromIsoDate(text)
		if iso then
			return iso
		end

		local timestamp, err = tryGetTimestamp(text)
		if err then
			return nil, err
		else
			return timestamp
		end
	elseif err then
		return nil, err
	else
		return date
	end
end

-- Main style stuff

type DatePickerOptions = {
	upToEndOfDay: boolean?, -- If use types in May 31, 2024, if this is true, the timestmap returned will be May 31 2024 23:59pm instead of 00:00
}
function DatePickerStyleState.from(theme, textBox: TextBox & { UIStroke: UIStroke }, options: DatePickerOptions?)
	local options: DatePickerOptions = options or {}

	local self = setmetatable(TextBoxStyleState.from(theme, textBox), DatePickerStyleState)

	self._time = nil :: Date?

	self._datePickerConnections = {
		textBox:GetPropertyChangedSignal("Text"):Connect(function()
			local trimmed = textBox.Text:gsub("^%s+", ""):gsub("%s$", "")
			if trimmed == "" then
				self._time = nil
				self:setError(nil):update("veryFast")
				return
			end

			local time, err = tryGetTime(trimmed, options.upToEndOfDay or false)
			if err then
				self._time = nil
				self:setError(err):update("veryFast")
			else
				self._time = time
				self:setError(nil):update("veryFast")
			end
		end),
	}

	return self
end

-- Second value is if it has an error
function DatePickerStyleState:getValue(): Timestamp?
	if self._time then
		if typeof(self._time) == "DateTime" then
			return self._time.UnixTimestampMillis, false
		else
			return self._time, false
		end
	end
	return nil
end

function DatePickerStyleState:destroy(completely: boolean)
	for _, connection in self._datePickerConnections do
		connection:Disconnect()
	end
	TextBoxStyleState.destroy(self, completely)
end

return DatePickerStyleState


--!strict

-- Only works with text box that are not multiline
-- Does not support text boxes with UIPadding that uses Scale

local TextService = game:GetService("TextService")
local Players = game:GetService("Players")

local StyleStateHelper = require(script.Parent.Parent.StyleStateHelper)
local Theme = require(script.Parent.Parent.Parent.Theme)

local TextBoxStyleState = require(script.Parent.TextBoxStyleState)

local OverlayTextBoxStyleState = setmetatable({}, { __index = TextBoxStyleState })
OverlayTextBoxStyleState.__index = OverlayTextBoxStyleState

export type Piece = {
	text: string,
	hg: string?, -- Highlight group. `nil` means do not highlight
}

local function escapeRichText(source: string): string
	return source:gsub("&", "&"):gsub("'", "'"):gsub('"', """):gsub("<", "<"):gsub(">", ">")
end

local function getLengthOfText(textBox: TextBox, text: string)
	local params = Instance.new("GetTextBoundsParams")
	params.Text = text
	params.Font = textBox.FontFace
	params.Size = textBox.TextSize
	params.Width = math.huge
	return TextService:GetTextBoundsAsync(params).X
end

local function getLengthToCursor(textBox: TextBox)
	return getLengthOfText(textBox, textBox.Text:sub(1, textBox.CursorPosition - 1))
end

function OverlayTextBoxStyleState.from(theme: Theme.Theme, textBox: TextBox & { UIStroke: UIStroke })
	local self = setmetatable(TextBoxStyleState.from(theme, textBox), OverlayTextBoxStyleState)
	self._highlightColors = nil

	self.overlay = Instance.new("TextLabel")
	self.overlay.BackgroundTransparency = 1
	self.overlay.TextColor3 = Color3.new(1, 0, 0)
	self.overlay.TextSize = textBox.TextSize
	self.overlay.FontFace = textBox.FontFace
	self.overlay.RichText = true
	self.overlay.TextXAlignment = textBox.TextXAlignment
	self.overlay.TextYAlignment = textBox.TextYAlignment
	self.overlay.Size = UDim2.fromScale(1, 1)
	self.overlay.TextWrapped = false
	self.overlay.Text = textBox.Text
	self.overlay.Parent = textBox

	local paddingX = 0
	do
		local uiPadding = textBox:FindFirstChildOfClass("UIPadding")
		if uiPadding then
			paddingX = uiPadding.PaddingLeft.Offset + uiPadding.PaddingRight.Offset
		end
	end
	self._lastShift = 0
	self._atEnd = false
	self._pieces = {} :: { Piece }

	self._cursorPositionConnection = textBox:GetPropertyChangedSignal("CursorPosition"):Connect(function()
		if textBox.CursorPosition <= 0 then
			self:_updateShift(0)
			self._atEnd = false
		else
			local startBoundsX = self._lastShift
			local endBoundsX = startBoundsX + (self.textBox.AbsoluteSize.X - paddingX) - 1
			local cursorX = getLengthToCursor(textBox)

			self.overlay.Size = UDim2.new(0, cursorX, 1, 0) -- The RichTextLabel will stop rendering when it is too far away, so make it bigger

			if cursorX < startBoundsX then
				self:_updateShift(cursorX)
				self._atEnd = false
			elseif cursorX > endBoundsX then
				local shifted = cursorX - endBoundsX
				self:_updateShift(startBoundsX + shifted)
				self._atEnd = true
			elseif cursorX < endBoundsX then
				-- User backspaced, have to move back
				-- This will also fire for inserts, so check it actually moved down
				if self._atEnd then
					local lengthX = getLengthOfText(textBox, textBox.Text)
					local shifted = endBoundsX - lengthX
					if shifted > 0 then
						self:_updateShift(startBoundsX - shifted)

						if self._lastShift == 0 then
							self._atEnd = false
						end
					end
				end
			end
		end
	end)

	return self
end

function OverlayTextBoxStyleState:setPieces(pieces: { Piece })
	self._pieces = pieces
	self:_updateOverlayText()
end

function OverlayTextBoxStyleState:setHighlightColors(colors: { [string]: Color3 })
	self._highlightColors = colors
	self:_updateOverlayText()
end

function OverlayTextBoxStyleState:_updateOverlayText()
	local pieces = self._pieces :: { Piece }
	local text = table.create(#pieces)
	for _, piece in pieces do
		if not piece.hg then
			table.insert(text, `<font transparency="1">{escapeRichText(piece.text)}</font>`)
		else
			table.insert(
				text,
				`{escapeRichText(
					piece.text
				)}`
			)
		end
	end
	self.overlay.Text = table.concat(text)
end

function OverlayTextBoxStyleState:_updateShift(amount: number)
	amount = math.max(0, amount)
	self._lastShift = amount
	self.overlay.Position = UDim2.fromOffset(-amount, 0)
end

function OverlayTextBoxStyleState:update(speed: StyleStateHelper.TransitionSpeed)
	TextBoxStyleState.update(self, speed)
	StyleStateHelper.tween(self.overlay, StyleStateHelper.getTweenInfoForSpeed(speed), {
		TextTransparency = if self.disabled then 0.75 else 0,
	})
	self:_updateOverlayText(true)
end

function OverlayTextBoxStyleState:destroy(completely: boolean)
	self._cursorPositionConnection:Disconnect()
	TextBoxStyleState.destroy(self, completely)
end

return OverlayTextBoxStyleState


--!strict

local TextService = game:GetService("TextService")
local TweenService = game:GetService("TweenService")

local Theme = require(script.Parent.Parent.Parent.Theme)
local PopupHelper = require(script.Parent.Parent.Parent.PopupHelper)

local StyleStateHelper = require(script.Parent.Parent.StyleStateHelper)
local ButtonStyleState = require(script.Parent.Parent.ButtonStyleState)

local SELECT_MENU_OFFSET = 2

local SelectStyleState = {}
SelectStyleState.__index = SelectStyleState

export type Options = { string | { name: string, icon: string } }
export type SelectStyleStateOptions = {
	default: string,
	defaultIndex: number?,
	options: Options,
	buttonStyle: ButtonStyleState.ButtonStyle?,

	fitButton: boolean?, -- default: true. Whether or not the button should fit the options
}

function SelectStyleState.from(
	theme: Theme.Theme,
	button: TextButton & { Selection: TextLabel, Arrow: ImageLabel, Icon: ImageLabel? },
	selectStyleStateOptions: SelectStyleStateOptions -- Required
)
	if selectStyleStateOptions.fitButton == nil then
		selectStyleStateOptions.fitButton = true
	end

	local self = setmetatable({
		options = selectStyleStateOptions.options,
		open = false,

		selection = nil :: string?,
		selectionIndex = nil :: number?,

		fitButton = selectStyleStateOptions.fitButton,

		menu = nil :: any,
		menuStyleStates = {} :: { any },
		menuModal = nil :: any,
		menuAttachment = nil :: any,

		button = button,
		buttonStyleState = ButtonStyleState.from(
			theme,
			button,
			{ style = selectStyleStateOptions.buttonStyle or "input" }
		),

		theme = theme,
	}, SelectStyleState)

	if self.fitButton then
		self:_fitSelectedText()
	end

	self._changed = Instance.new("BindableEvent")
	self.changed = self._changed.Event

	self._clickConnection = button.Activated:Connect(function()
		self:setOpen(not self.open)
	end)

	self:setSelection(selectStyleStateOptions.default, {
		index = selectStyleStateOptions.defaultIndex,
		silently = true,
	})
	return self
end

function SelectStyleState:setOpen(open: boolean)
	self.open = open
	self.buttonStyleState:setSelecting(open)
	self:update()

	if self.open then
		self:_makeMenu()
	elseif self.menu then
		self:_clearMenu()
	end
end

function SelectStyleState:_clearMenu()
	for _, styleState in self.menuStyleStates do
		styleState:destroy()
	end
	self.menuStyleStates = {}
	self.menuModal:destroy()
	self.menuAttachment:destroy()
	self.menu:Destroy()
	self.menu = nil :: any
	self.menuModal = nil :: any
	self.menuAttachment = nil :: any
end

function SelectStyleState:_makeMenu()
	if self.menu then
		self:_clearMenu()
	end

	self.menu = script.SelectMenu:Clone()
	self.menu.BackgroundColor3 = self.theme.colors.floatingAction.background
	self.menu.UIStroke.Color = self.theme.colors.floatingAction.outline
	if self.fitButton then
		self.menu.Size = UDim2.fromOffset(self.button.AbsoluteSize.X, 0)
	else
		local longestX = 0

		for _, option in self.options do
			local text = if typeof(option) == "string" then option else option.name

			local textBoundsParams = Instance.new("GetTextBoundsParams")
			textBoundsParams.Text = text
			textBoundsParams.Font = self.button.Selection.FontFace
			textBoundsParams.Size = self.button.Selection.TextSize
			textBoundsParams.Width = math.huge

			local textBounds = TextService:GetTextBoundsAsync(textBoundsParams)
			local boundsX = textBounds.X

			if typeof(option) == "table" then
				boundsX += script.SelectOption.ImageLabel.AbsoluteSize.X
				boundsX += script.SelectOption.UIListLayout.Padding.Offset
			end

			longestX = math.max(longestX, boundsX)
		end

		self.menu.Size = UDim2.fromOffset(
			longestX
				+ script.SelectOption.UIPadding.PaddingLeft.Offset
				+ script.SelectOption.UIPadding.PaddingRight.Offset,
			0
		)
	end

	for index, option in self.options do
		local text = if typeof(option) == "string" then option else option.name

		local button = script.SelectOption:Clone()
		button.TextLabel.Text = text
		if typeof(option) == "table" then
			button.ImageLabel.Image = option.icon
		else
			button.ImageLabel:Destroy()
		end

		button.Activated:Connect(function()
			self:setSelection(text, {
				index = index,
			})

			self:setOpen(false)
		end)
		button.Size = UDim2.new(1, 0, 0, self.button.AbsoluteSize.Y)

		button.Parent = self.menu
		table.insert(self.menuStyleStates, ButtonStyleState.from(self.theme, button, { style = "transparent" }))
	end

	local gui = (self.button:FindFirstAncestorWhichIsA("LayerCollector") :: any) :: LayerCollector
	self.menuModal = PopupHelper.modal(self.menu, gui, {
		zIndex = 20,
		passThroughInput = true,
	})
	self.menuAttachment = PopupHelper.attach(self.menu, self.button, {
		anchor = Vector2.new(0, 1),
		offset = Vector2.new(0, 2),
		beforeUpdate = function(options, object, target)
			local height = object.AbsoluteSize.Y
			if target.AbsolutePosition.Y + target.AbsoluteSize.Y + height + SELECT_MENU_OFFSET > gui.AbsoluteSize.Y then
				-- Try flipping it
				if target.AbsolutePosition.Y - height - SELECT_MENU_OFFSET < 0 then
					-- Can't be flipped, just clamp
					local position = target.AbsolutePosition + Vector2.new(0, target.AbsoluteSize.Y)
					object.Position = UDim2.fromOffset(position.X, position.Y)
					PopupHelper.clamp(object, gui)
					return "manual"
				else
					options.anchor = Vector2.new(0, 0)
					object.AnchorPoint = Vector2.new(0, 1)
				end
			else
				options.anchor = Vector2.new(0, 1)
				object.AnchorPoint = Vector2.new(0, 0)
			end
			return nil
		end,
	})

	self.menuModal.cancelled:Connect(function()
		self:setOpen(false)
	end)

	self:_animateMenu()
end

function SelectStyleState:_animateMenu()
	local menu = self.menu
	local menuAttachment = self.menuAttachment

	local start = os.clock()
	local startOffset = -8
	while (self.menu and self.menu == menu) and (self.menuAttachment and self.menuAttachment == menuAttachment) do
		local delta = os.clock() - start
		local a = math.min(delta / 0.1, 1)
		a = TweenService:GetValue(a, Enum.EasingStyle.Cubic, Enum.EasingDirection.Out)

		self.menu.GroupTransparency = 1 - a
		self.menu.UIStroke.Transparency = 1 - a
		self.menuAttachment.options.offset = Vector2.new(0, startOffset + a * (SELECT_MENU_OFFSET - startOffset))
		self.menuAttachment:updatePosition()

		if a >= 1 then
			break
		end

		task.wait()
	end
end

-- Makes sure the Selection TextLabel can always fit the biggest option
function SelectStyleState:_fitSelectedText()
	local longestX = self.button.Selection.TextBounds.X
	if longestX ~= longestX then -- It's nan
		longestX = 0
	end

	for _, option in self.options do
		local text = if typeof(option) == "string" then option else option.name

		local textBoundsParams = Instance.new("GetTextBoundsParams")
		textBoundsParams.Text = text
		textBoundsParams.Font = self.button.Selection.FontFace
		textBoundsParams.Size = self.button.Selection.TextSize
		textBoundsParams.Width = math.huge

		-- add 2 extra padding because it's sometimes too tight
		local bounds = TextService:GetTextBoundsAsync(textBoundsParams)
		longestX = math.max(longestX, bounds.X + 2)
	end

	self.button.Selection.Size =
		UDim2.new(0, longestX, self.button.Selection.Size.Y.Scale, self.button.Selection.Size.Y.Offset)
end

function SelectStyleState:changeOptions(options: Options)
	self.options = options
	if self.fitButton then
		self:_fitSelectedText()
	end

	if self.menu then
		-- Reset the menu
		self:_makeMenu()
	end
end

function SelectStyleState:_calculateSelectionIndex(): number?
	for index, option in self.options do
		if typeof(option) == "table" and option.name == self.selection then
			return index
		elseif option == self.selection then
			return index
		end
	end
	return nil
end

export type SetSelectionOptions = {
	silently: boolean?,
	index: number?,
}
-- An index should be provided in case two options have the same name
function SelectStyleState:setSelection(selection: string, options: SetSelectionOptions?)
	local options: SetSelectionOptions = options or {}

	self.selection = selection
	self.selectionIndex = options.index or self:_calculateSelectionIndex()
	self.button.Selection.Text = selection

	-- Set image if possible
	local icon = self.button:FindFirstChild("Icon") :: ImageLabel
	if icon then
		local option = self.options[self.selectionIndex or 1]

		if option and typeof(option) == "table" and option.icon then
			icon.Image = option.icon
		else
			icon.Image = ""
		end
	end

	if not options.silently then
		self._changed:Fire(selection, self.selectionIndex)
	end
end

function SelectStyleState:update(...)
	if not self.button.Parent then
		return
	end

	self.button.Arrow.Image = if self.open then self.theme.icons.upArrow else self.theme.icons.downArrow
	self.buttonStyleState:update(...)

	if self.menu then
		local tweenInfo = StyleStateHelper.getTweenInfoForSpeed(...)
		StyleStateHelper.tween(self.menu, tweenInfo, {
			BackgroundColor3 = self.theme.colors.floatingAction.background,
		})
		StyleStateHelper.tween(self.menu.UIStroke, tweenInfo, {
			Color = self.theme.colors.floatingAction.outline,
		})
		for _, styleStates in self.menuStyleStates do
			styleStates:update(...)
		end
	end
end

function SelectStyleState:destroy(completely: boolean)
	self._clickConnection:Disconnect()
	self.buttonStyleState:destroy()
	self._changed:Destroy()
	if self.menu then
		self:_clearMenu()
	end

	if completely then
		self.button:Destroy()
	end
end

return SelectStyleState


--!strict

-- This handles "shortcuts text box" which are text box with shortcuts that auto replace special sequences.

-- Shortcuts:
--		@username: This will replace @username with the id of the user with that name

-- Use :getText(errorHandler) to get the text instead of textBox.Text
-- Must pass in a function to handle any errors that occur.
-- Errors are non-fatal, but should be reported.

local Players = game:GetService("Players")

local Theme = require(script.Parent.Parent.Parent.Theme)

local TextBoxStyleState = require(script.Parent.TextBoxStyleState)
local OverlayTextBoxStyleState = require(script.Parent.OverlayTextBoxStyleState)

local parseShortcuts = require(script.parseShortcuts)

local ShortcutsTextBoxStyleState = setmetatable({}, { __index = OverlayTextBoxStyleState })
ShortcutsTextBoxStyleState.__index = ShortcutsTextBoxStyleState

function ShortcutsTextBoxStyleState.from(theme: Theme.Theme, textBox: TextBox & { UIStroke: UIStroke })
	local self = setmetatable(OverlayTextBoxStyleState.from(theme, textBox), ShortcutsTextBoxStyleState)

	self._shortcutPieces = {}

	self._textChangedConnection = textBox:GetPropertyChangedSignal("Text"):Connect(function()
		self:_updatePieces()
	end)

	return self
end

function ShortcutsTextBoxStyleState:_updatePieces()
	local shortcutPieces = parseShortcuts(self.textBox.Text)
	self._shortcutPieces = shortcutPieces
	local highlightPieces = table.create(#shortcutPieces)

	for _, piece in shortcutPieces do
		table.insert(highlightPieces, {
			hg = if piece.type == "mention" then "root" elseif piece.type == "escapedNormal" then "string" else nil,
			text = piece.content,
		})
	end

	self:setPieces(highlightPieces)
end

local noop = function(err: string) end

type TextResult = {
	type: "text",
	content: string,
} | {
	type: "error",
	message: string,
}
function ShortcutsTextBoxStyleState:getText(errorHandler: TextBoxStyleState.GetTextErrorHandler?): string
	local errorHandler = errorHandler or noop
	local pieces = self._shortcutPieces :: parseShortcuts.ShortcutsString
	local text = table.create(#pieces)

	for _, piece in pieces do
		if piece.type == "normal" then
			table.insert(text, piece.content)
		elseif piece.type == "escapedNormal" then
			table.insert(text, piece.content:sub(2))
		elseif piece.type == "mention" then
			local name: string
			local id = piece.content:sub(2)
			local success, err = pcall(function()
				name = Players:GetUserIdFromNameAsync(id)
			end)

			if not success then
				if err:find("Unknown user") then
					errorHandler(`User not found: {id}`)
				else
					errorHandler(err)
				end
				table.insert(text, piece.content)
			else
				table.insert(text, name)
			end
		end
	end

	return table.concat(text)
end

function ShortcutsTextBoxStyleState:destroy(completely: boolean)
	self._textChangedConnection:Disconnect()
	OverlayTextBoxStyleState.destroy(self, completely)
end

return ShortcutsTextBoxStyleState


--!strict

export type ShortcutType = "normal" | "escapedNormal" | "mention"
export type ShortcutPiece = {
	type: ShortcutType,
	content: string,

	-- "escapedNormal" will always start with \\ which should be ignored
	-- "mention" will always start with @ which should be ignored
}
export type ShortcutsString = { ShortcutPiece }

local validGraphemes = {
	a = true,
	b = true,
	c = true,
	d = true,
	e = true,
	f = true,
	g = true,
	h = true,
	i = true,
	j = true,
	k = true,
	l = true,
	m = true,
	n = true,
	o = true,
	p = true,
	q = true,
	r = true,
	s = true,
	t = true,
	u = true,
	v = true,
	w = true,
	x = true,
	y = true,
	z = true,
	A = true,
	B = true,
	C = true,
	D = true,
	E = true,
	F = true,
	G = true,
	H = true,
	I = true,
	J = true,
	K = true,
	L = true,
	M = true,
	N = true,
	O = true,
	P = true,
	Q = true,
	R = true,
	S = true,
	T = true,
	U = true,
	V = true,
	W = true,
	X = true,
	Y = true,
	Z = true,
	["1"] = true,
	["2"] = true,
	["3"] = true,
	["4"] = true,
	["5"] = true,
	["6"] = true,
	["7"] = true,
	["8"] = true,
	["9"] = true,
	["0"] = true,
	["_"] = true,
}

-- Probably a better way to do this...
local function isValidUsernameGrapheme(grapheme: string): boolean
	return validGraphemes[grapheme] or false
end

return function(source: string): ShortcutsString
	local lastGrapheme: string

	local pieces: ShortcutsString = {}

	local pieceType: ShortcutType = "normal"
	local pieceContent = table.create(16)

	local function insertCurrentPiece()
		if #pieceContent > 0 then
			table.insert(pieces, {
				type = pieceType,
				content = table.concat(pieceContent, nil),
			})
		end
	end

	local function startPiece(type: ShortcutType)
		pieceType = type
		pieceContent = table.create(16)
	end

	for first, last in utf8.graphemes(source) do
		local grapheme = source:sub(first, last)
		if grapheme == "@" or grapheme == "$" then
			if lastGrapheme == "\\" then
				lastGrapheme = ""

				insertCurrentPiece()
				startPiece("escapedNormal")

				table.insert(pieceContent, "\\")
				table.insert(pieceContent, grapheme)

				insertCurrentPiece()
				startPiece("normal")
				continue
			elseif lastGrapheme == "@" or lastGrapheme == "$" then
				if pieceType == "mention" then
					insertCurrentPiece()
					startPiece("normal")
				end
				table.insert(pieceContent, lastGrapheme)
			end
		elseif grapheme == "\\" then
			if lastGrapheme == "\\" then
				lastGrapheme = ""

				insertCurrentPiece()
				startPiece("escapedNormal")

				table.insert(pieceContent, "\\\\")

				insertCurrentPiece()
				startPiece("normal")

				continue
			end
		else
			if pieceType == "mention" then
				if (lastGrapheme == "\\") or (not isValidUsernameGrapheme(grapheme)) then
					insertCurrentPiece()
					startPiece("normal")
				end
			end

			if lastGrapheme == "\\" then
				table.insert(pieceContent, "\\")
			elseif lastGrapheme == "@" or lastGrapheme == "$" then
				if isValidUsernameGrapheme(grapheme) then
					insertCurrentPiece()
					startPiece("mention")
				end
				table.insert(pieceContent, lastGrapheme)
			end
			table.insert(pieceContent, grapheme)
		end

		lastGrapheme = grapheme
	end

	if lastGrapheme == "\\" then
		if pieceType == "normal" then
			table.insert(pieceContent, "\\")
		else
			insertCurrentPiece()
			startPiece("normal")
			table.insert(pieceContent, "\\")
		end
	elseif lastGrapheme == "@" or lastGrapheme == "$" then
		if pieceType == "normal" then
			table.insert(pieceContent, lastGrapheme)
		else
			insertCurrentPiece()
			startPiece("normal")
			table.insert(pieceContent, lastGrapheme)
		end
	end
	insertCurrentPiece()

	return pieces
end


--!strict
-- Same as Checkbox, just styled differently

local StyleStateHelper = require(script.Parent.Parent.StyleStateHelper)
local ButtonStyleState = require(script.Parent.Parent.ButtonStyleState)

local SwitchStyleState = {}
SwitchStyleState.__index = SwitchStyleState

export type SwitchOptions = {
	default: boolean?,
}
function SwitchStyleState.from(theme, switch: TextButton & { Circle: GuiObject }, options: SwitchOptions?)
	local options: SwitchOptions = options or {}

	local self = setmetatable({
		value = options.default or false,
		switch = switch,
		theme = theme,

		buttonStyleState = ButtonStyleState.from(theme, switch, {
			style = "secondary",
		}),
	}, SwitchStyleState)

	self._toggled = Instance.new("BindableEvent")
	self.toggled = self._toggled.Event

	self._clickedConnection = self.switch.Activated:Connect(function()
		self:setValue(not self.value)
	end)

	self:_updateCircle("instant")
	self:update("instant")
	return self
end

function SwitchStyleState:_getCircleColor()
	return if self.value
		then self.theme.colors.button.primary.text.select
		else self.theme.colors.button.secondary.text.default
end

function SwitchStyleState:_updateCircle(speed: StyleStateHelper.TransitionSpeed)
	local tweenInfo = StyleStateHelper.getTweenInfoForSpeed(speed)
	if self.value then
		self.buttonStyleState.style = "primary"
	else
		self.buttonStyleState.style = "secondary"
	end
	StyleStateHelper.tween(self.switch.Circle, tweenInfo, {
		BackgroundColor3 = self:_getCircleColor(),
		Position = if self.value then UDim2.fromScale(1, 0) else UDim2.fromScale(0, 0),
		AnchorPoint = if self.value then Vector2.new(1, 0) else Vector2.new(0, 0),
	})
end

function SwitchStyleState:update(speed: StyleStateHelper.TransitionSpeed)
	local tweenInfo = StyleStateHelper.getTweenInfoForSpeed(speed)
	self.buttonStyleState:update(speed)
	StyleStateHelper.tween(self.switch.Circle, tweenInfo, {
		BackgroundColor3 = self:_getCircleColor(),
	})
end

function SwitchStyleState:setValue(value)
	local lastValue = self.value
	self.value = value
	self:_updateCircle("veryFast")
	self.buttonStyleState:update("veryFast")

	if self.value ~= lastValue then
		self._toggled:Fire(self.value)
	end
end

function SwitchStyleState:destroy(completely: boolean)
	self.buttonStyleState:destroy(completely)
	self._clickedConnection:Disconnect()
	self._toggled:Destroy()
	if completely then
		-- it will be destroyed by the button style state
	end
end

return SwitchStyleState


local PopupHelper = require(script.Parent.Parent.Parent.PopupHelper)
local StyleStateHelper = require(script.Parent.Parent.StyleStateHelper)

local TextBoxStyleState = {}
TextBoxStyleState.__index = TextBoxStyleState

function TextBoxStyleState.from(theme: any, textBox: TextBox & { UIStroke: UIStroke })
	local self = setmetatable({
		textBox = textBox,
		stroke = textBox.UIStroke,
		theme = theme,

		overridenBackgroundColor3 = nil :: Color3?,
		overriddenTextColor = nil :: Color3?,

		disabled = false,
		_pressing = false,
		_hovering = false,

		error = nil :: string?,
		_errorPopup = nil :: typeof(script.ErrorPopup)?,
	}, TextBoxStyleState)

	self._errorChanged = Instance.new("BindableEvent")
	self.errorChanged = self._errorChanged.Event

	self._beganConnection = textBox.InputBegan:Connect(function(input)
		if
			input.UserInputType == Enum.UserInputType.Touch
			or input.UserInputType == Enum.UserInputType.MouseButton1
		then
			self._pressing = true
			self:update()

			local connection = nil
			connection = input:GetPropertyChangedSignal("UserInputState"):Connect(function()
				if
					input.UserInputState == Enum.UserInputState.End
					or input.UserInputState == Enum.UserInputState.Cancel
				then
					self._pressing = false
					connection:Disconnect()
					self:update()
				end
			end)
		elseif input.UserInputType == Enum.UserInputType.MouseMovement then
			self._hovering = true
			self:update()
		end
	end)

	self._endedConnection = textBox.InputEnded:Connect(function(input)
		if input.UserInputType == Enum.UserInputType.MouseMovement then
			self._hovering = false
			self:update()
		end
	end)

	self._focusedConnection = textBox.Focused:Connect(function()
		self:update()

		if self.error then
			if not self._errorPopup then
				self:_makeErrorPopup()
			end
		end
	end)

	self._focusLostConnection = textBox.FocusLost:Connect(function()
		self:update()
		if self._errorPopup then
			self:_deleteErrorPopup()
		end
	end)

	self:update("instant")
	return self
end

function TextBoxStyleState:update(speed: StyleStateHelper.TransitionSpeed?)
	local state = if self.disabled
		then "disable"
		elseif self.textBox:IsFocused() then "select"
		elseif self._pressing then "press"
		elseif self._hovering then "hover"
		else "default"

	local tweenInfo = StyleStateHelper.getTweenInfoForSpeed(speed)

	StyleStateHelper.tween(self.textBox, tweenInfo, {
		BackgroundColor3 = self.overridenBackgroundColor or self.theme.colors.button.input.body[state],
		TextColor3 = self.overriddenTextColor or self.theme.colors.button.input.text[state],
		PlaceholderColor3 = self.theme.colors.button.input.text.default:lerp(
			self.theme.colors.button.input.body.default,
			0.5
		),
	})
	StyleStateHelper.tween(self.stroke, tweenInfo, {
		Color = if self.error then self.theme.colors.error else self.theme.colors.button.input.outline[state],
	})
end

function TextBoxStyleState:setDisabled(disabled: boolean)
	self.disabled = disabled
	self.textBox.TextEditable = not disabled

	return self
end

function TextBoxStyleState:overrideTextColor(color: Color3)
	self.overriddenTextColor = color
	return self
end

function TextBoxStyleState:overrideBackgroundColor(color: Color3)
	self.overridenBackgroundColor = color
	return self
end

function TextBoxStyleState:_deleteErrorPopup()
	if self._errorPopup then
		self._errorPopup:destroy(true)
		self._errorPopup = nil
	end
end

function TextBoxStyleState:_makeErrorPopup()
	if self.error then
		local popupFrame = script.ErrorPopup:Clone()
		popupFrame.Text = self.error
		popupFrame.ZIndex = 100
		popupFrame.Parent = self.textBox:FindFirstAncestorWhichIsA("LayerCollector")

		self._errorPopup = PopupHelper.attach(popupFrame, self.textBox, {
			anchor = Vector2.new(0.5, 1),
			offset = Vector2.new(0, 6),

			afterUpdate = function(_, object, _)
				PopupHelper.clamp(object)
			end,
		})
	end
end

function TextBoxStyleState:setError(err: string?)
	local lastError = self.error
	self.error = err

	if lastError ~= err then
		self._errorChanged:Fire(err)
		self:_deleteErrorPopup()
		self:_makeErrorPopup()
	end

	return self
end

export type GetTextErrorHandler = (string) -> ()
function TextBoxStyleState:getText(_: GetTextErrorHandler?): string
	return self.textBox.Text
end

function TextBoxStyleState:destroy(completely: boolean)
	self._beganConnection:Disconnect()
	self._endedConnection:Disconnect()
	self._focusedConnection:Disconnect()
	self._focusLostConnection:Disconnect()

	self:_deleteErrorPopup()

	if completely then
		self.textBox:Destroy()
	end
end

export type TextBoxStyleState = typeof(TextBoxStyleState.from({} :: any, {} :: any))
return TextBoxStyleState


local TweenService = game:GetService("TweenService")
local StyleStateHelper = require(script.Parent.StyleStateHelper)

local LabelStyleState = {}
LabelStyleState.__index = LabelStyleState

export type LabelStyleStateFromOptions = {
	style: "text" | "error" | nil,
	emphasis: "normal" | "light" | nil
}
function LabelStyleState.from(theme, label: GuiLabel, options: LabelStyleStateFromOptions?)
	options = options or {}
	assert(options)

	local self = setmetatable({
		label = label,
		theme = theme,
		style = options.style or "text",
		emphasis = options.emphasis or "normal",

		overridenColor = nil :: Color3?,
	}, LabelStyleState)

	self:update("instant")
	return self
end

-- Convenience function goes through all descendants and returns a dictionary
-- with all textlabel and imagelabel descendants as LabelStyleStates and the key as the name of the labels.
-- Will error if there are duplicate label names.
function LabelStyleState.fromDescendants(theme, ancestor: GuiObject, options: LabelStyleStateFromOptions?)
	local styleStates = {}
	for _, descendant in ancestor:GetDescendants() do
		if
			(not descendant.Parent:IsA("GuiButton")) and (descendant:IsA("TextLabel") or descendant:IsA("ImageLabel"))
			and (not descendant:GetAttribute("ExcludeStyleState"))
		then
			assert(not styleStates[descendant.Name], `Duplicate label name: {descendant:GetFullName()}`)
			styleStates[descendant.Name] = LabelStyleState.from(theme, descendant, options)
		end
	end
	return styleStates
end

function LabelStyleState:update(speed: StyleStateHelper.TransitionSpeed)
	local property = if self.label:IsA("ImageLabel") then "ImageColor3" else "TextColor3"
	local tweenInfo = StyleStateHelper.getTweenInfoForSpeed(speed)
	local color = self.overridenColor
		or self.theme.colors[if self.style == "text" then "text" elseif self.style == "error" then "error" else "text"]

	local property2 = if self.label:IsA("ImageLabel") then "ImageTransparency" else "TextTransparency"
	local transparency = if self.emphasis == "normal" then 0 else 0.1

	StyleStateHelper.tween(self.label, tweenInfo, {
		[property] = color,
		[property2] = transparency,
	})
end

function LabelStyleState:overrideColor(color: Color3)
	self.overridenColor = color
	return self
end

function LabelStyleState:destroy(completely: boolean)
	if completely then
		self.label:Destroy()
	end
end

return LabelStyleState


-- Provide a `createItemStyleState(...)` function.
-- Other consumers of the StyleState can then call `:add(...)` which will use the `createItemStyleState` to add a StyleState into the list.
-- `Pages` does this when a new page is loaded.
--
-- StyleStates produced from `createItemStyleState` can have method `setSelecting(bool)` to make them
-- eligible for the `setSelectingStyleState` method of this object which is used when you right click
-- and you want to have the item look selected.
--
-- `createItemStyleState` can optionally return a second return which is a key.
-- You can then access the StyleState created using loadMoreListStyleState.itemStyleStates[key]
-- When multiple items with the same key are added, a warning will be emitted.
-- If no key is specified, the item will be inserted in the array section of itemStyleStates sequentially.

local StyleStateHelper = require(script.Parent.StyleStateHelper)
local ButtonStyleState = require(script.Parent.ButtonStyleState)

local LoadMoreListStyleState = {}
LoadMoreListStyleState.__index = LoadMoreListStyleState

export type LoadMoreList = Frame & {
	LoadMore: TextButton,
	LoadMorePadding: GuiObject,
}

export type LoadMoreListOptions = {
	createItemStyleState: (parent: LoadMoreListStyleState, ...any) -> (StyleStateHelper.StyleState, unknown?),
}

function LoadMoreListStyleState.from(theme, frame: LoadMoreList, options: LoadMoreListOptions)
	local self = setmetatable({
		theme = theme,
		frame = frame,

		buttonStyleState = ButtonStyleState.from(theme, frame.LoadMore, {
			style = "secondary",
		}),

		itemStyleStates = {},
		selectingStyleState = nil,
		selectingId = 0,
		options = options,
	}, LoadMoreListStyleState)

	return self
end

function LoadMoreListStyleState:add(...)
	local itemStyleState, key = self.options.createItemStyleState(self, ...)
	if key then
		if self.itemStyleStates[key] then
			warn(`Duplicate key ({self.frame:GetFullName()}): {key}`)
			self.itemStyleStates[key]:destroy(true)
		end
		self.itemStyleStates[key] = itemStyleState
	else
		table.insert(self.itemStyleStates, itemStyleState)
	end

	return itemStyleState
end

function LoadMoreListStyleState:get(key: string)
	return self.itemStyleStates[key]
end

function LoadMoreListStyleState:tryRemove(key: string?): boolean
	local styleState = self.itemStyleStates[key]
	if styleState then
		styleState:destroy(true)
		self.itemStyleStates[key] = nil

		return true
	end

	return false
end

function LoadMoreListStyleState:isEmpty(): boolean
	return not next(self.itemStyleStates)
end

function LoadMoreListStyleState:clear()
	for _, styleState in self.itemStyleStates do
		styleState:destroy(true)
	end

	if self.selectingStyleState then
		self.selectingStyleState = nil
	end

	self.itemStyleStates = {}
end

function LoadMoreListStyleState:setLoadMoreVisible(visible: boolean)
	self.frame.LoadMorePadding.Visible = visible
	self.frame.LoadMore.Visible = visible
end

-- Returns a function that lets you reset the selection (only if the styleState was not nil)
function LoadMoreListStyleState:setSelectingStyleState(styleState)
	if self.selectingStyleState then
		self.selectingStyleState:setSelecting(false):update()
	end
	self.selectingStyleState = styleState

	if styleState then
		styleState:setSelecting(true):update()

		self.selectingId += 1
		local id = self.selectingId
		return function()
			if self.selectingStyleState == styleState and self.selectingId == id then
				self:setSelectingStyleState(nil)
			end
		end
	end
end

function LoadMoreListStyleState:update(speed: StyleStateHelper.TransitionSpeed?)
	for _, styleState in self.itemStyleStates do
		styleState:update(speed)
	end
	self.buttonStyleState:update(speed)
end

function LoadMoreListStyleState:destroy(completely: boolean)
	self:clear()
	if completely then
		self.frame:Destroy()
	end
end

export type LoadMoreListStyleState = typeof(setmetatable({}, LoadMoreListStyleState))

return LoadMoreListStyleState


--!strict
local TweenService = game:GetService("TweenService")
local StyleStateHelper = require(script.Parent.StyleStateHelper)

local ScrollingFrame = {}
ScrollingFrame.__index = ScrollingFrame

function ScrollingFrame.from(theme, scrollingFrame: ScrollingFrame)
	local self = setmetatable({
		scrollingFrame = scrollingFrame,
		theme = theme,
	}, ScrollingFrame)

	self:update("instant")
	return self
end

function ScrollingFrame:update(speed: StyleStateHelper.TransitionSpeed)
	local tweenInfo = StyleStateHelper.getTweenInfoForSpeed(speed)

	TweenService:Create(self.scrollingFrame, tweenInfo, {
		ScrollBarImageColor3 = (self :: any).theme.colors.scrollbar,
	}):Play()
end

function ScrollingFrame:destroy(completely: boolean)
	if completely then
		self.scrollingFrame:Destroy()
	end
end

return ScrollingFrame


local TweenService = game:GetService("TweenService")
local StyleStateHelper = require(script.Parent.StyleStateHelper)

local SeparatorStyleState = {}
SeparatorStyleState.__index = SeparatorStyleState

function SeparatorStyleState.from(theme, separator: Frame)
	local self = setmetatable({
		separator = separator,
		theme = theme,
	}, SeparatorStyleState)

	self:update("instant")
	return self
end

-- Convenience function goes through all descendants and returns an array
-- with all frames with Separator in their name as a SeparatorStyleState
function SeparatorStyleState.fromDescendants(theme, ancestor: GuiObject)
	local styleStates = {}
	for _, descendant in ancestor:GetDescendants() do
		if descendant:IsA("Frame") and descendant.Name:find("Separator") then
			table.insert(styleStates, SeparatorStyleState.from(theme, descendant))
		end
	end
	return styleStates
end

function SeparatorStyleState:update(speed: StyleStateHelper.TransitionSpeed)
	TweenService:Create(self.separator, StyleStateHelper.getTweenInfoForSpeed(speed), {
		BackgroundColor3 = self.theme.colors.separator,
	}):Play()
end

function SeparatorStyleState:destroy(completely: boolean)
	if completely then
		self.separator:Destroy()
	end
end

return SeparatorStyleState


export type TransitionSpeed = "fast" | "slow" | "instant" | "veryFast" | "superDuperFast" | nil
export type StyleState = {
	from: (theme: any, object: GuiObject, ...any) -> StyleState,

	update: (StyleState, TransitionSpeed) -> (),
	destroy: (StyleState, completely: boolean?) -> (),
}

local TweenService = game:GetService("TweenService")

local StyleStateHelper = {}

StyleStateHelper.superDuperFastTween = TweenInfo.new(0.04, Enum.EasingStyle.Sine)
StyleStateHelper.veryFastTween = TweenInfo.new(0.075, Enum.EasingStyle.Quad)
StyleStateHelper.fastTween = TweenInfo.new(0.1, Enum.EasingStyle.Sine)
StyleStateHelper.slowTween = TweenInfo.new(0.3, Enum.EasingStyle.Quad)
StyleStateHelper.instantTween = TweenInfo.new(0, Enum.EasingStyle.Linear)

function StyleStateHelper.getTweenInfoForSpeed(speed: TransitionSpeed)
	local tweenInfo = StyleStateHelper.fastTween
	if speed == "slow" then
		tweenInfo = StyleStateHelper.slowTween
	elseif speed == "instant" then
		tweenInfo = StyleStateHelper.instantTween
	elseif speed == "veryFast" then
		tweenInfo = StyleStateHelper.veryFastTween
	elseif speed == "superDuperFast" then
		tweenInfo = StyleStateHelper.superDuperFastTween
	end

	return tweenInfo
end

-- Updates a table that contains style states or more tables that contain style states
type StyleStateTable = {
	[string]: StyleState | StyleStateTable,
}
function StyleStateHelper.update(t: StyleStateTable, speed: TransitionSpeed)
	for _, styleState in t do
		if styleState.update then
			styleState:update(speed)
		else
			StyleStateHelper.update(styleState)
		end
	end
end

function StyleStateHelper.destroy(t: StyleStateTable, completely: boolean?)
	for _, styleState in t do
		if styleState.update then
			styleState:destroy(completely)
		else
			StyleStateHelper.destroy(styleState)
		end
	end
end

-- TODO: handle race conditions
function StyleStateHelper.tween(object: Instance, info: TweenInfo, props: { [string]: unknown })
	if info.Time == 0 then
		for prop, val in props do
			object[prop] = val
		end
	else
		TweenService:Create(object, info, props):Play()
	end
end

return StyleStateHelper


-- Utility class useful for one-off style states.
-- See `UI.createUserCard` for example

local StyleStateHelper = require(script.Parent.StyleStateHelper)

local StyleStateWrapper = {}
StyleStateWrapper.__index = StyleStateWrapper

export type StyleStateWrapperOptions = {
	autoFollowTheme: boolean?, -- default: true
	onThemeChange: (() -> ())?, -- Will only be called if autoFollowTheme is true
}
function StyleStateWrapper.new<
	T --[[: {[string]: StyleState}]]
>(theme, styleStates: T, options: StyleStateWrapperOptions?)
	options = options or {}
	assert(options)

	local self = setmetatable({
		styleStates = styleStates,
		connections = {},
	}, StyleStateWrapper)

	if (options.autoFollowTheme == nil) or options.autoFollowTheme then
		table.insert(
			self.connections,
			theme.colorsChanged:Connect(function()
				self:update("slow")
				if options.onThemeChange then
					options.onThemeChange()
				end
			end)
		)
	end

	return self
end

function StyleStateWrapper:update(speed: StyleStateHelper.TransitionSpeed)
	StyleStateHelper.update(self.styleStates, speed)
end

local function destroy(styleStates, completely: boolean)
	for _, state in styleStates do
		-- Otherwise it's probably a table with stylestates in it
		if state.destroy then
			state:destroy(completely)
		else
			destroy(state, completely)
		end
	end
end

function StyleStateWrapper:destroy(completely: boolean)
	destroy(self.styleStates, completely)
	for _, connection in self.connections do
		connection:Disconnect()
	end
end

return StyleStateWrapper


local TweenService = game:GetService("TweenService")
local StyleStateHelper = require(script.Parent.StyleStateHelper)
local ButtonStyleState = require(script.Parent.ButtonStyleState)

local TabsStyleState = {}
TabsStyleState.__index = TabsStyleState

function TabsStyleState.from(theme, frame: Frame)
	local buttons = {}
	for _, v in frame:GetChildren() do
		if v:IsA("GuiButton") then
			buttons[v.Name] = v
		end
	end

	local self = setmetatable({
		theme = theme,
		frame = frame,

		buttons = buttons,
		buttonStyleStates = {},

		selecting = nil,
		_selectingStyleState = nil,

		selectBar = script.SelectBar:Clone(),
	}, TabsStyleState)

	for _, button in buttons do
		local styleState = ButtonStyleState.from(self.theme, button, {
			style = "dormant",
		})
		self.buttonStyleStates[button.Name] = styleState

		local fakeBar = nil

		button.InputBegan:Connect(function(input)
			if input.UserInputType == Enum.UserInputType.MouseMovement then
				if (not fakeBar) and not styleState.selecting then
					fakeBar = self.selectBar:Clone()
					fakeBar.BackgroundTransparency = 0.75
					fakeBar.Parent = button
				end
			end
		end)

		button.InputEnded:Connect(function(input)
			if input.UserInputType == Enum.UserInputType.MouseMovement then
				if fakeBar then
					fakeBar:Destroy()
					fakeBar = nil
				end
			end
		end)

		button.Activated:Connect(function()
			self:select(button.Name)
		end)
	end

	self._selectingChanged = Instance.new("BindableEvent")
	self.selectingChanged = self._selectingChanged.Event

	self:update("instant")
	return self
end

function TabsStyleState:isSelecting(name: string?): boolean
	if self.selecting == nil or name == nil then
		return self.selecting == name
	else
		return self.selecting:lower() == name:lower()
	end
end

function TabsStyleState:select(name: string)
	if self.selecting == name then
		return
	end

	local lastSelectedStyleState = self._selectingStyleState
	if lastSelectedStyleState then
		lastSelectedStyleState:setSelecting(false):update()
	end

	local styleState = self.buttonStyleStates[name]
	self.selecting = name
	self._selectingStyleState = styleState

	styleState:setSelecting(true):update()
	self._selectingChanged:Fire(name)

	-- Style select bar
	self.selectBar.BackgroundTransparency = 1
	self.selectBar.Parent = self.buttons[name]
	TweenService:Create(self.selectBar, StyleStateHelper.getTweenInfoForSpeed("fast"), {
		BackgroundTransparency = 0,
	}):Play()
end

function TabsStyleState:update(speed: StyleStateHelper.TransitionSpeed?)
	TweenService:Create(self.selectBar, StyleStateHelper.getTweenInfoForSpeed(speed), {
		BackgroundColor3 = self.theme.colors.mainAccent,
	}):Play()
	for _, button in self.buttonStyleStates do
		button:update(speed)
	end
end

function TabsStyleState:destroy(completely: boolean)
	self.SelectBar:Destroy()
	for _, button in self.buttons do
		button:destroy(completely)
	end
	if completely then
		self.frame:Destroy()
	end
end

return TabsStyleState


-- TODO: should auto-follow theme when using.createDialog

local StyleStateHelper = require(script.Parent.Parent.StyleStateHelper)
local PopupHelper = require(script.Parent.Parent.Parent.PopupHelper)

local BackgroundStyleState = require(script.Parent.Parent.BackgroundStyleState)
local ButtonStyleState = require(script.Parent.Parent.ButtonStyleState)
local LabelStyleState = require(script.Parent.Parent.LabelStyleState)

local ConfirmDialogStyleState = {}
ConfirmDialogStyleState.__index = ConfirmDialogStyleState
ConfirmDialogStyleState.template = script.ConfirmDialog

export type ContextMenuOptions = {
	title: string,
	message: string,
	yesText: string?,
	noText: string?,
}

-- For convenience
function ConfirmDialogStyleState.confirm(
	theme,
	container: GuiBase2d,
	position: Vector2,
	options: ContextMenuOptions
): boolean
	local dialog, modal = ConfirmDialogStyleState.createDialog(theme, container, position, options)

	local confirmed = dialog.interacted:Wait()

	modal:destroy()
	dialog:destroy(true)

	return confirmed
end

function ConfirmDialogStyleState.createDialog(
	theme,
	container: GuiBase2d,
	position: Vector2,
	options: ContextMenuOptions
)
	local frame = script.ConfirmDialog:Clone()
	frame.Parent = container
	frame.Position = UDim2.fromOffset(position.X, position.Y)

	local styleState = ConfirmDialogStyleState.from(theme, frame, options)

	local modal = PopupHelper.modal(frame, container, {
		zIndex = 20,
	})
	modal.cancelled:Connect(function()
		styleState._interacted:Fire(false)
	end)

	if options.yesText then
		frame.Buttons.Yes.TextLabel.Text = options.yesText
	end
	if options.noText then
		frame.Buttons.No.TextLabel.Text = options.noText
	end

	PopupHelper.clamp(frame, container)

	return styleState, modal
end

function ConfirmDialogStyleState.from(theme, dialog: typeof(script.ConfirmDialog), options: ContextMenuOptions)
	local self = setmetatable({
		theme = theme,
		frame = dialog,
		options = options,

		_styleStates = {
			background = BackgroundStyleState.from(theme, dialog, {
				style = "popup",
			}),
			title = LabelStyleState.from(theme, dialog.Title),
			message = LabelStyleState.from(theme, dialog.Message),
			yes = ButtonStyleState.from(theme, dialog.Buttons.Yes, {
				style = "primary",
			}),
			no = ButtonStyleState.from(theme, dialog.Buttons.No, {
				style = "secondary",
			}),
		},
	}, ConfirmDialogStyleState)

	dialog.Title.Text = options.title
	dialog.Message.Text = options.message

	self._interacted = Instance.new("BindableEvent")
	self.interacted = self._interacted.Event

	dialog.Buttons.Yes.Activated:Connect(function()
		self._interacted:Fire(true)
	end)
	dialog.Buttons.No.Activated:Connect(function()
		self._interacted:Fire(false)
	end)

	self:update("instant")
	return self
end

function ConfirmDialogStyleState:interact(confirmed: boolean)
	self._interacted:Fire(confirmed)
end

function ConfirmDialogStyleState:update(speed: StyleStateHelper.TransitionSpeed?)
	for _, styleState in self._styleStates do
		styleState:update(speed)
	end
end

function ConfirmDialogStyleState:destroy(completely: boolean)
	for _, styleState in self._styleStates do
		styleState:destroy(completely)
	end
	if completely then
		self.frame:Destroy()
	end
end

return ConfirmDialogStyleState


-- TODO: should auto-follow theme when using.createMenu

local TweenService = game:GetService("TweenService")

local StyleStateHelper = require(script.Parent.Parent.StyleStateHelper)
local PopupHelper = require(script.Parent.Parent.Parent.PopupHelper)

local TRY_CLOSE_SUBMENU_DELAY = 0.275

local ContextMenuStyleState = {}
ContextMenuStyleState.__index = ContextMenuStyleState

ContextMenuStyleState.SEPARATOR = newproxy()

export type Option =
	| string
	| { icon: string, text: string, hotkey: string? }
	| { icon: string?, options: {Option} }

export type ContextMenuOptions = {
	options: {Option},
	submenuDirection: "left" | "right" | nil, -- default: right
	passThroughInput: boolean?,
}

-- For convenience
function ContextMenuStyleState.prompt(theme, container: GuiBase2d, position: Vector2, options: ContextMenuOptions)
	local contextMenu, modal = ContextMenuStyleState.createMenu(theme, container, options)
	contextMenu.frame.Position = UDim2.fromOffset(position.X, position.Y)
	PopupHelper.clamp(contextMenu.frame, container, {
		tryPivot = true,
	})

	local option = contextMenu.selected:Wait()

	modal:destroy()
	contextMenu:destroy(true)

	return option
end

function ContextMenuStyleState.createMenu(theme, container: GuiBase2d, options: ContextMenuOptions)
	local frame = script.ContextMenu:Clone()
	frame.Parent = container

	local styleState = ContextMenuStyleState.from(theme, frame, options)

	local modal = PopupHelper.modal(frame, container, {
		zIndex = 20,
		passThroughInput = if options.passThroughInput == nil then false else options.passThroughInput,
		scrimTransparency = if options.passThroughInput or (options.passThroughInput == nil) then 0.8 else 0,
	})
	modal.cancelled:Connect(function()
		styleState:selectOption(nil)
	end)

	return styleState, modal
end

local function optionsHasIcon(options: Options): boolean
	for _, option in options do
		if typeof(option) == "table" and option.icon then
			return true
		end
	end
	return false
end

function ContextMenuStyleState.from(theme, contextMenu: typeof(script.ContextMenu), options: ContextMenuOptions)
	local self = setmetatable({
		theme = theme,
		frame = contextMenu,
		options = options.options,
		submenuDirection = options.submenuDirection,

		_hasIcons = optionsHasIcon(options.options),
		_subContextMenu = nil,

		_buttons = table.create(#options.options),
		_separators = table.create(1),
		_hoveringOption = nil,
	}, ContextMenuStyleState)

	local lastInserted: "option" | "separator" | nil = nil
	for i, option in options.options do
		if option == ContextMenuStyleState.SEPARATOR then
			if lastInserted == "option" then
				local separator = script.Separator:Clone()
				table.insert(self._separators, separator)
				separator.Parent = self.frame

				lastInserted = "separator"
			end
		else
			local icon = ""
			local text = option
			local hotkey = nil
			local suboptions: {Option}? = nil
			if typeof(option) == "table" then
				text = option.text
				icon = option.icon
				hotkey = option.hotkey
				suboptions = option.options
			end

			local button = script.Option:Clone()
			button.Name = text
			button.TextLabel.Text = text

			if self._hasIcons then
				button.Icon.Visible = true
				button.Icon.Image = icon or ""

				button.UIPadding.PaddingLeft = UDim.new(0, 3)
			end

			if hotkey then
				button.Hotkey.Visible = true
				button.Hotkey.Text = hotkey
			else
				button.Hotkey.Visible = false
			end

			if suboptions then
				script.Arrow:Clone().Parent = button
			end

			table.insert(self._buttons, button)

			button.InputBegan:Connect(function(input)
				if input.UserInputType == Enum.UserInputType.MouseMovement then
					self._hoveringOption = text
					self:update("veryFast")

					if suboptions and ((self._subContextMenu and self._subContextMenu.options ~= suboptions) or (not self._subContextMenu)) then
						self:_openSub(button, text, suboptions)
					end
				end
			end)

			button.InputEnded:Connect(function(input)
				if input.UserInputType == Enum.UserInputType.MouseMovement then
					if suboptions and self._subContextMenu and self._subContextMenu.options == suboptions then
						task.delay(TRY_CLOSE_SUBMENU_DELAY, function()
							if suboptions and self._subContextMenu and self._subContextMenu.options == suboptions then
								self:_tryCloseSub(button, text, input.Position)
							end
						end)
					else
						if self._hoveringOption == text then
							self._hoveringOption = nil
							self:update("veryFast")
						end
					end
				end
			end)

			if not suboptions then
				button.Activated:Connect(function()
					self:selectOption(text)
				end)
			end

			button.Parent = self.frame

			lastInserted = "option"
		end
	end

	self._selected = Instance.new("BindableEvent")
	self.selected = self._selected.Event

	self:_updateSeparatorSizes()
	contextMenu:GetPropertyChangedSignal("AbsoluteSize"):Connect(function()
		self:_updateSeparatorSizes()
	end)

	self:update("instant")
	return self
end

-- open sub menu
function ContextMenuStyleState:_openSub(button: GuiObject, buttonText: string, options: {Option})
	self:_closeSub()

	local subframe = script.ContextMenu:Clone()
	subframe.Parent = self.frame.Parent
	subframe.ZIndex = self.frame.ZIndex
	subframe.Position = UDim2.fromOffset(button.AbsolutePosition.X, button.AbsolutePosition.Y)
	if self.submenuDirection == "left" then
		subframe.AnchorPoint = Vector2.new(1, 0)
	else
		subframe.Position += UDim2.fromOffset(button.AbsoluteSize.X, 0)
	end

	subframe.InputEnded:Connect(function(input)
		if input.UserInputType == Enum.UserInputType.MouseMovement then
			task.delay(TRY_CLOSE_SUBMENU_DELAY, function()
				if self._subContextMenu and self._subContextMenu.frame == subframe then
					self:_tryCloseSub(button, buttonText, input.Position)
				end
			end)
		end
	end)

	self._subContextMenu = ContextMenuStyleState.from(self.theme, subframe, {
		options = options,
	})

	self._subContextMenu.selected:Connect(function(option)
		if typeof(option) == "table" then
			table.insert(option, 1, buttonText)
		else
			option = {buttonText, option}
		end

		self:selectOption(option)
	end)

	if self.submenuDirection == "left" then
		self._subContextMenu:animate(-16, 0)
	else
		self._subContextMenu:animate(16, 0)
	end
end

function ContextMenuStyleState:_closeSub()
	if self._subContextMenu then
		self._subContextMenu:destroy(true)
		self._subContextMenu = nil
	end
end

-- try to close submenu if mouse is not in safe zone
-- `button` parameter is the button the submenu belongs to
function ContextMenuStyleState:_tryCloseSub(button: TextButton, buttonText: string, position: Vector2)
	if self._subContextMenu then
		local shouldStayOpen = false

		if position.X > self._subContextMenu.frame.AbsolutePosition.X
			and position.X < self._subContextMenu.frame.AbsolutePosition.X + self._subContextMenu.frame.AbsoluteSize.X
			and position.Y > self._subContextMenu.frame.AbsolutePosition.Y
			and position.Y < self._subContextMenu.frame.AbsolutePosition.Y + self._subContextMenu.frame.AbsoluteSize.Y
		then
			shouldStayOpen = true
		end

		if not shouldStayOpen then
			if position.X > button.AbsolutePosition.X - 1
				and position.X < button.AbsolutePosition.X + button.AbsoluteSize.X + 1
				and position.Y > button.AbsolutePosition.Y
				and position.Y < button.AbsolutePosition.Y + button.AbsoluteSize.Y
			then
				shouldStayOpen = true
			end
		end

		if not shouldStayOpen then
			self:_closeSub()

			if self._hoveringOption == buttonText then
				self._hoveringOption = nil
				self:update("veryFast")
			end
		end
	end
end

-- Can't use scale because AutomaticSize will expand it all the way
function ContextMenuStyleState:_updateSeparatorSizes()
	for _, separator in self._separators do
		separator.Size = UDim2.fromOffset(
			self.frame.AbsoluteSize.X
				- (script.ContextMenu.UIPadding.PaddingLeft.Offset + script.ContextMenu.UIPadding.PaddingRight.Offset),
			1
		)
	end
end

-- `option` could be an array if it is coming from a submenu.
-- In that case it will be {"option", "submenuOption", ...}
function ContextMenuStyleState:selectOption(option: string | {string} | nil)
	self._selected:Fire(option)
end

function ContextMenuStyleState:update(speed: StyleStateHelper.TransitionSpeed?)
	local tweenInfo = StyleStateHelper.getTweenInfoForSpeed(speed)
	StyleStateHelper.tween(self.frame, tweenInfo, {
		BackgroundColor3 = self.theme.colors.floatingAction.background,
	})
	StyleStateHelper.tween(self.frame.UIStroke, tweenInfo, {
		Color = self.theme.colors.floatingAction.outline,
	})
	for _, separator in self._separators do
		StyleStateHelper.tween(separator, tweenInfo, {
			BackgroundColor3 = self.theme.colors.separator,
		})
	end
	for _, button in self._buttons do
		local hovering = (self._hoveringOption == button.Name)
		StyleStateHelper.tween(button, tweenInfo, {
			BackgroundTransparency = if hovering then self.theme.colors.button.transparent.press else 1,
			BackgroundColor3 = self.theme.colors.button.transparent.baseColor,
		})
		StyleStateHelper.tween(button.TextLabel, tweenInfo, {
			TextColor3 = self.theme.colors.text,
		})
		StyleStateHelper.tween(button.Hotkey, tweenInfo, {
			TextColor3 = if hovering then self.theme.colors.text else self.theme.colors.disabledText,
		})
		if self._hasIcons then
			StyleStateHelper.tween(button.Icon, tweenInfo, {
				ImageColor3 = self.theme.colors.text,
			})
		end

		local arrow = button:FindFirstChild("Arrow")
		if arrow then
			StyleStateHelper.tween(button.Arrow.ImageLabel, tweenInfo, {
				ImageColor3 = self.theme.colors.text,
			})
		end
	end
end

-- Assumes ContextMenu is parented directly to the GUI
function ContextMenuStyleState:animate(xOffset: number?, yOffset: number?)
	local originalPosition = self.frame.Position
	local originalAnchor = self.frame.AnchorPoint
	local originalParent = self.frame.Parent

	local targetX = self.frame.AbsolutePosition.X - 2
	local targetY = self.frame.AbsolutePosition.Y - 2
	local startX = targetX - (xOffset or 0)
	local startY = targetY - (yOffset or 16)

	local canvasGroup = Instance.new("CanvasGroup")
	canvasGroup.BackgroundTransparency = 1
	canvasGroup.Size = UDim2.fromOffset(4 + self.frame.AbsoluteSize.X, 4 + self.frame.AbsoluteSize.Y)
	canvasGroup.Position = UDim2.fromOffset(targetX, startY)

	self.frame.Position = UDim2.fromOffset(2, 2)
	self.frame.AnchorPoint = Vector2.new(0, 0)
	self.frame.Size = UDim2.fromOffset(self.frame.AbsoluteSize.X, self.frame.AbsoluteSize.Y)
	self.frame.Parent = canvasGroup
	canvasGroup.ZIndex = self.frame.ZIndex
	canvasGroup.Parent = originalParent

	local start = os.clock()
	while self.frame and self._selected do
		local delta = os.clock() - start
		local a = math.min(delta / 0.1, 1)
		a = TweenService:GetValue(a, Enum.EasingStyle.Cubic, Enum.EasingDirection.Out)

		canvasGroup.Position = UDim2.fromOffset(startX + a * (targetX - startX), startY + a * (targetY - startY))
		canvasGroup.GroupTransparency = 1 - a

		if a >= 1 then
			break
		end

		task.wait()
	end

	if self.frame and self.frame.Parent then
		self.frame.Position = originalPosition
		self.frame.AnchorPoint = originalAnchor
		self.frame.Parent = originalParent
	end

	canvasGroup:Destroy()
end

function ContextMenuStyleState:destroy(completely: boolean)
	self:_closeSub()
	self._selected:Destroy()
	self._selected = nil

	for _, button in self._buttons do
		button:Destroy()
	end
	self._buttons = nil

	if completely then
		self.frame:Destroy()
		self.frame = nil
	end
end

return ContextMenuStyleState


-- Should be used ephemerally

-- Example:
-- ```
-- local styleState = LoadingBarStyleState.from(theme, frame)
-- task.spawn(styleState.animate, styleState)
-- task.wait(5)
-- styleState:destroy()
-- ```

local Theme = require(script.Parent.Parent.Parent.Theme)

local GRADIENT_BILINEAR = NumberSequence.new({
	NumberSequenceKeypoint.new(0, 1),
	NumberSequenceKeypoint.new(0.5, 0),
	NumberSequenceKeypoint.new(1, 1),
})
local GRADIENT_MOSTLY_SOLID = NumberSequence.new({
	NumberSequenceKeypoint.new(0, 0.5),
	NumberSequenceKeypoint.new(0.5, 0),
	NumberSequenceKeypoint.new(1, 0.5),
})

local LoadingBarStyleState = {}
LoadingBarStyleState.__index = LoadingBarStyleState

-- For convenience
-- Returns a function to delete the loading bar
function LoadingBarStyleState.show(theme, container: Frame): () -> ()
	local loadingBar = LoadingBarStyleState.from(theme, container)
	loadingBar.container.Visible = true
	task.spawn(loadingBar.animate, loadingBar)

	return function()
		loadingBar.container.Visible = false
		loadingBar:destroy()
	end
end

function LoadingBarStyleState.from(theme, container: Frame)
	local self = setmetatable({
		theme = theme,
		container = container,
		fill = script.Fill:Clone(),
	}, LoadingBarStyleState)

	self.fill.Parent = container

	self:update("instant")
	return self
end

function LoadingBarStyleState:update()
	if Theme.getLuminance(self.theme.colors.background) > 0.5 then
		self.container.BackgroundTransparency = 0
		local hue, _, _ = Theme.getAccentComponents(self.theme.preset, self.theme.accent)
		self.container.BackgroundColor3 = Color3.new(0.6, 0.6, 0.6)
		self.fill.UIGradient.Transparency = GRADIENT_MOSTLY_SOLID
	else
		self.container.BackgroundTransparency = 0.75
		self.container.BackgroundColor3 = self.theme.colors.mainAccent
		self.fill.UIGradient.Transparency = GRADIENT_BILINEAR
	end
	self.fill.BackgroundColor3 = self.theme.colors.mainAccent
end

-- destroy completely for animation to end
function LoadingBarStyleState:animate()
	local start = os.clock()
	-- Light theme it's hard to see the bar at first so fast forward so they see it
	if Theme.getLuminance(self.theme.colors.background) > 0.5 then
		start += 0.5
	end

	while self.fill.Parent do
		local delta = os.clock() - start
		local a = 1.5 * delta
		local b = a % 1
		self.fill.Position = UDim2.fromScale(-0.5 + 2 * b, 0)
		task.wait()
	end
end

function LoadingBarStyleState:destroy(completely)
	assert(not completely, "Loading bar should not own the frame that contains the fill.")

	self.fill:Destroy()
	if completely then
		self.frame:Destroy()
	end
end

return LoadingBarStyleState


--!strict

local StyleStateHelper = require(script.Parent.Parent.StyleStateHelper)
local StyleStateWrapper = require(script.Parent.Parent.StyleStateWrapper)
local PopupHelper = require(script.Parent.Parent.Parent.PopupHelper)

local BackgroundStyleState = require(script.Parent.Parent.BackgroundStyleState)
local ButtonStyleState = require(script.Parent.Parent.ButtonStyleState)
local LabelStyleState = require(script.Parent.Parent.LabelStyleState)

local PopupFormStyleState = {}
PopupFormStyleState.__index = PopupFormStyleState
PopupFormStyleState.template = script.PopupForm
PopupFormStyleState.buttonTemplate = script.ButtonTemplate

export type PopupFormFromOptions = {
	title: string,
	buttons: { { text: string, options: ButtonStyleState.ButtonStyleStateFromOptions? } },
	addContents: (Frame) -> { StyleStateHelper.StyleState } | { [string]: StyleStateHelper.StyleState },
}

-- For convenience
function PopupFormStyleState.createPopup(theme, container: GuiBase2d, position: Vector2, options: PopupFormFromOptions)
	local frame = script.PopupForm:Clone()
	frame.Parent = container
	frame.Position = UDim2.fromOffset(position.X, position.Y)

	local styleState = PopupFormStyleState.from(theme, frame, options)

	local modal = PopupHelper.modal(frame, container, {
		zIndex = 20,
	})
	modal.cancelled:Connect(function()
		styleState._interacted:Fire(nil)
	end)

	PopupHelper.clamp(frame, container)

	return styleState, modal
end

function PopupFormStyleState.from(theme, dialog: typeof(PopupFormStyleState.template), options: PopupFormFromOptions)
	local self = setmetatable({
		theme = theme,
		frame = dialog,
	}, PopupFormStyleState)

	local buttonStyleStates = {}
	for _, buttonInfo in options.buttons do
		local button = script.ButtonTemplate:Clone()
		button.TextLabel.Text = buttonInfo.text
		local styleState = ButtonStyleState.from(theme, button, buttonInfo.options)
		buttonStyleStates[buttonInfo.text] = styleState

		button.Activated:Connect(function()
			self:interact(buttonInfo.text)
		end)

		button.Parent = dialog.Form.Buttons
	end

	self.wrapper = StyleStateWrapper.new(theme, {
		background = BackgroundStyleState.from(theme, dialog, {
			style = "popup",
		}),
		title = LabelStyleState.from(theme, dialog.Title),
		contents = options.addContents(dialog.Form),
		buttons = buttonStyleStates,
	})

	dialog.Title.Text = options.title

	self._interacted = Instance.new("BindableEvent")
	self.interacted = self._interacted.Event

	self:update("instant")
	return self
end

function PopupFormStyleState:interact(clicked: string?)
	self._interacted:Fire(clicked)
end

function PopupFormStyleState:update(speed: StyleStateHelper.TransitionSpeed?)
	self.wrapper:update(speed)
end

function PopupFormStyleState:destroy(completely: boolean)
	self.wrapper:destroy(completely)
	if completely then
		self.frame:Destroy()
	end
end

return PopupFormStyleState


-- Should be used ephemerally

-- Just apply this to any object.
-- Objects in the children that have attribute "SkeletonPlaceholder" will be styled.
-- If they also have "DeleteWithSkeleton" attribute they will be deleted when the skeleton is deleted.

--- Call :animate to animate the skeleton

local TweenService = game:GetService("TweenService")

local StyleStateHelper = require(script.Parent.Parent.StyleStateHelper)

local SkeletonStyleState = {}
SkeletonStyleState.__index = SkeletonStyleState

function SkeletonStyleState.from(theme, object: GuiObject)
	local self = setmetatable({
		theme = theme,
		object = object,

		skeletonPlaceholders = {},

		_destroyed = false,
	}, SkeletonStyleState)

	for _, child in object:GetDescendants() do
		if child:GetAttribute("SkeletonPlaceholder") then
			local gradient = script.Gradient:Clone()
			gradient.Parent = child
			table.insert(self.skeletonPlaceholders, {
				deleteWith = child:GetAttribute("DeleteWithSkeleton"),
				object = child,
				gradient = gradient,
				originalColor = child.BackgroundColor3,
				originalTransparency = child.BackgroundTransparency,
			})
		end
	end

	self:update("instant")
	return self
end

function SkeletonStyleState:animate()
	local start = os.clock()
	while not self._destroyed do
		local delta = os.clock() - start
		local a = delta % 2
		if a <= 0.5 then
			a = 2 * a
		else
			a = 1
		end

		local offset = Vector2.new(2 * (0.5 - a), 0)

		for _, placeholder in self.skeletonPlaceholders do
			placeholder.gradient.Offset = offset
		end

		task.wait()
	end
end

function SkeletonStyleState:update(speed: StyleStateHelper.TransitionSpeed)
	local tweenInfo = StyleStateHelper.getTweenInfoForSpeed(speed)
	for _, placeholder in self.skeletonPlaceholders do
		TweenService:Create(placeholder.object, tweenInfo, {
			BackgroundColor3 = Color3.new(0.5, 0.5, 0.5),
			BackgroundTransparency = 0.5,
		}):Play()
	end
end

function SkeletonStyleState:destroy(completely: boolean)
	if self._destroyed then
		return
	end
	self._destroyed = true

	for _, placeholder in self.skeletonPlaceholders do
		if placeholder.deleteWith then
			placeholder.object:Destroy()
		else
			placeholder.object.BackgroundColor3 = placeholder.originalColor
			placeholder.object.BackgroundTransparency = placeholder.originalTransparency
			placeholder.gradient:Destroy()
		end
	end

	if completely then
		self.object:Destroy()
	end
end

return SkeletonStyleState


local Oklab = {}

function Oklab.fromLinearSrgb(c: vector): vector
	local l = 0.4122214708 * c.x + 0.5363325363 * c.y + 0.0514459929 * c.z;
	local m = 0.2119034982 * c.x + 0.6806995451 * c.y + 0.1073969566 * c.z;
	local s = 0.0883024619 * c.x + 0.2817188376 * c.y + 0.6299787005 * c.z;

	local l_ = l ^ (1/3);
	local m_ = m ^ (1/3);
	local s_ = s ^ (1/3);

	return vector.create(
		0.2104542553*l_ + 0.7936177850*m_ - 0.0040720468*s_,
		1.9779984951*l_ - 2.4285922050*m_ + 0.4505937099*s_,
		0.0259040371*l_ + 0.7827717662*m_ - 0.8086757660*s_
	);
end

function Oklab.toLinearSrgb(c: vector): vector
	local l_ = c.x + 0.3963377774 * c.y + 0.2158037573 * c.z;
	local m_ = c.x - 0.1055613458 * c.y - 0.0638541728 * c.z;
	local s_ = c.x - 0.0894841775 * c.y - 1.2914855480 * c.z;

	local l = l_^3;
	local m = m_^3;
	local s = s_^3;

	return vector.create(
		4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s,
		-1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s,
		-0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s
	);
end

local function linearizeComponent(x: number): number
	if x >= 0.04045 then
		return ((x + 0.055)/(1 + 0.055))^2.4
	else
		return x / 12.92
	end
end

local function inverseLinearizeComponent(x: number): number
	if x >= 0.0031308 then
		return (1.055) * x^(1.0/2.4) - 0.055
	else
		return 12.92 * x
	end
end

function Oklab.fromColor3(c: Color3)
	return Oklab.fromLinearSrgb(
		vector.create(
			linearizeComponent(c.R),
			linearizeComponent(c.G),
			linearizeComponent(c.B)
		)
	)
end

function Oklab.toColor3(c: vector)
	local srgb = Oklab.toLinearSrgb(c)
	return Color3.new(
		math.clamp(inverseLinearizeComponent(srgb.x), 0, 1),
		math.clamp(inverseLinearizeComponent(srgb.y), 0, 1),
		math.clamp(inverseLinearizeComponent(srgb.z), 0, 1)
	)
end

function Oklab.oklab(l: number, a: number, b: number): Color3
	return Oklab.toColor3(vector.create(l, a, b))
end

function Oklab.oklch(l: number, c: number, h: number): Color3
	return Oklab.toColor3(vector.create(l, c * math.cos(math.rad(h)), c * math.sin(math.rad(h))))
end

return Oklab