--!strict

local root = script.Parent.Parent.Parent
local Common = root.Common

local CommonIncludes = Common.Includes
local Cryo = require(CommonIncludes.Cryo)

local Modules = root.Modules
local Enums = require(Modules.Enums)
local Types = require(Modules.Types)
local Util = require(Modules.Util)

---

type table = Types.table

type Color = Types.ColorPaletteColor
type ColorPalette = Types.ColorPalette

--[[
    Rodux reducers for color-editor-related updates.
]]
return {
    --[[
        Updates the color editor's working color.

        ```
        action = {
            color: Color,
            editor: Enums.EditorKey?
        }
        ```

        @param oldState The previous state
        @param action The action information
        @return The next state
    ]]
    [Enums.StoreActionType.ColorEditor_SetColor] = function(oldState: table, action: table): table
        return Util.table.deepFreeze(Cryo.Dictionary.join(oldState, {
            colorEditor = Cryo.Dictionary.join(oldState.colorEditor, {
                color = action.color,
                authoritativeEditor = action.editor or Enums.EditorKey.Default
            })
        }))
    end,

    --[[
        Adds a color to the color editor's quick palette.

        ```
        action = {
            color: Color
        }
        ```

        @param oldState The previous state
        @param action The action information
        @return The next state
    ]]
    [Enums.StoreActionType.ColorEditor_AddQuickPaletteColor] = function(oldState: table, action: table): table
        return Util.table.deepFreeze(Cryo.Dictionary.join(oldState, {
            colorEditor = Cryo.Dictionary.join(oldState.colorEditor, {
                quickPalette = Cryo.List.join({action.color}, oldState.colorEditor.quickPalette)
            })
        }))
    end,

    --[[
        Adds a new or pre-made palette to the list of color palettes.

        ```
        action = {
            name: string?,
            palette: ColorPalette?
        }
        ```

        @param oldState The previous state
        @param action The action information
        @return The next state
    ]]
    [Enums.StoreActionType.ColorEditor_AddPalette] = function(oldState: table, action: table): table
        local newPalette: ColorPalette
        local palettes: {ColorPalette} = oldState.colorEditor.palettes

        if (action.palette) then
            newPalette = action.palette
        else
            local desiredPaletteName: string = action.name or "New Palette"
            local actualPaletteName: string = Util.palette.getNewItemName(palettes, desiredPaletteName)

            newPalette = {
                name = actualPaletteName,
                colors = {}
            }
        end

        return Util.table.deepFreeze(Cryo.Dictionary.join(oldState, {
            colorEditor = Cryo.Dictionary.join(oldState.colorEditor, {
                palettes = Cryo.List.join(palettes, {newPalette})
            })
        }))
    end,

    --[[
        Removes a color palette.

        ```
        action = {
            paletteIndex: number
        }
        ```

        @param oldState The previous state
        @param action The action information
        @return The next state
    ]]
    [Enums.StoreActionType.ColorEditor_RemovePalette] = function(oldState: table, action: table): table
        local palettes: {ColorPalette} = oldState.colorEditor.palettes
        local paletteIndex: number = action.paletteIndex
        if (not palettes[paletteIndex]) then return oldState end

        return Util.table.deepFreeze(Cryo.Dictionary.join(oldState, {
            colorEditor = Cryo.Dictionary.join(oldState.colorEditor, {
                palettes = Cryo.List.removeIndex(palettes, paletteIndex)
            })
        }))
    end,

    --[[
        Duplicates a color palette and adds it to the color palette list.

        ```
        action = {
            paletteIndex: number
        }
        ```

        @param oldState The previous state
        @param action The action information
        @return The next state
    ]]
    [Enums.StoreActionType.ColorEditor_DuplicatePalette] = function(oldState: table, action: table): table
        local palettes: {ColorPalette} = oldState.colorEditor.palettes
        local paletteIndex: number = action.paletteIndex

        local palette: ColorPalette = palettes[paletteIndex]
        if (not palette) then return oldState end

        local matchStart: number? = string.find(palette.name, "%s*%(%d+%)$")
        local nameWithoutCounter: string = if (matchStart) then string.sub(palette.name, 1, matchStart - 1) else palette.name

        local newPaletteName: string = Util.palette.getNewItemName(palettes, nameWithoutCounter)
        local newPalette: ColorPalette = Util.table.deepCopy(palette)
        newPalette.name = newPaletteName

        return Util.table.deepFreeze(Cryo.Dictionary.join(oldState, {
            colorEditor = Cryo.Dictionary.join(oldState.colorEditor, {
                palettes = Cryo.List.join(palettes, {newPalette})
            })
        }))
    end,

    --[[
        Modifies the name of a color palette.

        ```
        action = {
            paletteIndex: number,
            newName: string
        }
        ```

        @param oldState The previous state
        @param action The action information
        @return The next state
    ]]
    [Enums.StoreActionType.ColorEditor_ChangePaletteName] = function(oldState: table, action: table): table
        local palettes: {ColorPalette} = oldState.colorEditor.palettes
        local paletteIndex: number = action.paletteIndex

        local palette: ColorPalette = palettes[paletteIndex]
        if (not palette) then return oldState end

        local newPaletteName: string = Util.palette.getNewItemName(palettes, action.newName, paletteIndex)
        if (newPaletteName == palette.name) then return oldState end

        return Util.table.deepFreeze(Cryo.Dictionary.join(oldState, {
            colorEditor = Cryo.Dictionary.join(oldState.colorEditor, {
                palettes = Cryo.List.replaceIndex(palettes, paletteIndex, Cryo.Dictionary.join(palette, {
                    name = newPaletteName
                }))
            })
        }))
    end,

    --[[
        Adds the current working color to a color palette.

        ```
        action = {
            paletteIndex: number,
            newName: string?
        }
        ```

        @param oldState The previous state
        @param action The action information
        @return The next state
    ]]
    [Enums.StoreActionType.ColorEditor_AddCurrentColorToPalette] = function(oldState: table, action: table): table
        local palettes: {ColorPalette} = oldState.colorEditor.palettes
        local paletteIndex: number = action.paletteIndex

        local palette: ColorPalette = palettes[paletteIndex]
        if (not palette) then return oldState end

        local paletteColors: {Color} = palette.colors
        local newColorName: string = Util.palette.getNewItemName(paletteColors, action.newName or "New Color")

        local newColor: Color = {
            name = newColorName,
            color = oldState.colorEditor.color:toColor3(),
        }

        return Util.table.deepFreeze(Cryo.Dictionary.join(oldState, {
            colorEditor = Cryo.Dictionary.join(oldState.colorEditor, {
                palettes = Cryo.List.replaceIndex(palettes, paletteIndex, Cryo.Dictionary.join(palette, {
                    colors = Cryo.List.join(paletteColors, {newColor})
                }))
            })
        }))
    end,

    --[[
        Removes a color from a color palette.

        ```
        action = {
            paletteIndex: number,
            colorIndex: number
        }
        ```

        @param oldState The previous state
        @param action The action information
        @return The next state
    ]]
    [Enums.StoreActionType.ColorEditor_RemovePaletteColor] = function(oldState: table, action: table): table
        local palettes: {ColorPalette} = oldState.colorEditor.palettes
        local paletteIndex: number = action.paletteIndex

        local palette: ColorPalette = palettes[paletteIndex]
        if (not palette) then return oldState end

        local paletteColors: {Color} = palette.colors
        local colorIndex = action.colorIndex

        local color: Color = paletteColors[colorIndex]
        if (not color) then return oldState end

        return Util.table.deepFreeze(Cryo.Dictionary.join(oldState, {
            colorEditor = Cryo.Dictionary.join(oldState.colorEditor, {
                palettes = Cryo.List.replaceIndex(palettes, paletteIndex, Cryo.Dictionary.join(palette, {
                    colors = Cryo.List.removeIndex(paletteColors, colorIndex)
                }))
            })
        }))
    end,

    --[[
        Modifies the name of a color in a color palette.

        ```
        action = {
            paletteIndex: number,
            colorIndex: number,
            newName: string
        }
        ```

        @param oldState The previous state
        @param action The action information
        @return The next state
    ]]
    [Enums.StoreActionType.ColorEditor_ChangePaletteColorName] = function(oldState: table, action: table): table
        local palettes: {ColorPalette} = oldState.colorEditor.palettes
        local paletteIndex: number = action.paletteIndex

        local palette: ColorPalette = palettes[paletteIndex]
        if (not palette) then return oldState end

        local paletteColors: {Color} = palette.colors
        local colorIndex: number = action.colorIndex

        local color: Color = paletteColors[colorIndex]
        if (not color) then return oldState end

        local newColorName: string = Util.palette.getNewItemName(paletteColors, action.newName, colorIndex)
        if (newColorName == color.name) then return oldState end

        return Util.table.deepFreeze(Cryo.Dictionary.join(oldState, {
            colorEditor = Cryo.Dictionary.join(oldState.colorEditor, {
                palettes = Cryo.List.replaceIndex(palettes, paletteIndex, Cryo.Dictionary.join(palette, {
                    colors = Cryo.List.replaceIndex(paletteColors, colorIndex, Cryo.Dictionary.join(color, {
                        name = newColorName
                    }))
                }))
            })
        }))
    end,

    --[[
        Modifies the position of a color in a color palette.

        ```
        action = {
            paletteIndex: number,
            colorIndex: number,
            offset: number,
        }
        ```

        @param oldState The previous state
        @param action The action information
        @return The next state
    ]]
    [Enums.StoreActionType.ColorEditor_ChangePaletteColorPosition] = function(oldState: table, action: table): table
        local palettes: {ColorPalette} = oldState.colorEditor.palettes
        local paletteIndex: number = action.paletteIndex

        local palette: ColorPalette = palettes[paletteIndex]
        if (not palette) then return oldState end

        local paletteColors: {Color} = palette.colors
        local colorIndex: number = action.colorIndex
        local otherColorIndex: number = colorIndex + action.offset
        if (not (paletteColors[colorIndex] and paletteColors[otherColorIndex])) then return oldState end

        return Util.table.deepFreeze(Cryo.Dictionary.join(oldState, {
            colorEditor = Cryo.Dictionary.join(oldState.colorEditor, {
                palettes = Cryo.List.replaceIndex(palettes, paletteIndex, Cryo.Dictionary.join(palette, {
                    colors = Cryo.Dictionary.join(paletteColors, {
                        [colorIndex] = paletteColors[otherColorIndex],
                        [otherColorIndex] = paletteColors[colorIndex]
                    })
                }))
            })
        }))
    end,

    --[[
        Overrides the current set of color palettes.

        ```
        action = {
            palettes: {ColorPalette}
        }
        ```

        @param oldState The previous state
        @param action The action information
        @return The next state
    ]]
    [Enums.StoreActionType.ColorEditor_SetPalettes] = function(oldState: table, action: table): table
        return Util.table.deepFreeze(Cryo.Dictionary.join(oldState, {
            colorEditor = Cryo.Dictionary.join(oldState.colorEditor, {
                palettes = action.palettes
            }),
        }))
    end,
}


--!strict

local root = script.Parent.Parent.Parent
local Common = root.Common

local CommonIncludes = Common.Includes
local Cryo = require(CommonIncludes.Cryo)

local Modules = root.Modules
local Enums = require(Modules.Enums)
local Types = require(Modules.Types)
local Util = require(Modules.Util)

---

type table = Types.table

type Gradient = Types.GradientPaletteGradient
type GradientPalette = Types.GradientPalette

---

--[[
    Rodux reducers for gradient-editor-related updates.
]]
return {
    --[[
        Clears out the working gradient's information from
        the gradient editor.

        @param oldState The previous state
        @return The next state
    ]]
    [Enums.StoreActionType.GradientEditor_ResetState] = function(oldState: table, _: table): table
        return Util.table.deepFreeze(Cryo.Dictionary.join(oldState, {
            gradientEditor = {
                palettes = oldState.gradientEditor.palettes,
            }
        }))
    end,

    --[[
        Updates the working gradient's list of keypoints.

        ```
        action = {
            keypoints: {GradientKeypoint}?,
            selectedKeypoint: number?
        }
        ```

        @param oldState The previous state
        @param action The action information
        @return The next state
    ]]
    [Enums.StoreActionType.GradientEditor_SetKeypoints] = function(oldState: table, action: table): table
        local gradientEditorState: table = oldState.gradientEditor

        return Util.table.deepFreeze(Cryo.Dictionary.join(oldState, {
            gradientEditor = Cryo.Dictionary.join(gradientEditorState, {
                keypoints = action.keypoints,
                selectedKeypoint = if (action.selectedKeypoint ~= -1) then action.selectedKeypoint else Cryo.None,

                displayKeypoints = Util.generateFullKeypointList(
                    action.keypoints or gradientEditorState.keypoints,
                    gradientEditorState.colorSpace,
                    gradientEditorState.hueAdjustment,
                    gradientEditorState.precision or 0
                )
            })
        }))
    end,

    --[[
        Updates the working gradient's information.

        ```
        action = {
            keypoints: {GradientKeypoint}?,
            colorSpace: MixableColorType?,
            hueAdjustment: HueAdjustment?,
            precision: number?
        }
        ```

        @param oldState The previous state
        @param action The action information
        @return The next state
    ]]
    [Enums.StoreActionType.GradientEditor_SetGradient] = function(oldState: table, action: table): table
        local newKeypointInfo = Cryo.Dictionary.join(oldState.gradientEditor, {
            keypoints = action.keypoints,
            colorSpace = action.colorSpace,
            hueAdjustment = action.hueAdjustment,
            precision = action.precision,
        })

        local newDisplayKeypoints = Util.generateFullKeypointList(
            newKeypointInfo.keypoints,
            newKeypointInfo.colorSpace,
            newKeypointInfo.hueAdjustment,
            newKeypointInfo.precision or 0
        )

        return Util.table.deepFreeze(Cryo.Dictionary.join(oldState, {
            gradientEditor = Cryo.Dictionary.join(oldState.gradientEditor, newKeypointInfo, {
                displayKeypoints = newDisplayKeypoints
            }),
        }))
    end,

    --[[
        Adds a new or pre-made palette to the list of gradient palettes.

        ```
        action = {
            name: string?,
            palette: GradientPalette?
        }
        ```

        @param oldState The previous state
        @param action The action information
        @return The next state
    ]]
    [Enums.StoreActionType.GradientEditor_AddPalette] = function(oldState: table, action: table): table
        local newPalette: GradientPalette
        local palettes: {GradientPalette} = oldState.gradientEditor.palettes

        if (action.palette) then
            newPalette = action.palette
        else
            local desiredPaletteName: string = action.name or "New Palette"
            local actualPaletteName: string = Util.palette.getNewItemName(palettes, desiredPaletteName)

            newPalette = {
                name = actualPaletteName,
                gradients = {}
            }
        end

        return Util.table.deepFreeze(Cryo.Dictionary.join(oldState, {
            gradientEditor = Cryo.Dictionary.join(oldState.gradientEditor, {
                palettes = Cryo.List.join(palettes, {newPalette})
            })
        }))
    end,

    --[[
        Removes a gradient palette.

        ```
        action = {
            paletteIndex: number
        }
        ```

        @param oldState The previous state
        @param action The action information
        @return The next state
    ]]
    [Enums.StoreActionType.GradientEditor_RemovePalette] = function(oldState: table, action: table): table
        local palettes: {GradientPalette} = oldState.gradientEditor.palettes
        local paletteIndex: number = action.paletteIndex
        if (not palettes[paletteIndex]) then return oldState end

        return Util.table.deepFreeze(Cryo.Dictionary.join(oldState, {
            gradientEditor = Cryo.Dictionary.join(oldState.gradientEditor, {
                palettes = Cryo.List.removeIndex(palettes, paletteIndex)
            })
        }))
    end,

    --[[
        Duplicates a gradient palette and adds it to the gradient palette list.

        ```
        action = {
            paletteIndex: number
        }
        ```

        @param oldState The previous state
        @param action The action information
        @return The next state
    ]]
    [Enums.StoreActionType.GradientEditor_DuplicatePalette] = function(oldState: table, action: table): table
        local palettes: {GradientPalette} = oldState.gradientEditor.palettes
        local paletteIndex: number = action.paletteIndex

        local palette: GradientPalette = palettes[paletteIndex]
        if (not palette) then return oldState end

        local matchStart: number? = string.find(palette.name, "%s*%(%d+%)$")
        local nameWithoutCounter: string = if (matchStart) then string.sub(palette.name, 1, matchStart - 1) else palette.name

        local newPaletteName: string = Util.palette.getNewItemName(palettes, nameWithoutCounter)
        local newPalette: GradientPalette = Util.table.deepCopy(palette)
        newPalette.name = newPaletteName

        return Util.table.deepFreeze(Cryo.Dictionary.join(oldState, {
            gradientEditor = Cryo.Dictionary.join(oldState.gradientEditor, {
                palettes = Cryo.List.join(palettes, {newPalette})
            })
        }))
    end,

    --[[
        Modifies the name of a gradient palette.

        ```
        action = {
            paletteIndex: number,
            newName: string
        }
        ```

        @param oldState The previous state
        @param action The action information
        @return The next state
    ]]
    [Enums.StoreActionType.GradientEditor_ChangePaletteName] = function(oldState: table, action: table): table
        local palettes: {GradientPalette} = oldState.gradientEditor.palettes
        local paletteIndex: number = action.paletteIndex

        local palette: GradientPalette = palettes[paletteIndex]
        if (not palette) then return oldState end

        local newPaletteName: string = Util.palette.getNewItemName(palettes, action.newName, paletteIndex)
        if (newPaletteName == palette.name) then return oldState end

        return Util.table.deepFreeze(Cryo.Dictionary.join(oldState, {
            gradientEditor = Cryo.Dictionary.join(oldState.gradientEditor, {
                palettes = Cryo.List.replaceIndex(palettes, paletteIndex, Cryo.Dictionary.join(palette, {
                    name = newPaletteName
                }))
            })
        }))
    end,

    --[[
        Adds the current working gradient to a gradient palette.

        ```
        action = {
            paletteIndex: number,
            newName: string?
        }
        ```

        @param oldState The previous state
        @param action The action information
        @return The next state
    ]]
    [Enums.StoreActionType.GradientEditor_AddCurrentGradientToPalette] = function(oldState: table, action: table): table
        local palettes: {GradientPalette} = oldState.gradientEditor.palettes
        local paletteIndex: number = action.paletteIndex

        local palette: GradientPalette = palettes[paletteIndex]
        if (not palette) then return oldState end

        local paletteGradients: {Gradient} = palette.gradients
        local newGradientName: string = Util.palette.getNewItemName(paletteGradients, action.newName or "New Gradient")

        local newGradient: Gradient = {
            name = newGradientName,

            keypoints = oldState.gradientEditor.keypoints,
            colorSpace = oldState.gradientEditor.colorSpace,
            hueAdjustment = oldState.gradientEditor.hueAdjustment,
            precision = oldState.gradientEditor.precision,
        }

        return Util.table.deepFreeze(Cryo.Dictionary.join(oldState, {
            gradientEditor = Cryo.Dictionary.join(oldState.gradientEditor, {
                palettes = Cryo.List.replaceIndex(palettes, paletteIndex, Cryo.Dictionary.join(palette, {
                    gradients = Cryo.List.join(paletteGradients, {newGradient})
                }))
            })
        }))
    end,

    --[[
        Removes a gradient from a gradient palette.

        ```
        action = {
            paletteIndex: number,
            gradientIndex: number
        }
        ```

        @param oldState The previous state
        @param action The action information
        @return The next state
    ]]
    [Enums.StoreActionType.GradientEditor_RemovePaletteGradient] = function(oldState: table, action: table): table
        local palettes: {GradientPalette} = oldState.gradientEditor.palettes
        local paletteIndex: number = action.paletteIndex

        local palette: GradientPalette = palettes[paletteIndex]
        if (not palette) then return oldState end

        local paletteGradients: {Gradient} = palette.gradients
        local gradientIndex: number = action.gradientIndex

        local gradient: Gradient = paletteGradients[gradientIndex]
        if (not gradient) then return oldState end

        return Util.table.deepFreeze(Cryo.Dictionary.join(oldState, {
            gradientEditor = Cryo.Dictionary.join(oldState.gradientEditor, {
                palettes = Cryo.List.replaceIndex(palettes, paletteIndex, Cryo.Dictionary.join(palette, {
                    gradients = Cryo.List.removeIndex(paletteGradients, gradientIndex)
                }))
            })
        }))
    end,

    --[[
        Modifies the name of a gradient in a gradient palette.

        ```
        action = {
            paletteIndex: number,
            gradientIndex: number,
            newName: string
        }
        ```

        @param oldState The previous state
        @param action The action information
        @return The next state
    ]]
    [Enums.StoreActionType.GradientEditor_ChangePaletteGradientName] = function(oldState: table, action: table): table
        local palettes: {GradientPalette} = oldState.gradientEditor.palettes
        local paletteIndex: number = action.paletteIndex

        local palette: GradientPalette = palettes[paletteIndex]
        if (not palette) then return oldState end

        local paletteGradients: {Gradient} = palette.gradients
        local gradientIndex: number = action.gradientIndex

        local gradient: Gradient = paletteGradients[gradientIndex]
        if (not gradient) then return oldState end

        local newGradientName: string = Util.palette.getNewItemName(paletteGradients, action.newName, gradientIndex)
        if (newGradientName == gradient.name) then return oldState end

        return Util.table.deepFreeze(Cryo.Dictionary.join(oldState, {
            gradientEditor = Cryo.Dictionary.join(oldState.gradientEditor, {
                palettes = Cryo.List.replaceIndex(palettes, paletteIndex, Cryo.Dictionary.join(palette, {
                    gradients = Cryo.List.replaceIndex(paletteGradients, gradientIndex, Cryo.Dictionary.join(gradient, {
                        name = newGradientName
                    }))
                }))
            })
        }))
    end,

    --[[
        Modifies the position of a gradient in a gradient palette.

        ```
        action = {
            paletteIndex: number,
            gradientIndex: number,
            offset: number,
        }
        ```

        @param oldState The previous state
        @param action The action information
        @return The next state
    ]]
    [Enums.StoreActionType.GradientEditor_ChangePaletteGradientPosition] = function(oldState: table, action: table): table
        local palettes: {GradientPalette} = oldState.gradientEditor.palettes
        local paletteIndex: number = action.paletteIndex

        local palette: GradientPalette = palettes[paletteIndex]
        if (not palette) then return oldState end

        local paletteGradients: {Gradient} = palette.gradients
        local gradientIndex: number = action.gradientIndex
        local otherGradientIndex: number = gradientIndex + action.offset
        if (not (paletteGradients[gradientIndex] and paletteGradients[otherGradientIndex])) then return oldState end

        return Util.table.deepFreeze(Cryo.Dictionary.join(oldState, {
            gradientEditor = Cryo.Dictionary.join(oldState.gradientEditor, {
                palettes = Cryo.List.replaceIndex(palettes, paletteIndex, Cryo.Dictionary.join(palette, {
                    gradients = Cryo.Dictionary.join(paletteGradients, {
                        [gradientIndex] = paletteGradients[otherGradientIndex],
                        [otherGradientIndex] = paletteGradients[gradientIndex]
                    })
                }))
            })
        }))
    end,

    --[[
        Overrides the current set of gradient palettes.

        ```
        action = {
            palettes: {GradientPalette}
        }
        ```

        @param oldState The previous state
        @param action The action information
        @return The next state
    ]]
    [Enums.StoreActionType.GradientEditor_SetPalettes] = function(oldState: table, action: table): table
        return Util.table.deepFreeze(Cryo.Dictionary.join(oldState, {
            gradientEditor = Cryo.Dictionary.join(oldState.gradientEditor, {
                palettes = action.palettes
            })
        }))
    end,
}


--!strict

local root = script.Parent.Parent
local Common = root.Common

local CommonIncludes = Common.Includes
local Cryo = require(CommonIncludes.Cryo)

local Modules = root.Modules
local Enums = require(Modules.Enums)
local Types = require(Modules.Types)
local Util = require(Modules.Util)

local ColorEditorReducers = require(script.ColorEditorReducers)
local GradientEditorReducers = require(script.GradientEditorReducers)

---

type table = Types.table

---

--[[
    Rodux reducers for store updates.
]]
return Cryo.Dictionary.join({
    --[[
        Updates the UI theme.

        ```
        action =  {
            theme: StudioTheme
        }
        ```

        @param oldState The previous state
        @param action The action information
        @return The next state
    ]]
    [Enums.StoreActionType.SetTheme] = function(oldState: table, action: table): table
        return Util.table.deepFreeze(Cryo.Dictionary.join(oldState, {
            theme = action.theme
        }))
    end,

    --[[
        Updates the upstream availability status.

        ```
        action = {
            available: boolean
        }
        ```

        @param oldState The previous state
        @param action The action information
        @return The next state
    ]]
    [Enums.StoreActionType.UpstreamAvailabilityChanged] = function(oldState: table, action: table): table
        return Util.table.deepFreeze(Cryo.Dictionary.join(oldState, {
            upstreamAvailable = action.available,

            sessionData = if (oldState.upstreamAvailable == false) and (action.available == true) then
                Cryo.Dictionary.join(oldState.sessionData, {
                    lastPalettePage = {1, 1}
                })
            else nil,
        }))
    end,

    --[[
        Updates some session information.

        ```
        action = {
            slice: {[any]: any}
        }
        ```

        @param oldState The previous state
        @param action The action information
        @return The next state
    ]]
    [Enums.StoreActionType.UpdateSessionData] = function(oldState: table, action: table): table
        return Util.table.deepFreeze(Cryo.Dictionary.join(oldState, {
            sessionData = Cryo.Dictionary.join(oldState.sessionData, action.slice)
        }))
    end,

    --[[
        Updates a stored user data value.

        ```
        action = {
            key: string,
            value: any
        }
        ```

        @param oldState The previous state
        @param action The action information
        @return The next state
    ]]
    [Enums.StoreActionType.UpdateUserData] = function(oldState: table, action: table): table
        return Util.table.deepFreeze(Cryo.Dictionary.join(oldState, {
            userData = Cryo.Dictionary.join(oldState.userData, {
                [action.key] = action.value
            })
        }))
    end,
}, ColorEditorReducers, GradientEditorReducers)


--!strict
-- Common Luau types used in ColorPane

local root = script.Parent.Parent
local Common = root.Common
local CommonIncludes = Common.Includes
local Color = require(CommonIncludes.Color)

---

export type table = {[any]: any}
export type Color = Color.Color
export type Gradient = Color.Gradient

export type ColorPaletteColor = {
    name: string,
    color: Color3
}

export type ColorPalette = {
    name: string,
    colors: {ColorPaletteColor},
}

export type GradientKeypoint = {
    time: number,
    color: Color
}

export type GradientPaletteGradient = {
    name: string,
    colorSpace: string,
    hueAdjustment: string,
    precision: number,

    keypoints: {GradientKeypoint}
}

export type GradientPalette = {
    name: string,
    gradients: {GradientPaletteGradient}
}

export type ColorPromptInfoArgument = {
    PromptTitle: string?,
    InitialColor: (Color | Color3)?,
    ColorType: ("Color" | "Color3")?,
    OnColorChanged: (((Color) -> ()) | ((Color3) -> ()))?
}

export type GradientPromptInfoArgument = {
    PromptTitle: string?,
    InitialGradient: (Gradient | ColorSequence)?,
    InitialColorSpace: Color.MixableColorType?,
    InitialHueAdjustment: Color.HueAdjustment?,
    InitialPrecision: number?,
    GradientType: ("Gradient" | "ColorSequence")?,
    OnGradientChanged: (((Gradient) -> ()) | ((ColorSequence) -> ()))?
}

---

return {}


--!strict

local StudioService: StudioService = game:GetService("StudioService")

---

local root = script.Parent.Parent
local Common = root.Common

local CommonIncludes = Common.Includes
local Signal = require(CommonIncludes.Signal)

local CommonModules = Common.Modules
local CommonConstants = require(CommonModules.Constants)
local CommonEnums = require(CommonModules.Enums)
local CommonTypes = require(CommonModules.Types)
local ColorPaneUserDataInterfaceValidator = require(CommonModules.ColorPaneUserDataInterfaceValidator)
local ColorPaneUserDataInterfaceVersion = require(CommonModules.ColorPaneUserDataInterfaceVersion)
local PluginProvider =  require(CommonModules.PluginProvider)

---

local plugin: Plugin = PluginProvider()
local childAdded: RBXScriptConnection

local upstreamInterfaceFolder: Folder?
local getValueFunction: BindableFunction?
local getAllValuesFunction: BindableFunction?
local setValueFunction: BindableFunction?
local valueChangedEvent: BindableEvent?

local valueChangedConnection: RBXScriptConnection
local interfaceChildRemoved: RBXScriptConnection
local interfaceAncestryChanged: RBXScriptConnection
local interfaceDestroying: RBXScriptConnection

local upstreamValueChangedSignal: Signal.Signal<CommonTypes.KeyValue>, fireUpstreamValueChanged: Signal.FireSignal<CommonTypes.KeyValue> = Signal.createSignal()
local upstreamAvailabilityChangedSignal: Signal.Signal<boolean>, fireUpstreamAvailabilityChanged: Signal.FireSignal<boolean> = Signal.createSignal()

local resetUpstreamInterface = function()
    valueChangedConnection:Disconnect()
    interfaceChildRemoved:Disconnect()
    interfaceAncestryChanged:Disconnect()
    interfaceDestroying:Disconnect()

    upstreamInterfaceFolder = nil
    getValueFunction = nil
    getAllValuesFunction = nil
    setValueFunction = nil
    valueChangedEvent = nil

    fireUpstreamAvailabilityChanged(false)
end

local hookUpstreamInterface = function(child: Instance)
    if (child.Name ~= CommonConstants.COLORPANE_USERDATA_INTERFACE_NAME) then return end
    if (not ColorPaneUserDataInterfaceValidator(child)) then return end

    local getVersion: BindableFunction = child:FindFirstChild("GetVersion")::BindableFunction
    local thisVersion: number = getVersion:Invoke()
    if (thisVersion ~= ColorPaneUserDataInterfaceVersion) then return end

    upstreamInterfaceFolder = child::Folder
    getValueFunction = child:FindFirstChild("GetValue")::BindableFunction
    getAllValuesFunction = child:FindFirstChild("GetAllValues")::BindableFunction
    setValueFunction = child:FindFirstChild("SetValue")::BindableFunction

    local valueChanged: BindableEvent = child:FindFirstChild("ValueChanged")::BindableEvent
    valueChangedConnection = valueChanged.Event:Connect(fireUpstreamValueChanged)
    valueChangedEvent = valueChanged

    interfaceChildRemoved = child.ChildRemoved:Connect(function()
        if (
            (child == getValueFunction) or
            (child == getAllValuesFunction) or
            (child == setValueFunction) or
            (child == valueChangedEvent)
        ) then
            resetUpstreamInterface()
        end
    end)

    interfaceAncestryChanged = child.AncestryChanged:Connect(function(this: Instance)
        if (this ~= child) then return end
        resetUpstreamInterface()
    end)

    interfaceDestroying = child.Destroying:Connect(resetUpstreamInterface)
    fireUpstreamAvailabilityChanged(true)
end

---

--[[
    Interface for interacting with the upstream user data provider
    (the Companion plugin).
]]
local UpstreamUserData = {}

--[[
    Fires when a value in the upstream provider changes.
]]
UpstreamUserData.ValueChanged = upstreamValueChangedSignal

--[[
    Fires when the upstream provider's availability changes.
]]
UpstreamUserData.AvailabilityChanged = upstreamAvailabilityChangedSignal

--[[
    Returns if the upstream provider is available.
]]
UpstreamUserData.IsAvailable = function(): boolean
    return (
        (upstreamInterfaceFolder ~= nil) and
        (getValueFunction ~= nil) and
        (getAllValuesFunction ~= nil) and
        (setValueFunction ~= nil) and
        (valueChangedEvent ~= nil)
    )
end

--[[
    Retrives a value from the upstream provider.

    @param key The name of the value to retrieve
    @return The user data value
]]
UpstreamUserData.GetValue = function(key: string): any
    assert(UpstreamUserData.IsAvailable(), CommonEnums.UpstreamUserDataProviderError.Unavailable)
    return (getValueFunction::BindableFunction):Invoke(key)
end

--[[
    Retrives all values from the upstream provider.

    @return The values table
]]
UpstreamUserData.GetAllValues = function(): CommonTypes.ColorPaneUserData
    assert(UpstreamUserData.IsAvailable(), CommonEnums.UpstreamUserDataProviderError.Unavailable)
    return (getAllValuesFunction::BindableFunction):Invoke()
end

--[[
    Updates a value in the upstream provider.

    @param key The name of the value to update
    @param value The new value of the value
    @return If the value was actually updated
]]
UpstreamUserData.SetValue = function(key: string, value: any): boolean
    assert(UpstreamUserData.IsAvailable(), CommonEnums.UpstreamUserDataProviderError.Unavailable)
    return (setValueFunction::BindableFunction):Invoke(key, value)
end

---

do
    -- check if the upstream is already available
    local children: {Instance} = StudioService:GetChildren()

    for i = 1, #children do
        local child: Instance = children[i]

        if (ColorPaneUserDataInterfaceValidator(child)) then
            hookUpstreamInterface(child)
            break
        end
    end
end

childAdded = StudioService.ChildAdded:Connect(hookUpstreamInterface)

plugin.Unloading:Connect(function()
    childAdded:Disconnect()
    resetUpstreamInterface()
end)

return UpstreamUserData


--!strict

local root = script.Parent.Parent
local Common = root.Common

local CommonIncludes = Common.Includes
local ColorLib = require(CommonIncludes.Color)

local CommonModules = Common.Modules
local CommonUtil = require(CommonModules.Util)
local ColorPaneUserDataValidators = require(CommonModules.ColorPaneUserDataValidators)

local Color = ColorLib.Color

---

type Color = ColorLib.Color
type GradientKeypoint = ColorLib.GradientKeypoint

local noYieldReturnHandler = function(routine: thread, success: boolean, ...: any)
    if (not success) then
        error(debug.traceback(routine, (...)))
    end

    if (coroutine.status(routine) ~= "dead") then
        error(debug.traceback("callback must not yield"))
    end

    return ...
end

---

--[[
    Utility functions
]]
local Util = {}

--[[
    Table utilities
]]
Util.table = {}
Util.table.deepCopy = CommonUtil.table.deepCopy

--[[
    Freezes a table and its sub-tables.
    Does not freeze metatables.

    @param t The table to deep-freeze
    @return `t` itself
]]
Util.table.deepFreeze = function(tbl: {[any]: any}): {[any]: any}
    for _, v in pairs(tbl) do
        if ((type(v) == "table") and (not table.isfrozen(v))) then
            Util.table.deepFreeze(v)
        end
    end

    table.freeze(tbl)
    return tbl
end

--[[
    Compares two tables and returns a list of keys with different values.

    @param this The table to be compared to
    @param that The table to compare
    @return A list of keys that have different values between the two tables
]]
Util.table.shallowCompare = function(this: {[any]: any}, that: {[any]: any}): {any}
    local diff = {}

    for k, v in pairs(this) do
        if (that[k] ~= v) then
            table.insert(diff, k)
        end
    end

    for k, v in pairs(that) do
        if ((not table.find(diff, k)) and (this[k] ~= v)) then
            table.insert(diff, k)
        end
    end

    return diff
end

--[[
    Palette utilities
]]
Util.palette = {}

Util.palette.getNewItemName = function(items: {[any]: any}, originalName: string, selfIndex: number?): (string, number)
    local found: boolean = false
    local numDuplicates: number = 0
    local itemName: string = originalName

    repeat
        found = false

        for i, item in ipairs(items) do
            if ((item.name == itemName) and (i ~= selfIndex)) then
                found = true
                numDuplicates = numDuplicates + 1
                itemName = originalName .. " (" .. numDuplicates .. ")"

                break
            end
        end
    until (not found)

    return itemName, numDuplicates
end

Util.palette.validate = function(palette: any): (boolean, string?)
    -- type check
    local typeCheckSuccess, message = ColorPaneUserDataValidators._colorPalette(palette)
    if (not typeCheckSuccess) then return false, message end

    -- check for "blank" name
    local substitutedPaletteName = string.gsub(palette.name, "%s+", "")
    if (string.len(substitutedPaletteName) < 1) then return false, "palette name is blank" end

    -- check for colors with the same or "blank" names
    local colorNameMap = {}

    for i = 1, #palette.colors do
        local color = palette.colors[i]
        local name = color.name

        local substitutedName = string.gsub(name, "%s+", "")
        if (string.len(substitutedName) < 1) then return false, "color name is blank" end

        if (colorNameMap[name]) then
            return false, "duplicate color name"
        else
            colorNameMap[name] = true
        end
    end

    return true, nil
end

-- GENERAL UTIL

Util.lerp = function(a: number, b: number, time: number): number
    return ((1 - time) * a) + (time * b)
end

Util.inverseLerp = function(a: number, b: number, v: number): number
    return (v - a) / (b - a)
end

Util.round = function(n: number, e: number?): number
    local p: number = 10^(e or 0)

    if (p >= 0) then
        return math.floor((n / p) + 0.5) * p
    else
        return math.floor((n * p) + 0.5) / p
    end
end

Util.noYield = function(callback: (...any) -> any, ...: any)
    local routine = coroutine.create(callback)

    return noYieldReturnHandler(routine, coroutine.resume(routine, ...))
end

Util.escapeText = function(s: string): string
    return (string.gsub(s, "([%^%$%(%)%%%.%[%]%*%+%-%?])", "%%%0"))
end

Util.makeBugMessage = function(msg: string): string
    return "[ColorPane] Error: " .. msg .. "\nThis is a bug. If you're seeing this, please submit a bug report."
end

-- GRADIENT UTIL

Util.getMaxUserKeypoints = function(maxKeypoints: number, precision: number): number
    return math.floor(((maxKeypoints - 1) / (precision + 1)) + 1)
end

Util.getUtilisedKeypoints = CommonUtil.gradient.getUtilisedKeypoints

Util.generateFullKeypointList = function(keypoints: {GradientKeypoint}, colorSpace: ColorLib.MixableColorType?, hueAdjustment: ColorLib.HueAdjustment?, precision: number): {GradientKeypoint}
    local fullKeypoints: {GradientKeypoint} = {}

    for i = 1, (#keypoints - 1) do
        local thisKeypoint: GradientKeypoint = keypoints[i]
        local nextKeypoint: GradientKeypoint = keypoints[i + 1]

        table.insert(fullKeypoints, thisKeypoint)

        for j = 1, precision do
            local time: number = (j + 1) / (precision + 2)

            local newKeypointTime: number = Util.lerp(thisKeypoint.Time, nextKeypoint.Time, time)
            local newKeypointColor: Color = thisKeypoint.Color:mix(nextKeypoint.Color, time, colorSpace, hueAdjustment)

            table.insert(fullKeypoints, {
                Time = newKeypointTime,
                Color = newKeypointColor
            })
        end
    end

    table.insert(fullKeypoints, keypoints[#keypoints])
    return fullKeypoints
end

-- converts a palette's colors into the specified type
Util.typeColorPalette = function(palette, colorType: string)
    local paletteCopy = Util.table.deepCopy(palette)
    local colors = paletteCopy.colors

    for i = 1, #colors do
        local color = colors[i]
        local colorValue = color.color

        if (colorType == "Color3") then
            color.color = Color3.fromRGB(colorValue[1], colorValue[2], colorValue[3])
        elseif (colorType == "Color") then
            color.color = Color.fromRGB(colorValue[1], colorValue[2], colorValue[3])
        end
    end

    return paletteCopy
end

---

return Util


--!strict
-- Holds information for the common plugin widgets

local root = script.Parent.Parent
local Common = root.Common

local CommonModules = Common.Modules
local Style = require(CommonModules.Style)

local Modules = script.Parent
local ProjectId = require(Modules.ProjectId)

---

local PROJECT_ID: string = ProjectId()

local colorEditorDefaultWidth = Style.Constants.PagePadding +
    (Style.Constants.EditorPageWidth + Style.Constants.MajorElementPadding) * 2 +
    Style.Constants.LargeButtonHeight +
    4 +
    Style.Constants.PagePadding

local colorEditorMinWidth = Style.Constants.PagePadding +
    Style.Constants.EditorPageWidth +
    Style.Constants.MajorElementPadding +
    Style.Constants.LargeButtonHeight +
    2 +
    Style.Constants.PagePadding

local gradientEditorMinWidth = Style.Constants.PagePadding +
    (Style.Constants.EditorPageWidth + Style.Constants.MajorElementPadding) * 2 +
    Style.Constants.LargeButtonHeight +
    2 +
    Style.Constants.PagePadding

local gradientPaletteMinHeight = (Style.Constants.PagePadding * 2) +
    Style.Constants.StandardInputHeight +
    Style.Constants.MinorElementPadding +
    ((Style.Constants.StandardButtonHeight * 1) + (Style.Constants.MinorElementPadding * 2)) * 6 +
    2

local gradientInfoMinHeight = (Style.Constants.PagePadding * 2) +
    (Style.Constants.StandardTextSize * 3) +
    (Style.Constants.StandardButtonHeight * 6) +
    Style.Constants.StandardInputHeight +
    (Style.Constants.MinorElementPadding * 7) +
    (Style.Constants.SpaciousElementPadding * 2)

---

return {
    ColorEditor = {
        Id = PROJECT_ID .. "_ColorEditor",
        Info = DockWidgetPluginGuiInfo.new(Enum.InitialDockState.Float, false, true, colorEditorDefaultWidth, 400, colorEditorMinWidth, 400),
    },

    GradientEditor = {
        Id = PROJECT_ID .. "_GradientEditor",
        Info = DockWidgetPluginGuiInfo.new(Enum.InitialDockState.Float, false, true, gradientEditorMinWidth, 130, gradientEditorMinWidth, 130),
    },

    GradientPalette = {
        Id = PROJECT_ID .. "_GradientPalette",
        Info = DockWidgetPluginGuiInfo.new(Enum.InitialDockState.Float, false, true, Style.Constants.PagePadding + 230, gradientPaletteMinHeight, Style.Constants.PagePadding + 230, gradientPaletteMinHeight),
    },

    GradientInfo = {
        Id = PROJECT_ID .. "_GradientInfo",
        Info = DockWidgetPluginGuiInfo.new(Enum.InitialDockState.Float, false, true, Style.Constants.PagePadding + 230, gradientInfoMinHeight, Style.Constants.PagePadding + 230, gradientInfoMinHeight),
    },
}


--!strict
--[[
    v0.5.0

    ColorPane is a suite of color tools for Roblox Studio projects.
    Learn more here: https://devforum.roblox.com/t/1106112

    Please refer to the documentation for integrating these tools.
]]

local PluginProvider = require(script.Common.Modules.PluginProvider)
local ProjectId = require(script.Modules.ProjectId)
local Types = require(script.Modules.Types)

---

export type ColorPromptInfoArgument = Types.ColorPromptInfoArgument
export type GradientPromptInfoArgument = Types.GradientPromptInfoArgument

--[[
    Initialises ColorPane and returns the API. **This function should only be called once.**

    The project ID is used when creating plugin widgets and in error output.
    If you do not provide an ID, it will be set to a random UUID.
    @param plugin The Plugin object of your project
    @param id (Optional) The ID of your project
    @return The ColorPane API
]]
return function(plugin: Plugin, id: string?)
    PluginProvider(plugin)

    if (id) then
        ProjectId(id)
    end

    return require(script.API)
end


-- A button that can display multiple types of content

local root = script.Parent.Parent

local Modules = root.Modules
local Style = require(Modules.Style)

local Includes = root.Includes
local Cryo = require(Includes.Cryo)
local Roact = require(Includes.RoactRodux.Roact)

local Components = root.Components
local ConnectTheme = require(Components.ConnectTheme)
local StandardUICorner = require(Components.StandardComponents.UICorner)

---

local buttonTypes = {
    image = "ImageButton",
    text = "TextButton",
    color = "ImageButton",
    colorSequence = "ImageButton",
}

local getImageButtonProps = function(image, imageColor)
    return {
        Image = image,
        ScaleType = Enum.ScaleType.Fit,
        ImageColor3 = imageColor,
    }
end

local getTextButtonProps = function(text, textColor)
    return {
        Font = Style.Fonts.Standard,
        TextSize = Style.Constants.StandardTextSize,
        TextXAlignment = Enum.TextXAlignment.Center,
        TextYAlignment = Enum.TextYAlignment.Center,
        Text = text,
        TextColor3 = textColor,
    }
end

local getColorButtonProps = function(color)
    return {
        BackgroundColor3 = color,
        Image = "",
        ImageTransparency = 1,
    }
end

local colorSequenceButtonProps = {
    BackgroundColor3 = Color3.new(1, 1, 1),
    Image = "",
    ImageTransparency = 1,
}

---

--[[
    props
        AnchorPoint?
        LayoutOrder?
        Position?
        Size?

        borderColor: Color3?
        backgroundColor: Color3?
        hoverColor: Color3?
        displayColor: Color3?

        disabledBackgroundColor: Color3?
        disabledDisplayColor: Color3?

        displayType: "image" | "text" | "color" | "colorSequence"
        image: Content?
        text: string?
        color: Color3? | ColorSequence?

        disabled: boolean?
        onActivated: () -> nil

    store props
        theme: StudioTheme
]]

local Button = Roact.PureComponent:extend("Button")

Button.render = function(self)
    local theme = self.props.theme
    local displayType = self.props.displayType

    local borderColor = self.props.borderColor or theme:GetColor(Enum.StudioStyleGuideColor.ButtonBorder)
    local backgroundColor
    local displayColor

    if (not self.props.disabled) then
        backgroundColor = self.props.backgroundColor or theme:GetColor(Enum.StudioStyleGuideColor.Button)
        displayColor = self.props.displayColor or theme:GetColor(Enum.StudioStyleGuideColor.ButtonText)
    else
        backgroundColor = self.props.disabledBackgroundColor or theme:GetColor(
            Enum.StudioStyleGuideColor.Button,
            Enum.StudioStyleGuideModifier.Disabled
        )

        displayColor = self.props.disabledDisplayColor or theme:GetColor(
            Enum.StudioStyleGuideColor.ButtonText,
            Enum.StudioStyleGuideModifier.Disabled
        )
    end

    local buttonProps: {[any]: any} = {
        AnchorPoint = Vector2.new(0.5, 0.5),
        Position = UDim2.new(0.5, 0, 0.5, 0),
        Size = UDim2.new(1, -2, 1, -2),
        BackgroundTransparency = 0,
        BorderSizePixel = 0,
        AutoButtonColor = false,

        BackgroundColor3 = backgroundColor,

        [Roact.Event.Activated] = function()
            if (self.props.disabled) then return end

            self.props.onActivated()
        end
    }

    if (displayType == "image") then
        buttonProps = Cryo.Dictionary.join(buttonProps, getImageButtonProps(self.props.image, displayColor))
    elseif (displayType == "text") then
        buttonProps = Cryo.Dictionary.join(buttonProps, getTextButtonProps(self.props.text, displayColor))
    elseif (displayType == "color") then
        buttonProps = Cryo.Dictionary.join(buttonProps, getColorButtonProps(self.props.color))
    elseif (displayType == "colorSequence") then
        buttonProps = Cryo.Dictionary.join(buttonProps, colorSequenceButtonProps)
    end

    if ((not self.props.disabled) and ((displayType == "image") or (displayType == "text"))) then
        buttonProps[Roact.Event.MouseEnter] = function(obj)
            obj.BackgroundColor3 = self.props.hoverColor or theme:GetColor(Enum.StudioStyleGuideColor.Button, Enum.StudioStyleGuideModifier.Hover)
        end

        buttonProps[Roact.Event.MouseLeave] = function(obj)
            obj.BackgroundColor3 = backgroundColor
        end
    end

    return Roact.createElement("Frame", {
        AnchorPoint = self.props.AnchorPoint,
        Position = self.props.Position,
        Size = self.props.Size or Style.UDim2.StandardButtonSize,
        LayoutOrder = self.props.LayoutOrder,
        BackgroundTransparency = 0,
        BorderSizePixel = 0,

        BackgroundColor3 = borderColor,
    }, {
        UICorner = Roact.createElement(StandardUICorner),

        Button = Roact.createElement(buttonTypes[self.props.displayType], buttonProps, {
            UICorner = Roact.createElement(StandardUICorner),

            UIGradient = if (displayType == "colorSequence") then
                Roact.createElement("UIGradient", {
                    Color = self.props.color,
                })
            else nil,
        })
    })
end

return ConnectTheme(Button)


-- A checkbox

local root = script.Parent.Parent

local Modules = root.Modules
local Style = require(Modules.Style)

local Includes = root.Includes
local Roact = require(Includes.RoactRodux.Roact)

local Components = root.Components
local ConnectTheme = require(Components.ConnectTheme)

local StandardComponents = Components.StandardComponents
local StandardTextLabel = require(StandardComponents.TextLabel)
local StandardUICorner = require(StandardComponents.UICorner)

---

--[[
    props
        AnchorPoint?
        Position?
        Size?
        LayoutOrder?

        disabled: boolean?
        value: boolean
        text: string
        onChecked: (boolean) -> nil

    store props
        theme: StudioTheme
]]

local Checkbox = Roact.PureComponent:extend("Checkbox")

Checkbox.init = function(self)
    self:setState({
        hover = false
    })
end

Checkbox.render = function(self)
    local theme = self.props.theme
    local disabled = self.props.disabled
    local hover = self.state.hover

    return Roact.createElement("Frame", {
        AnchorPoint = self.props.AnchorPoint,
        Position = self.props.Position,
        Size = self.props.Size,
        LayoutOrder = self.props.LayoutOrder,
        BackgroundTransparency = 1,
        BorderSizePixel = 0,
    }, {
        CheckboxBorder = Roact.createElement("TextButton", {
            AnchorPoint = Vector2.new(0, 0),
            Position = UDim2.new(0, 0, 0, 0),
            Size = Style.UDim2.StandardButtonSize,
            AutoButtonColor = false,
            BackgroundTransparency = 0,
            BorderSizePixel = 0,

            Text = "",
            TextTransparency = 1,

            BackgroundColor3 = if disabled then
                theme:GetColor(Enum.StudioStyleGuideColor.InputFieldBorder)
            else theme:GetColor(Enum.StudioStyleGuideColor.InputFieldBorder, if hover then Enum.StudioStyleGuideModifier.Hover else nil),

            [Roact.Event.MouseButton1Click] = function()
                if (disabled) then return end

                self.props.onChecked(not self.props.value)
            end,

            [Roact.Event.MouseEnter] = function()
                if (disabled) then return end

                self:setState({
                    hover = true
                })
            end,

            [Roact.Event.MouseLeave] = function()
                if (disabled) then return end

                self:setState({
                    hover = false
                })
            end
        }, {
            CheckboxBackground = Roact.createElement("Frame", {
                AnchorPoint = Vector2.new(0.5, 0.5),
                Position = UDim2.new(0.5, 0, 0.5, 0),
                Size = UDim2.new(1, -2, 1, -2),
                BackgroundTransparency = 0,
                BorderSizePixel = 0,

                BackgroundColor3 = if disabled then
                    theme:GetColor(Enum.StudioStyleGuideColor.InputFieldBackground)
                else theme:GetColor(Enum.StudioStyleGuideColor.InputFieldBackground, if hover then Enum.StudioStyleGuideModifier.Hover else nil),
            }, {
                UICorner = Roact.createElement(StandardUICorner),
            }),

            CheckboxIndicator = if self.props.value then
                Roact.createElement("Frame", {
                    AnchorPoint = Vector2.new(0.5, 0.5),
                    Position = UDim2.new(0.5, 0, 0.5, 0),
                    Size = UDim2.new(1, -Style.Constants.SpaciousElementPadding, 1, -Style.Constants.SpaciousElementPadding),
                    BackgroundTransparency = 0,
                    BorderSizePixel = 0,

                    BackgroundColor3 = theme:GetColor(
                        Enum.StudioStyleGuideColor.InputFieldBorder,
                        if disabled then Enum.StudioStyleGuideModifier.Disabled else Enum.StudioStyleGuideModifier.Selected
                    ),
                }, {
                    UICorner = Roact.createElement(StandardUICorner),
                })
            else nil,

            UICorner = Roact.createElement(StandardUICorner),
        }),

        Text = Roact.createElement(StandardTextLabel, {
            AnchorPoint = Vector2.new(1, 0),
            Position = UDim2.new(1, 0, 0, 0),
            Size = UDim2.new(1, -(Style.Constants.StandardButtonHeight + Style.Constants.SpaciousElementPadding), 1, 0),

            Text = self.props.text,
            TextXAlignment = Enum.TextXAlignment.Left,
            TextYAlignment = Enum.TextYAlignment.Top,
            TextWrapped = true,

            TextColor3 = theme:GetColor(
                Enum.StudioStyleGuideColor.MainText,
                if disabled then Enum.StudioStyleGuideModifier.Disabled else nil
            )
        })
    })
end

return ConnectTheme(Checkbox)


-- Allows components to use theme coloring
-- This is a (higher-order) component, so it should stay in the Components folder

local RoactRodux = require(script.Parent.Parent.Includes.RoactRodux.RoactRodux)

---

return RoactRodux.connect(function(state)
    return {
        theme = state.theme,
    }
end)


-- Component that presents text to be copied

local TextService = game:GetService("TextService")

---

local root = script.Parent.Parent

local Modules = root.Modules
local Style = require(Modules.Style)

local Includes = root.Includes
local Roact = require(Includes.RoactRodux.Roact)

local Components = root.Components
local ConnectTheme = require(Components.ConnectTheme)

local StandardComponents = Components.StandardComponents
local StandardTextLabel = require(StandardComponents.TextLabel)
local StandardUICorner = require(StandardComponents.UICorner)
local StandardUIPadding = require(StandardComponents.UIPadding)

---

--[[
    props

        text: string
        promptText: string

    store props

        theme: StudioTheme
]]
local ExportText = Roact.PureComponent:extend("ExportText")

ExportText.init = function(self)
    self:setState({
        width = 0,
    })
end

ExportText.render = function(self)
    local theme = self.props.theme
    local text = self.props.text
    local promptText = self.props.promptText

    local promptTextSize = TextService:GetTextSize(
        promptText,
        Style.Constants.StandardTextSize,
        Style.Fonts.Standard,
        Vector2.new(self.state.width - (Style.Constants.PagePadding * 2), math.huge)
    )

    local textSize = TextService:GetTextSize(
        text,
        Style.Constants.StandardTextSize,
        Enum.Font.Code,
        Vector2.new(math.huge, math.huge)
    )

    return Roact.createElement("Frame", {
        AnchorPoint = Vector2.new(0.5, 0.5),
        Position = UDim2.new(0.5, 0, 0.5, 0),
        Size = UDim2.new(1, 0, 1, 0),
        BackgroundTransparency = 0,
        BorderSizePixel = 0,

        BackgroundColor3 = theme:GetColor(Enum.StudioStyleGuideColor.MainBackground),

        [Roact.Change.AbsoluteSize] = function(obj)
            local absoluteSize = obj.AbsoluteSize

            self:setState({
                width = absoluteSize.X
            })
        end,
    }, {
        UIPadding = Roact.createElement(StandardUIPadding, {
            paddings = {Style.Constants.PagePadding}
        }),

        PromptText = Roact.createElement(StandardTextLabel, {
            AnchorPoint = Vector2.new(0, 0),
            Position = UDim2.new(0, 0, 0, 0),
            Size = UDim2.new(1, 0, 0, promptTextSize.Y),
            BackgroundTransparency = 1,
            BorderSizePixel = 0,

            Text = promptText,
            TextXAlignment = Enum.TextXAlignment.Left,
            TextYAlignment = Enum.TextYAlignment.Top,
            TextWrapped = true,
        }),

        CopyTextWrapper = Roact.createElement("Frame", {
            AnchorPoint = Vector2.new(0.5, 1),
            Position = UDim2.new(0.5, 0, 1, 0),
            Size = UDim2.new(1, 0, 1, -(promptTextSize.Y + Style.Constants.SpaciousElementPadding)),
            BorderSizePixel = 0,

            BackgroundColor3 = theme:GetColor(Enum.StudioStyleGuideColor.Border),
        }, {
            UICorner = Roact.createElement(StandardUICorner),

            CopyText = Roact.createElement("ScrollingFrame", {
                AnchorPoint = Vector2.new(0.5, 0.5),
                Position = UDim2.new(0.5, 0, 0.5, 0),
                Size = UDim2.new(1, 0, 1, 0),
                BackgroundTransparency = 1,
                BorderSizePixel = 0,

                CanvasSize = UDim2.new(
                    0, textSize.X + (Style.Constants.MinorElementPadding * 2),
                    0, textSize.Y + (Style.Constants.MinorElementPadding * 2)
                ),

                ClipsDescendants = true,
                TopImage = Style.Images.ScrollbarImage,
                MidImage = Style.Images.ScrollbarImage,
                BottomImage = Style.Images.ScrollbarImage,
                HorizontalScrollBarInset = Enum.ScrollBarInset.Always,
                VerticalScrollBarInset = Enum.ScrollBarInset.Always,
                VerticalScrollBarPosition = Enum.VerticalScrollBarPosition.Right,
                ScrollBarThickness = Style.Constants.ScrollbarThickness / 4,

                ScrollBarImageColor3 = theme:GetColor(Enum.StudioStyleGuideColor.ScrollBar),
            }, {
                UIPadding = Roact.createElement(StandardUIPadding, {
                    paddings = {Style.Constants.MinorElementPadding}
                }),

                TextBox = Roact.createElement("TextBox", {
                    AnchorPoint = Vector2.new(0, 0),
                    Position = UDim2.new(0, 0, 0, 0),
                    Size = UDim2.new(1, 0, 1, 0),
                    BackgroundTransparency = 1,

                    Text = text,
                    Font = Enum.Font.Code,
                    TextSize = Style.Constants.StandardTextSize,
                    TextXAlignment = Enum.TextXAlignment.Left,
                    TextYAlignment = Enum.TextYAlignment.Top,
                    ClearTextOnFocus = false,
                    TextEditable = false,

                    TextColor3 = theme:GetColor(Enum.StudioStyleGuideColor.MainText),

                    [Roact.Event.Focused] = function(obj)
                        obj.CursorPosition = string.len(obj.Text) + 1
                        obj.SelectionStart = 1
                    end,
                })
            })
        })
    })
end

return ConnectTheme(ExportText)


-- A prompt to take (or avoid) an action

local root = script.Parent.Parent

local Modules = root.Modules
local Style = require(Modules.Style)

local Includes = root.Includes
local Roact = require(Includes.RoactRodux.Roact)

local Components = root.Components
local Button = require(Components.Button)
local ConnectTheme = require(Components.ConnectTheme)

local StandardComponents = Components.StandardComponents
local StandardTextLabel = require(StandardComponents.TextLabel)
local StandardUIPadding = require(StandardComponents.UIPadding)

---

--[[
    props

        promptText: string
        cancelText: string
        confirmText: string

        onDone: (boolean) -> ()

    store props

        theme: StudioTheme
]]

local Prompt = Roact.PureComponent:extend("Prompt")

Prompt.render = function(self)
    local theme = self.props.theme

    return Roact.createElement("Frame", {
        AnchorPoint = Vector2.new(0.5, 0.5),
        Size = UDim2.new(1, 0, 1, 0),
        Position = UDim2.new(0.5, 0, 0.5, 0),
        BackgroundTransparency = 0,
        BorderSizePixel = 0,

        BackgroundColor3 = theme:GetColor(Enum.StudioStyleGuideColor.MainBackground)
    }, {
        UIPadding = Roact.createElement(StandardUIPadding, {
            paddings = {Style.Constants.PagePadding},
        }),

        PromptText = Roact.createElement(StandardTextLabel, {
            AnchorPoint = Vector2.new(0.5, 0),
            Position = UDim2.new(0.5, 0, 0, 0),
            Size = UDim2.new(1, 0, 1, -(Style.UDim2.DialogButtonSize.Y.Offset + Style.Constants.MinorElementPadding)),

            Text = self.props.promptText,
            TextXAlignment = Enum.TextXAlignment.Center,
            TextYAlignment = Enum.TextYAlignment.Center,
            TextWrapped = true,
            RichText = true,
        }),

        CancelButton = Roact.createElement(Button, {
            AnchorPoint = Vector2.new(1, 1),
            Position = UDim2.new(0.5, -Style.Constants.MinorElementPadding, 1, 0),
            Size = UDim2.new(0, Style.Constants.DialogButtonWidth + 10, 0, Style.Constants.StandardButtonHeight),

            backgroundColor = theme:GetColor(Enum.StudioStyleGuideColor.DialogButton),
            borderColor = theme:GetColor(Enum.StudioStyleGuideColor.DialogButtonBorder),
            hoverColor = theme:GetColor(Enum.StudioStyleGuideColor.DialogButton, Enum.StudioStyleGuideModifier.Hover),
            displayColor = theme:GetColor(Enum.StudioStyleGuideColor.DialogButtonText),

            displayType = "text",
            text = self.props.cancelText,

            onActivated = function()
                self.props.onDone(false)
            end
        }),

        ConfirmButton = Roact.createElement(Button, {
            AnchorPoint = Vector2.new(0, 1),
            Position = UDim2.new(0.5, Style.Constants.MinorElementPadding, 1, 0),
            Size = UDim2.new(0, Style.Constants.DialogButtonWidth + 10, 0, Style.Constants.StandardButtonHeight),

            backgroundColor = theme:GetColor(Enum.StudioStyleGuideColor.ErrorText),
            borderColor = theme:GetColor(Enum.StudioStyleGuideColor.DialogButtonBorder),
            hoverColor = theme:GetColor(Enum.StudioStyleGuideColor.ErrorText),
            displayColor = theme:GetColor(Enum.StudioStyleGuideColor.DialogMainButtonText),

            displayType = "text",
            text = self.props.confirmText,

            onActivated = function()
                self.props.onDone(true)
            end
        })
    })
end

return ConnectTheme(Prompt)


-- An group of radio buttons, where only one radio button can be selected at a time

local root = script.Parent.Parent

local Components = root.Components
local ConnectTheme = require(Components.ConnectTheme)

local Includes = root.Includes
local Roact = require(Includes.RoactRodux.Roact)

local Modules = root.Modules
local Style = require(Modules.Style)

local StandardComponents = Components.StandardComponents
local StandardTextLabel = require(StandardComponents.TextLabel)
local StandardUICorner = require(StandardComponents.UICorner)
local StandardUIListLayout = require(StandardComponents.UIListLayout)

---

--[[
    props
        AnchorPoint?
        Size?
        Position?

        selected: number?
        options: array<string>
        onSelected: (number) -> nil

    store props
        theme: StudioTheme
]]

local RadioButtonGroup = Roact.PureComponent:extend("RadioButtonGroup")

RadioButtonGroup.render = function(self)
    local theme = self.props.theme

    local options = self.props.options
    local optionsChildren = {}

    for i = 1, #options do
        optionsChildren[i] = Roact.createElement("TextButton", {
            Size = UDim2.new(1, 0, 0, Style.Constants.StandardInputHeight),
            BackgroundTransparency = 1,
            BorderSizePixel = 0,
            AutoButtonColor = false,
            LayoutOrder = i,

            Text = "",
            TextTransparency = 1,

            [Roact.Event.Activated] = function()
                self.props.onSelected(i)
            end,
        }, {
            RadioButton = Roact.createElement("Frame", {
                AnchorPoint = Vector2.new(0, 0.5),
                Size = UDim2.new(0, Style.Constants.StandardInputHeight, 0, Style.Constants.StandardInputHeight),
                Position = UDim2.new(0, 0, 0.5, 0),
                BackgroundTransparency = 0,
                BorderSizePixel = 1,

                BackgroundColor3 = theme:GetColor(Enum.StudioStyleGuideColor.InputFieldBorder)
            }, {
                UICorner = Roact.createElement(StandardUICorner, { circular = true }),

                Inner = Roact.createElement("Frame", {
                    AnchorPoint = Vector2.new(0.5, 0.5),
                    Size = UDim2.new(1, -2, 1, -2),
                    Position = UDim2.new(0.5, 0, 0.5, 0),
                    BackgroundTransparency = 0,
                    BorderSizePixel = 1,

                    BackgroundColor3 = theme:GetColor(Enum.StudioStyleGuideColor.InputFieldBackground),
                }, {
                    UICorner = Roact.createElement(StandardUICorner, { circular = true }),
                }),

                Indicator = if (self.props.selected == i) then
                    Roact.createElement("Frame", {
                        AnchorPoint = Vector2.new(0.5, 0.5),
                        Size = UDim2.new(1, -Style.Constants.SpaciousElementPadding, 1, -Style.Constants.SpaciousElementPadding),
                        Position = UDim2.new(0.5, 0, 0.5, 0),
                        BackgroundTransparency = 0,
                        BorderSizePixel = 0,
                        ZIndex = 2,

                        BackgroundColor3 = theme:GetColor(Enum.StudioStyleGuideColor.Button, Enum.StudioStyleGuideModifier.Selected)
                    }, {
                        UICorner = Roact.createElement(StandardUICorner, { circular = true }),
                    })
                else nil,
            }),

            Label = Roact.createElement(StandardTextLabel, {
                AnchorPoint = Vector2.new(1, 0.5),
                Size = UDim2.new(1, -(Style.Constants.StandardInputHeight + Style.Constants.SpaciousElementPadding), 1, 0),
                Position = UDim2.new(1, 0, 0.5, 0),
                BackgroundTransparency = 1,
                Text = options[i],
            })
        })
    end

    optionsChildren.UIListLayout = Roact.createElement(StandardUIListLayout, {
        Padding = UDim.new(0, Style.Constants.MinorElementPadding),

        preset = 1,
    })

    return Roact.createElement("Frame", {
        AnchorPoint = self.props.AnchorPoint,
        Size = self.props.Size,
        Position = self.props.Position,
        BackgroundTransparency = 1,
        BorderSizePixel = 0,
    }, optionsChildren)
end

---

return ConnectTheme(RadioButtonGroup)


-- Standard ScrollingFrame component with theme coloring

local root = script.Parent.Parent.Parent

local Modules = root.Modules
local Style = require(Modules.Style)

local Includes = root.Includes
local Roact = require(Includes.RoactRodux.Roact)

local Components = root.Components
local ConnectTheme = require(Components.ConnectTheme)

---

--[[
    props
        AnchorPoint?
        Position?
        Size?
        CanvasSize?

        BackgroundTransparency? = 0
        BorderSizePixel? = 1

        [Roact.Event.InputBegan]?
        [Roact.Event.InputEnded]?

        useMainBackgroundColor: boolean?

    store props
        theme: StudioTheme
]]

local StandardScrollingFrame = Roact.PureComponent:extend("StandardScrollingFrame")

StandardScrollingFrame.render = function(self)
    local props = self.props
    local theme = props.theme

    return Roact.createElement("ScrollingFrame", {
        AnchorPoint = props.AnchorPoint,
        Position = props.Position,
        Size = props.Size,
        BackgroundTransparency = props.BackgroundTransparency or 0,
        BorderSizePixel = props.BorderSizePixel or 1,
        ClipsDescendants = true,

        CanvasPosition = Vector2.new(0, 0),
        CanvasSize = props.CanvasSize,
        TopImage = Style.Images.ScrollbarImage,
        MidImage = Style.Images.ScrollbarImage,
        BottomImage = Style.Images.ScrollbarImage,
        HorizontalScrollBarInset = Enum.ScrollBarInset.ScrollBar,
        VerticalScrollBarInset = Enum.ScrollBarInset.ScrollBar,
        VerticalScrollBarPosition = Enum.VerticalScrollBarPosition.Right,
        ScrollBarThickness = Style.Constants.ScrollbarThickness,

        ScrollBarImageColor3 = theme:GetColor(Enum.StudioStyleGuideColor.ScrollBar),
        BorderColor3 = theme:GetColor(Enum.StudioStyleGuideColor.Border),

        BackgroundColor3 = if props.useMainBackgroundColor then
            theme:GetColor(Enum.StudioStyleGuideColor.ScrollBarBackground)
        else theme:GetColor(Enum.StudioStyleGuideColor.ColorPickerFrame),

        [Roact.Event.InputBegan] = props[Roact.Event.InputBegan],
        [Roact.Event.InputEnded] = props[Roact.Event.InputEnded]
    }, props[Roact.Children])
end

return ConnectTheme(StandardScrollingFrame)


-- Standard TextLabel component with theme coloring

local root = script.Parent.Parent.Parent

local Modules = root.Modules
local Style = require(Modules.Style)

local Includes = root.Includes
local Roact = require(Includes.RoactRodux.Roact)

local Components = root.Components
local ConnectTheme = require(Components.ConnectTheme)

---

--[[
    props
        AnchorPoint?
        Position?
        Size?
        LayoutOrder?
        BackgroundTransparency? = 1

        Text
        Font? = Style.Fonts.Standard
        TextSize? = Style.Constants.StandardTextSize
        TextXAlignment? = Enum.TextXAlignment.Left
        TextYAlignment? = Enum.TextYAlignment.Center
        TextWrapped?
        RichText?

        BackgroundColor3? = StudioTheme:GetColor(Enum.StudioStyleGuideColor.MainBackground)
        TextColor3? = StudioTheme:GetColor(Enum.StudioStyleGuideColor.MainText)

    store props
        theme: StudioTheme
]]

local StandardTextLabel = Roact.PureComponent:extend("StandardTextLabel")

StandardTextLabel.render = function(self)
    local props = self.props
    local theme = props.theme

    return Roact.createElement("TextLabel", {
        AnchorPoint = props.AnchorPoint,
        Position = props.Position,
        Size = props.Size,
        LayoutOrder = props.LayoutOrder,
        BackgroundTransparency = props.BackgroundTransparency or 1,
        BorderSizePixel = 0,

        RichText = props.RichText,
        Text = props.Text,
        Font = props.Font or Style.Fonts.Standard,
        TextSize = props.TextSize or Style.Constants.StandardTextSize,
        TextXAlignment = props.TextXAlignment or Enum.TextXAlignment.Left,
        TextYAlignment = props.TextYAlignment or Enum.TextYAlignment.Center,
        TextWrapped = props.TextWrapped,

        BackgroundColor3 = props.BackgroundColor3 or theme:GetColor(Enum.StudioStyleGuideColor.MainBackground),
        TextColor3 = props.TextColor3 or theme:GetColor(Enum.StudioStyleGuideColor.MainText)
    }, props[Roact.Children])
end

return ConnectTheme(StandardTextLabel)


--[[
    UICorner component that can either be round
    or use the standard style corner radius
]]

local root = script.Parent.Parent.Parent

local Modules = root.Modules
local Style = require(Modules.Style)

local Includes = root.Includes
local Roact = require(Includes.RoactRodux.Roact)

---

--[[
    props
        circular: boolean?
]]

local StandardUICorner = Roact.PureComponent:extend("StandardUICorner")

StandardUICorner.render = function(self)
    return Roact.createElement("UICorner", {
        CornerRadius = if self.props.circular then
            UDim.new(1, 0)
        else UDim.new(0, Style.Constants.StandardCornerRadius),
    })
end

return StandardUICorner


-- Standard UIListLayout component with layout information presets

local root = script.Parent.Parent.Parent

local Includes = root.Includes
local Roact = require(Includes.RoactRodux.Roact)

---

--[[
    props
        Padding?
        FillDirection? = Enum.FillDirection.Vertical
        HorizontalAlignment? = Enum.HorizontalAlignment.Left
        VerticalAlignment? = Enum.VerticalAlignment.Top
        SortOrder? = Enum.SortOrder.LayoutOrder

        [Roact.Change.AbsoluteContentSize]?

        preset: number?

    Notes
        Setting a preset will override the FillDirection, HorizontalAlignment,
        and VerticalAlignment properties, even if they're specified.

        Preset 1:
            FillDirection = Enum.FillDirection.Vertical
            HorizontalAlignment = Enum.HorizontalAlignment.Left
            VerticalAlignment = Enum.VerticalAlignment.Top

        Preset 2:
            FillDirection = Enum.FillDirection.Horizontal
            HorizontalAlignment = Enum.HorizontalAlignment.Right
            VerticalAlignment = Enum.VerticalAlignment.Center
]]

local StandardUIListLayout = Roact.PureComponent:extend("StandardUIListLayout")

StandardUIListLayout.render = function(self)
    local props = self.props
    local preset = props.preset
    local overrides = {}

    if (preset == 1) then
        overrides.FillDirection = Enum.FillDirection.Vertical
        overrides.HorizontalAlignment = Enum.HorizontalAlignment.Left
        overrides.VerticalAlignment = Enum.VerticalAlignment.Top
    elseif (preset == 2) then
        overrides.FillDirection = Enum.FillDirection.Horizontal
        overrides.HorizontalAlignment = Enum.HorizontalAlignment.Right
        overrides.VerticalAlignment = Enum.VerticalAlignment.Center
    end

    return Roact.createElement("UIListLayout", {
        Padding = props.Padding,
        FillDirection = overrides.FillDirection or props.FillDirection or Enum.FillDirection.Vertical,
        HorizontalAlignment = overrides.HorizontalAlignment or props.HorizontalAlignment or Enum.HorizontalAlignment.Left,
        VerticalAlignment = overrides.VerticalAlignment or props.VerticalAlignment or Enum.VerticalAlignment.Top,
        SortOrder = props.SortOrder or Enum.SortOrder.LayoutOrder,

        [Roact.Change.AbsoluteContentSize] = props[Roact.Change.AbsoluteContentSize]
    }, props[Roact.Children])
end

return StandardUIListLayout


-- UIPadding component where the paddings are specified in an array

local root = script.Parent.Parent.Parent

local Includes = root.Includes
local Roact = require(Includes.RoactRodux.Roact)

---

--[[
    props

        paddings: array<number>

    Notes
        The paddings array must contain either 1, 2, or 4 numbers.
        If it doesn't, an error will occur.

        If only 1 number is provided in the array,
        all paddings are set to that number.

            Top = Bottom = Left = Right = paddings[1]

        If 2 numbers are provided, the top and bottom paddings
        are set to the first number, and the left and right
        paddings are set to the second number.

            Top, Bottom = paddings[1]
            Left, Right = paddings[2]

        If 4 numbers are provided, the paddings are set as such:

            Top = paddings[1]
            Bottom = paddings[2]
            Left = paddings[3]
            Right = paddings[4]
]]

local StandardUIPadding = Roact.PureComponent:extend("StandardUIPadding")

StandardUIPadding.render = function(self)
    local top, bottom, left, right
    local paddings = self.props.paddings

    if (#paddings == 1) then
        top, bottom, left, right = paddings[1], paddings[1], paddings[1], paddings[1]
    elseif (#paddings == 2) then
        top, bottom, left, right = paddings[1], paddings[1], paddings[2], paddings[2]
    elseif (#paddings == 4) then
        top, bottom, left, right = paddings[1], paddings[2], paddings[3], paddings[4]
    end

    return Roact.createElement("UIPadding", {
        PaddingTop = UDim.new(0, top),
        PaddingBottom = UDim.new(0, bottom),
        PaddingLeft = UDim.new(0, left),
        PaddingRight = UDim.new(0, right),
    })
end

return StandardUIPadding


-- A text input with validation capability

local root = script.Parent.Parent

local Modules = root.Modules
local Style = require(Modules.Style)

local Includes = root.Includes
local Roact = require(Includes.RoactRodux.Roact)

local Components = root.Components
local ConnectTheme = require(Components.ConnectTheme)

local StandardComponents = Components.StandardComponents
local StandardUICorner = require(StandardComponents.UICorner)
local StandardUIPadding = require(StandardComponents.UIPadding)

---

--[[
    props
        AnchorPoint?
        Position?
        Size?
        LayoutOrder?

        Text
        PlaceholderText?
        TextSize?
        TextXAlignment?

        canSubmitEmptyString: boolean?
        disabled: boolean?
        selectTextOnFocus: boolean?

        isTextAValidValue: (string)? -> boolean
        onTextChanged: (string)?
        onSubmit: (string)?

    store props
        theme: StudioTheme
]]

local TextInput = Roact.PureComponent:extend("TextInput")

TextInput.init = function(self)
    self:setState({
        focused = false,
        hover = false,
        invalidInput = false,
    })
end

TextInput.didUpdate = function(self, prevProps)
    local newText = self.props.Text
    local prevText = prevProps.Text

    if (newText ~= prevText) then
        self:setState({
            invalidInput = false
        })
    end
end

TextInput.willUnmount = function(self)
    --[[
        Some of TextBox's events that are used will fire when the
        object is about to be destroyed, which causes problems.
    ]]

    self.unmounting = true
end

TextInput.render = function(self)
    local theme = self.props.theme
    local disabled = self.props.disabled

    local isTextAValidValue = self.props.isTextAValidValue
    local onTextChanged = self.props.onTextChanged
    local onSubmit = self.props.onSubmit

    local borderColor
    local backgroundColor

    if (self.state.focused) then
        borderColor = theme:GetColor(Enum.StudioStyleGuideColor.InputFieldBorder, Enum.StudioStyleGuideModifier.Selected)
        backgroundColor = theme:GetColor(Enum.StudioStyleGuideColor.InputFieldBackground, Enum.StudioStyleGuideModifier.Selected)
    elseif (self.state.invalidInput) then
        borderColor = theme:GetColor(Enum.StudioStyleGuideColor.ErrorText)

        backgroundColor = theme:GetColor(
            Enum.StudioStyleGuideColor.InputFieldBackground,
            if self.state.hover then Enum.StudioStyleGuideModifier.Hover else nil
        )
    else
        borderColor = theme:GetColor(
            Enum.StudioStyleGuideColor.InputFieldBorder,
            if self.state.hover then Enum.StudioStyleGuideModifier.Hover else nil
        )

        backgroundColor = theme:GetColor(
            Enum.StudioStyleGuideColor.InputFieldBackground,
            if self.state.hover then Enum.StudioStyleGuideModifier.Hover else nil
        )
    end

    return Roact.createElement("Frame", {
        AnchorPoint = self.props.AnchorPoint,
        Position = self.props.Position,
        Size = self.props.Size,
        BackgroundTransparency = 0,
        BorderSizePixel = 0,
        LayoutOrder = self.props.LayoutOrder,

        BackgroundColor3 = borderColor,

        [Roact.Event.MouseEnter] = function()
            if (self.state.focused or disabled) then return end

            self:setState({
                hover = true
            })
        end,

        [Roact.Event.MouseLeave] = function()
            if (not self.state.hover) then return end

            self:setState({
                hover = false,
            })
        end
    }, {
        UICorner = Roact.createElement(StandardUICorner),

        Background = Roact.createElement("Frame", {
            AnchorPoint = Vector2.new(0.5, 0.5),
            Position = UDim2.new(0.5, 0, 0.5, 0),
            Size = UDim2.new(1, -2, 1, -2),
            BackgroundTransparency = 0,
            BorderSizePixel = 0,
            ClipsDescendants = true,

            BackgroundColor3 = backgroundColor,
        }, {
            UICorner = Roact.createElement(StandardUICorner),

            UIPadding = Roact.createElement(StandardUIPadding, {
                paddings = {0, Style.Constants.TextObjectPadding}
            }),

            Input = Roact.createElement("TextBox", {
                AnchorPoint = Vector2.new(0, 0.5),
                Position = UDim2.new(0, 0, 0.5, 0),
                Size = UDim2.new(1, 0, 1, 0),
                BackgroundTransparency = 1,
                BorderSizePixel = 0,
                TextEditable = (not disabled),

                Font = Style.Fonts.Standard,
                MultiLine = false,
                ClearTextOnFocus = false,
                TextSize = self.props.TextSize or Style.Constants.StandardTextSize,
                TextXAlignment = self.props.TextXAlignment or Enum.TextXAlignment.Left,
                TextYAlignment = Enum.TextYAlignment.Center,
                PlaceholderText = self.props.PlaceholderText or "",
                Text = self.props.Text,

                TextColor3 = theme:GetColor(
                    Enum.StudioStyleGuideColor.MainText,
                    if disabled then Enum.StudioStyleGuideModifier.Disabled else nil
                ),

                PlaceholderColor3 = theme:GetColor(Enum.StudioStyleGuideColor.MainText, Enum.StudioStyleGuideModifier.Disabled),

                [Roact.Event.Focused] = function(obj)
                    if (disabled) then return end

                    if (self.props.selectTextOnFocus) then
                        obj.CursorPosition = string.len(obj.Text) + 1
                        obj.SelectionStart = 1
                    end

                    self:setState({
                        focused = true,
                        hover = false,
                    })
                end,

                [Roact.Event.FocusLost] = function(obj)
                    if (self.unmounting) then return end -- we could also check if the InputObject is nil
                    if (disabled) then return end

                    local newText = string.match(obj.Text, "^%s*(.-)%s*$")
                    local isValid = if (not isTextAValidValue) then true else isTextAValidValue(newText)
                    local originalText = self.props.Text

                    if (newText == originalText) then
                        self:setState({
                            focused = false,
                        })

                        obj.Text = originalText
                        return
                    end

                    if ((not self.props.canSubmitEmptyString) and (newText == "")) then
                        self:setState({
                            focused = false,
                        })

                        obj.Text = originalText
                        return
                    end

                    if (isValid and onSubmit) then
                        --[[
                            Since we can't know if the Text prop will change after onSubmit,
                            we need to reset the TextBox's Text to its original value.

                            If Text does change, the TextBox's Text will briefly flicker.
                            If Text doesn't change, that's what this line of code is for.
                        --]]
                        obj.Text = originalText

                        onSubmit(newText)
                    end

                    self:setState({
                        focused = false,
                        invalidInput = (not isValid),
                    })
                end,

                [Roact.Change.Text] = function(obj)
                    if (self.unmounting) then return end
                    if (disabled) then return end

                    local newText = string.match(obj.Text, "^%s*(.-)%s*$")

                    if (onTextChanged) then
                        onTextChanged(newText)
                    end
                end
            })
        })
    })
end

return ConnectTheme(TextInput)


--!strict
--[[
    Provides the default user data values for ColorPane user data.
]]

local Modules = script.Parent
local Types = require(Modules.Types)

---

return {
    SnapValue = 0.001,
    UserColorPalettes = {},
    UserGradientPalettes = {},
    AskNameBeforePaletteCreation = true,
}::Types.ColorPaneUserData


--!strict
--[[
    Functions for comparing ColorPane user data values.
]]

local Modules = script.Parent
local Types = require(Modules.Types)

---

type ColorPalettes = Types.ColorPalettes
type GradientPalettes = Types.GradientPalettes
type ColorPaneUserData = Types.ColorPaneUserData

--[[
    Functions for comparing user data values
]]
local ColorPaneUserDataDiffs = {}

--[[
    Checks if two color palettes are different.

    @param this The palette to be compared to
    @param that The palette to compare
    @return `true` if the palettes are different, `false` otherwise
]]
ColorPaneUserDataDiffs.ColorPalettesAreDifferent = function(this: ColorPalettes, that: ColorPalettes): boolean
    if (#that ~= #this) then
        return true
    else
        for i, thatPalette in ipairs(that) do
            local thisPalette = this[i]

            if ((thatPalette.name ~= thisPalette.name) or (#thatPalette.colors ~= #thisPalette.colors)) then
                return true
            else
                for j, thatColor in ipairs(thatPalette.colors) do
                    local thisColor = thisPalette.colors[j]

                    if (thatColor.name ~= thisColor.name) then
                        return true
                    else
                        local thatTuple = thatColor.color
                        local thisTuple = thisColor.color

                        if ((thatTuple[1] ~= thisTuple[1]) or (thatTuple[2] ~= thisTuple[2]) or (thatTuple[3] ~= thisTuple[3])) then
                            return true
                        end
                    end
                end
            end
        end
    end

    return false
end

--[[
    Checks if two gradient palettes are different.

    @param this The palette to be compared to
    @param that The palette to compare
    @return `true` if the palettes are different, `false` otherwise
]]
ColorPaneUserDataDiffs.GradientPalettesAreDifferent = function(this: GradientPalettes, that: GradientPalettes): boolean
    if (#that ~= #this) then
        return true
    else
        for i, thatPalette in ipairs(that) do
            local thisPalette = this[i]

            if ((thatPalette.name ~= thisPalette.name) or (#thatPalette.gradients ~= #thisPalette.gradients)) then
                return true
            else
                for j, thatGradient in ipairs(thatPalette.gradients) do
                    local thisGradient = thisPalette.gradients[j]

                    if (
                        (thatGradient.name ~= thisGradient.name)
                        or (thatGradient.colorSpace ~= thisGradient.colorSpace)
                        or (thatGradient.hueAdjustment ~= thisGradient.hueAdjustment)
                        or (thatGradient.precision ~= thisGradient.precision)
                        or (#thatGradient.keypoints ~= #thisGradient.keypoints)
                    ) then
                        return true
                    else
                        for k, thatKeypoint in ipairs(thatGradient.keypoints) do
                            local thisKeypoint = thisGradient.keypoints[k]

                            if (thatKeypoint.time ~= thisKeypoint.time) then
                                return true
                            else
                                local thatTuple = thatKeypoint.color
                                local thisTuple = thisKeypoint.color

                                if ((thatTuple[1] ~= thisTuple[1]) or (thatTuple[2] ~= thisTuple[2]) or (thatTuple[3] ~= thisTuple[3])) then
                                    return true
                                end
                            end
                        end
                    end
                end
            end
        end
    end

    return false
end

--[[
    Compares user data values and returns a table with the differing values.
    The values

    @param this The values to be compared to
    @param that The values to compare
    @return A table containing the differing values between `this` and `that`, whose values come from `that`
]]
ColorPaneUserDataDiffs.GetModifiedValues = function(this: ColorPaneUserData, that: ColorPaneUserData)
    local modifiedValues = {}

    -- compare non-palettes
    if (that.AskNameBeforePaletteCreation ~= this.AskNameBeforePaletteCreation) then
        modifiedValues.AskNameBeforePaletteCreation = that.AskNameBeforePaletteCreation
    end

    if (that.SnapValue ~= this.SnapValue) then
        modifiedValues.SnapValue = that.SnapValue
    end

    -- compare palettes
    if (ColorPaneUserDataDiffs.ColorPalettesAreDifferent(this.UserColorPalettes, that.UserColorPalettes)) then
        modifiedValues.UserColorPalettes = that.UserColorPalettes
    end

    if (ColorPaneUserDataDiffs.GradientPalettesAreDifferent(this.UserGradientPalettes, that.UserGradientPalettes)) then
        modifiedValues.UserGradientPalettes = that.UserGradientPalettes
    end

    return modifiedValues
end

---

return ColorPaneUserDataDiffs


--!strict

local root = script.Parent.Parent

local Modules = root.Modules
local ColorPaneUserDataDiffs = require(Modules.ColorPaneUserDataDiffs)
local ColorPaneUserDataValidators = require(Modules.ColorPaneUserDataValidators)
local Enums = require(Modules.Enums)
local UserData = require(Modules.UserData)

---

--[[
    Helper function for creating a ColorPane UserData object.
]]
return function(initialValues)
    return UserData.new(Enums.ColorPaneUserDataKey, ColorPaneUserDataValidators, {
        [Enums.ColorPaneUserDataKey.UserColorPalettes] = ColorPaneUserDataDiffs.ColorPalettesAreDifferent,
        [Enums.ColorPaneUserDataKey.UserGradientPalettes] = ColorPaneUserDataDiffs.GradientPalettesAreDifferent,
    }, initialValues)
end


--!strict

local root = script.Parent.Parent
local Includes = root.Includes
local t = require(Includes.t)

---

--[[
    Type validator for the upstream user data provider's
    physical interface to ColorPane instances.

    @param value The value to check
    @return If the value is a valid interface
    @return An error message if the value was not valid
]]
return function(value: any): (boolean, string?)
    local success: boolean, failReason: string? = t.instanceOf("Folder", {
        GetVersion = t.instanceOf("BindableFunction"),
        GetValue = t.instanceOf("BindableFunction"),
        GetAllValues = t.instanceOf("BindableFunction"),
        SetValue = t.instanceOf("BindableFunction"),

        ValueChanged = t.instanceOf("BindableEvent"),
    })(value)

    if (not success) then
        return success, failReason
    else
        return true
    end
end


--!strict
--[[
    Returns the current revision of the user data interface.
    This should be an number (integer).
]]

return 1