diff --git a/.moonwave/custom.css b/.moonwave/custom.css index 418ea09..79bdd17 100644 --- a/.moonwave/custom.css +++ b/.moonwave/custom.css @@ -1,5 +1,5 @@ td:nth-child(1) { - background-color: rgb(46, 46, 46); + background-color: rgb(60, 60, 61); } td:nth-child(2) { diff --git a/.moonwave/static/components/collapsible/dark.png b/.moonwave/static/components/collapsible/dark.png new file mode 100644 index 0000000..8fd4245 Binary files /dev/null and b/.moonwave/static/components/collapsible/dark.png differ diff --git a/.moonwave/static/components/collapsible/light.png b/.moonwave/static/components/collapsible/light.png new file mode 100644 index 0000000..b946b97 Binary files /dev/null and b/.moonwave/static/components/collapsible/light.png differ diff --git a/CHANGELOG.md b/CHANGELOG.md index 990dc40..6feefbf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,10 @@ # Changelog -## Unreleased +## 1.4.0 +- Components: `Collapsible` (for containing content in an expandable section) +- Hooks: `useToggleState` + +## 1.3.0 - Added an optional `DisplayTitle` prop to `TabContainer` children tabs to allow displaying custom text on tabs ## 1.2.0 diff --git a/moonwave.toml b/moonwave.toml index 1493673..25b6d14 100644 --- a/moonwave.toml +++ b/moonwave.toml @@ -22,7 +22,7 @@ classes = ["Constants", "CommonProps"] [[classOrder]] section = "Components" collapsed = false -classes = ["Background", "Button", "Checkbox", "ColorPicker", "DatePicker", "Dropdown", "DropShadowFrame", "Label", "LoadingDots", "MainButton", "NumberSequencePicker", "NumericInput", "PluginProvider", "ProgressBar", "RadioButton", "ScrollFrame", "Slider", "Splitter", "TabContainer", "TextInput"] +classes = ["Background", "Button", "Checkbox", "Collapsible", "ColorPicker", "DatePicker", "Dropdown", "DropShadowFrame", "Label", "LoadingDots", "MainButton", "NumberSequencePicker", "NumericInput", "PluginProvider", "ProgressBar", "RadioButton", "ScrollFrame", "Slider", "Splitter", "TabContainer", "TextInput"] [[classOrder]] section = "Hooks" diff --git a/src/Components/Collapsible/Header.luau b/src/Components/Collapsible/Header.luau new file mode 100644 index 0000000..7d5261c --- /dev/null +++ b/src/Components/Collapsible/Header.luau @@ -0,0 +1,109 @@ +local CommonProps = require(script.Parent.Parent.Parent.CommonProps) +local React = require("@pkg/@jsdotlua/react") + +local e = React.createElement +local useTheme = require("../../Hooks/useTheme") +local useToggleState = require("../../Hooks/useToggleState") + +local HEADER_HEIGHT = 24 +local ARROW_RIGHT = "rbxasset://textures/ui/MenuBar/arrow_right.png" +local ARROW_DOWN = "rbxasset://textures/ui/MenuBar/arrow_down.png" + +local function HeaderIcon(props: CommonProps.T & { Icon: { Image: string, UseThemeColor: boolean? } }) + local theme = useTheme() + + return e("ImageLabel", { + BackgroundTransparency = 1, + BorderSizePixel = 0, + LayoutOrder = props.LayoutOrder, + Size = props.Size or UDim2.fromOffset(HEADER_HEIGHT, HEADER_HEIGHT), + ImageColor3 = if props.Icon.UseThemeColor ~= true + then Color3.fromRGB(255, 255, 255) + else theme:GetColor(Enum.StudioStyleGuideColor.MainText), + ImageTransparency = if props.Disabled then 0.6 else 0, + Image = props.Icon.Image, + }) +end + +local function Header(props: CommonProps.T & { + Selected: boolean?, + Expanded: boolean?, + IsBlockStyle: boolean?, + OnActivated: () -> (), + Text: string, + Icon: { + Image: string?, + UseThemeColor: boolean?, + }, +}) + local theme = useTheme() + local hovering = useToggleState(false) + + local modifier = Enum.StudioStyleGuideModifier.Default + + return e("TextButton", { + AutomaticSize = Enum.AutomaticSize.X, + AutoButtonColor = false, + BackgroundColor3 = if hovering.on + then theme:GetColor(Enum.StudioStyleGuideColor.ViewPortBackground) + elseif props.IsBlockStyle then theme:GetColor(Enum.StudioStyleGuideColor.Titlebar) + else theme:GetColor(Enum.StudioStyleGuideColor.MainBackground), + BackgroundTransparency = 0, + BorderSizePixel = if props.IsBlockStyle then 1 else 0, + BorderColor3 = theme:GetColor(Enum.StudioStyleGuideColor.Border, modifier), + Size = UDim2.new(1, 0, 0, HEADER_HEIGHT), + LayoutOrder = props.LayoutOrder, + Text = "", + ClipsDescendants = true, + + [React.Event.MouseEnter] = if not props.Disabled then hovering.enable else nil, + [React.Event.MouseLeave] = if not props.Disabled then hovering.disable else nil, + [React.Event.Activated] = if not props.Disabled then props.OnActivated else nil, + }, { + Layout = e("UIListLayout", { + FillDirection = Enum.FillDirection.Horizontal, + Padding = UDim.new(0, 0), + HorizontalAlignment = Enum.HorizontalAlignment.Left, + VerticalAlignment = Enum.VerticalAlignment.Center, + SortOrder = Enum.SortOrder.LayoutOrder, + }), + + UIPadding = e("UIPadding", { + PaddingLeft = UDim.new(0, 5), + }), + + ArrowIconFrame = e(HeaderIcon, { + Icon = { + Image = if props.Expanded then ARROW_DOWN else ARROW_RIGHT, + UseThemeColor = true, + }, + Disabled = props.Disabled, + LayoutOrder = 1, + Size = UDim2.fromOffset(HEADER_HEIGHT * 0.75, HEADER_HEIGHT * 0.75), + }), + + IconFrame = props.Icon and e(HeaderIcon, { + Icon = props.Icon, + Disabled = props.Disabled, + LayoutOrder = 2, + }), + + Title = e("TextLabel", { + AutomaticSize = Enum.AutomaticSize.X, + BackgroundTransparency = 1, + BorderSizePixel = 0, + LayoutOrder = 3, + TextTransparency = if props.Disabled then 0.5 else 0, + Size = UDim2.fromOffset(0, HEADER_HEIGHT), + Text = props.Text, + TextColor3 = theme:GetColor(Enum.StudioStyleGuideColor.MainText), + TextXAlignment = Enum.TextXAlignment.Left, + }, { + UIPadding = e("UIPadding", { + PaddingLeft = UDim.new(0, 4), + }), + }), + }) +end + +return Header diff --git a/src/Components/Collapsible/init.luau b/src/Components/Collapsible/init.luau new file mode 100644 index 0000000..bcac112 --- /dev/null +++ b/src/Components/Collapsible/init.luau @@ -0,0 +1,138 @@ +--[=[ + @class Collapsible + A simple collapsible component that reveals content when clicked. + + | Dark | Light | + | - | - | + | ![Dark](/StudioComponents/components/collapsible/dark.png) | ![Light](/StudioComponents/components/collapsible/light.png) | + + ```lua + local function MySettingsPage() + return React.createElement(StudioComponents.Background, {}, { + General = e(StudioComponents.Collapsible, { + Title = "General", + Icon = { + Image = "path.to.icon", + UseThemeColor = false, + }, + IsBlockStyle = true, + LayoutOrder = 1 + }, { + -- general settings here + + -- collapsibles can also be nested to create tree structures + AnotherCollapsible = e(StudioComponents.Collapsible, { + Title = "Another Collapsible", + IsBlockStyle = false, -- for this nested collapsible let's not use the block style + }), + }), + + Graphics = e(StudioComponents.Collapsible, { + Title = "Graphics", + IsBlockStyle = true, + LayoutOrder = 2 + }, { + -- graphics settings here + }), + + Audio = e(StudioComponents.Collapsible, { + Title = "Audio", + IsBlockStyle = true, + LayoutOrder = 3 + }, { + -- audio settings here + }), + }) + end + ``` +]=] + +local React = require("@pkg/@jsdotlua/react") + +local e = React.createElement +local CommonProps = require(script.Parent.Parent.CommonProps) +local Header = require(script.Header) +local useToggleState = require("../Hooks/useToggleState") + +--[=[ + @within Collapsible + @interface IconProps + + @field Image string + @field UseThemeColor boolean? +]=] + +--[=[ + @within Collapsible + @interface Props + @tag Component Props + + @field ... CommonProps + @field Title string + @field IsBlockStyle boolean? + @field KeepContentMounted boolean? -- if true, uses `Visible` based rendering instead of re-mounting + @field Icon IconProps? + @field Layout React.Element? + @field ContentPadding React.Element? +]=] + +local function Collapsible(props: CommonProps.T & { + Title: string, + IsBlockStyle: boolean?, + KeepContentMounted: boolean?, + Icon: { + Image: string?, + UseThemeColor: boolean?, + }, + Layout: React.Element?, + ContentPadding: React.Element?, + children: React.ReactNode, +}) + local collapsible = useToggleState(false) + + return e("Frame", { + BackgroundTransparency = 1, + Size = UDim2.fromScale(1, 0), + AutomaticSize = Enum.AutomaticSize.Y, + LayoutOrder = props.LayoutOrder, + }, { + UIListLayout = e("UIListLayout", { + Padding = UDim.new(0, 0), + SortOrder = Enum.SortOrder.LayoutOrder, + }), + + Header = e(Header, { + OnActivated = collapsible.toggle, + IsBlockStyle = props.IsBlockStyle or false, + Expanded = collapsible.on, + Disabled = props.Disabled, + Icon = props.Icon, + Text = props.Title, + LayoutOrder = 1, + }), + + Content = if not props.KeepContentMounted and not (collapsible.on and props.Disabled ~= true) + then nil + else e("Frame", { + BackgroundTransparency = 1, + Position = UDim2.fromOffset(30, 0), + AutomaticSize = Enum.AutomaticSize.Y, + Size = UDim2.fromScale(1, 0), + AnchorPoint = Vector2.new(0, 0), + Visible = if props.KeepContentMounted then (collapsible.on and props.Disabled ~= true) else true, + LayoutOrder = 2, + }, { + UIListLayout = props.Layout or e("UIListLayout", { + Padding = UDim.new(0, 5), + SortOrder = Enum.SortOrder.LayoutOrder, + }), + + UIPadding = props.ContentPadding or e("UIPadding", { + PaddingLeft = UDim.new(0, 10), + PaddingTop = UDim.new(0, 5), + }), + }, props.children), + }) +end + +return Collapsible diff --git a/src/Hooks/useTheme.luau b/src/Hooks/useTheme.luau index 0affba9..126a181 100644 --- a/src/Hooks/useTheme.luau +++ b/src/Hooks/useTheme.luau @@ -26,7 +26,7 @@ local React = require("@pkg/@jsdotlua/react") local ThemeContext = require("../Contexts/ThemeContext") -local function useTheme() +local function useTheme(): StudioTheme local theme = React.useContext(ThemeContext) local studioTheme, setStudioTheme = React.useState(Studio.Theme) diff --git a/src/Hooks/useToggleState.luau b/src/Hooks/useToggleState.luau new file mode 100644 index 0000000..dd7e0d2 --- /dev/null +++ b/src/Hooks/useToggleState.luau @@ -0,0 +1,33 @@ +local React = require("@pkg/@jsdotlua/react") + +local function useToggleState(default: boolean): { + on: boolean, + enable: () -> (), + disable: () -> (), + toggle: () -> (), +} + local toggled, setToggled = React.useState(default) + + local enable = React.useCallback(function() + setToggled(true) + end, {}) + + local disable = React.useCallback(function() + setToggled(false) + end, {}) + + local toggle = React.useCallback(function() + setToggled(function(currentToggled) + return not currentToggled + end) + end, {}) + + return { + on = toggled, + enable = enable, + disable = disable, + toggle = toggle, + } +end + +return useToggleState diff --git a/src/Stories/Collapsible.story.luau b/src/Stories/Collapsible.story.luau new file mode 100644 index 0000000..4916b39 --- /dev/null +++ b/src/Stories/Collapsible.story.luau @@ -0,0 +1,84 @@ +local Checkbox = require("../Components/Checkbox") +local React = require("@pkg/@jsdotlua/react") + +local Collapsible = require("../Components/Collapsible") +local createStory = require("./Helpers/createStory") + +local e = React.createElement + +local function Content() + local checked, setChecked = React.useState(false) + + return e(Checkbox, { + Value = checked, + Label = "Test", + OnChanged = function() + setChecked(not checked) + end, + }) +end + +local function RecursiveCollapsible(props: { number: number }) + return e(Collapsible, { + Title = if props.number == 1 then "Recursive Collapsible (Tree)" else `Collapsible {props.number - 1}`, + LayoutOrder = 3, + }, e(RecursiveCollapsible, { number = props.number + 1 })) +end + +local function Story() + return e("Frame", { + Position = UDim2.fromScale(0.5, 0.5), + AnchorPoint = Vector2.new(0.5, 0.5), + Size = UDim2.fromScale(1, 1), + BackgroundTransparency = 1, + BackgroundColor3 = Color3.fromRGB(255, 255, 255), + BorderSizePixel = 0, + }, { + UIListLayout = e("UIListLayout", { + Padding = UDim.new(0, 30), + SortOrder = Enum.SortOrder.LayoutOrder, + }), + + BlockCollapsible = e(Collapsible, { + Title = "Block Collapsible", + LayoutOrder = 1, + IsBlockStyle = true, + }, { + Content = e(Content), + }), + + CollapsibleWithIcon = e(Collapsible, { + Title = "Block Collapsible w/ Icon", + LayoutOrder = 1, + Icon = { + Image = "rbxasset://textures/TerrainTools/icon_shape_cube.png", + }, + IsBlockStyle = true, + }, { + Content = e(Content), + }), + + Collapsible = e(Collapsible, { + Title = "Collapsible", + LayoutOrder = 2, + }, { + EnabledCollapsible = e(Collapsible, { + Title = "Enabled Collapsible", + }, { + Content = e(Content), + }), + + DisabledCollapsible = e(Collapsible, { + Title = "Disabled Collapsible", + LayoutOrder = 2, + Disabled = true, + }), + }), + + CollapsibleTree = e(RecursiveCollapsible, { + number = 1, + }), + }) +end + +return createStory(Story) diff --git a/src/init.luau b/src/init.luau index 233dfbc..38726e2 100644 --- a/src/init.luau +++ b/src/init.luau @@ -4,6 +4,7 @@ return { Background = require("./Components/Background"), Button = require("./Components/Button"), Checkbox = require("./Components/Checkbox"), + Collapsible = require("./Components/Collapsible"), ColorPicker = require("./Components/ColorPicker"), DatePicker = require("./Components/DatePicker"), Dropdown = require("./Components/Dropdown"),