From 76ebc2f72cd8c77c469a9d8d0e674dd5f7fcd24a Mon Sep 17 00:00:00 2001 From: Marin Minnerly Date: Sat, 12 Oct 2024 15:10:52 -0700 Subject: [PATCH 1/6] Update copyright holder --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index 5e72e584..e868b9a6 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright 2021 David Minnerly +Copyright 2021 flipbook-labs Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in From efe83a7dbcf61d4bb88b3b02c932c68551958b5d Mon Sep 17 00:00:00 2001 From: Marin Minnerly Date: Sun, 3 Nov 2024 08:56:59 -0800 Subject: [PATCH 2/6] Merge hotfix to trunk --- src/Common/useDescendants.luau | 15 +++++++++++++++ wally.toml | 2 +- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/Common/useDescendants.luau b/src/Common/useDescendants.luau index b0d8dd17..60d5458d 100644 --- a/src/Common/useDescendants.luau +++ b/src/Common/useDescendants.luau @@ -1,6 +1,13 @@ local React = require("@pkg/React") local Sift = require("@pkg/Sift") +local function hasPermission(instance: Instance) + local success = pcall(function() + return instance.Name + end) + return success +end + local function useDescendants(parent: Instance, predicate: (descendant: Instance) -> boolean): { Instance } local descendants: { Instance }, setDescendants = React.useState({}) @@ -8,6 +15,10 @@ local function useDescendants(parent: Instance, predicate: (descendant: Instance setDescendants(function(prev) local exists = table.find(prev, descendant) + if not hasPermission(descendant) then + return prev + end + if predicate(descendant) then if exists then -- Force a re-render. Nothing about the state changed, but the @@ -41,6 +52,10 @@ local function useDescendants(parent: Instance, predicate: (descendant: Instance -- Listen for name changes and update the list of descendants for _, descendant in parent:GetDescendants() do + if not hasPermission(descendant) then + continue + end + table.insert( connections, descendant:GetPropertyChangedSignal("Name"):Connect(function() diff --git a/wally.toml b/wally.toml index e6900a4e..ed032e02 100644 --- a/wally.toml +++ b/wally.toml @@ -1,6 +1,6 @@ [package] name = "flipbook-labs/flipbook" -version = "1.5.0" +version = "1.5.1" license = "MIT" registry = "https://github.com/UpliftGames/wally-index" realm = "shared" From eb289ede1c4c61ad24816724d480b34f4e8dc07a Mon Sep 17 00:00:00 2001 From: vocksel Date: Sun, 3 Nov 2024 09:09:27 -0800 Subject: [PATCH 3/6] Controls panel QOL improvements (#259) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Problem Story controls need some sprucing up # Solution Before: Screenshot 2024-08-11 at 6 57 49 PM After: Screenshot 2024-08-11 at 6 57 30 PM Fixes #253 # Checklist - [x] Ran `lune run test` locally before merging --- foreman.toml | 2 +- src/Forms/InputField.luau | 8 +- src/Storybook/StoryControls.luau | 108 +++++++++++++++++++------ src/Storybook/StoryControls.story.luau | 2 + src/Storybook/StoryPreview.luau | 5 +- src/Storybook/StoryView.luau | 10 +-- 6 files changed, 96 insertions(+), 39 deletions(-) diff --git a/foreman.toml b/foreman.toml index 2b14f229..53389e03 100644 --- a/foreman.toml +++ b/foreman.toml @@ -7,5 +7,5 @@ selene = { source = "Kampfkarren/selene", version = "0.27.1" } stylua = { source = "JohnnyMorganz/StyLua", version = "0.20.0" } tarmac = { source = "Roblox/tarmac", version = "0.7.0" } wally = { source = "UpliftGames/wally", version = "0.3.2" } -luau-lsp = { source = "JohnnyMorganz/luau-lsp", version = "1.32.3" } +luau-lsp = { source = "JohnnyMorganz/luau-lsp", version = "1.34.0" } wally-package-types = { source = "JohnnyMorganz/wally-package-types", version = "1.3.2" } diff --git a/src/Forms/InputField.luau b/src/Forms/InputField.luau index 8ecb47b1..fc93de21 100644 --- a/src/Forms/InputField.luau +++ b/src/Forms/InputField.luau @@ -27,7 +27,7 @@ local function InputField(providedProps: Props) local ref = React.useRef(nil :: TextBox?) local text, setText = React.useState("") - local isValid, setIsValid = React.useState(false) + local isValid, setIsValid = React.useState(true) local theme = useTheme() local onFocusLost = React.useCallback( @@ -41,7 +41,9 @@ local function InputField(providedProps: Props) end end, { + text, isValid, + props.onFocusLost, props.onSubmit, } :: { unknown } ) @@ -49,7 +51,9 @@ local function InputField(providedProps: Props) local onTextChanged = React.useCallback(function(rbx: TextBox) local newText = rbx.Text - if newText == text and newText ~= "" then + newText = newText:gsub("$%s+", ""):gsub("%s+^", "") + + if newText == text or newText == "" then return end diff --git a/src/Storybook/StoryControls.luau b/src/Storybook/StoryControls.luau index d30d51cf..e689f821 100644 --- a/src/Storybook/StoryControls.luau +++ b/src/Storybook/StoryControls.luau @@ -1,10 +1,12 @@ local React = require("@pkg/React") +local Sift = require("@pkg/Sift") local Checkbox = require("@root/Forms/Checkbox") local Dropdown = require("@root/Forms/Dropdown") local InputField = require("@root/Forms/InputField") local useTheme = require("@root/Common/useTheme") +local useMemo = React.useMemo local e = React.createElement type Props = { @@ -16,72 +18,131 @@ type Props = { local function StoryControls(props: Props) local theme = useTheme() - local controls: { [string]: React.Node } = {} - for key, value in props.controls do + local sortedControls: { { name: string, value: any } } = useMemo(function() + local result = {} + + for _, entry in Sift.Dictionary.entries(props.controls) do + table.insert(result, { + name = entry[1], + value = entry[2], + }) + end + + return Sift.List.sort(result, function(a, b) + return a.name < b.name + end) + end, { props.controls }) + + local controlElements: { [string]: React.Node } = {} + for index, control in sortedControls do local function setControl(newValue: any) - props.setControl(key, newValue) + local newValueAsNum = tonumber(newValue) + if newValueAsNum then + newValue = newValueAsNum + end + + props.setControl(control.name, newValue) end + local controlType = typeof(control.value) local option: React.Node - if typeof(value) == "boolean" then + if controlType == "boolean" then option = React.createElement(Checkbox, { - initialState = value, + initialState = control.value, onStateChange = setControl, }) - elseif typeof(value) == "table" then + elseif controlType == "table" then option = React.createElement(Dropdown, { - default = value[1], - options = value, + default = control.value[1], + options = control.value, onOptionChange = setControl, }) - else + elseif controlType == "number" or controlType == "string" then option = React.createElement(InputField, { - placeholder = value, - onTextChange = setControl, + placeholder = control.value, onSubmit = setControl, }) + else + option = React.createElement("TextLabel", { + Text = `ERR: Controls of type "{controlType}" are unsupported`, + AutomaticSize = Enum.AutomaticSize.XY, + BackgroundTransparency = 1, + Font = theme.font, + TextColor3 = theme.alert, + TextSize = theme.textSize, + TextXAlignment = Enum.TextXAlignment.Left, + TextYAlignment = Enum.TextYAlignment.Center, + TextTruncate = Enum.TextTruncate.AtEnd, + }) end - controls[key] = e("Frame", { - BackgroundTransparency = 1, + controlElements[control.name] = e("Frame", { + LayoutOrder = index, + BackgroundColor3 = theme.background, + BackgroundTransparency = if index % 2 == 0 then 1 else 0, + BorderSizePixel = 0, Size = UDim2.fromScale(1, 0), AutomaticSize = Enum.AutomaticSize.Y, }, { Layout = React.createElement("UIListLayout", { FillDirection = Enum.FillDirection.Horizontal, + SortOrder = Enum.SortOrder.LayoutOrder, + Padding = theme.padding, + }), + + Padding = e("UIPadding", { + PaddingTop = theme.padding, + PaddingRight = theme.padding, + PaddingBottom = theme.padding, + PaddingLeft = theme.padding, }), Name = e("TextLabel", { - Text = key, - Size = UDim2.fromScale(1 / 2, 0), + LayoutOrder = 1, + Text = control.name, + Size = UDim2.fromScale(1 / 4, 0), AutomaticSize = Enum.AutomaticSize.Y, BackgroundTransparency = 1, Font = theme.font, TextColor3 = theme.text, TextSize = theme.textSize, TextXAlignment = Enum.TextXAlignment.Left, - TextYAlignment = Enum.TextYAlignment.Top, + TextYAlignment = Enum.TextYAlignment.Center, + TextTruncate = Enum.TextTruncate.AtEnd, }), Option = e("Frame", { + LayoutOrder = 2, BackgroundTransparency = 1, - Size = UDim2.fromScale(1 / 2, 0), + Size = UDim2.fromScale(1, 0), AutomaticSize = Enum.AutomaticSize.Y, + }, { + Flex = e("UIFlexItem", { + FlexMode = Enum.UIFlexMode.Shrink, + }), }, option), }) end return e("Frame", { - BackgroundTransparency = 1, + BackgroundTransparency = 0.4, + BackgroundColor3 = theme.sidebar, + BorderSizePixel = 0, LayoutOrder = props.layoutOrder, - Size = UDim2.fromScale(1, 0), - AutomaticSize = Enum.AutomaticSize.Y, + Size = UDim2.fromScale(1, 1), }, { Layout = e("UIListLayout", { SortOrder = Enum.SortOrder.LayoutOrder, Padding = theme.padding, }), + Padding = e("UIPadding", { + PaddingTop = theme.padding, + PaddingRight = theme.padding, + PaddingBottom = theme.padding, + PaddingLeft = theme.padding, + }), + Title = e("TextLabel", { LayoutOrder = 1, Text = "Controls", @@ -101,12 +162,9 @@ local function StoryControls(props: Props) BackgroundTransparency = 1, }, { Layout = e("UIListLayout", { - SortOrder = Enum.SortOrder.Name, - Padding = theme.padding, + SortOrder = Enum.SortOrder.LayoutOrder, }), - - ControlsFragment = React.createElement(React.Fragment, nil, controls), - }), + }, controlElements), }) end diff --git a/src/Storybook/StoryControls.story.luau b/src/Storybook/StoryControls.story.luau index 24c496fd..ff871bc4 100644 --- a/src/Storybook/StoryControls.story.luau +++ b/src/Storybook/StoryControls.story.luau @@ -19,6 +19,8 @@ return { "Option 2", "Option 3", }, + onlyTwentyCharacters = "OnlyTwentyCharacters", + badControlType = function() end, }, setControl = function() end, }), diff --git a/src/Storybook/StoryPreview.luau b/src/Storybook/StoryPreview.luau index 34d9551f..4f7ffdb6 100644 --- a/src/Storybook/StoryPreview.luau +++ b/src/Storybook/StoryPreview.luau @@ -16,11 +16,12 @@ local defaultProps = { } export type Props = { - layoutOrder: number, story: types.Story, - ref: any, controls: { [string]: any }, storyModule: ModuleScript, + + ref: any, + layoutOrder: number?, } type InternalProps = Props & typeof(defaultProps) diff --git a/src/Storybook/StoryView.luau b/src/Storybook/StoryView.luau index 2e79c932..3359a9c8 100644 --- a/src/Storybook/StoryView.luau +++ b/src/Storybook/StoryView.luau @@ -183,17 +183,9 @@ local function StoryView(props: Props) onResize = onControlsResized, }, { ScrollingFrame = e(ScrollingFrame, { - LayoutOrder = 2, - BackgroundTransparency = 0.4, + BackgroundTransparency = 1, BackgroundColor3 = theme.sidebar, }, { - Padding = e("UIPadding", { - PaddingTop = theme.padding, - PaddingRight = theme.padding, - PaddingBottom = theme.padding, - PaddingLeft = theme.padding, - }), - StoryControls = e(StoryControls, { controls = controls, setControl = setControl, From 5daea9c2d7b7fd120522dc38e51dbb25f6261dcd Mon Sep 17 00:00:00 2001 From: vocksel Date: Sun, 3 Nov 2024 09:21:55 -0800 Subject: [PATCH 4/6] About page (#252) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR adds an About page with information on the current flipbook version. Copyright year auto updates based on current year, and authors are loaded based on user ID. Dark theme | Light theme --- | --- Screenshot 2024-05-27 at 6 35 46 PM | Screenshot 2024-05-27 at 6 35 35 PM # Checklist - [ ] Ran `lune run test` locally before merging --- build.project.json | 5 +- default.project.json | 5 +- img/GitHubMark.png | Bin 0 -> 4837 bytes src/About/AboutView.luau | 147 ++++++++++++++++++++++++++ src/About/AboutView.story.luau | 15 +++ src/About/RobloxProfile.luau | 79 ++++++++++++++ src/About/RobloxProfile.story.luau | 27 +++++ src/Navigation/NavigationContext.luau | 1 + src/Navigation/Screen.luau | 5 +- src/Navigation/enums.luau | 3 +- src/Panels/Topbar.luau | 59 ++++------- src/Testing/MockPlugin.luau | 4 +- src/assets.luau | 41 +++---- src/themes.luau | 127 +++++++++++----------- tarmac-manifest.toml | 42 ++++---- 15 files changed, 421 insertions(+), 139 deletions(-) create mode 100644 img/GitHubMark.png create mode 100644 src/About/AboutView.luau create mode 100644 src/About/AboutView.story.luau create mode 100644 src/About/RobloxProfile.luau create mode 100644 src/About/RobloxProfile.story.luau diff --git a/build.project.json b/build.project.json index 446d3be9..42b0edf5 100644 --- a/build.project.json +++ b/build.project.json @@ -7,6 +7,9 @@ "Example": { "$path": "example" }, + "wally.toml": { + "$path": "wally.toml" + }, "$path": "src" } -} \ No newline at end of file +} diff --git a/default.project.json b/default.project.json index 9d407fbb..d10e409e 100644 --- a/default.project.json +++ b/default.project.json @@ -4,6 +4,9 @@ "Packages": { "$path": "Packages" }, + "wally.toml": { + "$path": "wally.toml" + }, "$path": "build" } -} \ No newline at end of file +} diff --git a/img/GitHubMark.png b/img/GitHubMark.png new file mode 100644 index 0000000000000000000000000000000000000000..50b81752278d084ba9d449fff25f4051df162b0f GIT binary patch literal 4837 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 literal 0 HcmV?d00001 diff --git a/src/About/AboutView.luau b/src/About/AboutView.luau new file mode 100644 index 00000000..fec7ba36 --- /dev/null +++ b/src/About/AboutView.luau @@ -0,0 +1,147 @@ +local React = require("@pkg/React") + +local RobloxProfile = require("@root/About/RobloxProfile") +local Sprite = require("@root/Common/Sprite") +local assets = require("@root/assets") +local nextLayoutOrder = require("@root/Common/nextLayoutOrder") +local useTheme = require("@root/Common/useTheme") +local wally = require(script.Parent.Parent["wally.toml"]) + +local useMemo = React.useMemo + +local AUTHOR_USER_IDS = { + 1343930, + 731053179, +} + +local function AboutView() + local theme = useTheme() + local currentYear = useMemo(function() + return (DateTime.now():ToUniversalTime() :: any).Year + end, {}) + + local authors: { [string]: React.Node } = {} + for _, userId in AUTHOR_USER_IDS do + authors[`Author{userId}`] = React.createElement(RobloxProfile, { + userId = userId, + LayoutOrder = nextLayoutOrder(), + }) + end + + return React.createElement("Frame", { + Size = UDim2.fromScale(1, 1), + BackgroundColor3 = theme.background, + BorderSizePixel = 0, + }, { + Layout = React.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder, + HorizontalAlignment = Enum.HorizontalAlignment.Center, + Padding = theme.paddingLarge, + }), + + Padding = React.createElement("UIPadding", { + PaddingTop = theme.paddingLarge, + PaddingRight = theme.paddingLarge, + PaddingBottom = theme.paddingLarge, + PaddingLeft = theme.paddingLarge, + }), + + Logo = React.createElement(Sprite, { + layoutOrder = nextLayoutOrder(), + image = assets.IconLight, + size = UDim2.fromOffset(42, 42), + }), + + Title = React.createElement("TextLabel", { + AutomaticSize = Enum.AutomaticSize.XY, + BackgroundTransparency = 1, + Font = theme.font, + LayoutOrder = nextLayoutOrder(), + Size = UDim2.fromScale(0, 0), + Text = `flipbook v{wally.package.version}`, + TextColor3 = theme.text, + TextSize = theme.headerTextSize, + TextWrapped = true, + }), + + GitHub = React.createElement("Frame", { + AutomaticSize = Enum.AutomaticSize.XY, + BackgroundTransparency = 1, + LayoutOrder = nextLayoutOrder(), + }, { + Layout = React.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder, + FillDirection = Enum.FillDirection.Horizontal, + Padding = theme.paddingSmall, + }), + + Icon = React.createElement(Sprite, { + layoutOrder = nextLayoutOrder(), + color = theme.github, + image = assets.GitHubMark, + size = UDim2.fromOffset(theme.textSize, theme.textSize), + }), + + Label = React.createElement("TextLabel", { + AutomaticSize = Enum.AutomaticSize.XY, + BackgroundTransparency = 1, + Font = theme.font, + LayoutOrder = nextLayoutOrder(), + Size = UDim2.fromScale(0, 0), + Text = "flipbook-labs/flipbook", + TextColor3 = theme.text, + TextSize = theme.textSize, + }), + }), + + Authors = React.createElement("Frame", { + AutomaticSize = Enum.AutomaticSize.XY, + BackgroundTransparency = 1, + LayoutOrder = nextLayoutOrder(), + }, { + Layout = React.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder, + HorizontalAlignment = Enum.HorizontalAlignment.Center, + Padding = theme.padding, + }), + + Title = React.createElement("TextLabel", { + AutomaticSize = Enum.AutomaticSize.XY, + BackgroundTransparency = 1, + Font = theme.font, + LayoutOrder = nextLayoutOrder(), + Size = UDim2.fromScale(0, 0), + Text = `Created by:`, + TextColor3 = theme.text, + TextSize = theme.textSize, + TextWrapped = true, + }), + + AuthorList = React.createElement("Frame", { + AutomaticSize = Enum.AutomaticSize.XY, + BackgroundTransparency = 1, + LayoutOrder = nextLayoutOrder(), + }, { + Layout = React.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder, + FillDirection = Enum.FillDirection.Horizontal, + Padding = theme.padding, + }), + }, authors), + }), + + Copy = React.createElement("TextLabel", { + AutomaticSize = Enum.AutomaticSize.XY, + BackgroundTransparency = 1, + Font = theme.font, + LayoutOrder = nextLayoutOrder(), + Size = UDim2.fromScale(0, 0), + Text = `Copyright © 2021—{currentYear} flipbook-labs`, + TextColor3 = theme.text, + TextSize = theme.textSize, + TextWrapped = true, + }), + }) +end + +return AboutView diff --git a/src/About/AboutView.story.luau b/src/About/AboutView.story.luau new file mode 100644 index 00000000..4b7d0b1b --- /dev/null +++ b/src/About/AboutView.story.luau @@ -0,0 +1,15 @@ +local React = require("@pkg/React") + +local AboutView = require("./AboutView") +local ContextProviders = require("@root/Common/ContextProviders") +local MockPlugin = require("@root/Testing/MockPlugin") + +return { + story = function() + return React.createElement(ContextProviders, { + plugin = MockPlugin.new(), + }, { + AboutView = React.createElement(AboutView), + }) + end, +} diff --git a/src/About/RobloxProfile.luau b/src/About/RobloxProfile.luau new file mode 100644 index 00000000..645b9684 --- /dev/null +++ b/src/About/RobloxProfile.luau @@ -0,0 +1,79 @@ +local Players = game:GetService("Players") + +local React = require("@pkg/React") +local ReactSpring = require("@pkg/ReactSpring") + +local nextLayoutOrder = require("@root/Common/nextLayoutOrder") +local useTheme = require("@root/Common/useTheme") + +local useState = React.useState +local useEffect = React.useEffect + +local AVATAR_SIZE = Enum.ThumbnailSize.Size48x48 + +export type Props = { + userId: number, + LayoutOrder: number?, +} + +local function RobloxProfile(props: Props) + local theme = useTheme() + local avatar, setAvatar = useState(nil :: string?) + local username, setUsername = useState(nil :: string?) + + local isLoading = avatar == nil and username == nil + + local styles = ReactSpring.useSpring({ + alpha = if isLoading then 1 else 0, + }) + + useEffect(function() + task.spawn(function() + setAvatar(Players:GetUserThumbnailAsync(props.userId, Enum.ThumbnailType.HeadShot, AVATAR_SIZE)) + end) + + task.spawn(function() + setUsername(Players:GetNameFromUserIdAsync(props.userId)) + end) + end, { props.userId }) + + return React.createElement("Frame", { + LayoutOrder = props.LayoutOrder, + AutomaticSize = Enum.AutomaticSize.XY, + BackgroundTransparency = 1, + }, { + Layout = React.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder, + FillDirection = Enum.FillDirection.Horizontal, + VerticalAlignment = Enum.VerticalAlignment.Center, + Padding = theme.paddingSmall, + }), + + Avatar = React.createElement("ImageLabel", { + LayoutOrder = nextLayoutOrder(), + Size = UDim2.fromOffset(48, 48), + BackgroundColor3 = theme.sidebar, + BackgroundTransparency = 0.6, + ImageTransparency = styles.alpha, + BorderSizePixel = 0, + Image = avatar, + }, { + Corner = React.createElement("UICorner", { + CornerRadius = UDim.new(0.5, 0), + }), + }), + + Username = React.createElement("TextLabel", { + LayoutOrder = nextLayoutOrder(), + Text = `@{username}`, + Font = theme.font, + TextColor3 = theme.text, + TextSize = theme.textSize, + BackgroundTransparency = 1, + TextTransparency = styles.alpha, + AutomaticSize = Enum.AutomaticSize.XY, + }), + }) +end + +return RobloxProfile diff --git a/src/About/RobloxProfile.story.luau b/src/About/RobloxProfile.story.luau new file mode 100644 index 00000000..c45ee2ce --- /dev/null +++ b/src/About/RobloxProfile.story.luau @@ -0,0 +1,27 @@ +local React = require("@pkg/React") + +local ContextProviders = require("@root/Common/ContextProviders") +local MockPlugin = require("@root/Testing/MockPlugin") +local RobloxProfile = require("./RobloxProfile") + +local controls = { + userId = 1, +} + +type Props = { + controls: typeof(controls), +} +return { + controls = controls, + story = function(props: Props) + local userId = assert(tonumber(props.controls.userId)) + + return React.createElement(ContextProviders, { + plugin = MockPlugin.new(), + }, { + RobloxProfiler = React.createElement(RobloxProfile, { + userId = userId, + }), + }) + end, +} diff --git a/src/Navigation/NavigationContext.luau b/src/Navigation/NavigationContext.luau index 93ed501b..ed183267 100644 --- a/src/Navigation/NavigationContext.luau +++ b/src/Navigation/NavigationContext.luau @@ -13,6 +13,7 @@ export type NavigationContext = { navigateTo: (newScreen: Screen) -> (), goBack: () -> (), getBreadcrumbs: () -> { string }, + canGoBack: () -> boolean, currentScreen: Screen, } diff --git a/src/Navigation/Screen.luau b/src/Navigation/Screen.luau index 70ee4a45..90cef980 100644 --- a/src/Navigation/Screen.luau +++ b/src/Navigation/Screen.luau @@ -1,6 +1,7 @@ local ModuleLoader = require("@pkg/ModuleLoader") local React = require("@pkg/React") +local AboutView = require("@root/About/AboutView") local NavigationContext = require("@root/Navigation/NavigationContext") local SettingsView = require("@root/UserSettings/SettingsView") local StoryCanvas = require("@root/Storybook/StoryCanvas") @@ -32,8 +33,10 @@ local function Screen(props: Props) end elseif currentScreen == "Settings" then return React.createElement(SettingsView) + elseif currentScreen == "About" then + return React.createElement(AboutView) end - return nil + return nil :: never end, { props, currentScreen } :: { unknown }) return screenElement diff --git a/src/Navigation/enums.luau b/src/Navigation/enums.luau index 5c54ab06..0514994d 100644 --- a/src/Navigation/enums.luau +++ b/src/Navigation/enums.luau @@ -1,9 +1,10 @@ local enums = {} -export type Screen = "Home" | "Settings" +export type Screen = "Home" | "Settings" | "About" enums.Screen = { Home = "Home" :: Screen, Settings = "Settings" :: Screen, + About = "About" :: Screen, } return enums diff --git a/src/Panels/Topbar.luau b/src/Panels/Topbar.luau index 251bd327..1261f726 100644 --- a/src/Panels/Topbar.luau +++ b/src/Panels/Topbar.luau @@ -17,12 +17,12 @@ local function Topbar(props: Props) local navigation = NavigationContext.use() local navigateToSettings = useCallback(function() - if navigation.currentScreen == "Settings" then - navigation.goBack() - else - navigation.navigateTo("Settings") - end - end, { navigation.navigateTo, navigation.currentScreen } :: { unknown }) + navigation.navigateTo("Settings") + end, { navigation.navigateTo }) + + local navigateToAbout = useCallback(function() + navigation.navigateTo("About") + end, { navigation.navigateTo }) return React.createElement("Frame", { BackgroundColor3 = theme.sidebar, @@ -43,6 +43,21 @@ local function Topbar(props: Props) PaddingLeft = theme.paddingSmall, }), + About = React.createElement(Navigation.Item, { + layoutOrder = nextLayoutOrder(), + onClick = navigateToAbout, + }, { + Text = React.createElement("TextLabel", { + AutomaticSize = Enum.AutomaticSize.XY, + BackgroundTransparency = 1, + Font = theme.font, + Size = UDim2.fromScale(0, 0), + Text = "About", + TextColor3 = theme.textFaded, + TextSize = theme.textSize, + }), + }), + Settings = React.createElement(Navigation.Item, { layoutOrder = nextLayoutOrder(), onClick = navigateToSettings, @@ -55,8 +70,6 @@ local function Topbar(props: Props) Text = "Settings", TextColor3 = theme.textFaded, TextSize = theme.textSize, - TextXAlignment = Enum.TextXAlignment.Left, - TextYAlignment = Enum.TextYAlignment.Top, }), }), }), @@ -64,33 +77,3 @@ local function Topbar(props: Props) end return Topbar - --- local function Topbar(props: Props) --- local theme = useTheme() --- local navigation = NavigationContext.use() - --- return React.createElement("Frame", { --- Size = UDim2.fromScale(1, 1), --- BackgroundColor3 = theme.sidebar, --- BorderSizePixel = 0, --- }, { --- LayoutOrder = React.createElement("UIListLayout", { --- SortOrder = Enum.SortOrder.LayoutOrder, --- HorizontalAlignment = Enum.HorizontalAlignment.Right, --- }), - --- Navbar = React.createElement("Frame", {}, { --- LayoutOrder = React.createElement("UIListLayout", { --- SortOrder = Enum.SortOrder.LayoutOrder, --- HorizontalAlignment = Enum.HorizontalAlignment.Right, --- }), - --- Settings = React.createElement(Button, { --- layoutOrder = nextLayoutOrder(), --- text = "Settings", --- }), --- }), --- }) --- end - --- return Topbar diff --git a/src/Testing/MockPlugin.luau b/src/Testing/MockPlugin.luau index 46ccd9e6..cd6752c4 100644 --- a/src/Testing/MockPlugin.luau +++ b/src/Testing/MockPlugin.luau @@ -1,12 +1,12 @@ local MockPlugin = {} MockPlugin.__index = MockPlugin -function MockPlugin.new() +function MockPlugin.new(): Plugin local self = { _settings = {}, } - return setmetatable(self, MockPlugin) + return setmetatable(self, MockPlugin) :: any end function MockPlugin:GetSetting(settingName: string) diff --git a/src/assets.luau b/src/assets.luau index 69f1667c..d00fe303 100644 --- a/src/assets.luau +++ b/src/assets.luau @@ -1,48 +1,53 @@ -- This file was @generated by Tarmac. It is not intended for manual editing. return { ChevronRight = { - Image = "rbxassetid://10175334645", - ImageRectOffset = Vector2.new(65, 49), + Image = "rbxassetid://18940815650", + ImageRectOffset = Vector2.new(49, 226), ImageRectSize = Vector2.new(32, 32), }, Component = { - Image = "rbxassetid://10175334645", - ImageRectOffset = Vector2.new(0, 114), + Image = "rbxassetid://18940815650", + ImageRectOffset = Vector2.new(0, 275), ImageRectSize = Vector2.new(32, 32), }, Folder = { - Image = "rbxassetid://10175334645", - ImageRectOffset = Vector2.new(157, 0), + Image = "rbxassetid://18940815650", + ImageRectOffset = Vector2.new(345, 0), ImageRectSize = Vector2.new(32, 32), }, + GitHubMark = { + Image = "rbxassetid://18940815650", + ImageRectOffset = Vector2.new(0, 0), + ImageRectSize = Vector2.new(230, 225), + }, IconLight = { - Image = "rbxassetid://10175334645", - ImageRectOffset = Vector2.new(114, 0), + Image = "rbxassetid://18940815650", + ImageRectOffset = Vector2.new(231, 65), ImageRectSize = Vector2.new(42, 42), }, Magnify = { - Image = "rbxassetid://10175334645", - ImageRectOffset = Vector2.new(65, 0), + Image = "rbxassetid://18940815650", + ImageRectOffset = Vector2.new(0, 226), ImageRectSize = Vector2.new(48, 48), }, Minify = { - Image = "rbxassetid://10175334645", - ImageRectOffset = Vector2.new(0, 65), + Image = "rbxassetid://18940815650", + ImageRectOffset = Vector2.new(296, 0), ImageRectSize = Vector2.new(48, 48), }, Search = { - Image = "rbxassetid://10175334645", - ImageRectOffset = Vector2.new(114, 43), + Image = "rbxassetid://18940815650", + ImageRectOffset = Vector2.new(296, 49), ImageRectSize = Vector2.new(32, 32), }, Storybook = { - Image = "rbxassetid://10175334645", - ImageRectOffset = Vector2.new(65, 82), + Image = "rbxassetid://18940815650", + ImageRectOffset = Vector2.new(231, 108), ImageRectSize = Vector2.new(32, 32), }, flipbook = { - Image = "rbxassetid://10175334645", - ImageRectOffset = Vector2.new(0, 0), + Image = "rbxassetid://18940815650", + ImageRectOffset = Vector2.new(231, 0), ImageRectSize = Vector2.new(64, 64), }, } diff --git a/src/themes.luau b/src/themes.luau index da8314c0..a2b57a8d 100644 --- a/src/themes.luau +++ b/src/themes.luau @@ -23,6 +23,8 @@ export type Theme = { directory: Color3, alert: Color3, + github: Color3, + padding: UDim, paddingSmall: UDim, paddingLarge: UDim, @@ -30,64 +32,71 @@ export type Theme = { corner: UDim, } +local Light: Theme = { + textSize = 18, + font = Enum.Font.BuilderSansMedium, + headerTextSize = 32, + headerFont = Enum.Font.BuilderSansExtraBold, + buttonTextSize = 14, + buttonFont = Enum.Font.BuilderSansBold, + + background = tailwind.white, + sidebar = tailwind.gray100, + canvas = tailwind.white, + scrollbar = tailwind.gray800, + button = tailwind.gray800, + buttonText = tailwind.white, + divider = tailwind.gray300, + text = tailwind.gray800, + textFaded = tailwind.gray600, + textSubtitle = tailwind.gray500, + selection = tailwind.purple500, + story = tailwind.green500, + directory = tailwind.purple500, + alert = tailwind.rose500, + + github = Color3.fromHex("#333333"), + + padding = UDim.new(0, 12), + paddingSmall = UDim.new(0, 6), + paddingLarge = UDim.new(0, 24), + + corner = UDim.new(0, 4), +} + +local Dark: Theme = { + textSize = 18, + font = Enum.Font.BuilderSansMedium, + headerTextSize = 32, + headerFont = Enum.Font.BuilderSansExtraBold, + buttonTextSize = 14, + buttonFont = Enum.Font.BuilderSansBold, + + background = tailwind.zinc800, + sidebar = tailwind.zinc900, + canvas = tailwind.zinc800, + scrollbar = tailwind.zinc100, + button = tailwind.zinc300, + buttonText = tailwind.zinc800, + divider = tailwind.zinc700, + text = tailwind.zinc200, + textFaded = tailwind.zinc300, + textSubtitle = tailwind.zinc400, + selection = tailwind.purple500, + story = tailwind.green500, + directory = tailwind.purple500, + alert = tailwind.rose500, + + github = Color3.fromHex("#ffffff"), + + padding = UDim.new(0, 12), + paddingSmall = UDim.new(0, 6), + paddingLarge = UDim.new(0, 24), + + corner = UDim.new(0, 6), +} + return { - Light = { - textSize = 18, - font = Enum.Font.BuilderSansMedium, - headerTextSize = 32, - headerFont = Enum.Font.BuilderSansExtraBold, - buttonTextSize = 14, - buttonFont = Enum.Font.BuilderSansBold, - - background = tailwind.white, - sidebar = tailwind.gray100, - canvas = tailwind.white, - scrollbar = tailwind.gray800, - button = tailwind.gray800, - buttonText = tailwind.white, - divider = tailwind.gray300, - text = tailwind.gray800, - textFaded = tailwind.gray600, - textSubtitle = tailwind.gray500, - selection = tailwind.purple500, - story = tailwind.green500, - directory = tailwind.purple500, - alert = tailwind.rose500, - - padding = UDim.new(0, 12), - paddingSmall = UDim.new(0, 6), - paddingLarge = UDim.new(0, 24), - - corner = UDim.new(0, 4), - } :: Theme, - - Dark = { - textSize = 18, - font = Enum.Font.BuilderSansMedium, - headerTextSize = 32, - headerFont = Enum.Font.BuilderSansExtraBold, - buttonTextSize = 14, - buttonFont = Enum.Font.BuilderSansBold, - - background = tailwind.zinc800, - sidebar = tailwind.zinc900, - canvas = tailwind.zinc800, - scrollbar = tailwind.zinc100, - button = tailwind.zinc300, - buttonText = tailwind.zinc800, - divider = tailwind.zinc700, - text = tailwind.zinc200, - textFaded = tailwind.zinc300, - textSubtitle = tailwind.zinc400, - selection = tailwind.purple500, - story = tailwind.green500, - directory = tailwind.purple500, - alert = tailwind.rose500, - - padding = UDim.new(0, 12), - paddingSmall = UDim.new(0, 6), - paddingLarge = UDim.new(0, 24), - - corner = UDim.new(0, 6), - } :: Theme, + Light = Light, + Dark = Dark, } diff --git a/tarmac-manifest.toml b/tarmac-manifest.toml index 0fb1e0df..9dd6cda0 100644 --- a/tarmac-manifest.toml +++ b/tarmac-manifest.toml @@ -1,53 +1,59 @@ [inputs."img/ChevronRight.png"] hash = "4c91853652b4e0a6317804c54db6397ac0c710c819b282f7be2179e3abdc879c" -id = 10175334645 -slice = [[65, 49], [97, 81]] +id = 18940815650 +slice = [[49, 226], [81, 258]] packable = true [inputs."img/Component.png"] hash = "d51b19d5f35ac87fd8156500828f3e217b6fc420b70fee5d97b3577e4e6a8843" -id = 10175334645 -slice = [[0, 114], [32, 146]] +id = 18940815650 +slice = [[0, 275], [32, 307]] packable = true [inputs."img/Folder.png"] hash = "4c0e80a363d36a4d127d410795cb13eec74d55871b70174636c0c962c242835c" -id = 10175334645 -slice = [[157, 0], [189, 32]] +id = 18940815650 +slice = [[345, 0], [377, 32]] +packable = true + +[inputs."img/GitHubMark.png"] +hash = "a0fb973cd9bf0c1123dbe783f7a4093dbcf9b4910d6e385c16a837a04a02306c" +id = 18940815650 +slice = [[0, 0], [230, 225]] packable = true [inputs."img/IconLight.png"] hash = "237c66813c2b5a58ae9c46166f72db8698fbb046922e2473ebaca8f7d7376ce1" -id = 10175334645 -slice = [[114, 0], [156, 42]] +id = 18940815650 +slice = [[231, 65], [273, 107]] packable = true [inputs."img/Magnify.png"] hash = "351db4b6132a9e02d01bc4cdc3c850099dce358673daaa0da66745b675ad38b1" -id = 10175334645 -slice = [[65, 0], [113, 48]] +id = 18940815650 +slice = [[0, 226], [48, 274]] packable = true [inputs."img/Minify.png"] hash = "694db8ce141475fc0f42980d2a6dfd058dfa155348a579fab29ca6dd5684ec14" -id = 10175334645 -slice = [[0, 65], [48, 113]] +id = 18940815650 +slice = [[296, 0], [344, 48]] packable = true [inputs."img/Search.png"] hash = "0ab89c0309f577f3ccae0e03b45292f6e2bca51cc9b8250f56ad756e04231c57" -id = 10175334645 -slice = [[114, 43], [146, 75]] +id = 18940815650 +slice = [[296, 49], [328, 81]] packable = true [inputs."img/Storybook.png"] hash = "2a4b8bd513a8fa7eebae482943ab3435a659359bd94b24eff1c3dcc6f1244799" -id = 10175334645 -slice = [[65, 82], [97, 114]] +id = 18940815650 +slice = [[231, 108], [263, 140]] packable = true [inputs."img/flipbook.png"] hash = "423e3dcff54fce8824f14d7cadcfb90eef2abecd817df2bee13951bd2674f79a" -id = 10175334645 -slice = [[0, 0], [64, 64]] +id = 18940815650 +slice = [[231, 0], [295, 64]] packable = true From a4cccd3e62fd17d30f135d5a3be7ef88d0144cce Mon Sep 17 00:00:00 2001 From: UltimateQuack Date: Sat, 9 Nov 2024 21:12:48 -0300 Subject: [PATCH 5/6] More informations to README (#271) Such as more descriptions about flipbook, on the `README.md`, very simple but I think it's better now. # Problem None. # Solution None. # Checklist - [ ] Ran `lune run test` locally before merging --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 14d768e9..dcd4dd90 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ You can install flipbook from the [Roblox marketplace](https://www.roblox.com/li ## Documentation -Learn how to use flipbook on the [documentation site](https://flipbook-labs.github.io/flipbook/). +Learn how to use flipbook on the [official documentation site](https://flipbook-labs.github.io/flipbook/). ## Contributing @@ -26,4 +26,4 @@ Before opening a pull request, check out our [contributing guide](https://flipbo ## License -[MIT License](LICENSE) +flipbook is licensed under [MIT License](LICENSE). From e85d110d88f646b176be93087b65eec66aaef6aa Mon Sep 17 00:00:00 2001 From: vocksel Date: Sun, 10 Nov 2024 06:51:18 -0800 Subject: [PATCH 6/6] Use Storyteller for handling all of our Story/Storybook needs (#267) # Problem Our org has a new package for handling all Story and Storybook logic we previously had baked into flipbook in the form of Storyteller. https://github.com/flipbook-labs/storyteller # Solution This PR adds Storyteller to our dependencies and starts consuming it. All Story and Storybook related logic has been stripped out and migrated over As a bonus, Storyteller gives us Fusion support out of the box. Closes #225 # Checklist - [ ] Ran `lune run test` locally before merging --- .vscode/settings.json | 3 - src/Common/useDescendants.luau | 77 -------------- src/Common/useDescendants.spec.luau | 108 -------------------- src/Explorer/types.luau | 4 +- src/Navigation/Screen.luau | 8 +- src/Panels/Sidebar.luau | 7 +- src/Plugin/PluginApp.luau | 5 +- src/Storybook/StoryCanvas.luau | 9 +- src/Storybook/StoryControls.luau | 9 +- src/Storybook/StoryMeta.luau | 9 +- src/Storybook/StoryMeta.story.luau | 4 + src/Storybook/StoryPreview.luau | 54 +++++++--- src/Storybook/StoryView.luau | 52 ++++++---- src/Storybook/createStoryNodes.luau | 10 +- src/Storybook/createStoryNodes.spec.luau | 5 +- src/Storybook/isStoryModule.luau | 10 -- src/Storybook/isStoryModule.spec.luau | 25 ----- src/Storybook/isStorybookModule.luau | 11 -- src/Storybook/isStorybookModule.spec.luau | 45 --------- src/Storybook/loadStoryModule.luau | 67 ------------- src/Storybook/mountStory.luau | 66 ------------ src/Storybook/types.luau | 117 ---------------------- src/Storybook/useStory.luau | 43 -------- src/Storybook/useStorybooks.luau | 65 ------------ src/init.storybook.luau | 6 +- src/stories.spec.luau | 87 ++++++++++------ wally.toml | 3 +- 27 files changed, 175 insertions(+), 734 deletions(-) delete mode 100644 src/Common/useDescendants.luau delete mode 100644 src/Common/useDescendants.spec.luau delete mode 100644 src/Storybook/isStoryModule.luau delete mode 100644 src/Storybook/isStoryModule.spec.luau delete mode 100644 src/Storybook/isStorybookModule.luau delete mode 100644 src/Storybook/isStorybookModule.spec.luau delete mode 100644 src/Storybook/loadStoryModule.luau delete mode 100644 src/Storybook/mountStory.luau delete mode 100644 src/Storybook/types.luau delete mode 100644 src/Storybook/useStory.luau delete mode 100644 src/Storybook/useStorybooks.luau diff --git a/.vscode/settings.json b/.vscode/settings.json index c82dc676..3d62bfd0 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -9,8 +9,5 @@ "@pkg": "./Packages", "@root": "./src", "@lune/": "~/.lune/.typedefs/0.8.3/" - }, - "files.associations": { - "*.luau": "lua" } } \ No newline at end of file diff --git a/src/Common/useDescendants.luau b/src/Common/useDescendants.luau deleted file mode 100644 index 60d5458d..00000000 --- a/src/Common/useDescendants.luau +++ /dev/null @@ -1,77 +0,0 @@ -local React = require("@pkg/React") -local Sift = require("@pkg/Sift") - -local function hasPermission(instance: Instance) - local success = pcall(function() - return instance.Name - end) - return success -end - -local function useDescendants(parent: Instance, predicate: (descendant: Instance) -> boolean): { Instance } - local descendants: { Instance }, setDescendants = React.useState({}) - - local onDescendantChanged = React.useCallback(function(descendant: Instance) - setDescendants(function(prev) - local exists = table.find(prev, descendant) - - if not hasPermission(descendant) then - return prev - end - - if predicate(descendant) then - if exists then - -- Force a re-render. Nothing about the state changed, but the - -- module uses a new name now - return table.clone(prev) - else - return Sift.Array.push(prev, descendant) - end - else - if exists then - return Sift.Array.filter(prev, function(other: Instance) - return descendant ~= other - end) - end - end - - return prev - end) - end, { predicate, descendants } :: { unknown }) - - -- Setup the initial list of descendants for the current parent - React.useEffect(function() - setDescendants(Sift.Array.filter(parent:GetDescendants(), predicate)) - end, { parent }) - - React.useEffect(function() - local connections = { - parent.DescendantAdded:Connect(onDescendantChanged), - parent.DescendantRemoving:Connect(onDescendantChanged), - } - - -- Listen for name changes and update the list of descendants - for _, descendant in parent:GetDescendants() do - if not hasPermission(descendant) then - continue - end - - table.insert( - connections, - descendant:GetPropertyChangedSignal("Name"):Connect(function() - onDescendantChanged(descendant) - end) - ) - end - - return function() - for _, conn in connections do - conn:Disconnect() - end - end - end, { parent, onDescendantChanged } :: { unknown }) - - return descendants -end - -return useDescendants diff --git a/src/Common/useDescendants.spec.luau b/src/Common/useDescendants.spec.luau deleted file mode 100644 index 336a8aa2..00000000 --- a/src/Common/useDescendants.spec.luau +++ /dev/null @@ -1,108 +0,0 @@ -local JestGlobals = require("@pkg/JestGlobals") -local React = require("@pkg/React") -local ReactRoblox = require("@pkg/ReactRoblox") -local newFolder = require("@root/Testing/newFolder") -local useDescendants = require("./useDescendants") - -local afterEach = JestGlobals.afterEach -local expect = JestGlobals.expect -local test = JestGlobals.test - -local container = Instance.new("ScreenGui") -local root = ReactRoblox.createRoot(container) - -afterEach(function() - ReactRoblox.act(function() - root:unmount() - end) -end) - -test("return an initial list of descendants that match the predicate", function() - local tree = newFolder({ - Match = Instance.new("Part"), - Foo = Instance.new("Part"), - }) - - local descendants - local function HookTester() - descendants = useDescendants(tree, function(descendant) - return descendant.Name == "Match" - end) - - return nil - end - - ReactRoblox.act(function() - root:render(React.createElement(HookTester)) - end) - - expect(descendants).toBeDefined() - expect(#descendants).toBe(1) - expect(descendants[1]).toBe(tree:FindFirstChild("Match")) -end) - -test("respond to changes in descendants that match the predicate", function() - local tree = newFolder({ - Match = Instance.new("Part"), - Foo = Instance.new("Part"), - }) - - local descendants - local function HookTester() - descendants = useDescendants(tree, function(descendant) - return descendant.Name == "Match" - end) - - return nil - end - - ReactRoblox.act(function() - root:render(React.createElement(HookTester)) - end) - - expect(descendants).toBeDefined() - expect(#descendants).toBe(1) - - local folder = newFolder({ - Match = Instance.new("Part"), - }) - - ReactRoblox.act(function() - folder.Parent = tree - end) - - expect(#descendants).toBe(2) -end) - -test("force an update when a matching descendant's name changes", function() - local descendants - - local tree = newFolder({ - Match = Instance.new("Part"), - }) - - local function HookTester() - descendants = useDescendants(tree, function(descendant) - return descendant:IsA("Part") - end) - - return nil - end - - ReactRoblox.act(function() - root:render(React.createElement(HookTester)) - end) - - expect(descendants).toBeDefined() - expect(#descendants).toBe(1) - - local prev = descendants - local match = tree:FindFirstChild("Match") :: Instance - - ReactRoblox.act(function() - match.Name = "Changed" - end) - - expect(descendants).never.toBe(prev) - expect(descendants[1]).toBe(match) -end) diff --git a/src/Explorer/types.luau b/src/Explorer/types.luau index 68b9535d..9b49b05b 100644 --- a/src/Explorer/types.luau +++ b/src/Explorer/types.luau @@ -1,6 +1,6 @@ -local storybookTypes = require("@root/Storybook/types") +local Storyteller = require("@pkg/Storyteller") -type Storybook = storybookTypes.Storybook +type Storybook = Storyteller.Storybook export type ComponentTreeNode = { name: string, diff --git a/src/Navigation/Screen.luau b/src/Navigation/Screen.luau index 90cef980..7dd39506 100644 --- a/src/Navigation/Screen.luau +++ b/src/Navigation/Screen.luau @@ -1,19 +1,19 @@ local ModuleLoader = require("@pkg/ModuleLoader") local React = require("@pkg/React") +local Storyteller = require("@pkg/Storyteller") local AboutView = require("@root/About/AboutView") local NavigationContext = require("@root/Navigation/NavigationContext") local SettingsView = require("@root/UserSettings/SettingsView") local StoryCanvas = require("@root/Storybook/StoryCanvas") -local storybookTypes = require("@root/Storybook/types") local useMemo = React.useMemo -type Story = storybookTypes.Story -type Storybook = storybookTypes.Storybook +type ModuleLoader = ModuleLoader.ModuleLoader +type Storybook = Storyteller.Storybook export type Props = { - loader: ModuleLoader.ModuleLoader, + loader: ModuleLoader, story: ModuleScript?, storybook: Storybook?, } diff --git a/src/Panels/Sidebar.luau b/src/Panels/Sidebar.luau index f805b6be..6e8930d9 100644 --- a/src/Panels/Sidebar.luau +++ b/src/Panels/Sidebar.luau @@ -1,15 +1,16 @@ +local React = require("@pkg/React") +local Storyteller = require("@pkg/Storyteller") + local Branding = require("@root/Common/Branding") local ComponentTree = require("@root/Explorer") -local React = require("@pkg/React") local ScrollingFrame = require("@root/Common/ScrollingFrame") local Searchbar = require("@root/Forms/Searchbar") local constants = require("@root/constants") local createStoryNodes = require("@root/Storybook/createStoryNodes") local explorerTypes = require("@root/Explorer/types") -local storybookTypes = require("@root/Storybook/types") local useTheme = require("@root/Common/useTheme") -type Storybook = storybookTypes.Storybook +type Storybook = Storyteller.Storybook type ComponentTreeNode = explorerTypes.ComponentTreeNode local e = React.createElement diff --git a/src/Plugin/PluginApp.luau b/src/Plugin/PluginApp.luau index aa345cba..7081df03 100644 --- a/src/Plugin/PluginApp.luau +++ b/src/Plugin/PluginApp.luau @@ -1,5 +1,6 @@ local ModuleLoader = require("@pkg/ModuleLoader") local React = require("@pkg/React") +local Storyteller = require("@pkg/Storyteller") local NavigationContext = require("@root/Navigation/NavigationContext") local ResizablePanel = require("@root/Panels/ResizablePanel") @@ -9,7 +10,6 @@ local Sidebar = require("@root/Panels/Sidebar") local Topbar = require("@root/Panels/Topbar") local constants = require("@root/constants") local nextLayoutOrder = require("@root/Common/nextLayoutOrder") -local useStorybooks = require("@root/Storybook/useStorybooks") local useTheme = require("@root/Common/useTheme") local TOPBAR_HEIGHT_PX = 32 @@ -21,7 +21,7 @@ export type Props = { local function App(props: Props) local theme = useTheme() local settingsContext = SettingsContext.use() - local storybooks = useStorybooks(game, props.loader) + local storybooks = Storyteller.useStorybooks(game, props.loader) local story: ModuleScript?, setStory = React.useState(nil :: ModuleScript?) local storybook, selectStorybook = React.useState(nil :: ModuleScript?) local initialSidebarWidth = settingsContext.getSetting("sidebarWidth") @@ -71,6 +71,7 @@ local function App(props: Props) MainWrapper = React.createElement("Frame", { LayoutOrder = nextLayoutOrder(), Size = UDim2.fromScale(1, 1) - UDim2.fromOffset(sidebarWidth, 0), + BackgroundTransparency = 1, }, { Layout = React.createElement("UIListLayout", { SortOrder = Enum.SortOrder.LayoutOrder, diff --git a/src/Storybook/StoryCanvas.luau b/src/Storybook/StoryCanvas.luau index 06c5cf03..389ccdbe 100644 --- a/src/Storybook/StoryCanvas.luau +++ b/src/Storybook/StoryCanvas.luau @@ -1,17 +1,20 @@ local ModuleLoader = require("@pkg/ModuleLoader") local React = require("@pkg/React") +local Storyteller = require("@pkg/Storyteller") local NoStorySelected = require("@root/Storybook/NoStorySelected") local StoryView = require("@root/Storybook/StoryView") -local types = require("@root/Storybook/types") local useTheme = require("@root/Common/useTheme") local e = React.createElement +type ModuleLoader = ModuleLoader.ModuleLoader +type Storybook = Storyteller.Storybook + type Props = { story: ModuleScript, - loader: ModuleLoader.ModuleLoader, - storybook: types.Storybook, + loader: ModuleLoader, + storybook: Storybook, layoutOrder: number?, } diff --git a/src/Storybook/StoryControls.luau b/src/Storybook/StoryControls.luau index e689f821..213c0912 100644 --- a/src/Storybook/StoryControls.luau +++ b/src/Storybook/StoryControls.luau @@ -111,7 +111,7 @@ local function StoryControls(props: Props) TextTruncate = Enum.TextTruncate.AtEnd, }), - Option = e("Frame", { + OptionWrapper = e("Frame", { LayoutOrder = 2, BackgroundTransparency = 1, Size = UDim2.fromScale(1, 0), @@ -120,7 +120,12 @@ local function StoryControls(props: Props) Flex = e("UIFlexItem", { FlexMode = Enum.UIFlexMode.Shrink, }), - }, option), + }, { + -- Keying by the identity of sortedControls fixes a bug where + -- the options visually do not update when two stories have the + -- exact same controls + [`Option_{sortedControls}`] = option, + }), }) end diff --git a/src/Storybook/StoryMeta.luau b/src/Storybook/StoryMeta.luau index 42207a0e..ca611ce0 100644 --- a/src/Storybook/StoryMeta.luau +++ b/src/Storybook/StoryMeta.luau @@ -1,13 +1,16 @@ local React = require("@pkg/React") -local types = require("@root/Storybook/types") +local Storyteller = require("@pkg/Storyteller") + local useTheme = require("@root/Common/useTheme") local MAX_SUMMARY_SIZE = 600 local e = React.createElement +type Story = Storyteller.Story + export type Props = { - story: types.Story, + story: Story, layoutOrder: number?, } @@ -31,7 +34,7 @@ local function StoryMeta(props: Props) BackgroundTransparency = 1, Font = theme.headerFont, Size = UDim2.fromScale(0, 0), - Text = props.story.name:sub(1, #props.story.name - 6), + Text = props.story.name, TextColor3 = theme.text, TextSize = theme.headerTextSize, }), diff --git a/src/Storybook/StoryMeta.story.luau b/src/Storybook/StoryMeta.story.luau index cbd5566c..54d28fa3 100644 --- a/src/Storybook/StoryMeta.story.luau +++ b/src/Storybook/StoryMeta.story.luau @@ -12,6 +12,10 @@ return { story = { name = "Story", summary = "Story summary", + source = Instance.new("ModuleScript"), + storybook = { + storyRoots = {}, + }, }, }), }), diff --git a/src/Storybook/StoryPreview.luau b/src/Storybook/StoryPreview.luau index 4f7ffdb6..56f7e282 100644 --- a/src/Storybook/StoryPreview.luau +++ b/src/Storybook/StoryPreview.luau @@ -2,11 +2,12 @@ local CoreGui = game:GetService("CoreGui") local React = require("@pkg/React") local ReactRoblox = require("@pkg/ReactRoblox") -local ScrollingFrame = require("@root/Common/ScrollingFrame") local Sift = require("@pkg/Sift") +local Storyteller = require("@pkg/Storyteller") + +local ScrollingFrame = require("@root/Common/ScrollingFrame") local StoryError = require("@root/Storybook/StoryError") -local mountStory = require("@root/Storybook/mountStory") -local types = require("@root/Storybook/types") +local usePrevious = require("@root/Common/usePrevious") local e = React.createElement @@ -15,42 +16,65 @@ local defaultProps = { zoom = 0, } +type Story = Storyteller.Story + export type Props = { - story: types.Story, + story: Story, controls: { [string]: any }, - storyModule: ModuleScript, - ref: any, + isMountedInViewport: boolean?, layoutOrder: number?, + zoom: number?, + ref: any, } type InternalProps = Props & typeof(defaultProps) local StoryPreview = React.forwardRef(function(providedProps: Props, ref: any) local props: InternalProps = Sift.Dictionary.merge(defaultProps, providedProps) - - local err, setErr = React.useState(nil) + local lifecycle = React.useRef(nil :: Storyteller.RenderLifecycle?) + local err, setErr = React.useState(nil :: string?) + local prevControls = usePrevious(props.controls) + local prevStory = usePrevious(props.story) React.useEffect(function() setErr(nil) end, { props.story, ref }) React.useEffect(function() + if props.story == prevStory and props.controls ~= prevControls then + local areControlsDifferent = prevControls and not Sift.Dictionary.equals(props.controls, prevControls) + + if lifecycle.current and areControlsDifferent then + local success, result = xpcall(function() + lifecycle.current.update(props.controls) + end, debug.traceback) + + if not success then + setErr(result) + end + end + end + end, { props.controls, prevControls, props.story, prevStory } :: { unknown }) + + React.useEffect(function(): (() -> ())? if props.story and ref.current then local success, result = xpcall(function() - return mountStory(props.story, props.controls, ref.current) + lifecycle.current = Storyteller.render(ref.current, props.story) end, debug.traceback) - if success then - return result - else + if not success then setErr(result) - return nil end end - return nil - end, { props.story, props.controls, props.isMountedInViewport, ref.current } :: { unknown }) + return function() + if lifecycle.current then + lifecycle.current.unmount() + lifecycle.current = nil + end + end + end, { props.story, props.isMountedInViewport } :: { unknown }) if err then return e(StoryError, { diff --git a/src/Storybook/StoryView.luau b/src/Storybook/StoryView.luau index 3359a9c8..e24ea64d 100644 --- a/src/Storybook/StoryView.luau +++ b/src/Storybook/StoryView.luau @@ -3,6 +3,7 @@ local Selection = game:GetService("Selection") local ModuleLoader = require("@pkg/ModuleLoader") local React = require("@pkg/React") local Sift = require("@pkg/Sift") +local Storyteller = require("@pkg/Storyteller") local PluginContext = require("@root/Plugin/PluginContext") local ResizablePanel = require("@root/Panels/ResizablePanel") @@ -14,49 +15,56 @@ local StoryMeta = require("@root/Storybook/StoryMeta") local StoryPreview = require("@root/Storybook/StoryPreview") local StoryViewNavbar = require("@root/Storybook/StoryViewNavbar") local constants = require("@root/constants") -local types = require("@root/Storybook/types") -local useStory = require("@root/Storybook/useStory") local useTheme = require("@root/Common/useTheme") local useZoom = require("@root/Common/useZoom") local e = React.createElement +type ModuleLoader = ModuleLoader.ModuleLoader +type Storybook = Storyteller.Storybook + type Props = { - loader: ModuleLoader.ModuleLoader, + loader: ModuleLoader, story: ModuleScript, - storybook: types.Storybook, + storybook: Storybook, } local function StoryView(props: Props) local theme = useTheme() local settingsContext = SettingsContext.use() - local story, storyErr = useStory(props.story, props.storybook, props.loader) + local story, storyErr = Storyteller.useStory(props.story, props.storybook, props.loader) local zoom = useZoom(props.story) local plugin = React.useContext(PluginContext.Context) - local extraControls, setExtraControls = React.useState({}) + local changedControls, setChangedControls = React.useState({}) local initialControlsHeight = settingsContext.getSetting("controlsHeight") local controlsHeight, setControlsHeight = React.useState(initialControlsHeight) local topbarHeight, setTopbarHeight = React.useState(0) local storyParentRef = React.useRef(nil :: GuiObject?) - local controls - - if story and story.controls then - controls = {} - for key, value in story.controls do - local override = extraControls[key] - - if override ~= nil and typeof(value) ~= "table" then - controls[key] = override - else - controls[key] = value + React.useEffect(function() + setChangedControls({}) + end, { story }) + + local controlsWithUserOverrides = React.useMemo(function() + local controls = {} + if story and story.controls then + for key, value in story.controls do + local override = changedControls[key] + + if override ~= nil and typeof(value) ~= "table" then + controls[key] = override + else + controls[key] = value + end end end - end + return controls + end, { story, changedControls } :: { unknown }) + + local showControls = controlsWithUserOverrides and not Sift.isEmpty(controlsWithUserOverrides) - local showControls = controls and not Sift.isEmpty(controls) local setControl = React.useCallback(function(control: string, newValue: any) - setExtraControls(function(prev) + setChangedControls(function(prev) return Sift.Dictionary.merge(prev, { [control] = newValue, }) @@ -164,7 +172,7 @@ local function StoryView(props: Props) StoryPreview = e(StoryPreview, { zoom = zoom.value, story = story, - controls = Sift.Dictionary.merge(controls, extraControls), + controls = Sift.Dictionary.merge(controlsWithUserOverrides, changedControls), storyModule = props.story, isMountedInViewport = isMountedInViewport, ref = storyParentRef, @@ -187,7 +195,7 @@ local function StoryView(props: Props) BackgroundColor3 = theme.sidebar, }, { StoryControls = e(StoryControls, { - controls = controls, + controls = controlsWithUserOverrides, setControl = setControl, }), }), diff --git a/src/Storybook/createStoryNodes.luau b/src/Storybook/createStoryNodes.luau index bf7b9ce8..a4741ccb 100644 --- a/src/Storybook/createStoryNodes.luau +++ b/src/Storybook/createStoryNodes.luau @@ -1,13 +1,13 @@ +local Storyteller = require("@pkg/Storyteller") + local explorerTypes = require("@root/Explorer/types") -local isStoryModule = require("@root/Storybook/isStoryModule") -local storybookTypes = require("@root/Storybook/types") -type Storybook = storybookTypes.Storybook +type Storybook = Storyteller.Storybook type ComponentTreeNode = explorerTypes.ComponentTreeNode local function hasStories(instance: Instance): boolean for _, descendant in ipairs(instance:GetDescendants()) do - if isStoryModule(descendant) then + if Storyteller.isStoryModule(descendant) then return true end end @@ -16,7 +16,7 @@ end local function createChildNodes(parent: ComponentTreeNode, instance: Instance, storybook: Storybook) for _, child in ipairs(instance:GetChildren()) do - local isStory = isStoryModule(child) + local isStory = Storyteller.isStoryModule(child) local isContainer = hasStories(child) if isStory or isContainer then diff --git a/src/Storybook/createStoryNodes.spec.luau b/src/Storybook/createStoryNodes.spec.luau index ec7f365d..fd6f6092 100644 --- a/src/Storybook/createStoryNodes.spec.luau +++ b/src/Storybook/createStoryNodes.spec.luau @@ -1,7 +1,8 @@ local JestGlobals = require("@pkg/JestGlobals") +local Storyteller = require("@pkg/Storyteller") + local createStoryNodes = require("./createStoryNodes") local newFolder = require("@root/Testing/newFolder") -local types = require("@root/Storybook/types") local expect = JestGlobals.expect local test = JestGlobals.test @@ -15,7 +16,7 @@ local mockStoryRoot = newFolder({ }), }) -local mockStorybook: types.Storybook = { +local mockStorybook: Storyteller.Storybook = { name = "MockStorybook", storyRoots = { mockStoryRoot }, } diff --git a/src/Storybook/isStoryModule.luau b/src/Storybook/isStoryModule.luau deleted file mode 100644 index 963821dc..00000000 --- a/src/Storybook/isStoryModule.luau +++ /dev/null @@ -1,10 +0,0 @@ -local constants = require("@root/constants") - -local function isStoryModule(instance: Instance) - if instance:IsA("ModuleScript") and instance.Name:match(constants.STORY_NAME_PATTERN) then - return true - end - return false -end - -return isStoryModule diff --git a/src/Storybook/isStoryModule.spec.luau b/src/Storybook/isStoryModule.spec.luau deleted file mode 100644 index 8ebc1b74..00000000 --- a/src/Storybook/isStoryModule.spec.luau +++ /dev/null @@ -1,25 +0,0 @@ -local JestGlobals = require("@pkg/JestGlobals") -local isStoryModule = require("./isStoryModule") - -local expect = JestGlobals.expect -local test = JestGlobals.test - -test("return `true` for a ModuleScript with .story in the name", function() - local module = Instance.new("ModuleScript") - module.Name = "Foo.story" - - expect(isStoryModule(module)).toBe(true) -end) - -test("return `false` if the given instance is not a ModuleScript", function() - local folder = Instance.new("Folder") - folder.Name = "Folder.story" - - expect(isStoryModule(folder)).toBe(false) -end) - -test("return `false` if a ModuleScript does not have .story in the name", function() - local module = Instance.new("ModuleScript") - - expect(isStoryModule(module)).toBe(false) -end) diff --git a/src/Storybook/isStorybookModule.luau b/src/Storybook/isStorybookModule.luau deleted file mode 100644 index 974c2954..00000000 --- a/src/Storybook/isStorybookModule.luau +++ /dev/null @@ -1,11 +0,0 @@ -local CoreGui = game:GetService("CoreGui") - -local constants = require("@root/constants") - -local function isStorybookModule(instance: Instance): boolean - return instance:IsA("ModuleScript") - and instance.Name:match(constants.STORYBOOK_NAME_PATTERN) ~= nil - and not instance:IsDescendantOf(CoreGui) -end - -return isStorybookModule diff --git a/src/Storybook/isStorybookModule.spec.luau b/src/Storybook/isStorybookModule.spec.luau deleted file mode 100644 index 3a893cdf..00000000 --- a/src/Storybook/isStorybookModule.spec.luau +++ /dev/null @@ -1,45 +0,0 @@ -local CoreGui = game:GetService("CoreGui") - -local JestGlobals = require("@pkg/JestGlobals") -local isStorybookModule = require("./isStorybookModule") - -local expect = JestGlobals.expect -local test = JestGlobals.test - -test("return true for ModuleScripts with the .storybook extension", function() - local storybook = Instance.new("ModuleScript") - storybook.Name = "Foo.storybook" - - expect(isStorybookModule(storybook)).toBe(true) -end) - -test("return false for non-ModuleScript instances", function() - local storybook = Instance.new("Folder") - storybook.Name = "Foo.storybook" - - expect(isStorybookModule(storybook)).toBe(false) -end) - -test("return false if .storybook is not part of the name", function() - local storybook = Instance.new("ModuleScript") - storybook.Name = "Foo" - - expect(isStorybookModule(storybook)).toBe(false) -end) - -test("return false if .storybook is in the wrong place", function() - local storybook = Instance.new("ModuleScript") - storybook.Name = "Foo.storybook.extra" - - expect(isStorybookModule(storybook)).toBe(false) -end) - -test("return false for storybooks in CoreGui", function() - local storybook = Instance.new("ModuleScript") - storybook.Name = "Foo.storybook" - storybook.Parent = CoreGui - - expect(isStorybookModule(storybook)).toBe(false) - - storybook:Destroy() -end) diff --git a/src/Storybook/loadStoryModule.luau b/src/Storybook/loadStoryModule.luau deleted file mode 100644 index a7c3368a..00000000 --- a/src/Storybook/loadStoryModule.luau +++ /dev/null @@ -1,67 +0,0 @@ -local ModuleLoader = require("@pkg/ModuleLoader") -local Sift = require("@pkg/Sift") - -local types = require("@root/Storybook/types") - -local Errors = { - MalformedStory = "Story is malformed. Check the source of %q and make sure its properties are correct", - Generic = "Failed to load story %q. Error: %s", -} - -local function loadStoryModule( - loader: ModuleLoader.ModuleLoader, - module: ModuleScript, - storybook: types.Storybook -): (types.Story?, string?) - if not module then - return nil, "Did not receive a module to load" - end - - local success, result = pcall(function() - return loader:require(module) - end) - - if not success then - return nil, Errors.Generic:format(module:GetFullName(), tostring(result)) - end - - local story: types.Story - if typeof(result) == "function" then - story = { - name = module.Name, - story = result, - } - else - local isValid, message = types.StoryMeta(result) - - if isValid then - local extraProps = {} - if types.ReactStorybook(storybook) then - local reactStorybook = storybook :: types.ReactStorybook - extraProps = { - react = reactStorybook.react, - reactRoblox = reactStorybook.reactRoblox, - } - elseif types.RoactStorybook(storybook) then - local roactStorybook = storybook :: types.RoactStorybook - extraProps = { - roact = roactStorybook.roact, - } - end - - story = Sift.Dictionary.merge({ - name = module.Name, - }, extraProps, result) - else - return nil, Errors.Generic:format(module:GetFullName(), message) - end - end - - if story then - return story, nil - else - return nil, Errors.MalformedStory:format(module:GetFullName()) - end -end - -return loadStoryModule diff --git a/src/Storybook/mountStory.luau b/src/Storybook/mountStory.luau deleted file mode 100644 index 74935ca1..00000000 --- a/src/Storybook/mountStory.luau +++ /dev/null @@ -1,66 +0,0 @@ -local types = require("@root/Storybook/types") - -local function mountFunctionalStory(story: types.FunctionalStory, props: types.StoryProps, parent: GuiObject) - local cleanup = story.story(parent, props) - - return function() - if typeof(cleanup) == "function" then - cleanup() - end - end -end - -local function mountRoactStory(story: types.RoactStory, props: types.StoryProps, parent: GuiObject) - local Roact = story.roact - - local element - if typeof(story.story) == "function" then - element = Roact.createElement(story.story, props) - else - element = story.story - end - - local handle = Roact.mount(element, parent, story.name) - - return function() - Roact.unmount(handle) - end -end - -local function mountReactStory(story: types.ReactStory, props: types.StoryProps, parent: GuiObject) - local React = story.react - local ReactRoblox = story.reactRoblox - - local root = ReactRoblox.createRoot(parent) - - local element - if typeof(story.story) == "function" then - element = React.createElement(story.story, props) - else - element = story.story - end - - root:render(element) - - return function() - root:unmount() - end -end - -local function mountStory(story: types.Story, controls: types.Controls, parent: GuiObject): (() -> ())? - local props: types.StoryProps = { - controls = controls, - } - - if story.roact then - return mountRoactStory(story :: types.RoactStory, props, parent) - elseif story.react and story.reactRoblox then - return mountReactStory(story :: types.ReactStory, props, parent) - elseif typeof(story.story) == "function" then - return mountFunctionalStory(story :: types.FunctionalStory, props, parent) - else - return nil - end -end - -return mountStory diff --git a/src/Storybook/types.luau b/src/Storybook/types.luau deleted file mode 100644 index 201d7c43..00000000 --- a/src/Storybook/types.luau +++ /dev/null @@ -1,117 +0,0 @@ -local t = require("@pkg/t") - -local types = {} - -export type RoactElement = { [string]: any } -export type Roact = { - createElement: (...any) -> any, - mount: (...any) -> any, - unmount: (...any) -> (), -} -types.Roact = t.interface({ - createElement = t.callback, - mount = t.callback, - unmount = t.callback, -}) - -type ReactElement = { [string]: any } - -type React = { - createElement: (...any) -> any, -} -types.React = t.interface({ - createElement = t.callback, -}) - -type ReactRoblox = { - createRoot: (Instance) -> { - render: (any, any) -> (), - unmount: (any) -> (), - }, -} -types.ReactRoblox = t.interface({ - createRoot = t.callback, -}) - -export type Controls = { - [string]: string | number | boolean, -} -types.Controls = t.map(t.string, t.union(t.string, t.number, t.boolean, t.map(t.number, t.any))) - -export type StoryProps = { - controls: Controls, -} - -export type StorybookMeta = { - storyRoots: { Instance }, - name: string?, -} -types.Storybook = t.interface({ - storyRoots = t.array(t.Instance), - - name = t.optional(t.string), - roact = t.optional(types.Roact), - react = t.optional(types.React), - reactRoblox = t.optional(types.ReactRoblox), -}) - -export type RoactStorybook = StorybookMeta & { - roact: Roact, -} -types.RoactStorybook = t.union( - types.Storybook, - t.interface({ - roact = t.optional(types.Roact), - }) -) - -export type ReactStorybook = StorybookMeta & { - react: React, - reactRoblox: ReactRoblox, -} -types.ReactStorybook = t.union( - types.Storybook, - t.interface({ - react = t.optional(types.React), - reactRoblox = t.optional(types.ReactRoblox), - }) -) - -export type Storybook = RoactStorybook | ReactStorybook | StorybookMeta - -export type StoryMeta = { - name: string, - story: any, - summary: string?, - controls: Controls?, - roact: Roact?, - react: React?, - reactRoblox: ReactRoblox?, -} -types.StoryMeta = t.interface({ - name = t.optional(t.string), - summary = t.optional(t.string), - controls = t.optional(types.Controls), - roact = t.optional(types.Roact), - react = t.optional(types.React), - reactRoblox = t.optional(types.ReactRoblox), -}) - -export type RoactStory = StoryMeta & { - story: RoactElement | (props: StoryProps) -> RoactElement, - roact: Roact, -} - -export type ReactStory = StoryMeta & { - story: ReactElement | (props: StoryProps) -> ReactElement, - react: React, - reactRoblox: ReactRoblox, -} - -export type FunctionalStory = StoryMeta & { - story: (target: GuiObject, props: StoryProps) -> (() -> ())?, -} - -export type Story = FunctionalStory | RoactStory | ReactStory | StoryMeta - -return types diff --git a/src/Storybook/useStory.luau b/src/Storybook/useStory.luau deleted file mode 100644 index 577931b4..00000000 --- a/src/Storybook/useStory.luau +++ /dev/null @@ -1,43 +0,0 @@ -local ModuleLoader = require("@pkg/ModuleLoader") -local React = require("@pkg/React") - -local loadStoryModule = require("@root/Storybook/loadStoryModule") -local types = require("@root/Storybook/types") - -local function useStory( - module: ModuleScript, - storybook: types.Storybook, - loader: ModuleLoader.ModuleLoader -): (types.Story?, string?) - local state, setState = React.useState({} :: { - story: types.Story?, - err: string?, - }) - - local loadStory = React.useCallback(function() - local story, err = loadStoryModule(loader, module, storybook) - - setState({ - story = story, - err = err, - }) - end, { loader, module, storybook } :: { unknown }) - - React.useEffect(function() - local conn = loader.loadedModuleChanged:Connect(function(other) - if other == module then - loadStory() - end - end) - - loadStory() - - return function() - conn:Disconnect() - end - end, { module, loadStory, loader } :: { unknown }) - - return state.story, state.err -end - -return useStory diff --git a/src/Storybook/useStorybooks.luau b/src/Storybook/useStorybooks.luau deleted file mode 100644 index 98d7833f..00000000 --- a/src/Storybook/useStorybooks.luau +++ /dev/null @@ -1,65 +0,0 @@ -local ModuleLoader = require("@pkg/ModuleLoader") -local React = require("@pkg/React") - -local constants = require("@root/constants") -local isStorybookModule = require("@root/Storybook/isStorybookModule") -local types = require("@root/Storybook/types") -local useDescendants = require("@root/Common/useDescendants") - -local function hasPermission(instance: Instance) - local success = pcall(function() - return instance.Name - end) - return success -end - -local function useStorybooks(parent: Instance, loader: ModuleLoader.ModuleLoader) - local storybooks, set = React.useState({}) - local modules = useDescendants(game, function(descendant) - return hasPermission(descendant) and isStorybookModule(descendant) - end) - - local loadStorybooks = React.useCallback(function() - local newStorybooks = {} - - for _, module in modules do - local wasRequired, result = pcall(function() - return loader:require(module :: ModuleScript) - end) - - if wasRequired then - local success, message = types.Storybook(result) - - if success then - result.name = if result.name - then result.name - else module.Name:gsub(constants.STORYBOOK_NAME_PATTERN, "") - - table.insert(newStorybooks, result) - else - warn(("Failed to load storybook %s. Error: %s"):format(module:GetFullName(), message)) - end - end - end - - set(newStorybooks) - end, { set, parent, loader, modules } :: { unknown }) - - React.useEffect(function() - local conn = loader.loadedModuleChanged:Connect(function(other) - if types.Storybook(other) then - loadStorybooks() - end - end) - - loadStorybooks() - - return function() - conn:Disconnect() - end - end, { loadStorybooks, loader } :: { unknown }) - - return storybooks -end - -return useStorybooks diff --git a/src/init.storybook.luau b/src/init.storybook.luau index af5486e7..5210a7e9 100644 --- a/src/init.storybook.luau +++ b/src/init.storybook.luau @@ -6,6 +6,8 @@ return { storyRoots = { script.Parent, }, - react = React, - reactRoblox = ReactRoblox, + packages = { + React = React, + ReactRoblox = ReactRoblox, + }, } diff --git a/src/stories.spec.luau b/src/stories.spec.luau index 4e7fe90d..0d26cc17 100644 --- a/src/stories.spec.luau +++ b/src/stories.spec.luau @@ -1,41 +1,66 @@ local CoreGui = game:GetService("CoreGui") local JestGlobals = require("@pkg/JestGlobals") +local ModuleLoader = require("@pkg/ModuleLoader") local React = require("@pkg/React") local ReactRoblox = require("@pkg/ReactRoblox") -local isStoryModule = require("@root/Storybook/isStoryModule") -local mountStory = require("@root/Storybook/mountStory") +local Sift = require("@pkg/Sift") +local Storyteller = require("@pkg/Storyteller") +local afterEach = JestGlobals.afterEach +local beforeEach = JestGlobals.beforeEach +local describe = JestGlobals.describe +local describeEach = describe.each :: any local expect = JestGlobals.expect local test = JestGlobals.test -local storyModules: { ModuleScript } = {} -for _, descendant in ipairs(script.Parent:GetDescendants()) do - if isStoryModule(descendant) then - table.insert(storyModules, descendant) - end -end - -for _, storyModule in storyModules do - test(`mount/unmount {storyModule:GetFullName()}`, function() - local story = (require :: any)(storyModule) - if typeof(story) == "function" then - story = { - name = storyModule.Name, - story = story, - } - end - - story.react = React - story.reactRoblox = ReactRoblox - - local cleanup - expect(function() - cleanup = mountStory(story, story.controls, CoreGui) - end).never.toThrow() - - if cleanup then - expect(cleanup).never.toThrow() - end +local container + +beforeEach(function() + container = Instance.new("Folder") + container.Parent = CoreGui +end) + +afterEach(function() + container:Destroy() +end) + +describeEach({ + Storyteller.findStorybookModules(script.Parent), +})("%s", function(storybookModule) + -- FIXME: This is needed to get around a bug with React renders. I'm hoping + -- to keep this for now, but in the future this should really be a + -- ModuleLoader instance + local mockModuleLoader = ( + { + require = function(_self, path) + return (require :: any)(path) + end, + } :: any + ) :: ModuleLoader.ModuleLoader + + local storybook = Storyteller.loadStorybookModule(mockModuleLoader, storybookModule) + + describeEach({ + Storyteller.findStoryModulesForStorybook(storybook), + })("%s", function(storyModule) + test("basic mount/unmount lifecycle", function() + local story = Storyteller.loadStoryModule(mockModuleLoader, storyModule, storybook) + + if story.packages then + story.packages = Sift.Dictionary.join(story.packages, { + React = React, + ReactRoblox = ReactRoblox, + }) + end + + local lifecycle = Storyteller.render(container, story) + + expect(#container:GetChildren()).toBe(1) + + lifecycle.unmount() + + expect(#container:GetChildren()).toBe(0) + end) end) -end +end) diff --git a/wally.toml b/wally.toml index ed032e02..6e08dd51 100644 --- a/wally.toml +++ b/wally.toml @@ -8,10 +8,11 @@ exclude = ["*"] [dependencies] ModuleLoader = "flipbook-labs/module-loader@0.6.2" +Storyteller = "flipbook-labs/storyteller@0.4.2" React = "jsdotlua/react@17.0.2" ReactRoblox = "jsdotlua/react-roblox@17.0.2" ReactSpring = "chriscerie/react-spring@2.0.0" -Sift = "csqrl/sift@0.0.4" +Sift = "csqrl/sift@0.0.8" t = "osyrisrblx/t@3.0.0" # dev dependencies