From f2eb567a9f847a17f7d0615b35b8e42fc4e6bd3c Mon Sep 17 00:00:00 2001 From: Marin Minnerly <me@vocksel.com> Date: Sat, 26 Oct 2024 20:05:02 -0700 Subject: [PATCH 01/79] Switch to Storyteller for all story handling Buggy, but it's progress --- .vscode/settings.json | 3 - example/ReactCounter.story.luau | 4 + src/Plugin/PluginApp.luau | 4 +- src/Storybook/StoryMeta.story.luau | 4 + src/Storybook/StoryPreview.luau | 25 +++-- src/Storybook/StoryView.luau | 4 +- src/Storybook/createStoryNodes.luau | 7 +- 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 | 121 ++-------------------- src/Storybook/useStory.luau | 43 -------- src/Storybook/useStorybooks.luau | 65 ------------ src/init.storybook.luau | 6 +- src/stories.spec.luau | 61 ++++++----- wally.toml | 3 +- 19 files changed, 74 insertions(+), 500 deletions(-) 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/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/example/ReactCounter.story.luau b/example/ReactCounter.story.luau index 7fbe9dd7..ceace6fd 100644 --- a/example/ReactCounter.story.luau +++ b/example/ReactCounter.story.luau @@ -16,6 +16,10 @@ return { controls = controls, react = React, reactRoblox = ReactRoblox, + packages = { + React = React, + ReactRoblox = ReactRoblox, + }, story = function(props: Props) return React.createElement(ReactCounter, { increment = props.controls.increment, diff --git a/src/Plugin/PluginApp.luau b/src/Plugin/PluginApp.luau index aa345cba..c494317d 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") 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 34d9551f..3d196c19 100644 --- a/src/Storybook/StoryPreview.luau +++ b/src/Storybook/StoryPreview.luau @@ -2,10 +2,11 @@ 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 e = React.createElement @@ -20,6 +21,7 @@ export type Props = { story: types.Story, ref: any, controls: { [string]: any }, + changedControls: { [string]: any }, storyModule: ModuleScript, } @@ -34,20 +36,25 @@ local StoryPreview = React.forwardRef(function(providedProps: Props, ref: any) setErr(nil) end, { props.story, ref }) - React.useEffect(function() + React.useEffect(function(): (() -> ())? if props.story and ref.current then + local renderer = Storyteller.createRendererForStory(props.story) + local lifecycle + local success, result = xpcall(function() - return mountStory(props.story, props.controls, ref.current) + lifecycle = Storyteller.render(renderer, ref.current, props.story, props.controls) end, debug.traceback) - if success then - return result - else + if not success then setErr(result) - return nil end - end + if lifecycle then + return function() + lifecycle.unmount() + end + end + end return nil end, { props.story, props.controls, props.isMountedInViewport, ref.current } :: { unknown }) diff --git a/src/Storybook/StoryView.luau b/src/Storybook/StoryView.luau index 2e79c932..27280785 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") @@ -15,7 +16,6 @@ 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") @@ -30,7 +30,7 @@ type Props = { 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({}) diff --git a/src/Storybook/createStoryNodes.luau b/src/Storybook/createStoryNodes.luau index bf7b9ce8..c71d7cd6 100644 --- a/src/Storybook/createStoryNodes.luau +++ b/src/Storybook/createStoryNodes.luau @@ -1,5 +1,6 @@ +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 @@ -7,7 +8,7 @@ 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 +17,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/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 index 201d7c43..24080fc7 100644 --- a/src/Storybook/types.luau +++ b/src/Storybook/types.luau @@ -1,117 +1,8 @@ -local t = require("@pkg/t") +local Storyteller = require("@pkg/Storyteller") -local types = {} +export type Controls = Storyteller.StoryControls +export type StoryProps = Storyteller.StoryProps<unknown> +export type Storybook = Storyteller.Storybook +export type Story = Storyteller.Story<unknown> -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 +return nil 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..6e47e527 100644 --- a/src/stories.spec.luau +++ b/src/stories.spec.luau @@ -3,39 +3,38 @@ local CoreGui = game:GetService("CoreGui") local JestGlobals = require("@pkg/JestGlobals") 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 expect = JestGlobals.expect local test = JestGlobals.test +local testEach = test.each :: any -local storyModules: { ModuleScript } = {} -for _, descendant in ipairs(script.Parent:GetDescendants()) do - if isStoryModule(descendant) then - table.insert(storyModules, descendant) +testEach({ + Storyteller.findStoryModules(script.Parent), +})("mount/unmount %s", function(storyModule) + local story = (require :: any)(storyModule) + + if typeof(story) == "function" then + story = { + name = storyModule.Name, + story = story, + } + end + + if story.packages then + story.packages = Sift.Dictionary.join(story.packages or {}, { + React = React, + ReactRoblox = ReactRoblox, + }) 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 - end) -end + + local renderer = Storyteller.createRendererForStory(story) + local lifecycle + + expect(function() + lifecycle = Storyteller.render(renderer, CoreGui, story, story.controls) + end).never.toThrow() + + expect(lifecycle.unmount).never.toThrow() +end) diff --git a/wally.toml b/wally.toml index e6900a4e..c0f1b6be 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.1.1" 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 From 45b13ff27b859cf646032ca766a90463182ae43c Mon Sep 17 00:00:00 2001 From: Marin Minnerly <me@vocksel.com> Date: Thu, 31 Oct 2024 16:32:04 -0700 Subject: [PATCH 02/79] Bump Storyteller to 0.2.0 --- wally.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wally.toml b/wally.toml index c0f1b6be..69ae4dbf 100644 --- a/wally.toml +++ b/wally.toml @@ -8,7 +8,7 @@ exclude = ["*"] [dependencies] ModuleLoader = "flipbook-labs/module-loader@0.6.2" -Storyteller = "flipbook-labs/storyteller@0.1.1" +Storyteller = "flipbook-labs/storyteller@0.2.0" React = "jsdotlua/react@17.0.2" ReactRoblox = "jsdotlua/react-roblox@17.0.2" ReactSpring = "chriscerie/react-spring@2.0.0" From 407301000704147ebf5cf443327c74ad1c67437b Mon Sep 17 00:00:00 2001 From: Marin Minnerly <me@vocksel.com> Date: Thu, 31 Oct 2024 16:32:51 -0700 Subject: [PATCH 03/79] Update stories.spec to be more like the e2e tests in Storyteller --- src/stories.spec.luau | 85 ++++++++++++++++++++++++++++--------------- 1 file changed, 56 insertions(+), 29 deletions(-) diff --git a/src/stories.spec.luau b/src/stories.spec.luau index 6e47e527..d500ba68 100644 --- a/src/stories.spec.luau +++ b/src/stories.spec.luau @@ -1,40 +1,67 @@ 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 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 testEach = test.each :: any - -testEach({ - Storyteller.findStoryModules(script.Parent), -})("mount/unmount %s", function(storyModule) - local story = (require :: any)(storyModule) - - if typeof(story) == "function" then - story = { - name = storyModule.Name, - story = story, - } - end - - if story.packages then - story.packages = Sift.Dictionary.join(story.packages or {}, { - React = React, - ReactRoblox = ReactRoblox, - }) - end - - local renderer = Storyteller.createRendererForStory(story) - local lifecycle - - expect(function() - lifecycle = Storyteller.render(renderer, CoreGui, story, story.controls) - end).never.toThrow() - - expect(lifecycle.unmount).never.toThrow() + +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) + local renderer = Storyteller.createRendererForStory(story) + + if story.packages then + story.packages = Sift.Dictionary.join(story.packages, { + React = React, + ReactRoblox = ReactRoblox, + }) + end + + local lifecycle = Storyteller.render(renderer, container, story) + + expect(#container:GetChildren()).toBe(1) + + lifecycle.unmount() + + expect(#container:GetChildren()).toBe(0) + end) + end) end) From a6d84e0d9fddb7fbe38cceca6f52fdd43477335a Mon Sep 17 00:00:00 2001 From: Marin Minnerly <me@vocksel.com> Date: Thu, 31 Oct 2024 16:52:07 -0700 Subject: [PATCH 04/79] Storyteller 0.2.1 --- wally.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wally.toml b/wally.toml index 69ae4dbf..9f0246a2 100644 --- a/wally.toml +++ b/wally.toml @@ -8,7 +8,7 @@ exclude = ["*"] [dependencies] ModuleLoader = "flipbook-labs/module-loader@0.6.2" -Storyteller = "flipbook-labs/storyteller@0.2.0" +Storyteller = "flipbook-labs/storyteller@0.2.1" React = "jsdotlua/react@17.0.2" ReactRoblox = "jsdotlua/react-roblox@17.0.2" ReactSpring = "chriscerie/react-spring@2.0.0" From 0adbbd23c1e4bfd2018a4755aa5060c38c8cca21 Mon Sep 17 00:00:00 2001 From: Marin Minnerly <me@vocksel.com> Date: Thu, 31 Oct 2024 17:03:07 -0700 Subject: [PATCH 05/79] Fix analysis errors --- foreman.toml | 2 +- src/Storybook/StoryPreview.luau | 2 +- src/Storybook/types.luau | 2 +- 3 files changed, 3 insertions(+), 3 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/Storybook/StoryPreview.luau b/src/Storybook/StoryPreview.luau index 3d196c19..fe2d91b0 100644 --- a/src/Storybook/StoryPreview.luau +++ b/src/Storybook/StoryPreview.luau @@ -42,7 +42,7 @@ local StoryPreview = React.forwardRef(function(providedProps: Props, ref: any) local lifecycle local success, result = xpcall(function() - lifecycle = Storyteller.render(renderer, ref.current, props.story, props.controls) + lifecycle = Storyteller.render(renderer, ref.current, props.story) end, debug.traceback) if not success then diff --git a/src/Storybook/types.luau b/src/Storybook/types.luau index 24080fc7..5f654549 100644 --- a/src/Storybook/types.luau +++ b/src/Storybook/types.luau @@ -1,7 +1,7 @@ local Storyteller = require("@pkg/Storyteller") export type Controls = Storyteller.StoryControls -export type StoryProps = Storyteller.StoryProps<unknown> +export type StoryProps = Storyteller.StoryProps export type Storybook = Storyteller.Storybook export type Story = Storyteller.Story<unknown> From c640e6dc5ae1aa46fdb8d53691ae00885aa25d69 Mon Sep 17 00:00:00 2001 From: Marin Minnerly <me@vocksel.com> Date: Sat, 2 Nov 2024 11:54:29 -0700 Subject: [PATCH 06/79] Fix story names getting cut off --- src/Storybook/StoryMeta.luau | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Storybook/StoryMeta.luau b/src/Storybook/StoryMeta.luau index 42207a0e..f58783bd 100644 --- a/src/Storybook/StoryMeta.luau +++ b/src/Storybook/StoryMeta.luau @@ -31,7 +31,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, }), From 39679509cc0810bfa22383e05aab292991f8eb89 Mon Sep 17 00:00:00 2001 From: Marin Minnerly <me@vocksel.com> Date: Sat, 2 Nov 2024 13:19:56 -0700 Subject: [PATCH 07/79] Get controls largely working --- src/Forms/InputField.luau | 5 +++-- src/Storybook/StoryControls.luau | 1 - src/Storybook/StoryPreview.luau | 31 +++++++++++++++++-------------- src/Storybook/StoryView.luau | 26 ++++++++------------------ 4 files changed, 28 insertions(+), 35 deletions(-) diff --git a/src/Forms/InputField.luau b/src/Forms/InputField.luau index 8ecb47b1..cb71b0fd 100644 --- a/src/Forms/InputField.luau +++ b/src/Forms/InputField.luau @@ -14,7 +14,7 @@ export type Props = { layoutOrder: number?, onSubmit: (text: string, isValid: boolean) -> (), onFocus: (() -> ())?, - onFocusLost: (() -> ())?, + onFocusLost: (text: string, isValid: boolean) -> (), onTextChange: ((new: string, old: string) -> ())?, validate: ((text: string) -> boolean)?, transform: ((newText: string, oldText: string) -> string)?, @@ -33,7 +33,7 @@ local function InputField(providedProps: Props) local onFocusLost = React.useCallback( function(_rbx: TextBox, enterPressed: boolean) if props.onFocusLost then - props.onFocusLost() + props.onFocusLost(text, isValid) end if enterPressed and props.onSubmit then @@ -41,6 +41,7 @@ local function InputField(providedProps: Props) end end, { + text, isValid, props.onSubmit, } :: { unknown } diff --git a/src/Storybook/StoryControls.luau b/src/Storybook/StoryControls.luau index d30d51cf..9c373ade 100644 --- a/src/Storybook/StoryControls.luau +++ b/src/Storybook/StoryControls.luau @@ -37,7 +37,6 @@ local function StoryControls(props: Props) else option = React.createElement(InputField, { placeholder = value, - onTextChange = setControl, onSubmit = setControl, }) end diff --git a/src/Storybook/StoryPreview.luau b/src/Storybook/StoryPreview.luau index fe2d91b0..94c7d878 100644 --- a/src/Storybook/StoryPreview.luau +++ b/src/Storybook/StoryPreview.luau @@ -21,42 +21,45 @@ export type Props = { story: types.Story, ref: any, controls: { [string]: any }, - changedControls: { [string]: any }, - storyModule: ModuleScript, } 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?) React.useEffect(function() setErr(nil) end, { props.story, ref }) + React.useEffect(function() + local areControlsDifferent = props.story.controls + and not Sift.Dictionary.equals(props.controls, props.story.controls) + + if lifecycle.current and areControlsDifferent then + lifecycle.current.update(props.controls) + end + end, { props.controls, props.story }) + React.useEffect(function(): (() -> ())? if props.story and ref.current then - local renderer = Storyteller.createRendererForStory(props.story) - local lifecycle - local success, result = xpcall(function() - lifecycle = Storyteller.render(renderer, ref.current, props.story) + lifecycle.current = Storyteller.render(ref.current, props.story) end, debug.traceback) if not success then setErr(result) end + end - if lifecycle then - return function() - lifecycle.unmount() - end + return function() + if lifecycle.current then + lifecycle.current.unmount() end end - return nil - end, { props.story, props.controls, props.isMountedInViewport, ref.current } :: { unknown }) + 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 27280785..f7fb2c62 100644 --- a/src/Storybook/StoryView.luau +++ b/src/Storybook/StoryView.luau @@ -38,23 +38,9 @@ local function StoryView(props: Props) local controlsHeight, setControlsHeight = React.useState(initialControlsHeight) local topbarHeight, setTopbarHeight = React.useState(0) local storyParentRef = React.useRef(nil :: GuiObject?) - local controls + local controlsWithUserOverrides = Sift.Dictionary.join(if story then story.controls else nil, extraControls) + local showControls = controlsWithUserOverrides and not Sift.isEmpty(controlsWithUserOverrides) - 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 - end - end - end - - local showControls = controls and not Sift.isEmpty(controls) local setControl = React.useCallback(function(control: string, newValue: any) setExtraControls(function(prev) return Sift.Dictionary.merge(prev, { @@ -92,6 +78,10 @@ local function StoryView(props: Props) setTopbarHeight(rbx.AbsoluteSize.Y) end, {}) + React.useEffect(function() + setExtraControls({}) + end, { story }) + return e("Frame", { Size = UDim2.fromScale(1, 1), BackgroundTransparency = 1, @@ -164,7 +154,7 @@ local function StoryView(props: Props) StoryPreview = e(StoryPreview, { zoom = zoom.value, story = story, - controls = Sift.Dictionary.merge(controls, extraControls), + controls = controlsWithUserOverrides, storyModule = props.story, isMountedInViewport = isMountedInViewport, ref = storyParentRef, @@ -195,7 +185,7 @@ local function StoryView(props: Props) }), StoryControls = e(StoryControls, { - controls = controls, + controls = controlsWithUserOverrides, setControl = setControl, }), }), From c35c5882b14915af7934b6ba3dc834afc48ecd55 Mon Sep 17 00:00:00 2001 From: Marin Minnerly <me@vocksel.com> Date: Sat, 2 Nov 2024 13:21:33 -0700 Subject: [PATCH 08/79] Revert some minor changes --- src/Forms/InputField.luau | 4 ++-- src/init.storybook.luau | 6 ++---- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/Forms/InputField.luau b/src/Forms/InputField.luau index cb71b0fd..945b5bd4 100644 --- a/src/Forms/InputField.luau +++ b/src/Forms/InputField.luau @@ -14,7 +14,7 @@ export type Props = { layoutOrder: number?, onSubmit: (text: string, isValid: boolean) -> (), onFocus: (() -> ())?, - onFocusLost: (text: string, isValid: boolean) -> (), + onFocusLost: (() -> ())?, onTextChange: ((new: string, old: string) -> ())?, validate: ((text: string) -> boolean)?, transform: ((newText: string, oldText: string) -> string)?, @@ -33,7 +33,7 @@ local function InputField(providedProps: Props) local onFocusLost = React.useCallback( function(_rbx: TextBox, enterPressed: boolean) if props.onFocusLost then - props.onFocusLost(text, isValid) + props.onFocusLost() end if enterPressed and props.onSubmit then diff --git a/src/init.storybook.luau b/src/init.storybook.luau index 5210a7e9..af5486e7 100644 --- a/src/init.storybook.luau +++ b/src/init.storybook.luau @@ -6,8 +6,6 @@ return { storyRoots = { script.Parent, }, - packages = { - React = React, - ReactRoblox = ReactRoblox, - }, + react = React, + reactRoblox = ReactRoblox, } From 1af8cd897027a9fb7ec2ba37a3adf70e37422e06 Mon Sep 17 00:00:00 2001 From: Marin Minnerly <me@vocksel.com> Date: Sat, 2 Nov 2024 18:08:50 -0700 Subject: [PATCH 09/79] Bump to Storyteller 0.3.0 --- wally.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wally.toml b/wally.toml index 9f0246a2..1c2eee74 100644 --- a/wally.toml +++ b/wally.toml @@ -8,7 +8,7 @@ exclude = ["*"] [dependencies] ModuleLoader = "flipbook-labs/module-loader@0.6.2" -Storyteller = "flipbook-labs/storyteller@0.2.1" +Storyteller = "flipbook-labs/storyteller@0.3.0" React = "jsdotlua/react@17.0.2" ReactRoblox = "jsdotlua/react-roblox@17.0.2" ReactSpring = "chriscerie/react-spring@2.0.0" From 92f432cbc9f0af0e4f4a68561083324b606844c4 Mon Sep 17 00:00:00 2001 From: Marin Minnerly <me@vocksel.com> Date: Sat, 2 Nov 2024 21:53:27 -0700 Subject: [PATCH 10/79] Implicitly install packages on first build --- .lune/build.luau | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.lune/build.luau b/.lune/build.luau index ded02cad..7d78c2e1 100644 --- a/.lune/build.luau +++ b/.lune/build.luau @@ -1,9 +1,11 @@ +local fs = require("@lune/fs") +local process = require("@lune/process") + local clean = require("./lib/clean") local compile = require("./lib/compile") local constants = require("./lib/constants") local getPluginsPath = require("./lib/getPluginsPath") local parseArgs = require("./lib/parseArgs") -local process = require("@lune/process") local run = require("./lib/run") local watch = require("./lib/watcher/watch") @@ -16,6 +18,10 @@ local output = if args.output then args.output else `{getPluginsPath(process.os) assert(typeof(output) == "string", `bad value for output (string expected, got {typeof(output)})`) local function build() + if not fs.isDir("Packages") then + run("lune", { "run", "wally-install" }) + end + clean() compile(target) From 38ca781386191098320dff9afdbf05fefc7b73ef Mon Sep 17 00:00:00 2001 From: Marin Minnerly <me@vocksel.com> Date: Sat, 2 Nov 2024 21:53:44 -0700 Subject: [PATCH 11/79] Fix giant gray box in story preview area --- src/Plugin/PluginApp.luau | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Plugin/PluginApp.luau b/src/Plugin/PluginApp.luau index c494317d..7081df03 100644 --- a/src/Plugin/PluginApp.luau +++ b/src/Plugin/PluginApp.luau @@ -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, From 7e612d398cb866d9168ff27592a666bcbce000bc Mon Sep 17 00:00:00 2001 From: Marin Minnerly <me@vocksel.com> Date: Sat, 2 Nov 2024 22:28:45 -0700 Subject: [PATCH 12/79] Remove unused hook --- src/Common/useDescendants.luau | 62 ---------------- src/Common/useDescendants.spec.luau | 108 ---------------------------- 2 files changed, 170 deletions(-) delete mode 100644 src/Common/useDescendants.luau delete mode 100644 src/Common/useDescendants.spec.luau diff --git a/src/Common/useDescendants.luau b/src/Common/useDescendants.luau deleted file mode 100644 index b0d8dd17..00000000 --- a/src/Common/useDescendants.luau +++ /dev/null @@ -1,62 +0,0 @@ -local React = require("@pkg/React") -local Sift = require("@pkg/Sift") - -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 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 - 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) From d42689dfea6fac6d9ebed0bb75f7a48793e297ae Mon Sep 17 00:00:00 2001 From: Marin Minnerly <me@vocksel.com> Date: Sat, 2 Nov 2024 22:42:17 -0700 Subject: [PATCH 13/79] Bump Storyteller to 0.4.0 --- wally.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wally.toml b/wally.toml index 1c2eee74..f1f4c128 100644 --- a/wally.toml +++ b/wally.toml @@ -8,7 +8,7 @@ exclude = ["*"] [dependencies] ModuleLoader = "flipbook-labs/module-loader@0.6.2" -Storyteller = "flipbook-labs/storyteller@0.3.0" +Storyteller = "flipbook-labs/storyteller@0.4.0" React = "jsdotlua/react@17.0.2" ReactRoblox = "jsdotlua/react-roblox@17.0.2" ReactSpring = "chriscerie/react-spring@2.0.0" From a20077b59853c27487f93ecd3346cbc9473368b6 Mon Sep 17 00:00:00 2001 From: Marin Minnerly <me@vocksel.com> Date: Sat, 2 Nov 2024 22:47:37 -0700 Subject: [PATCH 14/79] Revert a story change --- example/ReactCounter.story.luau | 4 ---- 1 file changed, 4 deletions(-) diff --git a/example/ReactCounter.story.luau b/example/ReactCounter.story.luau index ceace6fd..7fbe9dd7 100644 --- a/example/ReactCounter.story.luau +++ b/example/ReactCounter.story.luau @@ -16,10 +16,6 @@ return { controls = controls, react = React, reactRoblox = ReactRoblox, - packages = { - React = React, - ReactRoblox = ReactRoblox, - }, story = function(props: Props) return React.createElement(ReactCounter, { increment = props.controls.increment, From fcd240017982e68f60472b9eec213b724b314d5e Mon Sep 17 00:00:00 2001 From: Marin Minnerly <me@vocksel.com> Date: Sat, 2 Nov 2024 22:53:37 -0700 Subject: [PATCH 15/79] Use Storyteller type sdirectly --- src/Explorer/types.luau | 4 ++-- src/Navigation/Screen.luau | 8 ++++---- src/Panels/Sidebar.luau | 7 ++++--- src/Storybook/StoryCanvas.luau | 9 ++++++--- src/Storybook/StoryMeta.luau | 7 +++++-- src/Storybook/StoryPreview.luau | 7 ++++--- src/Storybook/StoryView.luau | 8 +++++--- src/Storybook/createStoryNodes.luau | 3 +-- src/Storybook/createStoryNodes.spec.luau | 5 +++-- src/Storybook/types.luau | 8 -------- 10 files changed, 34 insertions(+), 32 deletions(-) delete mode 100644 src/Storybook/types.luau 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 70ee4a45..c9d39c08 100644 --- a/src/Navigation/Screen.luau +++ b/src/Navigation/Screen.luau @@ -1,18 +1,18 @@ local ModuleLoader = require("@pkg/ModuleLoader") local React = require("@pkg/React") +local Storyteller = require("@pkg/Storyteller") 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/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/StoryMeta.luau b/src/Storybook/StoryMeta.luau index f58783bd..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<unknown> + export type Props = { - story: types.Story, + story: Story, layoutOrder: number?, } diff --git a/src/Storybook/StoryPreview.luau b/src/Storybook/StoryPreview.luau index 94c7d878..9b0ce885 100644 --- a/src/Storybook/StoryPreview.luau +++ b/src/Storybook/StoryPreview.luau @@ -7,7 +7,6 @@ local Storyteller = require("@pkg/Storyteller") local ScrollingFrame = require("@root/Common/ScrollingFrame") local StoryError = require("@root/Storybook/StoryError") -local types = require("@root/Storybook/types") local e = React.createElement @@ -16,9 +15,11 @@ local defaultProps = { zoom = 0, } +type Story = Storyteller.Story<unknown> + export type Props = { - layoutOrder: number, - story: types.Story, + layoutOrder: number?, + story: Story, ref: any, controls: { [string]: any }, } diff --git a/src/Storybook/StoryView.luau b/src/Storybook/StoryView.luau index f7fb2c62..c8c83e84 100644 --- a/src/Storybook/StoryView.luau +++ b/src/Storybook/StoryView.luau @@ -15,16 +15,18 @@ 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 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) diff --git a/src/Storybook/createStoryNodes.luau b/src/Storybook/createStoryNodes.luau index c71d7cd6..a4741ccb 100644 --- a/src/Storybook/createStoryNodes.luau +++ b/src/Storybook/createStoryNodes.luau @@ -1,9 +1,8 @@ local Storyteller = require("@pkg/Storyteller") local explorerTypes = require("@root/Explorer/types") -local storybookTypes = require("@root/Storybook/types") -type Storybook = storybookTypes.Storybook +type Storybook = Storyteller.Storybook type ComponentTreeNode = explorerTypes.ComponentTreeNode local function hasStories(instance: Instance): boolean 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/types.luau b/src/Storybook/types.luau deleted file mode 100644 index 5f654549..00000000 --- a/src/Storybook/types.luau +++ /dev/null @@ -1,8 +0,0 @@ -local Storyteller = require("@pkg/Storyteller") - -export type Controls = Storyteller.StoryControls -export type StoryProps = Storyteller.StoryProps -export type Storybook = Storyteller.Storybook -export type Story = Storyteller.Story<unknown> - -return nil From bd6c8f27ebe139f1e9f1a796fa774887f5b985b5 Mon Sep 17 00:00:00 2001 From: Marin Minnerly <me@vocksel.com> Date: Sat, 2 Nov 2024 22:53:50 -0700 Subject: [PATCH 16/79] Fix analysis errors --- src/init.storybook.luau | 6 ++++-- src/stories.spec.luau | 3 +-- 2 files changed, 5 insertions(+), 4 deletions(-) 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 d500ba68..0d26cc17 100644 --- a/src/stories.spec.luau +++ b/src/stories.spec.luau @@ -46,7 +46,6 @@ describeEach({ })("%s", function(storyModule) test("basic mount/unmount lifecycle", function() local story = Storyteller.loadStoryModule(mockModuleLoader, storyModule, storybook) - local renderer = Storyteller.createRendererForStory(story) if story.packages then story.packages = Sift.Dictionary.join(story.packages, { @@ -55,7 +54,7 @@ describeEach({ }) end - local lifecycle = Storyteller.render(renderer, container, story) + local lifecycle = Storyteller.render(container, story) expect(#container:GetChildren()).toBe(1) From 5834eba3fd0feb988f7d601964e16d965085171d Mon Sep 17 00:00:00 2001 From: Marin Minnerly <me@vocksel.com> Date: Tue, 5 Nov 2024 07:29:40 -0800 Subject: [PATCH 17/79] WIP changes for getting controls working consistently --- src/Storybook/StoryPreview.luau | 21 ++++++++++++++++----- src/Storybook/StoryView.luau | 20 ++++++++++++++++++-- 2 files changed, 34 insertions(+), 7 deletions(-) diff --git a/src/Storybook/StoryPreview.luau b/src/Storybook/StoryPreview.luau index 699088cf..611aca2d 100644 --- a/src/Storybook/StoryPreview.luau +++ b/src/Storybook/StoryPreview.luau @@ -7,6 +7,7 @@ local Storyteller = require("@pkg/Storyteller") local ScrollingFrame = require("@root/Common/ScrollingFrame") local StoryError = require("@root/Storybook/StoryError") +local usePrevious = require("@root/Common/usePrevious") local e = React.createElement @@ -33,22 +34,32 @@ local StoryPreview = React.forwardRef(function(providedProps: Props, ref: any) local props: InternalProps = Sift.Dictionary.merge(defaultProps, providedProps) 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() - local areControlsDifferent = props.story.controls - and not Sift.Dictionary.equals(props.controls, props.story.controls) + if props.story == prevStory then + local areControlsDifferent = prevControls and not Sift.Dictionary.equals(props.controls, prevControls) - if lifecycle.current and areControlsDifferent then - lifecycle.current.update(props.controls) + 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, props.story }) + end, { props.controls, prevControls, props.story, prevStory } :: { unknown }) React.useEffect(function(): (() -> ())? if props.story and ref.current then + -- TODO: Rendering before controls are applied local success, result = xpcall(function() lifecycle.current = Storyteller.render(ref.current, props.story) end, debug.traceback) diff --git a/src/Storybook/StoryView.luau b/src/Storybook/StoryView.luau index 60ba6732..b8bcc400 100644 --- a/src/Storybook/StoryView.luau +++ b/src/Storybook/StoryView.luau @@ -40,7 +40,23 @@ local function StoryView(props: Props) local controlsHeight, setControlsHeight = React.useState(initialControlsHeight) local topbarHeight, setTopbarHeight = React.useState(0) local storyParentRef = React.useRef(nil :: GuiObject?) - local controlsWithUserOverrides = Sift.Dictionary.join(if story then story.controls else nil, extraControls) + + local controlsWithUserOverrides = React.useMemo(function() + local controls = {} + if story and story.controls then + 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 + end + end + end + return controls + end, { story, extraControls } :: { unknown }) + local showControls = controlsWithUserOverrides and not Sift.isEmpty(controlsWithUserOverrides) local setControl = React.useCallback(function(control: string, newValue: any) @@ -156,7 +172,7 @@ local function StoryView(props: Props) StoryPreview = e(StoryPreview, { zoom = zoom.value, story = story, - controls = controlsWithUserOverrides, + controls = Sift.Dictionary.merge(controlsWithUserOverrides, extraControls), storyModule = props.story, isMountedInViewport = isMountedInViewport, ref = storyParentRef, From 50e78f5d3ecf051bd234277ce4456a5399fa0b28 Mon Sep 17 00:00:00 2001 From: Marin Minnerly <me@vocksel.com> Date: Tue, 5 Nov 2024 16:55:54 -0800 Subject: [PATCH 18/79] Install local Storyteller for convenience --- .lune/wally-install.luau | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/.lune/wally-install.luau b/.lune/wally-install.luau index de9144de..dde93e0e 100644 --- a/.lune/wally-install.luau +++ b/.lune/wally-install.luau @@ -1,5 +1,23 @@ local run = require("./lib/run") run("wally", { "install" }) + +do + local process = require("@lune/process") + + local homePath = process.env.HOME + assert(homePath, "no $HOME env var") + + local storytellerPath = run("realpath", { "../storyteller" }) + + run("rm", { `Packages/Storyteller.lua` }) + + run("lune", { "run", "build" }, { + cwd = storytellerPath, + }) + + run("ln", { "-s", `{storytellerPath}/dist`, `Packages/Storyteller` }) +end + run("rojo", { "sourcemap", "build.project.json", "-o", "sourcemap.json" }) run("wally-package-types", { "--sourcemap", "sourcemap.json", "Packages" }) From e72d9f367f5a47c98efadbd38678d9dc70d0bf7f Mon Sep 17 00:00:00 2001 From: Marin Minnerly <me@vocksel.com> Date: Wed, 6 Nov 2024 15:04:39 -0800 Subject: [PATCH 19/79] Install local ModuleLoader --- .lune/wally-install.luau | 21 ++++++++++++++------- wally.toml | 3 +++ 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/.lune/wally-install.luau b/.lune/wally-install.luau index dde93e0e..a21df008 100644 --- a/.lune/wally-install.luau +++ b/.lune/wally-install.luau @@ -1,23 +1,30 @@ +local process = require("@lune/process") + local run = require("./lib/run") run("wally", { "install" }) -do - local process = require("@lune/process") - +local function installPackageFromDisk(packageName: string, packagePath: string, build: ((packagePath: string) -> ())?) local homePath = process.env.HOME assert(homePath, "no $HOME env var") - local storytellerPath = run("realpath", { "../storyteller" }) + local absPackagePath = run("realpath", { packagePath }) - run("rm", { `Packages/Storyteller.lua` }) + run("rm", { `Packages/{packageName}.lua` }) + + run("lune", { "run", "wally-install" }, { + cwd = absPackagePath, + }) run("lune", { "run", "build" }, { - cwd = storytellerPath, + cwd = absPackagePath, }) - run("ln", { "-s", `{storytellerPath}/dist`, `Packages/Storyteller` }) + run("ln", { "-s", `{absPackagePath}/dist`, `Packages/{packageName}` }) end +installPackageFromDisk("Storyteller", "../storyteller") +installPackageFromDisk("ModuleLoader", "../module-loader") + run("rojo", { "sourcemap", "build.project.json", "-o", "sourcemap.json" }) run("wally-package-types", { "--sourcemap", "sourcemap.json", "Packages" }) diff --git a/wally.toml b/wally.toml index e927405e..42f02198 100644 --- a/wally.toml +++ b/wally.toml @@ -19,3 +19,6 @@ t = "osyrisrblx/t@3.0.0" Roact = "roblox/roact@1.4.4" Jest = "jsdotlua/jest@3.6.1-rc.2" JestGlobals = "jsdotlua/jest-globals@3.6.1-rc.2" + +# ModuleLoader dependencies +Janitor = "howmanysmall/janitor@1.13.15" From 595699422307a96b4a28a2376e2f7f6a768a5159 Mon Sep 17 00:00:00 2001 From: Marin Minnerly <me@vocksel.com> Date: Wed, 6 Nov 2024 16:37:56 -0800 Subject: [PATCH 20/79] Don't need the `build` arg --- .lune/wally-install.luau | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.lune/wally-install.luau b/.lune/wally-install.luau index a21df008..42ce7ecb 100644 --- a/.lune/wally-install.luau +++ b/.lune/wally-install.luau @@ -4,7 +4,7 @@ local run = require("./lib/run") run("wally", { "install" }) -local function installPackageFromDisk(packageName: string, packagePath: string, build: ((packagePath: string) -> ())?) +local function installPackageFromDisk(packageName: string, packagePath: string) local homePath = process.env.HOME assert(homePath, "no $HOME env var") From d860d4d9c8e056a86d1e66174bfe60dfef1d994f Mon Sep 17 00:00:00 2001 From: Marin Minnerly <me@vocksel.com> Date: Thu, 7 Nov 2024 13:42:15 -0800 Subject: [PATCH 21/79] Add support to build to engine --- .lune/build.luau | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/.lune/build.luau b/.lune/build.luau index 7d78c2e1..513c6b7f 100644 --- a/.lune/build.luau +++ b/.lune/build.luau @@ -17,6 +17,12 @@ assert(target == "dev" or target == "prod", `bad value for target (must be one o local output = if args.output then args.output else `{getPluginsPath(process.os)}/{constants.PLUGIN_FILENAME}` assert(typeof(output) == "string", `bad value for output (string expected, got {typeof(output)})`) +local gameEnginePath = args.gameEnginePath +assert( + not gameEnginePath or typeof(gameEnginePath) == "string", + `bad value for gameEnginePath (string expected, got {typeof(output)}` +) + local function build() if not fs.isDir("Packages") then run("lune", { "run", "wally-install" }) @@ -31,6 +37,16 @@ local function build() run("rm", { "-rf", `{constants.BUILD_PATH}/**/*.storybook.luau` }) end + if gameEnginePath then + local engineBuildPath = run("find", { `{gameEnginePath}/build`, "-name", "optimized" }) + assert(fs.isDir(engineBuildPath), `failed to find optimized engine build under {gameEnginePath}`) + + local builtInPlugins = run("find", { engineBuildPath, "-name", "BuiltInPlugins", "-print", "-quit" }) + assert(fs.isDir(builtInPlugins), `failed to find built-in plugins under {engineBuildPath}`) + + output = `{builtInPlugins}/{constants.PLUGIN_FILENAME}` + end + run("rojo", { "build", "-o", output }) end From d4b3142f26cd38e47474625247cddb9e84fa3683 Mon Sep 17 00:00:00 2001 From: Marin Minnerly <me@vocksel.com> Date: Thu, 7 Nov 2024 13:42:25 -0800 Subject: [PATCH 22/79] Arg can be nil --- .lune/lib/parseArgs.luau | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.lune/lib/parseArgs.luau b/.lune/lib/parseArgs.luau index a506f990..522f8f6c 100644 --- a/.lune/lib/parseArgs.luau +++ b/.lune/lib/parseArgs.luau @@ -2,7 +2,7 @@ local FLAG_PATTERN = "%-%-(%w+)" local FLAG_ALL_IN_ONE_PATTERN = `{FLAG_PATTERN}=(%w+)` local function parseArgs(args: { string }) - local parsedArgs: { [string]: string | boolean | number } = {} + local parsedArgs: { [string]: string | boolean | number | nil } = {} local skipNextToken = false From 2f6c38823510bfc340d336cad9e73ce8d231308b Mon Sep 17 00:00:00 2001 From: Marin Minnerly <me@vocksel.com> Date: Thu, 7 Nov 2024 16:07:04 -0800 Subject: [PATCH 23/79] Don't build the packages, just link --- .lune/wally-install.luau | 9 --------- 1 file changed, 9 deletions(-) diff --git a/.lune/wally-install.luau b/.lune/wally-install.luau index 42ce7ecb..d1aab840 100644 --- a/.lune/wally-install.luau +++ b/.lune/wally-install.luau @@ -11,15 +11,6 @@ local function installPackageFromDisk(packageName: string, packagePath: string) local absPackagePath = run("realpath", { packagePath }) run("rm", { `Packages/{packageName}.lua` }) - - run("lune", { "run", "wally-install" }, { - cwd = absPackagePath, - }) - - run("lune", { "run", "build" }, { - cwd = absPackagePath, - }) - run("ln", { "-s", `{absPackagePath}/dist`, `Packages/{packageName}` }) end From e686723159fb12cd3ea597180b4b408772736f11 Mon Sep 17 00:00:00 2001 From: Marin Minnerly <me@vocksel.com> Date: Thu, 7 Nov 2024 16:19:56 -0800 Subject: [PATCH 24/79] Output to the right place --- .lune/build.luau | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.lune/build.luau b/.lune/build.luau index 513c6b7f..d025145b 100644 --- a/.lune/build.luau +++ b/.lune/build.luau @@ -44,7 +44,7 @@ local function build() local builtInPlugins = run("find", { engineBuildPath, "-name", "BuiltInPlugins", "-print", "-quit" }) assert(fs.isDir(builtInPlugins), `failed to find built-in plugins under {engineBuildPath}`) - output = `{builtInPlugins}/{constants.PLUGIN_FILENAME}` + output = `{builtInPlugins}/Optimized_Embedded_Signature/{constants.PLUGIN_FILENAME}` end run("rojo", { "build", "-o", output }) From 75eb005b90ed9ba6f8608ff3f0e0ed7f8df58f67 Mon Sep 17 00:00:00 2001 From: Marin Minnerly <me@vocksel.com> Date: Mon, 11 Nov 2024 09:09:42 -0800 Subject: [PATCH 25/79] Add padding to StoryError --- src/Storybook/StoryError.luau | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/Storybook/StoryError.luau b/src/Storybook/StoryError.luau index 97878ad7..e4012bd1 100644 --- a/src/Storybook/StoryError.luau +++ b/src/Storybook/StoryError.luau @@ -14,6 +14,13 @@ local function StoryError(props: Props) LayoutOrder = props.layoutOrder, Text = props.err, TextColor3 = theme.alert, + }, { + Padding = React.createElement("UIPadding", { + PaddingTop = UDim.new(0, 8), + PaddingRight = UDim.new(0, 8), + PaddingBottom = UDim.new(0, 8), + PaddingLeft = UDim.new(0, 8), + }), }) end From 9f4c68f2b6e7065b3030b1e1b73ef3139c32e0da Mon Sep 17 00:00:00 2001 From: Marin Minnerly <me@vocksel.com> Date: Wed, 11 Dec 2024 16:37:31 -0800 Subject: [PATCH 26/79] Add Storyteller dependency --- wally.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/wally.toml b/wally.toml index 6c96e6da..d3b3b9d6 100644 --- a/wally.toml +++ b/wally.toml @@ -22,3 +22,6 @@ JestGlobals = "jsdotlua/jest-globals@3.6.1-rc.2" # ModuleLoader dependencies Janitor = "howmanysmall/janitor@1.13.15" + +# Storyteller dependencies +Prospector = "egomoose/prospector@1.1.0" From 0d3d197a0d8127edc9c1437dc3a6f75d28ce169b Mon Sep 17 00:00:00 2001 From: Marin Minnerly <me@vocksel.com> Date: Wed, 11 Dec 2024 16:47:12 -0800 Subject: [PATCH 27/79] Fix storybooks not appearing in tree view --- src/Plugin/PluginApp.luau | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Plugin/PluginApp.luau b/src/Plugin/PluginApp.luau index 7081df03..85b6cae5 100644 --- a/src/Plugin/PluginApp.luau +++ b/src/Plugin/PluginApp.luau @@ -64,7 +64,7 @@ local function App(props: Props) Sidebar = React.createElement(Sidebar, { selectStory = selectStory, selectStorybook = selectStorybook, - storybooks = storybooks, + storybooks = storybooks.available, }), }), From 8e9907047eb8d71b5752590841efba2434200dea Mon Sep 17 00:00:00 2001 From: Marin Minnerly <me@vocksel.com> Date: Wed, 11 Dec 2024 16:47:23 -0800 Subject: [PATCH 28/79] Don't worry about ModuleLoader for now --- .lune/wally-install.luau | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.lune/wally-install.luau b/.lune/wally-install.luau index 04d05e79..1798f17b 100644 --- a/.lune/wally-install.luau +++ b/.lune/wally-install.luau @@ -16,7 +16,7 @@ do run("wally", { "install" }) installPackageFromDisk("Storyteller", "../storyteller") - installPackageFromDisk("ModuleLoader", "../module-loader") + -- installPackageFromDisk("ModuleLoader", "../module-loader") run("rojo", { "sourcemap", "build.project.json", "-o", "sourcemap.json" }) run("wally-package-types", { "--sourcemap", "sourcemap.json", "Packages" }) From af73515d634925edb1498f3aa5a1f97cfcb7b66c Mon Sep 17 00:00:00 2001 From: Marin Minnerly <me@vocksel.com> Date: Thu, 12 Dec 2024 13:18:20 -0800 Subject: [PATCH 29/79] Allow the plugin to be constructed by a wrapper --- src/Plugin/createFlipbookPlugin.luau | 113 +++++++++++++++++++++++++++ src/Plugin/createToggleButton.luau | 26 ------ src/Plugin/createWidget.luau | 12 --- src/init.server.luau | 44 +---------- 4 files changed, 116 insertions(+), 79 deletions(-) create mode 100644 src/Plugin/createFlipbookPlugin.luau delete mode 100644 src/Plugin/createToggleButton.luau delete mode 100644 src/Plugin/createWidget.luau diff --git a/src/Plugin/createFlipbookPlugin.luau b/src/Plugin/createFlipbookPlugin.luau new file mode 100644 index 00000000..80af5315 --- /dev/null +++ b/src/Plugin/createFlipbookPlugin.luau @@ -0,0 +1,113 @@ +local RunService = game:GetService("RunService") + +if RunService:IsRunning() or not RunService:IsEdit() then + return +end + +local ModuleLoader = require("@pkg/ModuleLoader") +local React = require("@pkg/React") +local ReactRoblox = require("@pkg/ReactRoblox") + +local ContextProviders = require("@root/Common/ContextProviders") +local PluginApp = require("@root/Plugin/PluginApp") + +local function createFlipbookPlugin( + name: string, + plugin: Plugin, + toolbar: PluginToolbar +): { + mount: () -> (), + unmount: () -> (), + destroy: () -> (), +} + local isDestroyed = false + + local connections: { RBXScriptConnection } = {} + + local info = DockWidgetPluginGuiInfo.new(Enum.InitialDockState.Top, true) + + local widget = plugin:CreateDockWidgetPluginGui(name, info) + widget.Name = name + widget.Title = name + widget.ZIndexBehavior = Enum.ZIndexBehavior.Sibling + + local root = ReactRoblox.createRoot(widget) + local loader = ModuleLoader.new() + local button = toolbar:CreateButton(name, "Open story view", "rbxassetid://10277153751") + + local function unmount() + assert(not isDestroyed, "cannot call unmount (Flipbook plugin was destroyed)") + + root:unmount() + loader:clear() + + for _, connection in connections do + connection:Disconnect() + end + end + + local function mount() + assert(not isDestroyed, "cannot call mount (Flipbook plugin was destroyed)") + + local app = React.createElement(ContextProviders, { + plugin = plugin, + }, { + PluginApp = React.createElement(PluginApp, { + loader = loader, + }), + }) + + table.insert( + connections, + button.Click:Connect(function() + widget.Enabled = not widget.Enabled + end) + ) + + table.insert( + connections, + widget:GetPropertyChangedSignal("Enabled"):Connect(function() + button:SetActive(widget.Enabled) + end) + ) + + table.insert( + connections, + widget:GetPropertyChangedSignal("Enabled"):Connect(function() + if widget.Enabled then + root:render(app) + else + unmount() + end + end) + ) + + table.insert(connections, plugin.Unloading:Connect(unmount)) + + if widget.Enabled then + root:render(app) + end + end + + local function destroy() + assert(not isDestroyed, "cannot call destroy (Flipbook plugin was already destroyed)") + + unmount() + + isDestroyed = true + + widget:Destroy() + toolbar:Destroy() + button:Destroy() + root = nil :: any + loader = nil :: any + end + + return { + mount = mount, + unmount = unmount, + destroy = destroy, + } +end + +return createFlipbookPlugin diff --git a/src/Plugin/createToggleButton.luau b/src/Plugin/createToggleButton.luau deleted file mode 100644 index 5519493a..00000000 --- a/src/Plugin/createToggleButton.luau +++ /dev/null @@ -1,26 +0,0 @@ ---[[ - Creates the button to toggle the plugin widget. - - This function also sets up some events to toggle the widget when the button - is clicked, and to sync up the button's "active" state with the widget. - - @return () -> () -- Returns a callback for disconnecting button events -]] -local function createToggleButton(toolbar: PluginToolbar, widget: DockWidgetPluginGui) - local button = toolbar:CreateButton(widget.Name, "Open story view", "rbxassetid://10277153751") - - local click = button.Click:Connect(function() - widget.Enabled = not widget.Enabled - end) - - local enabled = widget:GetPropertyChangedSignal("Enabled"):Connect(function() - button:SetActive(widget.Enabled) - end) - - return function() - click:Disconnect() - enabled:Disconnect() - end -end - -return createToggleButton diff --git a/src/Plugin/createWidget.luau b/src/Plugin/createWidget.luau deleted file mode 100644 index d4e8e1a6..00000000 --- a/src/Plugin/createWidget.luau +++ /dev/null @@ -1,12 +0,0 @@ -local function createWidget(plugin: Plugin, name: string): DockWidgetPluginGui - local info = DockWidgetPluginGuiInfo.new(Enum.InitialDockState.Top, true) - - local widget = plugin:CreateDockWidgetPluginGui(name, info) - widget.Name = name - widget.Title = name - widget.ZIndexBehavior = Enum.ZIndexBehavior.Sibling - - return widget -end - -return createWidget diff --git a/src/init.server.luau b/src/init.server.luau index 3cbe3155..a667783b 100644 --- a/src/init.server.luau +++ b/src/init.server.luau @@ -4,49 +4,11 @@ if RunService:IsRunning() or not RunService:IsEdit() then return end -local ModuleLoader = require("@pkg/ModuleLoader") -local React = require("@pkg/React") -local ReactRoblox = require("@pkg/ReactRoblox") - -local ContextProviders = require("@root/Common/ContextProviders") -local PluginApp = require("@root/Plugin/PluginApp") -local createToggleButton = require("@root/Plugin/createToggleButton") -local createWidget = require("@root/Plugin/createWidget") +local createFlipbookPlugin = require("@root/Plugin/createFlipbookPlugin") local PLUGIN_NAME = "flipbook" local toolbar = plugin:CreateToolbar(PLUGIN_NAME) -local widget = createWidget(plugin, PLUGIN_NAME) -local root = ReactRoblox.createRoot(widget) -local disconnectButton = createToggleButton(toolbar, widget) - -local loader = ModuleLoader.new() - -local app = React.createElement(ContextProviders, { - plugin = plugin, -}, { - PluginApp = React.createElement(PluginApp, { - loader = loader, - }), -}) - -local widgetConn = widget:GetPropertyChangedSignal("Enabled"):Connect(function() - if widget.Enabled then - root:render(app) - else - root:unmount() - loader:clear() - end -end) - -if widget.Enabled then - root:render(app) -end - -plugin.Unloading:Connect(function() - disconnectButton() - widgetConn:Disconnect() +local flipbookPlugin = createFlipbookPlugin(PLUGIN_NAME, plugin, toolbar) - root:unmount() - loader:clear() -end) +flipbookPlugin.mount() From ac95f13a88cc17fc291ea38ee20e32867450ed63 Mon Sep 17 00:00:00 2001 From: Marin Minnerly <me@vocksel.com> Date: Thu, 12 Dec 2024 15:16:01 -0800 Subject: [PATCH 30/79] Support reloading with the wrapper --- src/Plugin/createFlipbookPlugin.luau | 81 ++++++++-------------------- src/init.server.luau | 2 +- 2 files changed, 23 insertions(+), 60 deletions(-) diff --git a/src/Plugin/createFlipbookPlugin.luau b/src/Plugin/createFlipbookPlugin.luau index 80af5315..e72b1989 100644 --- a/src/Plugin/createFlipbookPlugin.luau +++ b/src/Plugin/createFlipbookPlugin.luau @@ -18,12 +18,7 @@ local function createFlipbookPlugin( ): { mount: () -> (), unmount: () -> (), - destroy: () -> (), } - local isDestroyed = false - - local connections: { RBXScriptConnection } = {} - local info = DockWidgetPluginGuiInfo.new(Enum.InitialDockState.Top, true) local widget = plugin:CreateDockWidgetPluginGui(name, info) @@ -35,78 +30,46 @@ local function createFlipbookPlugin( local loader = ModuleLoader.new() local button = toolbar:CreateButton(name, "Open story view", "rbxassetid://10277153751") - local function unmount() - assert(not isDestroyed, "cannot call unmount (Flipbook plugin was destroyed)") + local app = React.createElement(ContextProviders, { + plugin = plugin, + }, { + PluginApp = React.createElement(PluginApp, { + loader = loader, + }), + }) + local function unmount() root:unmount() loader:clear() - - for _, connection in connections do - connection:Disconnect() - end end local function mount() - assert(not isDestroyed, "cannot call mount (Flipbook plugin was destroyed)") - - local app = React.createElement(ContextProviders, { - plugin = plugin, - }, { - PluginApp = React.createElement(PluginApp, { - loader = loader, - }), - }) - - table.insert( - connections, - button.Click:Connect(function() - widget.Enabled = not widget.Enabled - end) - ) - - table.insert( - connections, - widget:GetPropertyChangedSignal("Enabled"):Connect(function() - button:SetActive(widget.Enabled) - end) - ) + root:render(app) + end - table.insert( - connections, - widget:GetPropertyChangedSignal("Enabled"):Connect(function() - if widget.Enabled then - root:render(app) - else - unmount() - end - end) - ) + button.Click:Connect(function() + widget.Enabled = not widget.Enabled + end) - table.insert(connections, plugin.Unloading:Connect(unmount)) + widget:GetPropertyChangedSignal("Enabled"):Connect(function() + button:SetActive(widget.Enabled) + end) + widget:GetPropertyChangedSignal("Enabled"):Connect(function() if widget.Enabled then root:render(app) + else + unmount() end - end - - local function destroy() - assert(not isDestroyed, "cannot call destroy (Flipbook plugin was already destroyed)") - - unmount() - - isDestroyed = true + end) - widget:Destroy() - toolbar:Destroy() - button:Destroy() - root = nil :: any - loader = nil :: any + if widget.Enabled then + mount() end return { mount = mount, unmount = unmount, - destroy = destroy, } end diff --git a/src/init.server.luau b/src/init.server.luau index a667783b..35df6a1d 100644 --- a/src/init.server.luau +++ b/src/init.server.luau @@ -11,4 +11,4 @@ local PLUGIN_NAME = "flipbook" local toolbar = plugin:CreateToolbar(PLUGIN_NAME) local flipbookPlugin = createFlipbookPlugin(PLUGIN_NAME, plugin, toolbar) -flipbookPlugin.mount() +plugin.Unloading:Connect(flipbookPlugin.unmount) From 8eba4a3ee90c2ddf804910123010c8577cd5087a Mon Sep 17 00:00:00 2001 From: Marin Minnerly <me@vocksel.com> Date: Thu, 12 Dec 2024 16:41:45 -0800 Subject: [PATCH 31/79] Fix Flipbook crashing when attempting to open to a story in a restricted service --- src/Common/getInstanceFromFullName.luau | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/Common/getInstanceFromFullName.luau b/src/Common/getInstanceFromFullName.luau index e66aa8e6..a0e0d31a 100644 --- a/src/Common/getInstanceFromFullName.luau +++ b/src/Common/getInstanceFromFullName.luau @@ -10,12 +10,20 @@ local Sift = require("@pkg/Sift") local PATH_SEPERATOR = "." +local function canAccess(instance: Instance): boolean + local success = pcall(function() + return instance.Name + end) + + return success +end + local function maybeGetService(serviceName: string): Instance? local success, current: any = pcall(function() return game:GetService(serviceName) end) - if success and current and current:IsA("Instance") then + if success and current and canAccess(current) and current:IsA("Instance") then return current else return nil From 5f08f2b3b47b2967ab8cea91383b0d6bd6968d86 Mon Sep 17 00:00:00 2001 From: Marin Minnerly <me@vocksel.com> Date: Mon, 16 Dec 2024 10:54:19 -0800 Subject: [PATCH 32/79] Fix infinite loop --- src/Common/getInstanceFromFullName.luau | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/Common/getInstanceFromFullName.luau b/src/Common/getInstanceFromFullName.luau index a0e0d31a..1574c4d6 100644 --- a/src/Common/getInstanceFromFullName.luau +++ b/src/Common/getInstanceFromFullName.luau @@ -41,6 +41,11 @@ local function getInstanceFromFullName(fullName: string): Instance? local current = maybeGetService(serviceName) if current then + -- TODO: Verify this isn't also needed with the below TODO + -- if #parts == 1 then + -- return current + -- end + while #parts > 0 do -- Keep around a copy of the `parts` array. We are going to concat this -- into new paths, and incrementally remove from the right to narrow @@ -60,8 +65,16 @@ local function getInstanceFromFullName(fullName: string): Instance? parts = Sift.List.shift(parts, #name:split(PATH_SEPERATOR)) break else - -- Reduce from the right until we find the next instance - tempParts = Sift.List.pop(tempParts) + if #tempParts > 1 then + -- TODO: Need this early exit to handle cases with instances not existing later on. Verify + -- if this can be effectively repro'd by calling this function on an instance that doesn't + -- exist. Particularly make sure to test look-aheads for `Foo.story`, it seemed like the + -- loop got stuck looking for "story" when "ChromeWindow.story" didn't exist + return nil + else + -- Reduce from the right until we find the next instance + tempParts = Sift.List.pop(tempParts) + end end end end From 0d1acbd9d6749982495e06b5d12de661d8f03db4 Mon Sep 17 00:00:00 2001 From: Marin Minnerly <me@vocksel.com> Date: Mon, 16 Dec 2024 11:11:50 -0800 Subject: [PATCH 33/79] Manually build Plugin instances to handle reloading better --- src/Plugin/createFlipbookPlugin.luau | 57 +++++++++++++++++----------- src/init.server.luau | 12 +++++- 2 files changed, 45 insertions(+), 24 deletions(-) diff --git a/src/Plugin/createFlipbookPlugin.luau b/src/Plugin/createFlipbookPlugin.luau index e72b1989..d19ba68f 100644 --- a/src/Plugin/createFlipbookPlugin.luau +++ b/src/Plugin/createFlipbookPlugin.luau @@ -12,23 +12,16 @@ local ContextProviders = require("@root/Common/ContextProviders") local PluginApp = require("@root/Plugin/PluginApp") local function createFlipbookPlugin( - name: string, plugin: Plugin, - toolbar: PluginToolbar + widget: DockWidgetPluginGui, + button: PluginToolbarButton ): { mount: () -> (), unmount: () -> (), } - local info = DockWidgetPluginGuiInfo.new(Enum.InitialDockState.Top, true) - - local widget = plugin:CreateDockWidgetPluginGui(name, info) - widget.Name = name - widget.Title = name - widget.ZIndexBehavior = Enum.ZIndexBehavior.Sibling - + local connections: { RBXScriptConnection } = {} local root = ReactRoblox.createRoot(widget) local loader = ModuleLoader.new() - local button = toolbar:CreateButton(name, "Open story view", "rbxassetid://10277153751") local app = React.createElement(ContextProviders, { plugin = plugin, @@ -47,29 +40,47 @@ local function createFlipbookPlugin( root:render(app) end - button.Click:Connect(function() - widget.Enabled = not widget.Enabled - end) + table.insert( + connections, + button.Click:Connect(function() + widget.Enabled = not widget.Enabled + end) + ) - widget:GetPropertyChangedSignal("Enabled"):Connect(function() - button:SetActive(widget.Enabled) - end) + table.insert( + connections, + widget:GetPropertyChangedSignal("Enabled"):Connect(function() + button:SetActive(widget.Enabled) + end) + ) - widget:GetPropertyChangedSignal("Enabled"):Connect(function() - if widget.Enabled then - root:render(app) - else - unmount() - end - end) + table.insert( + connections, + widget:GetPropertyChangedSignal("Enabled"):Connect(function() + if widget.Enabled then + root:render(app) + else + unmount() + end + end) + ) if widget.Enabled then mount() end + local function destroy() + print("destroy") + unmount() + for _, connection in connections do + connection:Disconnect() + end + end + return { mount = mount, unmount = unmount, + destroy = destroy, } end diff --git a/src/init.server.luau b/src/init.server.luau index 35df6a1d..9e44fd69 100644 --- a/src/init.server.luau +++ b/src/init.server.luau @@ -9,6 +9,16 @@ local createFlipbookPlugin = require("@root/Plugin/createFlipbookPlugin") local PLUGIN_NAME = "flipbook" local toolbar = plugin:CreateToolbar(PLUGIN_NAME) -local flipbookPlugin = createFlipbookPlugin(PLUGIN_NAME, plugin, toolbar) + +local info = DockWidgetPluginGuiInfo.new(Enum.InitialDockState.Top, true) + +local widget = plugin:CreateDockWidgetPluginGui(PLUGIN_NAME, info) +widget.Name = PLUGIN_NAME +widget.Title = PLUGIN_NAME +widget.ZIndexBehavior = Enum.ZIndexBehavior.Sibling + +local button = toolbar:CreateButton(PLUGIN_NAME, "Open story view", "rbxassetid://10277153751") + +local flipbookPlugin = createFlipbookPlugin(plugin, widget, button) plugin.Unloading:Connect(flipbookPlugin.unmount) From 471b80f92958ac43b686b06743983e7e1c0e7e26 Mon Sep 17 00:00:00 2001 From: Marin Minnerly <me@vocksel.com> Date: Mon, 16 Dec 2024 13:16:40 -0800 Subject: [PATCH 34/79] Full error logging with line numbers --- src/Storybook/StoryError.luau | 50 ++++++++++++++++++++++++++++++----- 1 file changed, 43 insertions(+), 7 deletions(-) diff --git a/src/Storybook/StoryError.luau b/src/Storybook/StoryError.luau index e4012bd1..75a15d86 100644 --- a/src/Storybook/StoryError.luau +++ b/src/Storybook/StoryError.luau @@ -1,4 +1,7 @@ local React = require("@pkg/React") +local Sift = require("@pkg/Sift") + +local ScrollingFrame = require("@root/Common/ScrollingFrame") local SelectableTextLabel = require("@root/Forms/SelectableTextLabel") local useTheme = require("@root/Common/useTheme") @@ -10,16 +13,49 @@ export type Props = { local function StoryError(props: Props) local theme = useTheme() - return React.createElement(SelectableTextLabel, { + local lineNumbers = Sift.List.reduce(props.err:split("\n"), function(accumulator, _item, index) + return if index == 1 then tostring(index) else `{accumulator}\n{index}` + end, "") + + return React.createElement(ScrollingFrame, { + ScrollingDirection = Enum.ScrollingDirection.XY, LayoutOrder = props.layoutOrder, - Text = props.err, - TextColor3 = theme.alert, }, { + Layout = React.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder, + FillDirection = Enum.FillDirection.Horizontal, + Padding = theme.padding, + }), + Padding = React.createElement("UIPadding", { - PaddingTop = UDim.new(0, 8), - PaddingRight = UDim.new(0, 8), - PaddingBottom = UDim.new(0, 8), - PaddingLeft = UDim.new(0, 8), + PaddingTop = theme.paddingSmall, + PaddingRight = theme.paddingSmall, + PaddingBottom = theme.paddingSmall, + PaddingLeft = theme.paddingSmall, + }), + + LineNumbers = React.createElement("TextLabel", { + LayoutOrder = 1, + AutomaticSize = Enum.AutomaticSize.XY, + Text = lineNumbers, + TextSize = theme.textSize, + LineHeight = 1, + BackgroundTransparency = 1, + Font = Enum.Font.RobotoMono, + TextColor3 = theme.textFaded, + TextXAlignment = Enum.TextXAlignment.Right, + }), + + ErrorMessage = React.createElement(SelectableTextLabel, { + LayoutOrder = 2, + Size = UDim2.fromScale(1, 0), + AutomaticSize = Enum.AutomaticSize.Y, + Text = props.err, + TextColor3 = theme.alert, + TextSize = theme.textSize, + TextWrapped = false, + LineHeight = 1, + Font = Enum.Font.RobotoMono, }), }) end From 2a460caeb8fcfe8f515c5116261546a299d85a3d Mon Sep 17 00:00:00 2001 From: Marin Minnerly <me@vocksel.com> Date: Mon, 16 Dec 2024 13:21:23 -0800 Subject: [PATCH 35/79] Last opened story gets remembered again --- src/Common/getInstanceFromFullName.luau | 2 +- src/Common/getInstanceFromFullName.spec.luau | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Common/getInstanceFromFullName.luau b/src/Common/getInstanceFromFullName.luau index 1574c4d6..1486fe32 100644 --- a/src/Common/getInstanceFromFullName.luau +++ b/src/Common/getInstanceFromFullName.luau @@ -65,7 +65,7 @@ local function getInstanceFromFullName(fullName: string): Instance? parts = Sift.List.shift(parts, #name:split(PATH_SEPERATOR)) break else - if #tempParts > 1 then + if #tempParts == 1 then -- TODO: Need this early exit to handle cases with instances not existing later on. Verify -- if this can be effectively repro'd by calling this function on an instance that doesn't -- exist. Particularly make sure to test look-aheads for `Foo.story`, it seemed like the diff --git a/src/Common/getInstanceFromFullName.spec.luau b/src/Common/getInstanceFromFullName.spec.luau index d8f9d153..930fa8df 100644 --- a/src/Common/getInstanceFromFullName.spec.luau +++ b/src/Common/getInstanceFromFullName.spec.luau @@ -68,3 +68,8 @@ end) test("returns nil if the first part of the path is not a service", function() expect(getInstanceFromFullName("Part")).toBeUndefined() end) + +test("returns nil when instance with an extension does not exist", function() + expect(getInstanceFromFullName("foo.story")).toBeUndefined() + expect(getInstanceFromFullName("Path.To.Foo.story")).toBeUndefined() +end) From e1678054bed6580e0d908c2d16366d9174a4c6bc Mon Sep 17 00:00:00 2001 From: Marin Minnerly <me@vocksel.com> Date: Mon, 16 Dec 2024 13:40:51 -0800 Subject: [PATCH 36/79] Fix dropdown controls not changing visually --- src/Storybook/StoryControls.luau | 10 ++++++---- src/Storybook/StoryView.luau | 23 +++++------------------ 2 files changed, 11 insertions(+), 22 deletions(-) diff --git a/src/Storybook/StoryControls.luau b/src/Storybook/StoryControls.luau index 213c0912..49b17fcb 100644 --- a/src/Storybook/StoryControls.luau +++ b/src/Storybook/StoryControls.luau @@ -10,7 +10,8 @@ local useMemo = React.useMemo local e = React.createElement type Props = { - controls: { [string]: any }, + controlsSchema: { [string]: any }, + changedControls: { [string]: any }, setControl: (key: string, value: any) -> (), layoutOrder: number?, } @@ -21,7 +22,7 @@ local function StoryControls(props: Props) local sortedControls: { { name: string, value: any } } = useMemo(function() local result = {} - for _, entry in Sift.Dictionary.entries(props.controls) do + for _, entry in Sift.Dictionary.entries(props.controlsSchema) do table.insert(result, { name = entry[1], value = entry[2], @@ -31,7 +32,7 @@ local function StoryControls(props: Props) return Sift.List.sort(result, function(a, b) return a.name < b.name end) - end, { props.controls }) + end, { props.controlsSchema }) local controlElements: { [string]: React.Node } = {} for index, control in sortedControls do @@ -52,8 +53,9 @@ local function StoryControls(props: Props) onStateChange = setControl, }) elseif controlType == "table" then + local default = props.changedControls[control.name] option = React.createElement(Dropdown, { - default = control.value[1], + default = if default then default else control.value[1], options = control.value, onOptionChange = setControl, }) diff --git a/src/Storybook/StoryView.luau b/src/Storybook/StoryView.luau index 0f0ab3d1..6f4db7a8 100644 --- a/src/Storybook/StoryView.luau +++ b/src/Storybook/StoryView.luau @@ -45,23 +45,9 @@ local function StoryView(props: Props) 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 - return controls - end, { story, changedControls } :: { unknown }) + local controlsSchema = if story then story.controls else nil - local showControls = controlsWithUserOverrides and not Sift.isEmpty(controlsWithUserOverrides) + local showControls = controlsSchema and not Sift.isEmpty(controlsSchema) local setControl = React.useCallback(function(control: string, newValue: any) setChangedControls(function(prev) @@ -176,7 +162,7 @@ local function StoryView(props: Props) StoryPreview = e(StoryPreview, { zoom = zoom.value, story = story, - controls = Sift.Dictionary.merge(controlsWithUserOverrides, changedControls), + controls = Sift.Dictionary.merge(controlsSchema, changedControls), storyModule = props.story, isMountedInViewport = isMountedInViewport, ref = storyParentRef, @@ -199,7 +185,8 @@ local function StoryView(props: Props) BackgroundColor3 = theme.sidebar, }, { StoryControls = e(StoryControls, { - controls = controlsWithUserOverrides, + controlsSchema = controlsSchema, + changedControls = changedControls, setControl = setControl, }), }), From 829b34c738466dc3d3e08f8d286b701976720672 Mon Sep 17 00:00:00 2001 From: Marin Minnerly <me@vocksel.com> Date: Mon, 16 Dec 2024 14:36:34 -0800 Subject: [PATCH 37/79] Display unavailable storybooks --- src/Panels/Sidebar.luau | 5 ++++- src/Plugin/PluginApp.luau | 2 +- src/Storybook/StorybookTreeView.luau | 27 ++++++++++++++++++++++++--- 3 files changed, 29 insertions(+), 5 deletions(-) diff --git a/src/Panels/Sidebar.luau b/src/Panels/Sidebar.luau index f58b76b3..857b10f7 100644 --- a/src/Panels/Sidebar.luau +++ b/src/Panels/Sidebar.luau @@ -14,7 +14,10 @@ local e = React.createElement type Props = { layoutOrder: number?, onStoryChanged: (storyModule: ModuleScript?, storybook: LoadedStorybook?) -> (), - storybooks: { LoadedStorybook }, + storybooks: { + avialable: { LoadedStorybook }, + unavailable: { UnavailableStorybook }, + }, } local function Sidebar(props: Props) diff --git a/src/Plugin/PluginApp.luau b/src/Plugin/PluginApp.luau index 53877344..22a5ff67 100644 --- a/src/Plugin/PluginApp.luau +++ b/src/Plugin/PluginApp.luau @@ -67,7 +67,7 @@ local function App(props: Props) }, { Sidebar = React.createElement(Sidebar, { onStoryChanged = onStoryChanged, - storybooks = storybooks.available, + storybooks = storybooks, }), }), diff --git a/src/Storybook/StorybookTreeView.luau b/src/Storybook/StorybookTreeView.luau index 13a3c4e5..87d48d37 100644 --- a/src/Storybook/StorybookTreeView.luau +++ b/src/Storybook/StorybookTreeView.luau @@ -1,3 +1,5 @@ +local HttpService = game:GetService("HttpService") + local React = require("@pkg/React") local Storyteller = require("@pkg/Storyteller") local TreeView = require("@root/TreeView") @@ -8,13 +10,17 @@ local usePrevious = require("@root/Common/usePrevious") type TreeNode = TreeView.TreeNode type LoadedStorybook = Storyteller.LoadedStorybook +type UnavailableStorybook = Storyteller.UnavailableStorybook local useEffect = React.useEffect local useRef = React.useRef export type Props = { searchTerm: string?, - storybooks: { LoadedStorybook }, + storybooks: { + avialable: { LoadedStorybook }, + unavailable: { UnavailableStorybook }, + }, onStoryChanged: ((storyModule: ModuleScript?, storybook: LoadedStorybook?) -> ())?, layoutOrder: number?, } @@ -30,17 +36,32 @@ local function StorybookTreeView(props: Props) useEffect(function() storybookByNodeId.current = {} local roots: { TreeNode } = {} - for _, storybook in props.storybooks do + for _, storybook in props.storybooks.available do local root = createTreeNodesForStorybook(storybook) table.insert(roots, root) storybookByNodeId.current[root.id] = storybook end + + local unavailableStorybooks: TreeNode = { + id = HttpService:GenerateGUID(), + label = "Unavailable Storybooks", + icon = "folder", + isExpanded = false, + children = {}, + } + for _, unavailableStorybook in props.storybooks.unavailable do + local root = createTreeNodesForStorybook(unavailableStorybook.storybook) + table.insert(unavailableStorybooks.children, root) + storybookByNodeId.current[root.id] = unavailableStorybook.storybook + end + table.insert(roots, unavailableStorybooks) + treeViewContext.setRoots(roots) return function() treeViewContext.setRoots({}) end - end, { props.storybooks, treeViewContext.setRoots } :: { unknown }) + end, { props.storybooks.available, props.storybooks.unavailable, treeViewContext.setRoots } :: { unknown }) useEffect(function() treeViewContext.search(props.searchTerm) From eb0230d13c0464d94ea00f0e3ac4a36a7ccbf5bb Mon Sep 17 00:00:00 2001 From: Marin Minnerly <me@vocksel.com> Date: Mon, 16 Dec 2024 15:02:53 -0800 Subject: [PATCH 38/79] Only show unavailable storybooks if there are any --- src/Storybook/StorybookTreeView.luau | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/src/Storybook/StorybookTreeView.luau b/src/Storybook/StorybookTreeView.luau index 87d48d37..ad11aad4 100644 --- a/src/Storybook/StorybookTreeView.luau +++ b/src/Storybook/StorybookTreeView.luau @@ -42,19 +42,21 @@ local function StorybookTreeView(props: Props) storybookByNodeId.current[root.id] = storybook end - local unavailableStorybooks: TreeNode = { - id = HttpService:GenerateGUID(), - label = "Unavailable Storybooks", - icon = "folder", - isExpanded = false, - children = {}, - } - for _, unavailableStorybook in props.storybooks.unavailable do - local root = createTreeNodesForStorybook(unavailableStorybook.storybook) - table.insert(unavailableStorybooks.children, root) - storybookByNodeId.current[root.id] = unavailableStorybook.storybook + if #props.storybooks.unavailable > 0 then + local unavailableStorybooks: TreeNode = { + id = HttpService:GenerateGUID(), + label = "Unavailable Storybooks", + icon = "folder", + isExpanded = false, + children = {}, + } + for _, unavailableStorybook in props.storybooks.unavailable do + local root = createTreeNodesForStorybook(unavailableStorybook.storybook) + table.insert(unavailableStorybooks.children, root) + storybookByNodeId.current[root.id] = unavailableStorybook.storybook + end + table.insert(roots, unavailableStorybooks) end - table.insert(roots, unavailableStorybooks) treeViewContext.setRoots(roots) From dc72958bec8dbe38c99ca8cacd11b478fda0482c Mon Sep 17 00:00:00 2001 From: Marin Minnerly <me@vocksel.com> Date: Mon, 16 Dec 2024 16:26:03 -0800 Subject: [PATCH 39/79] Add a button to create the first storybook, story, and component --- src/Panels/Sidebar.luau | 17 +++++++++++-- .../OnboardingTemplate/ComponentTemplate.luau | 17 +++++++++++++ .../OnboardingTemplate/StoryTemplate.luau | 18 ++++++++++++++ .../OnboardingTemplate/StorybookTemplate.luau | 5 ++++ src/Storybook/createOnboardingStorybook.luau | 24 +++++++++++++++++++ 5 files changed, 79 insertions(+), 2 deletions(-) create mode 100644 src/Storybook/OnboardingTemplate/ComponentTemplate.luau create mode 100644 src/Storybook/OnboardingTemplate/StoryTemplate.luau create mode 100644 src/Storybook/OnboardingTemplate/StorybookTemplate.luau create mode 100644 src/Storybook/createOnboardingStorybook.luau diff --git a/src/Panels/Sidebar.luau b/src/Panels/Sidebar.luau index 857b10f7..f8f2d900 100644 --- a/src/Panels/Sidebar.luau +++ b/src/Panels/Sidebar.luau @@ -2,9 +2,12 @@ local React = require("@pkg/React") local Storyteller = require("@pkg/Storyteller") local Branding = require("@root/Common/Branding") +local Button = require("@root/Forms/Button") local ScrollingFrame = require("@root/Common/ScrollingFrame") local Searchbar = require("@root/Forms/Searchbar") local StorybookTreeView = require("@root/Storybook/StorybookTreeView") +local createOnboardingStorybook = require("@root/Storybook/createOnboardingStorybook") +local nextLayoutOrder = require("@root/Common/nextLayoutOrder") local useTheme = require("@root/Common/useTheme") type LoadedStorybook = Storyteller.LoadedStorybook @@ -58,7 +61,7 @@ local function Sidebar(props: Props) Header = e("Frame", { AutomaticSize = Enum.AutomaticSize.Y, BackgroundTransparency = 1, - LayoutOrder = 0, + LayoutOrder = nextLayoutOrder(), Size = UDim2.fromScale(1, 0), [React.Change.AbsoluteSize] = onHeaderSizeChanged, }, { @@ -77,8 +80,18 @@ local function Sidebar(props: Props) }), }), + CreateStorybook = if #props.storybooks.available == 0 + then React.createElement(Button, { + layoutOrder = nextLayoutOrder(), + text = "Create Storybook", + onClick = function() + createOnboardingStorybook(game.ReplicatedStorage) + end, + }) + else nil, + ScrollingFrame = e(ScrollingFrame, { - LayoutOrder = 1, + LayoutOrder = nextLayoutOrder(), Size = UDim2.fromScale(1, 1) - UDim2.fromOffset(0, headerHeight), }, { StorybookTreeView = e(StorybookTreeView, { diff --git a/src/Storybook/OnboardingTemplate/ComponentTemplate.luau b/src/Storybook/OnboardingTemplate/ComponentTemplate.luau new file mode 100644 index 00000000..c820ce8e --- /dev/null +++ b/src/Storybook/OnboardingTemplate/ComponentTemplate.luau @@ -0,0 +1,17 @@ +export type Props = { + nameToGreet: string, +} + +local function HelloWorld(props: Props) + local label = Instance.new("TextLabel") + label.Text = `Hello, {props.nameToGreet}!` + label.AutomaticSize = Enum.AutomaticSize.XY + label.BackgroundTransparency = 1 + label.TextColor3 = Color3.fromRGB(255, 255, 255) + label.TextSize = 24 + label.Font = Enum.Font.BuilderSansMedium + + return label +end + +return HelloWorld diff --git a/src/Storybook/OnboardingTemplate/StoryTemplate.luau b/src/Storybook/OnboardingTemplate/StoryTemplate.luau new file mode 100644 index 00000000..aa9a465e --- /dev/null +++ b/src/Storybook/OnboardingTemplate/StoryTemplate.luau @@ -0,0 +1,18 @@ +local HelloWorld = require(script.Parent.HelloWorld) + +local controls = { + nameToGreet = "World", +} + +type Props = { + controls: typeof(controls), +} + +return { + controls = controls, + story = function(props: Props) + return HelloWorld({ + nameToGreet = props.controls.nameToGreet, + }) + end, +} diff --git a/src/Storybook/OnboardingTemplate/StorybookTemplate.luau b/src/Storybook/OnboardingTemplate/StorybookTemplate.luau new file mode 100644 index 00000000..d42a9f59 --- /dev/null +++ b/src/Storybook/OnboardingTemplate/StorybookTemplate.luau @@ -0,0 +1,5 @@ +return { + storyRoots = { + script.Parent.Components, + }, +} diff --git a/src/Storybook/createOnboardingStorybook.luau b/src/Storybook/createOnboardingStorybook.luau new file mode 100644 index 00000000..4cc4393c --- /dev/null +++ b/src/Storybook/createOnboardingStorybook.luau @@ -0,0 +1,24 @@ +local STORYBOOK_TEMPLATE = script.Parent.OnboardingTemplate["StorybookTemplate"] +local STORY_TEMPLATE = script.Parent.OnboardingTemplate["StoryTemplate"] +local COMPONENT_TEMPLATE = script.Parent.OnboardingTemplate["ComponentTemplate"] + +local function createOnboardingStorybook(parent: Instance) + local components = Instance.new("Folder") + components.Name = "Components" + + local component = COMPONENT_TEMPLATE:Clone() + component.Name = "HelloWorld" + component.Parent = components + + local story = STORY_TEMPLATE:Clone() + story.Name = "HelloWorld.story" + story.Parent = components + + local storybookModule = STORYBOOK_TEMPLATE:Clone() + storybookModule.Name = `{string.gsub(game.Name, "%.", "_")}.storybook` + + components.Parent = parent + storybookModule.Parent = parent +end + +return createOnboardingStorybook From 08329ff3fd63938e95de6a04af9228d5c975b807 Mon Sep 17 00:00:00 2001 From: Marin Minnerly <me@vocksel.com> Date: Tue, 17 Dec 2024 11:01:20 -0800 Subject: [PATCH 40/79] Story pinning almost works, gonna come back to this later --- src/Storybook/StorybookTreeView.luau | 38 +++++++- .../createTreeNodesForStorybook.luau | 1 + src/TreeView/TreeNode.luau | 23 ++++- src/TreeView/TreeViewContext.luau | 2 +- src/TreeView/usePinnedInstances.luau | 93 +++++++++++++++++++ 5 files changed, 154 insertions(+), 3 deletions(-) create mode 100644 src/TreeView/usePinnedInstances.luau diff --git a/src/Storybook/StorybookTreeView.luau b/src/Storybook/StorybookTreeView.luau index ad11aad4..2ff00dd9 100644 --- a/src/Storybook/StorybookTreeView.luau +++ b/src/Storybook/StorybookTreeView.luau @@ -6,6 +6,7 @@ local TreeView = require("@root/TreeView") local createTreeNodesForStorybook = require("@root/Storybook/createTreeNodesForStorybook") local useLastOpenedStory = require("@root/Storybook/useLastOpenedStory") +local usePinnedInstances = require("@root/TreeView/usePinnedInstances") local usePrevious = require("@root/Common/usePrevious") type TreeNode = TreeView.TreeNode @@ -32,10 +33,43 @@ local function StorybookTreeView(props: Props) local prevSelectedNode = usePrevious(selectedNode) local storybookByNodeId = useRef({} :: { [string]: LoadedStorybook }) local lastOpenedStory, setLastOpenedStory = useLastOpenedStory() + local pinning = usePinnedInstances() useEffect(function() - storybookByNodeId.current = {} local roots: { TreeNode } = {} + + local pinnedInstances = pinning.getPinnedInstances() + if #pinnedInstances > 0 then + local pins: TreeNode = { + id = HttpService:GenerateGUID(), + label = "Starred", + icon = "folder", + isExpanded = false, + children = {}, + } + + for _, pinnedInstance in pinnedInstances do + local node: { TreeNode } + if pinnedInstance.instance then + node = treeViewContext.getNodeByInstance(pinnedInstance.instance) + end + + if not node then + node = { + id = HttpService:GenerateGUID(), + label = `ERR: {pinnedInstance.path}`, + icon = "folder", -- TODO: Use an error icon + isExpanded = true, + children = {}, + } + end + + table.insert(pins.children, node) + end + + table.insert(roots, pins) + end + for _, storybook in props.storybooks.available do local root = createTreeNodesForStorybook(storybook) table.insert(roots, root) @@ -50,11 +84,13 @@ local function StorybookTreeView(props: Props) isExpanded = false, children = {}, } + for _, unavailableStorybook in props.storybooks.unavailable do local root = createTreeNodesForStorybook(unavailableStorybook.storybook) table.insert(unavailableStorybooks.children, root) storybookByNodeId.current[root.id] = unavailableStorybook.storybook end + table.insert(roots, unavailableStorybooks) end diff --git a/src/Storybook/createTreeNodesForStorybook.luau b/src/Storybook/createTreeNodesForStorybook.luau index ed061634..49968f23 100644 --- a/src/Storybook/createTreeNodesForStorybook.luau +++ b/src/Storybook/createTreeNodesForStorybook.luau @@ -46,6 +46,7 @@ local function createTreeNodesForStorybook(storybook: LoadedStorybook): TreeNode local parentNode: TreeNode = { id = HttpService:GenerateGUID(), label = parentInstance.Name, + instance = parentInstance, icon = "folder", isExpanded = false, children = { currentNode }, diff --git a/src/TreeView/TreeNode.luau b/src/TreeView/TreeNode.luau index 0842f63f..10136ac7 100644 --- a/src/TreeView/TreeNode.luau +++ b/src/TreeView/TreeNode.luau @@ -6,6 +6,7 @@ local TreeViewContext = require("@root/TreeView/TreeViewContext") local assets = require("@root/assets") local constants = require("@root/constants") local types = require("@root/TreeView/types") +local usePinnedInstances = require("@root/TreeView/usePinnedInstances") local useTheme = require("@root/Common/useTheme") local useTreeNodeIcon = require("@root/TreeView/useTreeNodeIcon") @@ -39,6 +40,7 @@ local function TreeNode(props: Props) local treeViewContext = TreeViewContext.use() local isExpanded = treeViewContext.isExpanded(props.node) local isSelected = treeViewContext.isSelected(props.node) + local pinning = usePinnedInstances() local styles = useSpring({ hover = if isHovered or isSelected then 0 else 1, @@ -79,6 +81,12 @@ local function TreeNode(props: Props) treeViewContext.activateNode(props.node) end, { props.onActivated, treeViewContext, props.node } :: { unknown }) + local onTogglePin = useCallback(function() + if props.node.instance then + pinning.togglePin(props.node.instance) + end + end, { pinning, props.node }) + local backgroundColor = useMemo(function(): Color3? if isSelected then return theme.selection @@ -150,9 +158,22 @@ local function TreeNode(props: Props) }), }), + Pin = React.createElement("ImageButton", { + LayoutOrder = 3, + BackgroundTransparency = 1, + AutomaticSize = Enum.AutomaticSize.XY, + [React.Event.Activated] = onTogglePin, + }, { + Icon = React.createElement(Sprite, { + image = assets.Magnify, -- TODO: Use a new icon for pinning + color = theme.text, + size = UDim2.fromOffset(16, 16), + }), + }), + Toggle = if #props.node.children > 0 then React.createElement("Frame", { - LayoutOrder = 3, + LayoutOrder = 4, BackgroundTransparency = 1, AutomaticSize = Enum.AutomaticSize.XY, }, { diff --git a/src/TreeView/TreeViewContext.luau b/src/TreeView/TreeViewContext.luau index e89b824b..93d18f45 100644 --- a/src/TreeView/TreeViewContext.luau +++ b/src/TreeView/TreeViewContext.luau @@ -110,7 +110,7 @@ local function TreeNodeProvider(props: { local getNodeByInstance = useCallback(function(instance: Instance) return nodes.byInstance[instance] - end, { nodes.byId }) + end, { nodes.byInstance }) local getSelectedNode = useCallback(function() return selectedNode diff --git a/src/TreeView/usePinnedInstances.luau b/src/TreeView/usePinnedInstances.luau new file mode 100644 index 00000000..f0d9349a --- /dev/null +++ b/src/TreeView/usePinnedInstances.luau @@ -0,0 +1,93 @@ +local HttpService = game:GetService("HttpService") + +local React = require("@pkg/React") + +local PluginContext = require("@root/Plugin/PluginContext") +local getInstanceFromFullName = require("@root/Common/getInstanceFromFullName") + +local useContext = React.useContext +local useCallback = React.useCallback + +local PINNED_INSTANCES_KEY = "pinnedInstancePaths" + +export type PinnedInstance = { + path: string, + instance: Instance?, +} + +local function usePinnedInstances(): (ModuleScript?, (storyModule: ModuleScript?) -> ()) + local plugin = useContext(PluginContext.Context) + + local readPinnedPathsFromDisk = useCallback(function(): { string } + local data = plugin:GetSetting(PINNED_INSTANCES_KEY) + if data then + local json = HttpService:JSONDecode(data) + if json then + return json + end + end + return {} + end, { plugin }) + + local writePinnedPathsToDisk = useCallback(function(pins: { string }) + local data = HttpService:JSONEncode(pins) + plugin:SetSetting(PINNED_INSTANCES_KEY, data) + end, { plugin }) + + local pin = useCallback(function(instance: Instance) + local pinnedPaths = readPinnedPathsFromDisk() + + table.insert(pinnedPaths, instance:GetFullName()) + + writePinnedPathsToDisk(pinnedPaths) + end, { readPinnedPathsFromDisk, writePinnedPathsToDisk }) + + local unpin = useCallback(function(instance: Instance) + local pinnedPaths = readPinnedPathsFromDisk() + + local index = table.find(pinnedPaths, instance:GetFullName()) + if index then + table.remove(pinnedPaths, index) + end + + writePinnedPathsToDisk(pinnedPaths) + end, { readPinnedPathsFromDisk, writePinnedPathsToDisk }) + + local getPinnedInstances = useCallback(function(): { PinnedInstance } + local pinnedPaths = readPinnedPathsFromDisk() + local pinnedInstances: { PinnedInstance } = {} + + for _, pinnedPath in pinnedPaths do + table.insert(pinnedInstances, { + path = pinnedPath, + instance = getInstanceFromFullName(pinnedPath), + }) + end + + return pinnedInstances + end, { readPinnedPathsFromDisk }) + + local togglePin = useCallback(function(instance: Instance) + local pinnedPaths = readPinnedPathsFromDisk() + local path = instance:GetFullName() + + local index = table.find(pinnedPaths, path) + + if index then + table.remove(pinnedPaths, index) + else + table.insert(pinnedPaths, path) + end + + writePinnedPathsToDisk(pinnedPaths) + end, { readPinnedPathsFromDisk, writePinnedPathsToDisk }) + + return { + pin = pin, + unpin = unpin, + togglePin = togglePin, + getPinnedInstances = getPinnedInstances, + } +end + +return usePinnedInstances From 88e2d8e757a400d8ed8d839068bc236d13db70d9 Mon Sep 17 00:00:00 2001 From: Marin Minnerly <me@vocksel.com> Date: Tue, 17 Dec 2024 13:08:57 -0800 Subject: [PATCH 41/79] Sync onboarding storybook to filesystem --- src/Panels/Sidebar.luau | 26 +++++++++----- src/RobloxInternal/getInternalSyncItems.luau | 22 ++++++++++++ .../getMostLikelyProjectSources.luau | 35 +++++++++++++++++++ src/RobloxInternal/tryGetService.luau | 21 +++++++++++ src/Storybook/createOnboardingStorybook.luau | 11 +++--- 5 files changed, 102 insertions(+), 13 deletions(-) create mode 100644 src/RobloxInternal/getInternalSyncItems.luau create mode 100644 src/RobloxInternal/getMostLikelyProjectSources.luau create mode 100644 src/RobloxInternal/tryGetService.luau diff --git a/src/Panels/Sidebar.luau b/src/Panels/Sidebar.luau index f8f2d900..0c9856e7 100644 --- a/src/Panels/Sidebar.luau +++ b/src/Panels/Sidebar.luau @@ -1,3 +1,5 @@ +local ReplicatedStorage = game:GetService("ReplicatedStorage") + local React = require("@pkg/React") local Storyteller = require("@pkg/Storyteller") @@ -7,6 +9,7 @@ local ScrollingFrame = require("@root/Common/ScrollingFrame") local Searchbar = require("@root/Forms/Searchbar") local StorybookTreeView = require("@root/Storybook/StorybookTreeView") local createOnboardingStorybook = require("@root/Storybook/createOnboardingStorybook") +local getMostLikelyProjectSources = require("@root/RobloxInternal/getMostLikelyProjectSources") local nextLayoutOrder = require("@root/Common/nextLayoutOrder") local useTheme = require("@root/Common/useTheme") @@ -80,15 +83,20 @@ local function Sidebar(props: Props) }), }), - CreateStorybook = if #props.storybooks.available == 0 - then React.createElement(Button, { - layoutOrder = nextLayoutOrder(), - text = "Create Storybook", - onClick = function() - createOnboardingStorybook(game.ReplicatedStorage) - end, - }) - else nil, + --if #props.storybooks.available == 0 + CreateStorybook = React.createElement(Button, { + layoutOrder = nextLayoutOrder(), + text = "Create Storybook", + onClick = function() + local source = getMostLikelyProjectSources()[1] + + if source then + createOnboardingStorybook(source.Name, source) + else + createOnboardingStorybook(string.gsub(game.Name, "%.", "_"), ReplicatedStorage) + end + end, + }), ScrollingFrame = e(ScrollingFrame, { LayoutOrder = nextLayoutOrder(), diff --git a/src/RobloxInternal/getInternalSyncItems.luau b/src/RobloxInternal/getInternalSyncItems.luau new file mode 100644 index 00000000..3751541a --- /dev/null +++ b/src/RobloxInternal/getInternalSyncItems.luau @@ -0,0 +1,22 @@ +local tryGetService = require("@root/RobloxInternal/tryGetService") + +-- selene: allow(incorrect_standard_library_use) +type FileSyncService = typeof(game:GetService("FileSyncService")) + +local FileSyncService: FileSyncService? = tryGetService("FileSyncService") + +local function getInternalSyncItems(): { InternalSyncItem } + local internalSyncItems: { InternalSyncItem } = {} + + if FileSyncService then + for _, child in FileSyncService:GetChildren() do + if child:IsA("InternalSyncItem") then + table.insert(internalSyncItems, child) + end + end + end + + return internalSyncItems +end + +return getInternalSyncItems diff --git a/src/RobloxInternal/getMostLikelyProjectSources.luau b/src/RobloxInternal/getMostLikelyProjectSources.luau new file mode 100644 index 00000000..d8419cc0 --- /dev/null +++ b/src/RobloxInternal/getMostLikelyProjectSources.luau @@ -0,0 +1,35 @@ +local Sift = require("@pkg/Sift") + +local getInternalSyncItems = require("@root/RobloxInternal/getInternalSyncItems") + +local PATTERNS = { + "src.*$", + "dist.*$", + "modules.*$", +} + +local function getMostLikelyProjectSources(): { Instance } + local internalSyncItems = getInternalSyncItems() + + if #internalSyncItems > 0 then + local sorted = Sift.List.sort(internalSyncItems, function(a: InternalSyncItem, b: InternalSyncItem) + for _, pattern in PATTERNS do + local aMatches = a.Path:match(pattern) + local bMatches = b.Path:match(pattern) + + if aMatches and not bMatches then + return true + elseif bMatches and not aMatches then + return false + end + end + end) + + return Sift.List.map(sorted, function(internalSyncItem) + return internalSyncItem.Target + end) + end + return {} +end + +return getMostLikelyProjectSources diff --git a/src/RobloxInternal/tryGetService.luau b/src/RobloxInternal/tryGetService.luau new file mode 100644 index 00000000..d2dda643 --- /dev/null +++ b/src/RobloxInternal/tryGetService.luau @@ -0,0 +1,21 @@ +local function tryGetService(serviceName: string): Instance + local service + + pcall(function() + service = game:GetService(serviceName) + end) + + if service then + return service + end + + -- Some services cannot be retrieved by GetService but still exist in the DM + -- and can be retrieved by name. + pcall(function() + service = game:FindFirstChild(serviceName) + end) + + return service +end + +return tryGetService diff --git a/src/Storybook/createOnboardingStorybook.luau b/src/Storybook/createOnboardingStorybook.luau index 4cc4393c..fea9989f 100644 --- a/src/Storybook/createOnboardingStorybook.luau +++ b/src/Storybook/createOnboardingStorybook.luau @@ -2,9 +2,12 @@ local STORYBOOK_TEMPLATE = script.Parent.OnboardingTemplate["StorybookTemplate"] local STORY_TEMPLATE = script.Parent.OnboardingTemplate["StoryTemplate"] local COMPONENT_TEMPLATE = script.Parent.OnboardingTemplate["ComponentTemplate"] -local function createOnboardingStorybook(parent: Instance) - local components = Instance.new("Folder") - components.Name = "Components" +local function createOnboardingStorybook(storybookName: string, parent: Instance) + local components = parent:FindFirstChild("Components") + if not components then + components = Instance.new("Folder") + components.Name = "Components" + end local component = COMPONENT_TEMPLATE:Clone() component.Name = "HelloWorld" @@ -15,7 +18,7 @@ local function createOnboardingStorybook(parent: Instance) story.Parent = components local storybookModule = STORYBOOK_TEMPLATE:Clone() - storybookModule.Name = `{string.gsub(game.Name, "%.", "_")}.storybook` + storybookModule.Name = `{storybookName}.storybook` components.Parent = parent storybookModule.Parent = parent From ffc379ddcb06dc290582b90dab7c751a65cccc27 Mon Sep 17 00:00:00 2001 From: Marin Minnerly <me@vocksel.com> Date: Tue, 17 Dec 2024 13:49:39 -0800 Subject: [PATCH 42/79] Watch Packages folder for changes --- .lune/build.luau | 1 + 1 file changed, 1 insertion(+) diff --git a/.lune/build.luau b/.lune/build.luau index c34d257b..85d23d7b 100644 --- a/.lune/build.luau +++ b/.lune/build.luau @@ -51,6 +51,7 @@ if args.watch then filePatterns = { "src/.*%.luau", "example/.*%.luau", + "Packages/.*%.luau", }, onChanged = build, }) From ed5215388f0d3bfe82da37b7620c803d7d94f2e8 Mon Sep 17 00:00:00 2001 From: Marin Minnerly <me@vocksel.com> Date: Tue, 17 Dec 2024 13:49:46 -0800 Subject: [PATCH 43/79] Install ModuleLoader from disk --- .lune/wally-install.luau | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.lune/wally-install.luau b/.lune/wally-install.luau index 1798f17b..04d05e79 100644 --- a/.lune/wally-install.luau +++ b/.lune/wally-install.luau @@ -16,7 +16,7 @@ do run("wally", { "install" }) installPackageFromDisk("Storyteller", "../storyteller") - -- installPackageFromDisk("ModuleLoader", "../module-loader") + installPackageFromDisk("ModuleLoader", "../module-loader") run("rojo", { "sourcemap", "build.project.json", "-o", "sourcemap.json" }) run("wally-package-types", { "--sourcemap", "sourcemap.json", "Packages" }) From 58dd60e3465304f319694c010c9c5c5afb2e98b3 Mon Sep 17 00:00:00 2001 From: Marin Minnerly <me@vocksel.com> Date: Tue, 17 Dec 2024 15:05:19 -0800 Subject: [PATCH 44/79] Only allow pinning stories and storybooks for right now --- src/TreeView/TreeNode.luau | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/src/TreeView/TreeNode.luau b/src/TreeView/TreeNode.luau index 10136ac7..59303997 100644 --- a/src/TreeView/TreeNode.luau +++ b/src/TreeView/TreeNode.luau @@ -158,18 +158,20 @@ local function TreeNode(props: Props) }), }), - Pin = React.createElement("ImageButton", { - LayoutOrder = 3, - BackgroundTransparency = 1, - AutomaticSize = Enum.AutomaticSize.XY, - [React.Event.Activated] = onTogglePin, - }, { - Icon = React.createElement(Sprite, { - image = assets.Magnify, -- TODO: Use a new icon for pinning - color = theme.text, - size = UDim2.fromOffset(16, 16), - }), - }), + Pin = if props.node.icon == "story" or props.node.icon == "storybook" + then React.createElement("ImageButton", { + LayoutOrder = 3, + BackgroundTransparency = 1, + AutomaticSize = Enum.AutomaticSize.XY, + [React.Event.Activated] = onTogglePin, + }, { + Icon = React.createElement(Sprite, { + image = assets.Magnify, -- TODO: Use a new icon for pinning + color = theme.text, + size = UDim2.fromOffset(16, 16), + }), + }) + else nil, Toggle = if #props.node.children > 0 then React.createElement("Frame", { From 9136d1690405dd72e79f5645981b9ed714ccce76 Mon Sep 17 00:00:00 2001 From: Marin Minnerly <me@vocksel.com> Date: Tue, 17 Dec 2024 15:13:18 -0800 Subject: [PATCH 45/79] Remove todo --- src/Common/getInstanceFromFullName.luau | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/Common/getInstanceFromFullName.luau b/src/Common/getInstanceFromFullName.luau index 1486fe32..0b9fe2e9 100644 --- a/src/Common/getInstanceFromFullName.luau +++ b/src/Common/getInstanceFromFullName.luau @@ -41,11 +41,6 @@ local function getInstanceFromFullName(fullName: string): Instance? local current = maybeGetService(serviceName) if current then - -- TODO: Verify this isn't also needed with the below TODO - -- if #parts == 1 then - -- return current - -- end - while #parts > 0 do -- Keep around a copy of the `parts` array. We are going to concat this -- into new paths, and incrementally remove from the right to narrow From 0d135a2c25c0119cc78b1f84cb4d06c9d9e0041e Mon Sep 17 00:00:00 2001 From: Marin Minnerly <me@vocksel.com> Date: Tue, 17 Dec 2024 15:16:56 -0800 Subject: [PATCH 46/79] Add new icons --- img/Star.png | Bin 0 -> 453 bytes img/Star@2x.png | Bin 0 -> 857 bytes img/Star@3x.png | Bin 0 -> 1192 bytes img/StarFilled.png | Bin 0 -> 336 bytes img/StarFilled@2x.png | Bin 0 -> 578 bytes img/StarFilled@3x.png | Bin 0 -> 808 bytes img/Warning.png | Bin 0 -> 409 bytes img/Warning@2x.png | Bin 0 -> 659 bytes img/Warning@3x.png | Bin 0 -> 937 bytes src/assets.luau | 85 ++++++++++++++++++++++++++++++++++++------ tarmac-manifest.toml | 74 +++++++++++++++++++++++++++++++----- 11 files changed, 138 insertions(+), 21 deletions(-) create mode 100644 img/Star.png create mode 100644 img/Star@2x.png create mode 100644 img/Star@3x.png create mode 100644 img/StarFilled.png create mode 100644 img/StarFilled@2x.png create mode 100644 img/StarFilled@3x.png create mode 100644 img/Warning.png create mode 100644 img/Warning@2x.png create mode 100644 img/Warning@3x.png diff --git a/img/Star.png b/img/Star.png new file mode 100644 index 0000000000000000000000000000000000000000..e1bba5dac9494753ae04e9f298d7e9714d126b21 GIT binary patch literal 453 zcmV;$0XqJPP)<h;3K|Lk000e1NJLTq000>P000;W1^@s654Bdt00009a7bBm000XU z000XU0RWnu7ytkO0drDELIAGL9O(c600d`2O+f$vv5yP<VFdsH0a{5!K~#7F#Z-Z9 zgD?<$XMwUo*`Q>C(hV{K8>Ac5BhU%R2F(VrLDC6GCJ3Ft{Z}s1$2HU`R&&w`u<!qT z7B<m8z{Cojb2&fO_<T3U94IBkfY~!opuA_465OzHXXK#3QYxsXjT+k^QJOS5MZnn7 zT!d1nxl5Y-w?4)zl=ubs$XqdHZ0Y|^Xue|(%q{cC+zVMdP+(OVvvt=vVXUC(q%E`> z&5wj?-z{?$vjrmyEzv_vYhUoe+)*MRUF#24Nb*KAAwAHm3QgL%M#{iWiT-R9We_kb zDH3YCZEOwU?`Uq&*HSY!znG}~q8s5x?y^NdSkn2FcNC8fHAR$(67D_o40|6stesao z4IIqg8(X{UtE2bN0milznM5$4u*Au&k?e)J#CwCrC29`L2Z~H$)oL9QQ~_P3G?(XV vsHYh*67duBler0A-O7v~&&(t1EZqy=tghL9YCytM00000NkvXXu0mjfB@w&9 literal 0 HcmV?d00001 diff --git a/img/Star@2x.png b/img/Star@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..61f03b207e1d2665015b9b9f5a3f01de9cdf492c GIT binary patch literal 857 zcmV-f1E&0mP)<h;3K|Lk000e1NJLTq001!n001xu1^@s6xWJOR00009a7bBm000&x z000&x0ZCFM@Bjb+0drDELIAGL9O(c600d`2O+f$vv5yP<VFdsH0`5sfK~#7F?OKg> zn=lYQGJ!im=>&Brh&w^DLA^n}LD`_$z<7eZOn^H9WCF+pbc3t(bhw8QaM)5TznAa5 zd&o$-AKeL^Bm;aP4kQo=h`3UZ4gIW=Bx&H5pbYq_sKq{h!L2|mi{tnWu|p}jwUjOt zmDam0#0;fi&6Is+@1NNFn&wM<u)LrZO<CQ_nj;y+2+IqZqdsHxZFJt4@&aoUw>Rdz zz}m&>4T%?6+c>=;^#W@jmp9O?%pxzaKG*jKDY3!f%A76Wky(BB#Pck#Bol%(!!^-o zR(1($6n#@k-d_-aco)RahUQTby>(?N?@mCf=oyCz+!zW;7>&p&P>#Jwj%u_PD6i|w zE(G90C?1cBaxzFVa6fDw31g;RTq;UgedpF!Fd-9sU!VsPOvvizJ?X>4PKVVOXL!+w zbf|rdvM-<sy_)H_<Mnr}%&mP<mL4GzkoOPlRqx}%jc8c;9^%6TD>u#-I9!CH*@fY7 zO-iR4@2tjG-xuRKiz<EYqA<i^K)$5&)eX7G0LJ8MhIlc6F?)AIyc)om+J@{P5O8?p zlY=i<hO`g}ICA9Ui@CjqtidzTq!y=v!UdV}oum;ZTNQg1VoRB_@}fjjWCS7fUbtap z@1E|$=>!M_D`Zn?KdC1+tbBxUs77Q?;v6GhAsk*<SsYSQBfhfo5yF9c;z8I)cHL#o zgHLlO4`_CUdM~U;Q@3pt9p*M#Xtv1h5?LakIS=Yk&a~}LO2)9Co3Qx*QbtemM-V^~ zPAMzDfm^Wtdg;7M8#yI-tEk559PAH0SC;HiEH$FCD=Boe@)`8kl3dYuqJz~fqZDHc zL-6qNvo>KrDdSgII?M`3$Q0-}DZ$~1#fc~OZ<Y3}&QG$5=0MFBS~`?zB|>RXYDfPP zhx2IzzJT+`jrw9y?0Pg_d79x_gHNEDxUR2Gami?rG5HAJRLW=3-y+bY;3@q)dijP! j?*u*tlfhQ-fg8j>4`TF){03tu00000NkvXXu0mjf@a2R3 literal 0 HcmV?d00001 diff --git a/img/Star@3x.png b/img/Star@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..b155368ec472383391e79bc81e106936a9428949 GIT binary patch literal 1192 zcmV;Z1XufsP)<h;3K|Lk000e1NJLTq002n<002k`1^@s6x-Zyy00009a7bBm001F4 z001F40Y#QEU;qFB0drDELIAGL9O(c600d`2O+f$vv5yP<VFdsH1U*SaK~#7F?VO8s z+aMH%uX8$pH>fs<H>kQnx`8u6+zFCSkYob66SSQ`?gVirkUD|0!R@8sM#dKeLPpQ; z9FFzkq2E6U0XD$Q%*@Qpe-Lp=;hw?^z9Y8pC?qg-NQ4!I0?mkXE0`+6B@_-=&tZ~6 zDS--f=P<ddM>m`q#CW2xHJr;~Dkv9mth8&ms+htQFt}*LT(_B|n+z^0EZ66gbCbbE zYs>ZNq}=4|qDs7cGH&v9Q6=6q+|<-XmH7GS-PF`YmH6X!Q>cq7>ExqzQ>cq7>5S7& zZMmqDUOqZE(GC1)!$p<!I5&y0rzH#xl;cEsn=Y!P&8CYN98Ot40O}iDsqm3PqP7!5 zl-tE(v4yZaQ~EHqMQ;@LD*QQaQ=V@SfU`*In@T~7aTLbK=Fe2v4YUB2<$*r#J))e6 zM`hVT8>oi&5#2@*Z9GBC2%#_xaZeeB4CbL6hz}K~pl|~hg3?~2-w3A(t1B0V%tfRJ z@IzR-1ZnA4Xw%X)plqi^xQ6$3(U&ci_m(@cU@~0OM+RneG5&2+1|{?elx=7NQZQHX zzS4cvHw}0?|8+!EJJALtFmO<IPfXSH;F7?Si8##8nghspcF){)rR{Ted=Bkr^qB9= zu`@&HB9$(YMntoF!}G^}G2-f0Fx9R8UQtdE>SQo7P%hHYaT|I;^W>q=Ye26t2*ftp zwnR2#70M!z|Em+4=i~<~ltrKyXadSU)5}<dvI&GC52mpQbuob|84OyeiwjiAXpllh z6R48mpoEGpP$grVP^}EjwgJkw{P+S@3SbiIfIWGALH>0k?r4TZsLtJFa8WnIJxVD3 zW%ws_4D!?J2?THj4N+rEzqG^G&^?K^zY85&ZY+|b)b<nTo<!R(A&^j!K56?UbdNrY z+GOVBgiw+60=z=^{MPnYp^bK9kx-7k=$Y@Jd-lw+AUB0LiOYBo-LqqkrO*Z3qZWDs z(=`cN5!a@ZcF-U6u1WmrraEVRY_yN))qaTqi@ZKSGq?<cdfQ6)r3z(W2XMz6OCe^P ziTt#a_hS~{e}7vr=@l5Teu!z8;!ca<`MO2#fpm&<gKle!#5HU1gehb2dQIN~VAF32 z&5KT5#7aE!X~pdV?}pn@(Aq{DWz~A5P2su}{7EPXp)yCY_J%aP34JX&@@6|pH+4rJ z!tNVO{BIP#Q1}dTwMS{Vc1<D2`mw8$ITmM33HfR8wc*;-p=#xvGgLlXtgnJMmb`UR zhyn5oy`JFw1~aU`!PFtYx<6PzWU3(Y69K^^0?rr;Eea}@Gv{dMxU|sA`slXOHdkff zmQ_c8x;0%xEtcOU&=7R1f+nK(lSGv(JPIfV;!|h|^#6wnacu&zaV)5?{KaNa(5(uJ z^YPjurjWu>8$6v}5{1ez;_uK<KFPLBIlSAxnVFfHzVjFN36T_;y(ka>0000<MNUMn GLSTZjX)R~~ literal 0 HcmV?d00001 diff --git a/img/StarFilled.png b/img/StarFilled.png new file mode 100644 index 0000000000000000000000000000000000000000..fd0fbb122a9f443edb0397a278a63efff0773472 GIT binary patch literal 336 zcmeAS@N?(olHy`uVBq!ia0vp^l0YoM!3HGxw}u@9Qk(@Ik;M!Q+`=Ht$S`Y;1W=H% zILO_JVcj{Imp~3nx}&cn1H;CC?mvmFK>kxt7srqa#-~#r@-`U=w95Nj5Zu+6qo8cb zl=pybM`PXrj#n%@7?tO&U#PWGWJkdN7t{A#*mC^t-Xg{y)-FeacuNbjYnav6J@swA zVvwmhe~L%v{$&$N@|5&0?MxJM-YV=W#_K3ncGlf7_s``Zw$CvutxJnv>zqE<QBpgp z#ALBw#Rc6rRe~j-PtE23_T9tXM*MEbrg>b&YjjNF69Qf+RApsM_?Nz7X~)yk%D=^` zdbW9NS<QLHZ`+07tfsr>neOlIynVap*g5OmiFrRZeM!!3nxWMpobiD(>ZSWlzau*} gZS0aiezsw_dpw$9?|!=pKo2u`y85}Sb4q9e0O5j$Q2+n{ literal 0 HcmV?d00001 diff --git a/img/StarFilled@2x.png b/img/StarFilled@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..3962cfa47418793c796feb2a02ce55d03f7875e8 GIT binary patch literal 578 zcmV-I0=@l-P)<h;3K|Lk000e1NJLTq001!n001xu1^@s6xWJOR00009a7bBm000&x z000&x0ZCFM@Bjb+0drDELIAGL9O(c600d`2O+f$vv5yP<VFdsH0oO@HK~#7F?U{{j zgFp<0A5jNLH@FRwPLSK6>IT`M*&uX-c7ik$WQ1gcW`ocPnhji^AgUBD;S3y@ichjY zV2mF&KX(8lBO{$aL`$}PVM`1RPI^sc)))$$tfAXj3;|Abb4SD;+Nr3*Fse~e1^v|s ztb*=p1Xe+BHH1~rSq)(o^i{)MQL2wc-*6JIY(G>VckBucNkW|XL{nZEwmI9ZShs9b zw8NS0SlIVvJxP*t$iZF_Mbb;Q1@goV`r#sO%XVN<na`;tf&~~}xSP&E7a7|N3prlz z@QH8&EOg>p?CE-=PXs|RETWY2#N&5>UNYZCpNKc;B|JCosFd`PLJH{;lA<o8Lr99I zP`^S_v_(%sQnXFSLQ=$n@`<q6oFHT(^xq;r9}prHks&@up}$x}TjPg&7zmMyu!zrG z=r0yALwuegBxrbg!96<A4UeHiQ%7(msN0zdD{+>zlekwKU?t8E0^bE>xOO-of1(-4 zo;q>vJ#MG3z&Ea&OLCtm)2riG?rj_z12G6Z`Hu40l3Ov{ob6{wAjF{JD!P4E5X)4= zi&hEWS8StcNXq}Po;-0khy@3Kr~Ka7k*Du##x(TzyZBU`ql?d*k&%DPFY>?v2Hd=1 QjsO4v07*qoM6N<$f+TS8y8r+H literal 0 HcmV?d00001 diff --git a/img/StarFilled@3x.png b/img/StarFilled@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..952b430d1ae9ae7a7c0b349117c7692eb934f96f GIT binary patch literal 808 zcmV+@1K0eCP)<h;3K|Lk000e1NJLTq002n<002k`1^@s6x-Zyy00009a7bBm001F4 z001F40Y#QEU;qFB0drDELIAGL9O(c600d`2O+f$vv5yP<VFdsH0=-E@K~#7F?VOEm z+b|G?pF2UifjxoK391d+4eAZjPLN~+cLFODq?y3k!07~bCvZ1-JysN2AQ3HzCy65Y z0S5^-1e_lpY5p7qWHOmdCKDSA1OUk^x!g!xOPKoyi64u_;x{sX1zJiR`5N=t5~*Vx z)F9CRLR}$cM1UPH^N3Wj&8#g2QlzDeYW7HtjxI{mO&wj7o|}5QC@nYjbWu8P@^n!e zZt`?dO}uWRj*Dsv#0Yg<R1=?@sOzGd0x>~d7uCe$CiyY99FAMe8CFef&*9l~;B_aJ zl}VeVSyeRGwY|T_xV%W5B!1TSva<OC1v-h;AcnaQd11_3yo(}PVZKOwMc86=;Uw`a zaW8_M;DZQsBXMTjT<rW84uL+)C6%SqZ{nzJ)0Zxh%s8Y#8kl}+)%2xHq%-2_gDxT> zHEQwB@m$^*w75iiLOM8qtE;&!<#Iq8O>X5H3N$*&K6H=xQ5h@UF<we&!8nB~p$+2_ zs>Dz*4xvg64d*RXiLu~3g(@*NoR?4~rh@Yjs>IYt5?h9g%2JQ_8aJz&#v{U<zO1}Q z({N94TVczH9}pf-8Uh)-MY5>uN+_e2la@#)gvSX=Xo)oT9$OpK`baI2)(DSJP$Fh0 zspaG?694V$hK4zkp^}$|lR^}zrhoz(A{7W1mA+C#q!2spngR-FOiPHc@t&G^If?VA zTP)y-Z*vQ|)2{K((O0C8@W)J$&Hmm8iT8F--rRqO&eTF9$NorjJJ63uI_e!+)m@hK z<BTbx`~0knBRlkGY(C7~3QS3l#7))Q9eN_qZu*FYDPgw~<Q)}GX0v+}JYY)LZX;mN z0fq`ztTuh(4pYMFTz^xdM~MvvhVCY4K3~EUtz!2LsxoHtcm502D>R-lBG#>@7<O&* mFm>D8{3?%3CX>lz-r^6>amv0XDkiG{0000<MNUMnLSTZN99}d4 literal 0 HcmV?d00001 diff --git a/img/Warning.png b/img/Warning.png new file mode 100644 index 0000000000000000000000000000000000000000..23743d46efa0e16ce0ab6fe7a7df028d3aab857d GIT binary patch literal 409 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1|%Pp+x`GjoCO|{#S9GG!XV7ZFl!D-1!HlL zyA#8@b22Z19F}xPUq=Rpjs4tz5?O(AdY&$hAr*{QLj!paIq<yY_&=w`GC@d<k^Ky# z1p|;+kg$M(lZDlTf&F;4#**l=^4+P*4^{QuUu0KLiSsrwnxv<cz4qWnCgTbA@{?68 zF9<k%V6E7mAJZDPPkV*_gE`IOoX67dZMZvCx0f^Lz+4Oa16$prSGfF6P~w;-G-cz| zTkSPL8$wc}ceO;WpC9A@<!AjTGe=c(gLw)uyCxscTf!87*5zjKhqiCklcet~GoBt; zbN;~|UB>%=9GR1ylpp^W_R-{f?ok`VRO0D5u|sPj&pua&W4V8Fiq}1v_(M)ATdV)> ziDcWYlAjsfU0(^Uy;^do)Kg{qy%jH`TV#Hyv75?r_m?eQmY-?ub3aXK?_Q(SV!g!~ zH+}_Wt~(RpUK;&tzKcc0%mVIv2aF6V>zl<FInDiIe|(D%FeDi~UHx3vIVCg!031M_ Ad;kCd literal 0 HcmV?d00001 diff --git a/img/Warning@2x.png b/img/Warning@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..57e462fba3654901c1114b306b4fb6cb1328135f GIT binary patch literal 659 zcmV;E0&M+>P)<h;3K|Lk000e1NJLTq001xm001xu1^@s6R|5Hm00009a7bBm000&x z000&x0ZCFM@Bjb+0drDELIAGL9O(c600d`2O+f$vv5yP<VFdsH0w_sDK~#7F?V5p6 zgD@0^A3X<fglr%qFhWOQ1Kj{Oup6`+v`)Y|L3IMypl<M+9H|C)K|%<r9^c(Nb4Zf+ zk{9ItNVF1{gaQEIUjEoh6Z+D-vLs1rx@Dx}LRtwsdvy=p8fwL*6&O#UcPT#7kw<A4 z#>rCJPTDj0U8$ty6bm_dW^qhi%b5FxcA5)B&{AkejuuC@Bl_o-K=wfc!B3ud)8(Sm z#3`qBA24GTG&gExEvt6)Sv9?z2k^U@lP|D87S*+kW&+K5s%BjvJt&thPZS97=R?hz zx(*~5=biI>3VQAx6NGRXq(nO($9J9y^dGzB*p6u@dO;H~9H>0UFv+_2Y?yxLv;5%k z0+v(rGADG~89n#OxYRofTmZsozfpW%jrK$LmmB)ytO3Mk4Inma0I^vEh)u`<YD;@o z?kEuQJw{bp-)#y+umBVx9lx@kfg#W+89}SKU;*eNrjmAcu4&s0E&vxmZM1zR&h_Mi z+C`EpW)1N1>)mM7h-t-idz@64lRjf~OUiKzI#*Hea|R|C^}2J4zN}JU0d)erRPVlv zXyn9h5ETMS;^3uvcVWvIP;KKGeJc1pS-SHKN%ew!K_E#So>Kjk2{}nz;UtM;W~tr@ zcuL}k^?`$NB(*ql<{W|JC#l64SkY3a!;8i73u1ocGeMJ9w)4R`zKB%s4D9E_+!aAH t66#+K1T`;$*S{6Dw394wK56Bj@dag9G%G1HUfBQu002ovPDHLkV1hdw6>|Up literal 0 HcmV?d00001 diff --git a/img/Warning@3x.png b/img/Warning@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..27b1425459404fcf960df555439a8f29f6deda9c GIT binary patch literal 937 zcmV;a16KTrP)<h;3K|Lk000e1NJLTq002k;002k`1^@s6RqeA!00009a7bBm001F4 z001F40Y#QEU;qFB0drDELIAGL9O(c600d`2O+f$vv5yP<VFdsH13pPaK~#7F?VN#i z+At7?kG>Aj5j+Cjz$0XXutBl`*`Q$pk_qSr?gs4!W&_yZuM~*~39?Q$I&%hn@A(Bp zu%zF*v$1}es8A>fG9x7bsLY?rzunJfvww8&7*Q5xYs|L8IXnN^g6<8|>Dp{9(rNEP zDAUQg9~DE{z=h&%VLJ80Y`>6?_00%nI`V{Yjya_<+cR9~)&BK{`A&-|haJ-LOcNUk z_OnEx9%w4~PBjUgs(b8n0bcAiG>08ro>?-Tu%Y%s1F_!)AFfN=7SaUL^2}?FhQp1} zH>R!@8uAldo@toI+Rtmsj2Eg+5UQfFfuiIIP5BAOJQEA2k355s^MJ1tOuLpO1=5D} z8dp4%AU!`-loMZZY}7Om$TLF$e+`yTB*aIK6>ULEK|DX%^2^fzT-b!ja@v;0AK=%H zJ~VU*a6>gjO%ub9ro4SY_!{S#1i<lPvxU&v2JT02o>2sMO3$-1zOE`zmq_?_9Q4k* zzD4#`2g=?_hAL1tCmG)R)D;t`3Y7h(>GLO0Hb|heVP|F<_B3%!AGU$wK_OP4A`nXv zh@l9?Py}Kq0x=YU7>Ym)MIeSE5JM4&p$NoK1Y*!4&_1zjbW{lB4V$ZzqLmgC=XX+q z5yajve)F%@I9$~TWYK8-ElHZ82*gkXVo;NV;LHS1{_U#c=KAQw1UkD8u@h{9GaGhN zvhuGVOkF=E>N5KcP4RTr6AQ6zpiIOu=I6(&lcI{k^Kv-|3Dp1WsbK24A^{M#!wj3| z?wH4ZBmwZ*Lia)fz!@aIGJLieVf)JtLiQIwYNbpIj|fxKC7|2pyTk+*lpwy4W=)rZ zxCPIaFGwkf8{AuD^eN5}H*wm^r0goa0dH{6C(cQwQ?UVa2KT?BZ3J)Fw#XT_!)?-K zaEGD=CD6Q73NANsjT0HtxZ*;Z-i&z^;lgRn)TPhhj<{j_K#PGkVklln(+ddXCuCUv zst#lbJEX0%w>hq+sRHM2Z<$VRaK0?OVZLL-NyPO(I!Ft<K1^aEO>dC4WIRTLTYNOd zSCE#UPO<-6)4hS?<8eY99*=X?rz&WAz>>WiaL6lLEwdVhLP3=OB*Gr1)1G@a00000 LNkvXXu0mjf`){K0 literal 0 HcmV?d00001 diff --git a/src/assets.luau b/src/assets.luau index d00fe303..f65f006b 100644 --- a/src/assets.luau +++ b/src/assets.luau @@ -1,53 +1,116 @@ -- This file was @generated by Tarmac. It is not intended for manual editing. return { ChevronRight = { - Image = "rbxassetid://18940815650", + Image = "rbxassetid://128765597212202", ImageRectOffset = Vector2.new(49, 226), ImageRectSize = Vector2.new(32, 32), }, Component = { - Image = "rbxassetid://18940815650", + Image = "rbxassetid://128765597212202", ImageRectOffset = Vector2.new(0, 275), ImageRectSize = Vector2.new(32, 32), }, Folder = { - Image = "rbxassetid://18940815650", + Image = "rbxassetid://128765597212202", ImageRectOffset = Vector2.new(345, 0), ImageRectSize = Vector2.new(32, 32), }, GitHubMark = { - Image = "rbxassetid://18940815650", + Image = "rbxassetid://128765597212202", ImageRectOffset = Vector2.new(0, 0), ImageRectSize = Vector2.new(230, 225), }, IconLight = { - Image = "rbxassetid://18940815650", + Image = "rbxassetid://128765597212202", ImageRectOffset = Vector2.new(231, 65), ImageRectSize = Vector2.new(42, 42), }, Magnify = { - Image = "rbxassetid://18940815650", + Image = "rbxassetid://128765597212202", ImageRectOffset = Vector2.new(0, 226), ImageRectSize = Vector2.new(48, 48), }, Minify = { - Image = "rbxassetid://18940815650", + Image = "rbxassetid://128765597212202", ImageRectOffset = Vector2.new(296, 0), ImageRectSize = Vector2.new(48, 48), }, Search = { - Image = "rbxassetid://18940815650", + Image = "rbxassetid://128765597212202", ImageRectOffset = Vector2.new(296, 49), ImageRectSize = Vector2.new(32, 32), }, + Star = function(dpiScale) + if dpiScale >= 3 then + return { + Image = "rbxassetid://123049343108173", + ImageRectOffset = Vector2.new(0, 0), + ImageRectSize = Vector2.new(73, 72), + } + elseif dpiScale >= 2 then + return { + Image = "rbxassetid://118393146115398", + ImageRectOffset = Vector2.new(0, 0), + ImageRectSize = Vector2.new(49, 48), + } + else + return { + Image = "rbxassetid://128765597212202", + ImageRectOffset = Vector2.new(82, 226), + ImageRectSize = Vector2.new(25, 24), + } + end + end, + StarFilled = function(dpiScale) + if dpiScale >= 3 then + return { + Image = "rbxassetid://123049343108173", + ImageRectOffset = Vector2.new(74, 0), + ImageRectSize = Vector2.new(73, 72), + } + elseif dpiScale >= 2 then + return { + Image = "rbxassetid://118393146115398", + ImageRectOffset = Vector2.new(50, 0), + ImageRectSize = Vector2.new(49, 48), + } + else + return { + Image = "rbxassetid://128765597212202", + ImageRectOffset = Vector2.new(49, 259), + ImageRectSize = Vector2.new(25, 24), + } + end + end, Storybook = { - Image = "rbxassetid://18940815650", + Image = "rbxassetid://128765597212202", ImageRectOffset = Vector2.new(231, 108), ImageRectSize = Vector2.new(32, 32), }, + Warning = function(dpiScale) + if dpiScale >= 3 then + return { + Image = "rbxassetid://123049343108173", + ImageRectOffset = Vector2.new(0, 73), + ImageRectSize = Vector2.new(72, 72), + } + elseif dpiScale >= 2 then + return { + Image = "rbxassetid://118393146115398", + ImageRectOffset = Vector2.new(0, 49), + ImageRectSize = Vector2.new(48, 48), + } + else + return { + Image = "rbxassetid://128765597212202", + ImageRectOffset = Vector2.new(0, 308), + ImageRectSize = Vector2.new(24, 24), + } + end + end, flipbook = { - Image = "rbxassetid://18940815650", + Image = "rbxassetid://128765597212202", ImageRectOffset = Vector2.new(231, 0), ImageRectSize = Vector2.new(64, 64), }, -} +} \ No newline at end of file diff --git a/tarmac-manifest.toml b/tarmac-manifest.toml index 9dd6cda0..31a88cd8 100644 --- a/tarmac-manifest.toml +++ b/tarmac-manifest.toml @@ -1,59 +1,113 @@ [inputs."img/ChevronRight.png"] hash = "4c91853652b4e0a6317804c54db6397ac0c710c819b282f7be2179e3abdc879c" -id = 18940815650 +id = 128765597212202 slice = [[49, 226], [81, 258]] packable = true [inputs."img/Component.png"] hash = "d51b19d5f35ac87fd8156500828f3e217b6fc420b70fee5d97b3577e4e6a8843" -id = 18940815650 +id = 128765597212202 slice = [[0, 275], [32, 307]] packable = true [inputs."img/Folder.png"] hash = "4c0e80a363d36a4d127d410795cb13eec74d55871b70174636c0c962c242835c" -id = 18940815650 +id = 128765597212202 slice = [[345, 0], [377, 32]] packable = true [inputs."img/GitHubMark.png"] hash = "a0fb973cd9bf0c1123dbe783f7a4093dbcf9b4910d6e385c16a837a04a02306c" -id = 18940815650 +id = 128765597212202 slice = [[0, 0], [230, 225]] packable = true [inputs."img/IconLight.png"] hash = "237c66813c2b5a58ae9c46166f72db8698fbb046922e2473ebaca8f7d7376ce1" -id = 18940815650 +id = 128765597212202 slice = [[231, 65], [273, 107]] packable = true [inputs."img/Magnify.png"] hash = "351db4b6132a9e02d01bc4cdc3c850099dce358673daaa0da66745b675ad38b1" -id = 18940815650 +id = 128765597212202 slice = [[0, 226], [48, 274]] packable = true [inputs."img/Minify.png"] hash = "694db8ce141475fc0f42980d2a6dfd058dfa155348a579fab29ca6dd5684ec14" -id = 18940815650 +id = 128765597212202 slice = [[296, 0], [344, 48]] packable = true [inputs."img/Search.png"] hash = "0ab89c0309f577f3ccae0e03b45292f6e2bca51cc9b8250f56ad756e04231c57" -id = 18940815650 +id = 128765597212202 slice = [[296, 49], [328, 81]] packable = true +[inputs."img/Star.png"] +hash = "ae3c74c88729a8600531b2ade6cbaaa091033fffd0de5145b8462d3a15d0a510" +id = 128765597212202 +slice = [[82, 226], [107, 250]] +packable = true + +[inputs."img/Star@2x.png"] +hash = "ebaeeb5c227223841d5bd5c001f58044d1995e5258374305032611a75a9070eb" +id = 118393146115398 +slice = [[0, 0], [49, 48]] +packable = true + +[inputs."img/Star@3x.png"] +hash = "34cc831f584d6b54d60f5fe77f480e053e2e0091676f446aa0b9f92cc25f6a4f" +id = 123049343108173 +slice = [[0, 0], [73, 72]] +packable = true + +[inputs."img/StarFilled.png"] +hash = "d958666976bb5a1ab5672e5a79d393b28fca26fb348f9b07ceecbcaf059e78f3" +id = 128765597212202 +slice = [[49, 259], [74, 283]] +packable = true + +[inputs."img/StarFilled@2x.png"] +hash = "99216f340f265dcf7fa40c941427d9f09efeddc60ea5fd6a502f288e6d268e63" +id = 118393146115398 +slice = [[50, 0], [99, 48]] +packable = true + +[inputs."img/StarFilled@3x.png"] +hash = "b7d7defc50fb8d0dcc92327406e9eb2e8fe379656239424fab6982d449248ba5" +id = 123049343108173 +slice = [[74, 0], [147, 72]] +packable = true + [inputs."img/Storybook.png"] hash = "2a4b8bd513a8fa7eebae482943ab3435a659359bd94b24eff1c3dcc6f1244799" -id = 18940815650 +id = 128765597212202 slice = [[231, 108], [263, 140]] packable = true +[inputs."img/Warning.png"] +hash = "e2f8c263a469a521d876de814fb5e3251084dbce9d41e06bf9037ccfe4710fd3" +id = 128765597212202 +slice = [[0, 308], [24, 332]] +packable = true + +[inputs."img/Warning@2x.png"] +hash = "ecec999c799a628e0672fe1dd4d2c16df9f27caeb66b82a7bd5103326c6463ff" +id = 118393146115398 +slice = [[0, 49], [48, 97]] +packable = true + +[inputs."img/Warning@3x.png"] +hash = "9524173aa67880639e5d09b02925e631e1766336bff06ddff2bdf5d834822464" +id = 123049343108173 +slice = [[0, 73], [72, 145]] +packable = true + [inputs."img/flipbook.png"] hash = "423e3dcff54fce8824f14d7cadcfb90eef2abecd817df2bee13951bd2674f79a" -id = 18940815650 +id = 128765597212202 slice = [[231, 0], [295, 64]] packable = true From de6a3cc0c5e259198512b99771cd78ec879081a6 Mon Sep 17 00:00:00 2001 From: Marin Minnerly <me@vocksel.com> Date: Tue, 17 Dec 2024 15:28:31 -0800 Subject: [PATCH 47/79] Redo icons without dpi scaling --- img/{Warning.png => Alert.png} | Bin img/Star@2x.png | Bin 857 -> 0 bytes img/Star@3x.png | Bin 1192 -> 0 bytes img/StarFilled@2x.png | Bin 578 -> 0 bytes img/StarFilled@3x.png | Bin 808 -> 0 bytes img/Warning@2x.png | Bin 659 -> 0 bytes img/Warning@3x.png | Bin 937 -> 0 bytes src/assets.luau | 98 +++++++++------------------------ tarmac-manifest.toml | 72 ++++++------------------ 9 files changed, 43 insertions(+), 127 deletions(-) rename img/{Warning.png => Alert.png} (100%) delete mode 100644 img/Star@2x.png delete mode 100644 img/Star@3x.png delete mode 100644 img/StarFilled@2x.png delete mode 100644 img/StarFilled@3x.png delete mode 100644 img/Warning@2x.png delete mode 100644 img/Warning@3x.png diff --git a/img/Warning.png b/img/Alert.png similarity index 100% rename from img/Warning.png rename to img/Alert.png diff --git a/img/Star@2x.png b/img/Star@2x.png deleted file mode 100644 index 61f03b207e1d2665015b9b9f5a3f01de9cdf492c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 857 zcmV-f1E&0mP)<h;3K|Lk000e1NJLTq001!n001xu1^@s6xWJOR00009a7bBm000&x z000&x0ZCFM@Bjb+0drDELIAGL9O(c600d`2O+f$vv5yP<VFdsH0`5sfK~#7F?OKg> zn=lYQGJ!im=>&Brh&w^DLA^n}LD`_$z<7eZOn^H9WCF+pbc3t(bhw8QaM)5TznAa5 zd&o$-AKeL^Bm;aP4kQo=h`3UZ4gIW=Bx&H5pbYq_sKq{h!L2|mi{tnWu|p}jwUjOt zmDam0#0;fi&6Is+@1NNFn&wM<u)LrZO<CQ_nj;y+2+IqZqdsHxZFJt4@&aoUw>Rdz zz}m&>4T%?6+c>=;^#W@jmp9O?%pxzaKG*jKDY3!f%A76Wky(BB#Pck#Bol%(!!^-o zR(1($6n#@k-d_-aco)RahUQTby>(?N?@mCf=oyCz+!zW;7>&p&P>#Jwj%u_PD6i|w zE(G90C?1cBaxzFVa6fDw31g;RTq;UgedpF!Fd-9sU!VsPOvvizJ?X>4PKVVOXL!+w zbf|rdvM-<sy_)H_<Mnr}%&mP<mL4GzkoOPlRqx}%jc8c;9^%6TD>u#-I9!CH*@fY7 zO-iR4@2tjG-xuRKiz<EYqA<i^K)$5&)eX7G0LJ8MhIlc6F?)AIyc)om+J@{P5O8?p zlY=i<hO`g}ICA9Ui@CjqtidzTq!y=v!UdV}oum;ZTNQg1VoRB_@}fjjWCS7fUbtap z@1E|$=>!M_D`Zn?KdC1+tbBxUs77Q?;v6GhAsk*<SsYSQBfhfo5yF9c;z8I)cHL#o zgHLlO4`_CUdM~U;Q@3pt9p*M#Xtv1h5?LakIS=Yk&a~}LO2)9Co3Qx*QbtemM-V^~ zPAMzDfm^Wtdg;7M8#yI-tEk559PAH0SC;HiEH$FCD=Boe@)`8kl3dYuqJz~fqZDHc zL-6qNvo>KrDdSgII?M`3$Q0-}DZ$~1#fc~OZ<Y3}&QG$5=0MFBS~`?zB|>RXYDfPP zhx2IzzJT+`jrw9y?0Pg_d79x_gHNEDxUR2Gami?rG5HAJRLW=3-y+bY;3@q)dijP! j?*u*tlfhQ-fg8j>4`TF){03tu00000NkvXXu0mjf@a2R3 diff --git a/img/Star@3x.png b/img/Star@3x.png deleted file mode 100644 index b155368ec472383391e79bc81e106936a9428949..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1192 zcmV;Z1XufsP)<h;3K|Lk000e1NJLTq002n<002k`1^@s6x-Zyy00009a7bBm001F4 z001F40Y#QEU;qFB0drDELIAGL9O(c600d`2O+f$vv5yP<VFdsH1U*SaK~#7F?VO8s z+aMH%uX8$pH>fs<H>kQnx`8u6+zFCSkYob66SSQ`?gVirkUD|0!R@8sM#dKeLPpQ; z9FFzkq2E6U0XD$Q%*@Qpe-Lp=;hw?^z9Y8pC?qg-NQ4!I0?mkXE0`+6B@_-=&tZ~6 zDS--f=P<ddM>m`q#CW2xHJr;~Dkv9mth8&ms+htQFt}*LT(_B|n+z^0EZ66gbCbbE zYs>ZNq}=4|qDs7cGH&v9Q6=6q+|<-XmH7GS-PF`YmH6X!Q>cq7>ExqzQ>cq7>5S7& zZMmqDUOqZE(GC1)!$p<!I5&y0rzH#xl;cEsn=Y!P&8CYN98Ot40O}iDsqm3PqP7!5 zl-tE(v4yZaQ~EHqMQ;@LD*QQaQ=V@SfU`*In@T~7aTLbK=Fe2v4YUB2<$*r#J))e6 zM`hVT8>oi&5#2@*Z9GBC2%#_xaZeeB4CbL6hz}K~pl|~hg3?~2-w3A(t1B0V%tfRJ z@IzR-1ZnA4Xw%X)plqi^xQ6$3(U&ci_m(@cU@~0OM+RneG5&2+1|{?elx=7NQZQHX zzS4cvHw}0?|8+!EJJALtFmO<IPfXSH;F7?Si8##8nghspcF){)rR{Ted=Bkr^qB9= zu`@&HB9$(YMntoF!}G^}G2-f0Fx9R8UQtdE>SQo7P%hHYaT|I;^W>q=Ye26t2*ftp zwnR2#70M!z|Em+4=i~<~ltrKyXadSU)5}<dvI&GC52mpQbuob|84OyeiwjiAXpllh z6R48mpoEGpP$grVP^}EjwgJkw{P+S@3SbiIfIWGALH>0k?r4TZsLtJFa8WnIJxVD3 zW%ws_4D!?J2?THj4N+rEzqG^G&^?K^zY85&ZY+|b)b<nTo<!R(A&^j!K56?UbdNrY z+GOVBgiw+60=z=^{MPnYp^bK9kx-7k=$Y@Jd-lw+AUB0LiOYBo-LqqkrO*Z3qZWDs z(=`cN5!a@ZcF-U6u1WmrraEVRY_yN))qaTqi@ZKSGq?<cdfQ6)r3z(W2XMz6OCe^P ziTt#a_hS~{e}7vr=@l5Teu!z8;!ca<`MO2#fpm&<gKle!#5HU1gehb2dQIN~VAF32 z&5KT5#7aE!X~pdV?}pn@(Aq{DWz~A5P2su}{7EPXp)yCY_J%aP34JX&@@6|pH+4rJ z!tNVO{BIP#Q1}dTwMS{Vc1<D2`mw8$ITmM33HfR8wc*;-p=#xvGgLlXtgnJMmb`UR zhyn5oy`JFw1~aU`!PFtYx<6PzWU3(Y69K^^0?rr;Eea}@Gv{dMxU|sA`slXOHdkff zmQ_c8x;0%xEtcOU&=7R1f+nK(lSGv(JPIfV;!|h|^#6wnacu&zaV)5?{KaNa(5(uJ z^YPjurjWu>8$6v}5{1ez;_uK<KFPLBIlSAxnVFfHzVjFN36T_;y(ka>0000<MNUMn GLSTZjX)R~~ diff --git a/img/StarFilled@2x.png b/img/StarFilled@2x.png deleted file mode 100644 index 3962cfa47418793c796feb2a02ce55d03f7875e8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 578 zcmV-I0=@l-P)<h;3K|Lk000e1NJLTq001!n001xu1^@s6xWJOR00009a7bBm000&x z000&x0ZCFM@Bjb+0drDELIAGL9O(c600d`2O+f$vv5yP<VFdsH0oO@HK~#7F?U{{j zgFp<0A5jNLH@FRwPLSK6>IT`M*&uX-c7ik$WQ1gcW`ocPnhji^AgUBD;S3y@ichjY zV2mF&KX(8lBO{$aL`$}PVM`1RPI^sc)))$$tfAXj3;|Abb4SD;+Nr3*Fse~e1^v|s ztb*=p1Xe+BHH1~rSq)(o^i{)MQL2wc-*6JIY(G>VckBucNkW|XL{nZEwmI9ZShs9b zw8NS0SlIVvJxP*t$iZF_Mbb;Q1@goV`r#sO%XVN<na`;tf&~~}xSP&E7a7|N3prlz z@QH8&EOg>p?CE-=PXs|RETWY2#N&5>UNYZCpNKc;B|JCosFd`PLJH{;lA<o8Lr99I zP`^S_v_(%sQnXFSLQ=$n@`<q6oFHT(^xq;r9}prHks&@up}$x}TjPg&7zmMyu!zrG z=r0yALwuegBxrbg!96<A4UeHiQ%7(msN0zdD{+>zlekwKU?t8E0^bE>xOO-of1(-4 zo;q>vJ#MG3z&Ea&OLCtm)2riG?rj_z12G6Z`Hu40l3Ov{ob6{wAjF{JD!P4E5X)4= zi&hEWS8StcNXq}Po;-0khy@3Kr~Ka7k*Du##x(TzyZBU`ql?d*k&%DPFY>?v2Hd=1 QjsO4v07*qoM6N<$f+TS8y8r+H diff --git a/img/StarFilled@3x.png b/img/StarFilled@3x.png deleted file mode 100644 index 952b430d1ae9ae7a7c0b349117c7692eb934f96f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 808 zcmV+@1K0eCP)<h;3K|Lk000e1NJLTq002n<002k`1^@s6x-Zyy00009a7bBm001F4 z001F40Y#QEU;qFB0drDELIAGL9O(c600d`2O+f$vv5yP<VFdsH0=-E@K~#7F?VOEm z+b|G?pF2UifjxoK391d+4eAZjPLN~+cLFODq?y3k!07~bCvZ1-JysN2AQ3HzCy65Y z0S5^-1e_lpY5p7qWHOmdCKDSA1OUk^x!g!xOPKoyi64u_;x{sX1zJiR`5N=t5~*Vx z)F9CRLR}$cM1UPH^N3Wj&8#g2QlzDeYW7HtjxI{mO&wj7o|}5QC@nYjbWu8P@^n!e zZt`?dO}uWRj*Dsv#0Yg<R1=?@sOzGd0x>~d7uCe$CiyY99FAMe8CFef&*9l~;B_aJ zl}VeVSyeRGwY|T_xV%W5B!1TSva<OC1v-h;AcnaQd11_3yo(}PVZKOwMc86=;Uw`a zaW8_M;DZQsBXMTjT<rW84uL+)C6%SqZ{nzJ)0Zxh%s8Y#8kl}+)%2xHq%-2_gDxT> zHEQwB@m$^*w75iiLOM8qtE;&!<#Iq8O>X5H3N$*&K6H=xQ5h@UF<we&!8nB~p$+2_ zs>Dz*4xvg64d*RXiLu~3g(@*NoR?4~rh@Yjs>IYt5?h9g%2JQ_8aJz&#v{U<zO1}Q z({N94TVczH9}pf-8Uh)-MY5>uN+_e2la@#)gvSX=Xo)oT9$OpK`baI2)(DSJP$Fh0 zspaG?694V$hK4zkp^}$|lR^}zrhoz(A{7W1mA+C#q!2spngR-FOiPHc@t&G^If?VA zTP)y-Z*vQ|)2{K((O0C8@W)J$&Hmm8iT8F--rRqO&eTF9$NorjJJ63uI_e!+)m@hK z<BTbx`~0knBRlkGY(C7~3QS3l#7))Q9eN_qZu*FYDPgw~<Q)}GX0v+}JYY)LZX;mN z0fq`ztTuh(4pYMFTz^xdM~MvvhVCY4K3~EUtz!2LsxoHtcm502D>R-lBG#>@7<O&* mFm>D8{3?%3CX>lz-r^6>amv0XDkiG{0000<MNUMnLSTZN99}d4 diff --git a/img/Warning@2x.png b/img/Warning@2x.png deleted file mode 100644 index 57e462fba3654901c1114b306b4fb6cb1328135f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 659 zcmV;E0&M+>P)<h;3K|Lk000e1NJLTq001xm001xu1^@s6R|5Hm00009a7bBm000&x z000&x0ZCFM@Bjb+0drDELIAGL9O(c600d`2O+f$vv5yP<VFdsH0w_sDK~#7F?V5p6 zgD@0^A3X<fglr%qFhWOQ1Kj{Oup6`+v`)Y|L3IMypl<M+9H|C)K|%<r9^c(Nb4Zf+ zk{9ItNVF1{gaQEIUjEoh6Z+D-vLs1rx@Dx}LRtwsdvy=p8fwL*6&O#UcPT#7kw<A4 z#>rCJPTDj0U8$ty6bm_dW^qhi%b5FxcA5)B&{AkejuuC@Bl_o-K=wfc!B3ud)8(Sm z#3`qBA24GTG&gExEvt6)Sv9?z2k^U@lP|D87S*+kW&+K5s%BjvJt&thPZS97=R?hz zx(*~5=biI>3VQAx6NGRXq(nO($9J9y^dGzB*p6u@dO;H~9H>0UFv+_2Y?yxLv;5%k z0+v(rGADG~89n#OxYRofTmZsozfpW%jrK$LmmB)ytO3Mk4Inma0I^vEh)u`<YD;@o z?kEuQJw{bp-)#y+umBVx9lx@kfg#W+89}SKU;*eNrjmAcu4&s0E&vxmZM1zR&h_Mi z+C`EpW)1N1>)mM7h-t-idz@64lRjf~OUiKzI#*Hea|R|C^}2J4zN}JU0d)erRPVlv zXyn9h5ETMS;^3uvcVWvIP;KKGeJc1pS-SHKN%ew!K_E#So>Kjk2{}nz;UtM;W~tr@ zcuL}k^?`$NB(*ql<{W|JC#l64SkY3a!;8i73u1ocGeMJ9w)4R`zKB%s4D9E_+!aAH t66#+K1T`;$*S{6Dw394wK56Bj@dag9G%G1HUfBQu002ovPDHLkV1hdw6>|Up diff --git a/img/Warning@3x.png b/img/Warning@3x.png deleted file mode 100644 index 27b1425459404fcf960df555439a8f29f6deda9c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 937 zcmV;a16KTrP)<h;3K|Lk000e1NJLTq002k;002k`1^@s6RqeA!00009a7bBm001F4 z001F40Y#QEU;qFB0drDELIAGL9O(c600d`2O+f$vv5yP<VFdsH13pPaK~#7F?VN#i z+At7?kG>Aj5j+Cjz$0XXutBl`*`Q$pk_qSr?gs4!W&_yZuM~*~39?Q$I&%hn@A(Bp zu%zF*v$1}es8A>fG9x7bsLY?rzunJfvww8&7*Q5xYs|L8IXnN^g6<8|>Dp{9(rNEP zDAUQg9~DE{z=h&%VLJ80Y`>6?_00%nI`V{Yjya_<+cR9~)&BK{`A&-|haJ-LOcNUk z_OnEx9%w4~PBjUgs(b8n0bcAiG>08ro>?-Tu%Y%s1F_!)AFfN=7SaUL^2}?FhQp1} zH>R!@8uAldo@toI+Rtmsj2Eg+5UQfFfuiIIP5BAOJQEA2k355s^MJ1tOuLpO1=5D} z8dp4%AU!`-loMZZY}7Om$TLF$e+`yTB*aIK6>ULEK|DX%^2^fzT-b!ja@v;0AK=%H zJ~VU*a6>gjO%ub9ro4SY_!{S#1i<lPvxU&v2JT02o>2sMO3$-1zOE`zmq_?_9Q4k* zzD4#`2g=?_hAL1tCmG)R)D;t`3Y7h(>GLO0Hb|heVP|F<_B3%!AGU$wK_OP4A`nXv zh@l9?Py}Kq0x=YU7>Ym)MIeSE5JM4&p$NoK1Y*!4&_1zjbW{lB4V$ZzqLmgC=XX+q z5yajve)F%@I9$~TWYK8-ElHZ82*gkXVo;NV;LHS1{_U#c=KAQw1UkD8u@h{9GaGhN zvhuGVOkF=E>N5KcP4RTr6AQ6zpiIOu=I6(&lcI{k^Kv-|3Dp1WsbK24A^{M#!wj3| z?wH4ZBmwZ*Lia)fz!@aIGJLieVf)JtLiQIwYNbpIj|fxKC7|2pyTk+*lpwy4W=)rZ zxCPIaFGwkf8{AuD^eN5}H*wm^r0goa0dH{6C(cQwQ?UVa2KT?BZ3J)Fw#XT_!)?-K zaEGD=CD6Q73NANsjT0HtxZ*;Z-i&z^;lgRn)TPhhj<{j_K#PGkVklln(+ddXCuCUv zst#lbJEX0%w>hq+sRHM2Z<$VRaK0?OVZLL-NyPO(I!Ft<K1^aEO>dC4WIRTLTYNOd zSCE#UPO<-6)4hS?<8eY99*=X?rz&WAz>>WiaL6lLEwdVhLP3=OB*Gr1)1G@a00000 LNkvXXu0mjf`){K0 diff --git a/src/assets.luau b/src/assets.luau index f65f006b..10c4e9ea 100644 --- a/src/assets.luau +++ b/src/assets.luau @@ -1,115 +1,67 @@ -- This file was @generated by Tarmac. It is not intended for manual editing. return { + Alert = { + Image = "rbxassetid://74272179538762", + ImageRectOffset = Vector2.new(0, 308), + ImageRectSize = Vector2.new(24, 24), + }, ChevronRight = { - Image = "rbxassetid://128765597212202", + Image = "rbxassetid://74272179538762", ImageRectOffset = Vector2.new(49, 226), ImageRectSize = Vector2.new(32, 32), }, Component = { - Image = "rbxassetid://128765597212202", + Image = "rbxassetid://74272179538762", ImageRectOffset = Vector2.new(0, 275), ImageRectSize = Vector2.new(32, 32), }, Folder = { - Image = "rbxassetid://128765597212202", + Image = "rbxassetid://74272179538762", ImageRectOffset = Vector2.new(345, 0), ImageRectSize = Vector2.new(32, 32), }, GitHubMark = { - Image = "rbxassetid://128765597212202", + Image = "rbxassetid://74272179538762", ImageRectOffset = Vector2.new(0, 0), ImageRectSize = Vector2.new(230, 225), }, IconLight = { - Image = "rbxassetid://128765597212202", + Image = "rbxassetid://74272179538762", ImageRectOffset = Vector2.new(231, 65), ImageRectSize = Vector2.new(42, 42), }, Magnify = { - Image = "rbxassetid://128765597212202", + Image = "rbxassetid://74272179538762", ImageRectOffset = Vector2.new(0, 226), ImageRectSize = Vector2.new(48, 48), }, Minify = { - Image = "rbxassetid://128765597212202", + Image = "rbxassetid://74272179538762", ImageRectOffset = Vector2.new(296, 0), ImageRectSize = Vector2.new(48, 48), }, Search = { - Image = "rbxassetid://128765597212202", + Image = "rbxassetid://74272179538762", ImageRectOffset = Vector2.new(296, 49), ImageRectSize = Vector2.new(32, 32), }, - Star = function(dpiScale) - if dpiScale >= 3 then - return { - Image = "rbxassetid://123049343108173", - ImageRectOffset = Vector2.new(0, 0), - ImageRectSize = Vector2.new(73, 72), - } - elseif dpiScale >= 2 then - return { - Image = "rbxassetid://118393146115398", - ImageRectOffset = Vector2.new(0, 0), - ImageRectSize = Vector2.new(49, 48), - } - else - return { - Image = "rbxassetid://128765597212202", - ImageRectOffset = Vector2.new(82, 226), - ImageRectSize = Vector2.new(25, 24), - } - end - end, - StarFilled = function(dpiScale) - if dpiScale >= 3 then - return { - Image = "rbxassetid://123049343108173", - ImageRectOffset = Vector2.new(74, 0), - ImageRectSize = Vector2.new(73, 72), - } - elseif dpiScale >= 2 then - return { - Image = "rbxassetid://118393146115398", - ImageRectOffset = Vector2.new(50, 0), - ImageRectSize = Vector2.new(49, 48), - } - else - return { - Image = "rbxassetid://128765597212202", - ImageRectOffset = Vector2.new(49, 259), - ImageRectSize = Vector2.new(25, 24), - } - end - end, + Star = { + Image = "rbxassetid://74272179538762", + ImageRectOffset = Vector2.new(82, 226), + ImageRectSize = Vector2.new(25, 24), + }, + StarFilled = { + Image = "rbxassetid://74272179538762", + ImageRectOffset = Vector2.new(49, 259), + ImageRectSize = Vector2.new(25, 24), + }, Storybook = { - Image = "rbxassetid://128765597212202", + Image = "rbxassetid://74272179538762", ImageRectOffset = Vector2.new(231, 108), ImageRectSize = Vector2.new(32, 32), }, - Warning = function(dpiScale) - if dpiScale >= 3 then - return { - Image = "rbxassetid://123049343108173", - ImageRectOffset = Vector2.new(0, 73), - ImageRectSize = Vector2.new(72, 72), - } - elseif dpiScale >= 2 then - return { - Image = "rbxassetid://118393146115398", - ImageRectOffset = Vector2.new(0, 49), - ImageRectSize = Vector2.new(48, 48), - } - else - return { - Image = "rbxassetid://128765597212202", - ImageRectOffset = Vector2.new(0, 308), - ImageRectSize = Vector2.new(24, 24), - } - end - end, flipbook = { - Image = "rbxassetid://128765597212202", + Image = "rbxassetid://74272179538762", ImageRectOffset = Vector2.new(231, 0), ImageRectSize = Vector2.new(64, 64), }, diff --git a/tarmac-manifest.toml b/tarmac-manifest.toml index 31a88cd8..06d90870 100644 --- a/tarmac-manifest.toml +++ b/tarmac-manifest.toml @@ -1,113 +1,77 @@ +[inputs."img/Alert.png"] +hash = "e2f8c263a469a521d876de814fb5e3251084dbce9d41e06bf9037ccfe4710fd3" +id = 74272179538762 +slice = [[0, 308], [24, 332]] +packable = true + [inputs."img/ChevronRight.png"] hash = "4c91853652b4e0a6317804c54db6397ac0c710c819b282f7be2179e3abdc879c" -id = 128765597212202 +id = 74272179538762 slice = [[49, 226], [81, 258]] packable = true [inputs."img/Component.png"] hash = "d51b19d5f35ac87fd8156500828f3e217b6fc420b70fee5d97b3577e4e6a8843" -id = 128765597212202 +id = 74272179538762 slice = [[0, 275], [32, 307]] packable = true [inputs."img/Folder.png"] hash = "4c0e80a363d36a4d127d410795cb13eec74d55871b70174636c0c962c242835c" -id = 128765597212202 +id = 74272179538762 slice = [[345, 0], [377, 32]] packable = true [inputs."img/GitHubMark.png"] hash = "a0fb973cd9bf0c1123dbe783f7a4093dbcf9b4910d6e385c16a837a04a02306c" -id = 128765597212202 +id = 74272179538762 slice = [[0, 0], [230, 225]] packable = true [inputs."img/IconLight.png"] hash = "237c66813c2b5a58ae9c46166f72db8698fbb046922e2473ebaca8f7d7376ce1" -id = 128765597212202 +id = 74272179538762 slice = [[231, 65], [273, 107]] packable = true [inputs."img/Magnify.png"] hash = "351db4b6132a9e02d01bc4cdc3c850099dce358673daaa0da66745b675ad38b1" -id = 128765597212202 +id = 74272179538762 slice = [[0, 226], [48, 274]] packable = true [inputs."img/Minify.png"] hash = "694db8ce141475fc0f42980d2a6dfd058dfa155348a579fab29ca6dd5684ec14" -id = 128765597212202 +id = 74272179538762 slice = [[296, 0], [344, 48]] packable = true [inputs."img/Search.png"] hash = "0ab89c0309f577f3ccae0e03b45292f6e2bca51cc9b8250f56ad756e04231c57" -id = 128765597212202 +id = 74272179538762 slice = [[296, 49], [328, 81]] packable = true [inputs."img/Star.png"] hash = "ae3c74c88729a8600531b2ade6cbaaa091033fffd0de5145b8462d3a15d0a510" -id = 128765597212202 +id = 74272179538762 slice = [[82, 226], [107, 250]] packable = true -[inputs."img/Star@2x.png"] -hash = "ebaeeb5c227223841d5bd5c001f58044d1995e5258374305032611a75a9070eb" -id = 118393146115398 -slice = [[0, 0], [49, 48]] -packable = true - -[inputs."img/Star@3x.png"] -hash = "34cc831f584d6b54d60f5fe77f480e053e2e0091676f446aa0b9f92cc25f6a4f" -id = 123049343108173 -slice = [[0, 0], [73, 72]] -packable = true - [inputs."img/StarFilled.png"] hash = "d958666976bb5a1ab5672e5a79d393b28fca26fb348f9b07ceecbcaf059e78f3" -id = 128765597212202 +id = 74272179538762 slice = [[49, 259], [74, 283]] packable = true -[inputs."img/StarFilled@2x.png"] -hash = "99216f340f265dcf7fa40c941427d9f09efeddc60ea5fd6a502f288e6d268e63" -id = 118393146115398 -slice = [[50, 0], [99, 48]] -packable = true - -[inputs."img/StarFilled@3x.png"] -hash = "b7d7defc50fb8d0dcc92327406e9eb2e8fe379656239424fab6982d449248ba5" -id = 123049343108173 -slice = [[74, 0], [147, 72]] -packable = true - [inputs."img/Storybook.png"] hash = "2a4b8bd513a8fa7eebae482943ab3435a659359bd94b24eff1c3dcc6f1244799" -id = 128765597212202 +id = 74272179538762 slice = [[231, 108], [263, 140]] packable = true -[inputs."img/Warning.png"] -hash = "e2f8c263a469a521d876de814fb5e3251084dbce9d41e06bf9037ccfe4710fd3" -id = 128765597212202 -slice = [[0, 308], [24, 332]] -packable = true - -[inputs."img/Warning@2x.png"] -hash = "ecec999c799a628e0672fe1dd4d2c16df9f27caeb66b82a7bd5103326c6463ff" -id = 118393146115398 -slice = [[0, 49], [48, 97]] -packable = true - -[inputs."img/Warning@3x.png"] -hash = "9524173aa67880639e5d09b02925e631e1766336bff06ddff2bdf5d834822464" -id = 123049343108173 -slice = [[0, 73], [72, 145]] -packable = true - [inputs."img/flipbook.png"] hash = "423e3dcff54fce8824f14d7cadcfb90eef2abecd817df2bee13951bd2674f79a" -id = 128765597212202 +id = 74272179538762 slice = [[231, 0], [295, 64]] packable = true From 078849fd8ac127a8ed6c26b3ba2bae924403c0ed Mon Sep 17 00:00:00 2001 From: Marin Minnerly <me@vocksel.com> Date: Tue, 17 Dec 2024 15:33:01 -0800 Subject: [PATCH 48/79] Star icon for pinned nodes --- src/Storybook/StorybookTreeView.luau | 2 +- src/TreeView/TreeNode.luau | 4 ++-- src/TreeView/types.luau | 3 ++- src/TreeView/usePinnedInstances.luau | 6 ++++++ src/TreeView/useTreeNodeIcon.luau | 4 +++- src/themes.luau | 3 +++ 6 files changed, 17 insertions(+), 5 deletions(-) diff --git a/src/Storybook/StorybookTreeView.luau b/src/Storybook/StorybookTreeView.luau index 2ff00dd9..b818406b 100644 --- a/src/Storybook/StorybookTreeView.luau +++ b/src/Storybook/StorybookTreeView.luau @@ -43,7 +43,7 @@ local function StorybookTreeView(props: Props) local pins: TreeNode = { id = HttpService:GenerateGUID(), label = "Starred", - icon = "folder", + icon = "star", isExpanded = false, children = {}, } diff --git a/src/TreeView/TreeNode.luau b/src/TreeView/TreeNode.luau index 59303997..afdefb85 100644 --- a/src/TreeView/TreeNode.luau +++ b/src/TreeView/TreeNode.luau @@ -158,7 +158,7 @@ local function TreeNode(props: Props) }), }), - Pin = if props.node.icon == "story" or props.node.icon == "storybook" + Pin = if props.node.instance and (props.node.icon == "story" or props.node.icon == "storybook") then React.createElement("ImageButton", { LayoutOrder = 3, BackgroundTransparency = 1, @@ -166,7 +166,7 @@ local function TreeNode(props: Props) [React.Event.Activated] = onTogglePin, }, { Icon = React.createElement(Sprite, { - image = assets.Magnify, -- TODO: Use a new icon for pinning + image = if pinning.isPinned(props.node.instance) then assets.StarFilled else assets.Star, color = theme.text, size = UDim2.fromOffset(16, 16), }), diff --git a/src/TreeView/types.luau b/src/TreeView/types.luau index f84293fc..7f94bd48 100644 --- a/src/TreeView/types.luau +++ b/src/TreeView/types.luau @@ -1,12 +1,13 @@ local types = {} -export type TreeNodeIcon = "none" | "story" | "storybook" | "folder" +export type TreeNodeIcon = "none" | "story" | "storybook" | "folder" | "star" types.TreeNodeIcon = { None = "none" :: "none", Story = "story" :: "story", Storybook = "storybook" :: "storybook", Folder = "folder" :: "folder", + Star = "star" :: "star", } export type PartialTreeNode = { diff --git a/src/TreeView/usePinnedInstances.luau b/src/TreeView/usePinnedInstances.luau index f0d9349a..fed3a253 100644 --- a/src/TreeView/usePinnedInstances.luau +++ b/src/TreeView/usePinnedInstances.luau @@ -82,9 +82,15 @@ local function usePinnedInstances(): (ModuleScript?, (storyModule: ModuleScript? writePinnedPathsToDisk(pinnedPaths) end, { readPinnedPathsFromDisk, writePinnedPathsToDisk }) + local isPinned = useCallback(function(instance: Instance) + local pinnedPaths = readPinnedPathsFromDisk() + return table.find(pinnedPaths, instance:GetFullName()) ~= nil + end, { readPinnedPathsFromDisk }) + return { pin = pin, unpin = unpin, + isPinned = isPinned, togglePin = togglePin, getPinnedInstances = getPinnedInstances, } diff --git a/src/TreeView/useTreeNodeIcon.luau b/src/TreeView/useTreeNodeIcon.luau index 92525320..a2fc1da7 100644 --- a/src/TreeView/useTreeNodeIcon.luau +++ b/src/TreeView/useTreeNodeIcon.luau @@ -1,6 +1,6 @@ local assets = require("@root/assets") -local useTheme = require("@root/Common/useTheme") local types = require("./types") +local useTheme = require("@root/Common/useTheme") type TreeNodeIcon = types.TreeNodeIcon @@ -19,6 +19,8 @@ local function useTreeNodeIcon(icon: TreeNodeIcon): (Sprite, Color3) return assets.Storybook, theme.textFaded elseif icon == types.TreeNodeIcon.Folder then return assets.Folder, theme.directory + elseif icon == types.TreeNodeIcon.Star then + return assets.Star, theme.star else return assets.Folder, theme.textFaded end diff --git a/src/themes.luau b/src/themes.luau index a2b57a8d..b8f6bb72 100644 --- a/src/themes.luau +++ b/src/themes.luau @@ -22,6 +22,7 @@ export type Theme = { story: Color3, directory: Color3, alert: Color3, + star: Color3, github: Color3, @@ -54,6 +55,7 @@ local Light: Theme = { story = tailwind.green500, directory = tailwind.purple500, alert = tailwind.rose500, + star = tailwind.yellow400, github = Color3.fromHex("#333333"), @@ -86,6 +88,7 @@ local Dark: Theme = { story = tailwind.green500, directory = tailwind.purple500, alert = tailwind.rose500, + star = tailwind.yellow400, github = Color3.fromHex("#ffffff"), From 8bdce5ee4b3f0239bf950fb22f736f730ceac32e Mon Sep 17 00:00:00 2001 From: Marin Minnerly <me@vocksel.com> Date: Tue, 17 Dec 2024 15:33:41 -0800 Subject: [PATCH 49/79] Remove another todo --- src/Storybook/StoryPreview.luau | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Storybook/StoryPreview.luau b/src/Storybook/StoryPreview.luau index bcaecfc8..2e3c3cbd 100644 --- a/src/Storybook/StoryPreview.luau +++ b/src/Storybook/StoryPreview.luau @@ -59,7 +59,6 @@ local StoryPreview = React.forwardRef(function(providedProps: Props, ref: any) React.useEffect(function(): (() -> ())? if props.story and ref.current then - -- TODO: Rendering before controls are applied local success, result = xpcall(function() lifecycle.current = Storyteller.render(ref.current, props.story) end, debug.traceback) From 6614d871b4377b3e8bda3ff9567e08c3400f993e Mon Sep 17 00:00:00 2001 From: Marin Minnerly <me@vocksel.com> Date: Tue, 17 Dec 2024 15:45:55 -0800 Subject: [PATCH 50/79] Adjust star colors --- src/themes.luau | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/themes.luau b/src/themes.luau index b8f6bb72..db5a6325 100644 --- a/src/themes.luau +++ b/src/themes.luau @@ -55,7 +55,7 @@ local Light: Theme = { story = tailwind.green500, directory = tailwind.purple500, alert = tailwind.rose500, - star = tailwind.yellow400, + star = tailwind.amber600, github = Color3.fromHex("#333333"), @@ -88,7 +88,7 @@ local Dark: Theme = { story = tailwind.green500, directory = tailwind.purple500, alert = tailwind.rose500, - star = tailwind.yellow400, + star = tailwind.amber400, github = Color3.fromHex("#ffffff"), From 85e877bcb97c1d6a765032413a5ad3d4dad3231b Mon Sep 17 00:00:00 2001 From: Marin Minnerly <me@vocksel.com> Date: Wed, 18 Dec 2024 09:06:16 -0800 Subject: [PATCH 51/79] Remove engine building step --- .lune/build.luau | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/.lune/build.luau b/.lune/build.luau index 85d23d7b..3ed3cb56 100644 --- a/.lune/build.luau +++ b/.lune/build.luau @@ -1,4 +1,3 @@ -local fs = require("@lune/fs") local process = require("@lune/process") local clean = require("./lib/clean") @@ -17,30 +16,10 @@ assert(target == "dev" or target == "prod", `bad value for target (must be one o local output = if args.output then args.output else `{getPluginsPath(process.os)}/{constants.PLUGIN_FILENAME}` assert(typeof(output) == "string", `bad value for output (string expected, got {typeof(output)})`) -local gameEnginePath = args.gameEnginePath -assert( - not gameEnginePath or typeof(gameEnginePath) == "string", - `bad value for gameEnginePath (string expected, got {typeof(output)}` -) - local function build() - if not fs.isDir("Packages") then - run("lune", { "run", "wally-install" }) - end - clean() compile(target) - if gameEnginePath then - local engineBuildPath = run("find", { `{gameEnginePath}/build`, "-name", "optimized" }) - assert(fs.isDir(engineBuildPath), `failed to find optimized engine build under {gameEnginePath}`) - - local builtInPlugins = run("find", { engineBuildPath, "-name", "BuiltInPlugins", "-print", "-quit" }) - assert(fs.isDir(builtInPlugins), `failed to find built-in plugins under {engineBuildPath}`) - - output = `{builtInPlugins}/Optimized_Embedded_Signature/{constants.PLUGIN_FILENAME}` - end - run("rojo", { "build", "-o", output }) end @@ -51,7 +30,6 @@ if args.watch then filePatterns = { "src/.*%.luau", "example/.*%.luau", - "Packages/.*%.luau", }, onChanged = build, }) From 4d1e48f374f854005decab70370f8d2dbaf0ae1d Mon Sep 17 00:00:00 2001 From: Marin Minnerly <me@vocksel.com> Date: Wed, 18 Dec 2024 10:43:48 -0800 Subject: [PATCH 52/79] Fix stories spec --- src/stories.spec.luau | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/stories.spec.luau b/src/stories.spec.luau index 0d26cc17..bb2055b9 100644 --- a/src/stories.spec.luau +++ b/src/stories.spec.luau @@ -39,13 +39,13 @@ describeEach({ } :: any ) :: ModuleLoader.ModuleLoader - local storybook = Storyteller.loadStorybookModule(mockModuleLoader, storybookModule) + local storybook = Storyteller.loadStorybookModule(storybookModule, mockModuleLoader) describeEach({ Storyteller.findStoryModulesForStorybook(storybook), })("%s", function(storyModule) test("basic mount/unmount lifecycle", function() - local story = Storyteller.loadStoryModule(mockModuleLoader, storyModule, storybook) + local story = Storyteller.loadStoryModule(storyModule, storybook) if story.packages then story.packages = Sift.Dictionary.join(story.packages, { From 6ee4084af6d621ade14fd30c7371425d72cb8b28 Mon Sep 17 00:00:00 2001 From: Marin Minnerly <me@vocksel.com> Date: Wed, 18 Dec 2024 10:52:06 -0800 Subject: [PATCH 53/79] Make sure community Flipbook doesn't crash --- src/Permissions/canAccess.luau | 11 +++++++++++ src/Permissions/tryGetService.luau | 8 +++++++- 2 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 src/Permissions/canAccess.luau diff --git a/src/Permissions/canAccess.luau b/src/Permissions/canAccess.luau new file mode 100644 index 00000000..75f98107 --- /dev/null +++ b/src/Permissions/canAccess.luau @@ -0,0 +1,11 @@ +local function canAccess(instance: Instance) + local success = pcall(function() + -- Attempt any use of the instance. If it throws an error, we assume + -- that the current script context cannot access it + return instance.Name + end) + + return success +end + +return canAccess diff --git a/src/Permissions/tryGetService.luau b/src/Permissions/tryGetService.luau index 565fa95e..f6c050a3 100644 --- a/src/Permissions/tryGetService.luau +++ b/src/Permissions/tryGetService.luau @@ -1,3 +1,5 @@ +local canAccess = require("@root/Permissions/canAccess") + local function tryGetService(serviceName: string): Instance? local service @@ -15,7 +17,11 @@ local function tryGetService(serviceName: string): Instance? service = game:FindFirstChild(serviceName) end) - return service + if canAccess(service) then + return service + end + + return nil end return tryGetService From 47b69fe153a512cc5e393812daf50aba00fe3389 Mon Sep 17 00:00:00 2001 From: Marin Minnerly <me@vocksel.com> Date: Wed, 18 Dec 2024 10:52:17 -0800 Subject: [PATCH 54/79] Add back Packages watching --- .lune/build.luau | 1 + 1 file changed, 1 insertion(+) diff --git a/.lune/build.luau b/.lune/build.luau index 3ed3cb56..1e356620 100644 --- a/.lune/build.luau +++ b/.lune/build.luau @@ -30,6 +30,7 @@ if args.watch then filePatterns = { "src/.*%.luau", "example/.*%.luau", + "Packages/.*%.luau", }, onChanged = build, }) From 01051111aa49ebaa4c0289bedb9bee001a03ec17 Mon Sep 17 00:00:00 2001 From: Marin Minnerly <me@vocksel.com> Date: Wed, 18 Dec 2024 10:59:24 -0800 Subject: [PATCH 55/79] Also install Storyteller to CodeSamples to handle analysis errors --- .lune/wally-install.luau | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/.lune/wally-install.luau b/.lune/wally-install.luau index 04d05e79..3871877d 100644 --- a/.lune/wally-install.luau +++ b/.lune/wally-install.luau @@ -2,14 +2,14 @@ local process = require("@lune/process") local run = require("./lib/run") -local function installPackageFromDisk(packageName: string, packagePath: string) +local function installPackageFromDisk(packageName: string, packagePath: string, runOptions: { [string]: any }?) local homePath = process.env.HOME assert(homePath, "no $HOME env var") local absPackagePath = run("realpath", { packagePath }) - run("rm", { `Packages/{packageName}.lua` }) - run("ln", { "-s", `{absPackagePath}/dist`, `Packages/{packageName}` }) + run("rm", { `Packages/{packageName}.lua` }, runOptions) + run("ln", { "-s", `{absPackagePath}/dist`, `Packages/{packageName}` }, runOptions) end do @@ -26,6 +26,11 @@ do run("wally", { "install" }, { cwd = "code-samples", }) + + installPackageFromDisk("Storyteller", "../storyteller", { + cwd = "code-samples", + }) + run("rojo", { "sourcemap", "default.project.json", "-o", "sourcemap.json" }, { cwd = "code-samples", }) From 305b41d7363487fa8f573c60a49f7dae62de71e8 Mon Sep 17 00:00:00 2001 From: Marin Minnerly <me@vocksel.com> Date: Wed, 18 Dec 2024 12:02:34 -0800 Subject: [PATCH 56/79] Remove `loader` prop. Storyteller handles this for us now --- .lune/wally-install.luau | 1 + src/Navigation/Screen.luau | 4 ---- src/Plugin/PluginApp.luau | 10 ++-------- src/Plugin/PluginApp.story.luau | 6 +----- src/Plugin/createFlipbookPlugin.luau | 7 +------ src/Storybook/StoryCanvas.luau | 4 ---- src/Storybook/StoryView.luau | 6 +----- src/Storybook/StorybookTreeView.story.luau | 5 +---- src/stories.spec.luau | 13 +++++-------- wally.toml | 5 +---- 10 files changed, 13 insertions(+), 48 deletions(-) diff --git a/.lune/wally-install.luau b/.lune/wally-install.luau index 3871877d..4f5ae971 100644 --- a/.lune/wally-install.luau +++ b/.lune/wally-install.luau @@ -16,6 +16,7 @@ do run("wally", { "install" }) installPackageFromDisk("Storyteller", "../storyteller") + -- ModuleLoader is a dependency of Storyteller, make sure to install both. installPackageFromDisk("ModuleLoader", "../module-loader") run("rojo", { "sourcemap", "build.project.json", "-o", "sourcemap.json" }) diff --git a/src/Navigation/Screen.luau b/src/Navigation/Screen.luau index ac3909dd..4060a0ce 100644 --- a/src/Navigation/Screen.luau +++ b/src/Navigation/Screen.luau @@ -1,4 +1,3 @@ -local ModuleLoader = require("@pkg/ModuleLoader") local React = require("@pkg/React") local Storyteller = require("@pkg/Storyteller") @@ -10,11 +9,9 @@ local StoryCanvas = require("@root/Storybook/StoryCanvas") local useMemo = React.useMemo -type ModuleLoader = ModuleLoader.ModuleLoader type LoadedStorybook = Storyteller.LoadedStorybook export type Props = { - loader: ModuleLoader, story: ModuleScript?, storybook: LoadedStorybook?, } @@ -27,7 +24,6 @@ local function Screen(props: Props) if currentScreen == "Home" then if props.story and props.storybook then return React.createElement(StoryCanvas, { - loader = props.loader, story = props.story, storybook = props.storybook, }) diff --git a/src/Plugin/PluginApp.luau b/src/Plugin/PluginApp.luau index 22a5ff67..8af915ef 100644 --- a/src/Plugin/PluginApp.luau +++ b/src/Plugin/PluginApp.luau @@ -1,4 +1,3 @@ -local ModuleLoader = require("@pkg/ModuleLoader") local React = require("@pkg/React") local Storyteller = require("@pkg/Storyteller") @@ -16,14 +15,10 @@ local TOPBAR_HEIGHT_PX = 32 type LoadedStorybook = Storyteller.LoadedStorybook -export type Props = { - loader: ModuleLoader.ModuleLoader, -} - -local function App(props: Props) +local function App() local theme = useTheme() local settingsContext = SettingsContext.use() - local storybooks = Storyteller.useStorybooks(game, props.loader) + local storybooks = Storyteller.useStorybooks(game) local storyModule: ModuleScript?, setStoryModule = React.useState(nil :: ModuleScript?) local storybook, setStorybook = React.useState(nil :: LoadedStorybook?) local initialSidebarWidth = settingsContext.getSetting("sidebarWidth") @@ -91,7 +86,6 @@ local function App(props: Props) BackgroundTransparency = 1, }, { Screen = React.createElement(Screen, { - loader = props.loader, story = storyModule, storybook = storybook, }), diff --git a/src/Plugin/PluginApp.story.luau b/src/Plugin/PluginApp.story.luau index 858937b7..e62cede1 100644 --- a/src/Plugin/PluginApp.story.luau +++ b/src/Plugin/PluginApp.story.luau @@ -1,4 +1,3 @@ -local ModuleLoader = require("@pkg/ModuleLoader") local React = require("@pkg/React") local ContextProviders = require("@root/Common/ContextProviders") @@ -11,9 +10,6 @@ return { story = React.createElement(ContextProviders, { plugin = MockPlugin.new() :: any, }, { - PluginApp = React.createElement(PluginApp, { - loader = ModuleLoader.new(), - plugin = plugin, - }), + PluginApp = React.createElement(PluginApp), }), } diff --git a/src/Plugin/createFlipbookPlugin.luau b/src/Plugin/createFlipbookPlugin.luau index d19ba68f..62db0e5b 100644 --- a/src/Plugin/createFlipbookPlugin.luau +++ b/src/Plugin/createFlipbookPlugin.luau @@ -4,7 +4,6 @@ if RunService:IsRunning() or not RunService:IsEdit() then return end -local ModuleLoader = require("@pkg/ModuleLoader") local React = require("@pkg/React") local ReactRoblox = require("@pkg/ReactRoblox") @@ -21,19 +20,15 @@ local function createFlipbookPlugin( } local connections: { RBXScriptConnection } = {} local root = ReactRoblox.createRoot(widget) - local loader = ModuleLoader.new() local app = React.createElement(ContextProviders, { plugin = plugin, }, { - PluginApp = React.createElement(PluginApp, { - loader = loader, - }), + PluginApp = React.createElement(PluginApp), }) local function unmount() root:unmount() - loader:clear() end local function mount() diff --git a/src/Storybook/StoryCanvas.luau b/src/Storybook/StoryCanvas.luau index a1927253..f8888ae7 100644 --- a/src/Storybook/StoryCanvas.luau +++ b/src/Storybook/StoryCanvas.luau @@ -1,4 +1,3 @@ -local ModuleLoader = require("@pkg/ModuleLoader") local React = require("@pkg/React") local Storyteller = require("@pkg/Storyteller") @@ -7,12 +6,10 @@ local useTheme = require("@root/Common/useTheme") local e = React.createElement -type ModuleLoader = ModuleLoader.ModuleLoader type LoadedStorybook = Storyteller.LoadedStorybook type Props = { story: ModuleScript, - loader: ModuleLoader, storybook: LoadedStorybook, layoutOrder: number?, } @@ -43,7 +40,6 @@ local function Canvas(props: Props) }), StoryView = props.story and e(StoryView, { - loader = props.loader, story = props.story, storybook = props.storybook, }), diff --git a/src/Storybook/StoryView.luau b/src/Storybook/StoryView.luau index 6f4db7a8..6f8c6af1 100644 --- a/src/Storybook/StoryView.luau +++ b/src/Storybook/StoryView.luau @@ -1,6 +1,5 @@ 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") @@ -20,11 +19,9 @@ local useZoom = require("@root/Common/useZoom") local e = React.createElement -type ModuleLoader = ModuleLoader.ModuleLoader type LoadedStorybook = Storyteller.LoadedStorybook type Props = { - loader: ModuleLoader, story: ModuleScript, storybook: LoadedStorybook, } @@ -32,7 +29,7 @@ type Props = { local function StoryView(props: Props) local theme = useTheme() local settingsContext = SettingsContext.use() - local story, storyErr = Storyteller.useStory(props.story, props.storybook, props.loader) + local story, storyErr = Storyteller.useStory(props.story, props.storybook) local zoom = useZoom(props.story) local plugin = React.useContext(PluginContext.Context) local changedControls, setChangedControls = React.useState({}) @@ -46,7 +43,6 @@ local function StoryView(props: Props) end, { story }) local controlsSchema = if story then story.controls else nil - local showControls = controlsSchema and not Sift.isEmpty(controlsSchema) local setControl = React.useCallback(function(control: string, newValue: any) diff --git a/src/Storybook/StorybookTreeView.story.luau b/src/Storybook/StorybookTreeView.story.luau index fc31a4d3..6903f111 100644 --- a/src/Storybook/StorybookTreeView.story.luau +++ b/src/Storybook/StorybookTreeView.story.luau @@ -1,4 +1,3 @@ -local ModuleLoader = require("@pkg/ModuleLoader") local React = require("@pkg/React") local Storyteller = require("@pkg/Storyteller") @@ -6,10 +5,8 @@ local ContextProviders = require("@root/Common/ContextProviders") local MockPlugin = require("@root/Testing/MockPlugin") local StorybookTreeView = require("./StorybookTreeView") -local loader = ModuleLoader.new() - local function Story() - local storybooks = Storyteller.useStorybooks(game, loader) + local storybooks = Storyteller.useStorybooks(game) return React.createElement("Frame", { Size = UDim2.fromOffset(300, 0), diff --git a/src/stories.spec.luau b/src/stories.spec.luau index bb2055b9..a8a0bcf7 100644 --- a/src/stories.spec.luau +++ b/src/stories.spec.luau @@ -1,7 +1,6 @@ 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 Sift = require("@pkg/Sift") @@ -31,13 +30,11 @@ describeEach({ -- 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 mockModuleLoader = { + require = function(_self, path) + return (require :: any)(path) + end, + } :: any local storybook = Storyteller.loadStorybookModule(storybookModule, mockModuleLoader) diff --git a/wally.toml b/wally.toml index 8a954484..8189576d 100644 --- a/wally.toml +++ b/wally.toml @@ -7,7 +7,6 @@ realm = "shared" exclude = ["*"] [dependencies] -ModuleLoader = "flipbook-labs/module-loader@0.6.2" Storyteller = "flipbook-labs/storyteller@0.6.0" React = "jsdotlua/react@17.0.2" ReactRoblox = "jsdotlua/react-roblox@17.0.2" @@ -20,8 +19,6 @@ Roact = "roblox/roact@1.4.4" Jest = "jsdotlua/jest@3.6.1-rc.2" JestGlobals = "jsdotlua/jest-globals@3.6.1-rc.2" -# ModuleLoader dependencies -Janitor = "howmanysmall/janitor@1.13.15" - # Storyteller dependencies Prospector = "egomoose/prospector@1.1.0" +Janitor = "howmanysmall/janitor@1.13.15" From 6c3538b55592571a8ed55e098b156cb7a327c3fb Mon Sep 17 00:00:00 2001 From: Marin Minnerly <me@vocksel.com> Date: Wed, 18 Dec 2024 12:02:43 -0800 Subject: [PATCH 57/79] Couple small fixes --- src/RobloxInternal/getMostLikelyProjectSources.luau | 1 + src/TreeView/TreeNode.luau | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/RobloxInternal/getMostLikelyProjectSources.luau b/src/RobloxInternal/getMostLikelyProjectSources.luau index d8419cc0..57a73f6c 100644 --- a/src/RobloxInternal/getMostLikelyProjectSources.luau +++ b/src/RobloxInternal/getMostLikelyProjectSources.luau @@ -23,6 +23,7 @@ local function getMostLikelyProjectSources(): { Instance } return false end end + return nil end) return Sift.List.map(sorted, function(internalSyncItem) diff --git a/src/TreeView/TreeNode.luau b/src/TreeView/TreeNode.luau index afdefb85..f6cb4ba2 100644 --- a/src/TreeView/TreeNode.luau +++ b/src/TreeView/TreeNode.luau @@ -158,7 +158,7 @@ local function TreeNode(props: Props) }), }), - Pin = if props.node.instance and (props.node.icon == "story" or props.node.icon == "storybook") + Pin = if props.node.instance and (props.node.icon == "story" or props.node.icon == "storybook") then React.createElement("ImageButton", { LayoutOrder = 3, BackgroundTransparency = 1, @@ -166,7 +166,7 @@ local function TreeNode(props: Props) [React.Event.Activated] = onTogglePin, }, { Icon = React.createElement(Sprite, { - image = if pinning.isPinned(props.node.instance) then assets.StarFilled else assets.Star, + image = if pinning.isPinned(props.node.instance) then assets.StarFilled else assets.Star, color = theme.text, size = UDim2.fromOffset(16, 16), }), From 8e4b58ed7ab127f8798fc8c504e495774a34beea Mon Sep 17 00:00:00 2001 From: Marin Minnerly <me@vocksel.com> Date: Wed, 18 Dec 2024 12:50:46 -0800 Subject: [PATCH 58/79] Only read from disk once for pinned instances --- src/Common/ContextProviders.luau | 2 + src/Plugin/LocalStorageContext.luau | 99 ++++++++++++++++++++++++++++ src/Plugin/createFlipbookPlugin.luau | 1 - src/TreeView/usePinnedInstances.luau | 84 +++++++++-------------- 4 files changed, 132 insertions(+), 54 deletions(-) create mode 100644 src/Plugin/LocalStorageContext.luau diff --git a/src/Common/ContextProviders.luau b/src/Common/ContextProviders.luau index 430571ed..4d862c5e 100644 --- a/src/Common/ContextProviders.luau +++ b/src/Common/ContextProviders.luau @@ -2,6 +2,7 @@ local React = require("@pkg/React") local TreeView = require("@root/TreeView") local ContextStack = require("@root/Common/ContextStack") +local LocalStorageContext = require("@root/Plugin/LocalStorageContext") local NavigationContext = require("@root/Navigation/NavigationContext") local PluginContext = require("@root/Plugin/PluginContext") local SettingsContext = require("@root/UserSettings/SettingsContext") @@ -20,6 +21,7 @@ local function ContextProviders(props: Props) React.createElement(NavigationContext.Provider, { defaultScreen = "Home", }), + React.createElement(LocalStorageContext.Provider), React.createElement(SettingsContext.Provider), React.createElement(TreeView.TreeViewProvider), }, diff --git a/src/Plugin/LocalStorageContext.luau b/src/Plugin/LocalStorageContext.luau new file mode 100644 index 00000000..ddd9b9f0 --- /dev/null +++ b/src/Plugin/LocalStorageContext.luau @@ -0,0 +1,99 @@ +local HttpService = game:GetService("HttpService") + +local React = require("@pkg/React") +local Sift = require("@pkg/Sift") + +local PluginContext = require("@root/Plugin/PluginContext") +local usePrevious = require("@root/Common/usePrevious") + +local useCallback = React.useCallback +local useContext = React.useContext +local useEffect = React.useEffect +local useMemo = React.useMemo +local useState = React.useState + +export type LocalStorage = { + [string]: unknown, +} + +export type LocalStorageContext = { + get: (key: string) -> unknown, + set: (key: string, value: unknown) -> (), +} + +local LocalStorageContext = React.createContext({}) + +export type Props = { + storageKey: string?, + children: React.Node, +} + +local function LocalStorageProvider(props: Props) + local plugin = useContext(PluginContext.Context) + + local storageKey = useMemo(function() + return if props.storageKey then props.storageKey else `{plugin.Name}LocalStorage` + end, { props.storageKey, plugin }) + + local loadFromDisk = useCallback(function(): LocalStorage + local data = plugin:GetSetting(storageKey) + if data then + local json = HttpService:JSONDecode(data) + if json then + return json + end + end + return {} + end, { plugin, storageKey }) + + local storage, setStorage = useState(loadFromDisk) + local prevStorage = usePrevious(storage) + + local saveToDisk = useCallback(function() + local data = HttpService:JSONEncode(storage) + if data then + plugin:SetSetting(storageKey, data) + end + end, { plugin, storageKey, storage }) + + local get = useCallback(function(key: string) + return storage[key] + end, { storage }) + + local set = useCallback(function(key: string, value: unknown) + setStorage(function(prev) + return Sift.Dictionary.join(prev, { + [key] = if typeof(value) == "function" then value(prev[key]) else value, + }) + end) + end, { storage }) + + useEffect(function() + if storage and storage ~= prevStorage then + saveToDisk() + end + end, { storage, prevStorage, saveToDisk }) + + local context: LocalStorageContext = { + get = get, + set = set, + } + + return React.createElement(LocalStorageContext.Provider, { + value = context, + }, props.children) +end + +local function useLocalStorage(): TreeViewContext + local context = useContext(LocalStorageContext) + if not context then + local contextName = script.Name + error(`failed to use {contextName}, is \`{contextName}.Provider\` defined in the React hierarchy?`) + end + return context +end + +return { + Provider = LocalStorageProvider, + use = useLocalStorage, +} diff --git a/src/Plugin/createFlipbookPlugin.luau b/src/Plugin/createFlipbookPlugin.luau index 62db0e5b..2d8cd79f 100644 --- a/src/Plugin/createFlipbookPlugin.luau +++ b/src/Plugin/createFlipbookPlugin.luau @@ -65,7 +65,6 @@ local function createFlipbookPlugin( end local function destroy() - print("destroy") unmount() for _, connection in connections do connection:Disconnect() diff --git a/src/TreeView/usePinnedInstances.luau b/src/TreeView/usePinnedInstances.luau index fed3a253..0ca670dd 100644 --- a/src/TreeView/usePinnedInstances.luau +++ b/src/TreeView/usePinnedInstances.luau @@ -1,12 +1,12 @@ -local HttpService = game:GetService("HttpService") - local React = require("@pkg/React") +local Sift = require("@pkg/Sift") -local PluginContext = require("@root/Plugin/PluginContext") +local LocalStorageContext = require("@root/Plugin/LocalStorageContext") local getInstanceFromFullName = require("@root/Common/getInstanceFromFullName") -local useContext = React.useContext local useCallback = React.useCallback +local useState = React.useState +local useEffect = React.useEffect local PINNED_INSTANCES_KEY = "pinnedInstancePaths" @@ -16,45 +16,31 @@ export type PinnedInstance = { } local function usePinnedInstances(): (ModuleScript?, (storyModule: ModuleScript?) -> ()) - local plugin = useContext(PluginContext.Context) - - local readPinnedPathsFromDisk = useCallback(function(): { string } - local data = plugin:GetSetting(PINNED_INSTANCES_KEY) - if data then - local json = HttpService:JSONDecode(data) - if json then - return json - end - end - return {} - end, { plugin }) + local localStorage = LocalStorageContext.use() - local writePinnedPathsToDisk = useCallback(function(pins: { string }) - local data = HttpService:JSONEncode(pins) - plugin:SetSetting(PINNED_INSTANCES_KEY, data) - end, { plugin }) + local pinnedPaths, setPinnedPaths = useState(function() + return localStorage.get(PINNED_INSTANCES_KEY) or {} + end) - local pin = useCallback(function(instance: Instance) - local pinnedPaths = readPinnedPathsFromDisk() + useEffect(function() + localStorage.set(PINNED_INSTANCES_KEY, pinnedPaths) + end, { pinnedPaths }) - table.insert(pinnedPaths, instance:GetFullName()) - - writePinnedPathsToDisk(pinnedPaths) - end, { readPinnedPathsFromDisk, writePinnedPathsToDisk }) + local pin = useCallback(function(instance: Instance) + setPinnedPaths(function(prev) + return Sift.List.append(prev, instance:GetFullName()) + end) + end, {}) local unpin = useCallback(function(instance: Instance) - local pinnedPaths = readPinnedPathsFromDisk() - - local index = table.find(pinnedPaths, instance:GetFullName()) - if index then - table.remove(pinnedPaths, index) - end - - writePinnedPathsToDisk(pinnedPaths) - end, { readPinnedPathsFromDisk, writePinnedPathsToDisk }) + setPinnedPaths(function(prev) + return Sift.List.filter(prev, function(pinnedPath) + return pinnedPath ~= instance:GetFullName() + end) + end) + end, {}) local getPinnedInstances = useCallback(function(): { PinnedInstance } - local pinnedPaths = readPinnedPathsFromDisk() local pinnedInstances: { PinnedInstance } = {} for _, pinnedPath in pinnedPaths do @@ -65,27 +51,19 @@ local function usePinnedInstances(): (ModuleScript?, (storyModule: ModuleScript? end return pinnedInstances - end, { readPinnedPathsFromDisk }) + end, { pinnedPaths }) - local togglePin = useCallback(function(instance: Instance) - local pinnedPaths = readPinnedPathsFromDisk() - local path = instance:GetFullName() - - local index = table.find(pinnedPaths, path) + local isPinned = useCallback(function(instance: Instance) + return table.find(pinnedPaths, instance:GetFullName()) ~= nil + end, { pinnedPaths }) - if index then - table.remove(pinnedPaths, index) + local togglePin = useCallback(function(instance: Instance) + if isPinned(instance) then + unpin(instance) else - table.insert(pinnedPaths, path) + pin(instance) end - - writePinnedPathsToDisk(pinnedPaths) - end, { readPinnedPathsFromDisk, writePinnedPathsToDisk }) - - local isPinned = useCallback(function(instance: Instance) - local pinnedPaths = readPinnedPathsFromDisk() - return table.find(pinnedPaths, instance:GetFullName()) ~= nil - end, { readPinnedPathsFromDisk }) + end, { isPinned, unpin, pin }) return { pin = pin, From e370b09a7f9f33c4ddbeaf582fb69caa2bdfebec Mon Sep 17 00:00:00 2001 From: Marin Minnerly <me@vocksel.com> Date: Wed, 18 Dec 2024 13:20:19 -0800 Subject: [PATCH 59/79] Use LocalStorageContext for useLastOpenedStory --- src/Storybook/useLastOpenedStory.luau | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/Storybook/useLastOpenedStory.luau b/src/Storybook/useLastOpenedStory.luau index bd9e1caa..b59457f5 100644 --- a/src/Storybook/useLastOpenedStory.luau +++ b/src/Storybook/useLastOpenedStory.luau @@ -1,29 +1,31 @@ local React = require("@pkg/React") -local PluginContext = require("@root/Plugin/PluginContext") +local LocalStorageContext = require("@root/Plugin/LocalStorageContext") local SettingsContext = require("@root/UserSettings/SettingsContext") local getInstanceFromFullName = require("@root/Common/getInstanceFromFullName") -local useContext = React.useContext local useCallback = React.useCallback local useMemo = React.useMemo -local function useLastOpenedStory(): (ModuleScript?, (storyModule: ModuleScript?) -> ()) - local plugin = useContext(PluginContext.Context) +local REMEMBER_LAST_OPENED_STORY_KEY = "rememberLastOpenedStory" +local LAST_OPENED_STORY_PATH_KEY = "lastOpenedStoryPath" +local function useLastOpenedStory(): (ModuleScript?, (storyModule: ModuleScript?) -> ()) + local localStorage = LocalStorageContext.use() local settingsContext = SettingsContext.use() - local rememberLastOpenedStory = settingsContext.getSetting("rememberLastOpenedStory") local setLastOpenedStory = useCallback(function(storyModule: ModuleScript?) - plugin:SetSetting("lastOpenedStoryPath", if storyModule then storyModule:GetFullName() else nil) - end, { plugin }) + localStorage.set(LAST_OPENED_STORY_PATH_KEY, if storyModule then storyModule:GetFullName() else nil) + end, { localStorage }) local lastOpenedStory = useMemo(function(): ModuleScript? + local rememberLastOpenedStory = settingsContext.getSetting(REMEMBER_LAST_OPENED_STORY_KEY) + if not rememberLastOpenedStory then return nil end - local lastOpenedStoryPath = plugin:GetSetting("lastOpenedStoryPath") + local lastOpenedStoryPath = localStorage.get(LAST_OPENED_STORY_PATH_KEY) if lastOpenedStoryPath then local instance = getInstanceFromFullName(lastOpenedStoryPath) @@ -34,7 +36,7 @@ local function useLastOpenedStory(): (ModuleScript?, (storyModule: ModuleScript? end return nil - end, { rememberLastOpenedStory, plugin }) + end, { settingsContext, localStorage }) return lastOpenedStory, setLastOpenedStory end From 4c76445e1599c6fbe9a7dfcc3567f45236bbb3d7 Mon Sep 17 00:00:00 2001 From: Marin Minnerly <me@vocksel.com> Date: Wed, 18 Dec 2024 13:37:11 -0800 Subject: [PATCH 60/79] Fix dropdown controls when the user passes a dictionary --- src/Storybook/StoryControls.luau | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Storybook/StoryControls.luau b/src/Storybook/StoryControls.luau index 49b17fcb..23f6eab4 100644 --- a/src/Storybook/StoryControls.luau +++ b/src/Storybook/StoryControls.luau @@ -9,6 +9,10 @@ local useTheme = require("@root/Common/useTheme") local useMemo = React.useMemo local e = React.createElement +local function isArray(obj: { [any]: any }): boolean + return #obj > 0 and next(obj, #obj) == nil +end + type Props = { controlsSchema: { [string]: any }, changedControls: { [string]: any }, @@ -52,7 +56,7 @@ local function StoryControls(props: Props) initialState = control.value, onStateChange = setControl, }) - elseif controlType == "table" then + elseif controlType == "table" and isArray(control.value) then local default = props.changedControls[control.name] option = React.createElement(Dropdown, { default = if default then default else control.value[1], From b1ae5550e7093a809b81fa869b3e03020b57ff0e Mon Sep 17 00:00:00 2001 From: Marin Minnerly <me@vocksel.com> Date: Wed, 18 Dec 2024 14:16:01 -0800 Subject: [PATCH 61/79] Render pages for unavialable storybooks --- src/Navigation/Screen.luau | 5 +++ src/Panels/Sidebar.luau | 3 ++ src/Plugin/PluginApp.luau | 10 +++++ src/Storybook/StorybookTreeView.luau | 64 +++++++++++++++++++--------- src/TreeView/types.luau | 3 +- src/TreeView/useTreeNodeIcon.luau | 2 + 6 files changed, 65 insertions(+), 22 deletions(-) diff --git a/src/Navigation/Screen.luau b/src/Navigation/Screen.luau index 4060a0ce..682a72b3 100644 --- a/src/Navigation/Screen.luau +++ b/src/Navigation/Screen.luau @@ -6,6 +6,7 @@ local NavigationContext = require("@root/Navigation/NavigationContext") local NoStorySelected = require("@root/Storybook/NoStorySelected") local SettingsView = require("@root/UserSettings/SettingsView") local StoryCanvas = require("@root/Storybook/StoryCanvas") +local StoryError = require("@root/Storybook/StoryError") local useMemo = React.useMemo @@ -27,6 +28,10 @@ local function Screen(props: Props) story = props.story, storybook = props.storybook, }) + elseif props.unavailableStorybook then + return React.createElement(StoryError, { + err = `Failed to load {props.unavailableStorybook.storybook.name}\n\n{props.unavailableStorybook.problem}`, + }) else return React.createElement(NoStorySelected) end diff --git a/src/Panels/Sidebar.luau b/src/Panels/Sidebar.luau index 0c9856e7..c9dead18 100644 --- a/src/Panels/Sidebar.luau +++ b/src/Panels/Sidebar.luau @@ -14,12 +14,14 @@ local nextLayoutOrder = require("@root/Common/nextLayoutOrder") local useTheme = require("@root/Common/useTheme") type LoadedStorybook = Storyteller.LoadedStorybook +type UnavailableStorybook = Storyteller.UnavailableStorybook local e = React.createElement type Props = { layoutOrder: number?, onStoryChanged: (storyModule: ModuleScript?, storybook: LoadedStorybook?) -> (), + onShowErrorPage: (unavailableStorybook: UnavailableStorybook) -> (), storybooks: { avialable: { LoadedStorybook }, unavailable: { UnavailableStorybook }, @@ -106,6 +108,7 @@ local function Sidebar(props: Props) searchTerm = search, storybooks = props.storybooks, onStoryChanged = props.onStoryChanged, + onShowErrorPage = props.onShowErrorPage, }), }), }) diff --git a/src/Plugin/PluginApp.luau b/src/Plugin/PluginApp.luau index 8af915ef..a7c4cf91 100644 --- a/src/Plugin/PluginApp.luau +++ b/src/Plugin/PluginApp.luau @@ -14,6 +14,7 @@ local useTheme = require("@root/Common/useTheme") local TOPBAR_HEIGHT_PX = 32 type LoadedStorybook = Storyteller.LoadedStorybook +type UnavailableStorybook = Storyteller.UnavailableStorybook local function App() local theme = useTheme() @@ -21,6 +22,7 @@ local function App() local storybooks = Storyteller.useStorybooks(game) local storyModule: ModuleScript?, setStoryModule = React.useState(nil :: ModuleScript?) local storybook, setStorybook = React.useState(nil :: LoadedStorybook?) + local unavailableStorybook, setUnavailableStorybook = React.useState(nil :: LoadedStorybook?) local initialSidebarWidth = settingsContext.getSetting("sidebarWidth") local sidebarWidth, setSidebarWidth = React.useState(initialSidebarWidth) local navigation = NavigationContext.use() @@ -35,6 +37,12 @@ local function App() setStorybook(newStorybook) end, { navigation.navigateTo } :: { unknown }) + local onShowErrorPage = React.useCallback(function(newUnavailableStorybook: UnavailableStorybook) + setStoryModule(nil) + setStorybook(nil) + setUnavailableStorybook(newUnavailableStorybook) + end, {}) + local onSidebarResized = React.useCallback(function(newSize: Vector2) setSidebarWidth(newSize.X) end, {}) @@ -62,6 +70,7 @@ local function App() }, { Sidebar = React.createElement(Sidebar, { onStoryChanged = onStoryChanged, + onShowErrorPage = onShowErrorPage, storybooks = storybooks, }), }), @@ -88,6 +97,7 @@ local function App() Screen = React.createElement(Screen, { story = storyModule, storybook = storybook, + unavailableStorybook = unavailableStorybook, }), }), }), diff --git a/src/Storybook/StorybookTreeView.luau b/src/Storybook/StorybookTreeView.luau index b818406b..49d9ea06 100644 --- a/src/Storybook/StorybookTreeView.luau +++ b/src/Storybook/StorybookTreeView.luau @@ -23,6 +23,7 @@ export type Props = { unavailable: { UnavailableStorybook }, }, onStoryChanged: ((storyModule: ModuleScript?, storybook: LoadedStorybook?) -> ())?, + onShowErrorPage: ((unavailableStorybook: UnavailableStorybook) -> ())?, layoutOrder: number?, } @@ -32,6 +33,7 @@ local function StorybookTreeView(props: Props) local selectedNode = treeViewContext.getSelectedNode() local prevSelectedNode = usePrevious(selectedNode) local storybookByNodeId = useRef({} :: { [string]: LoadedStorybook }) + local unavailableStorybookByNodeId = useRef({} :: { [string]: UnavailableStorybook }) local lastOpenedStory, setLastOpenedStory = useLastOpenedStory() local pinning = usePinnedInstances() @@ -86,9 +88,15 @@ local function StorybookTreeView(props: Props) } for _, unavailableStorybook in props.storybooks.unavailable do - local root = createTreeNodesForStorybook(unavailableStorybook.storybook) + local root = { + id = HttpService:GenerateGUID(), + label = unavailableStorybook.storybook.name, + icon = "alert", + isExpanded = false, + children = {}, + } table.insert(unavailableStorybooks.children, root) - storybookByNodeId.current[root.id] = unavailableStorybook.storybook + unavailableStorybookByNodeId.current[root.id] = unavailableStorybook end table.insert(roots, unavailableStorybooks) @@ -121,28 +129,42 @@ local function StorybookTreeView(props: Props) end end, { lastOpenedStory, treeViewContext.getNodeByInstance, treeViewContext.activateNode } :: { unknown }) - useEffect(function() - if props.onStoryChanged and selectedNode ~= prevSelectedNode then - if selectedNode then - if - selectedNode.icon == TreeView.TreeNodeIcon.Story - and selectedNode.instance - and selectedNode.instance:IsA("ModuleScript") - then - local ancestry = TreeView.getAncestry(selectedNode) - local root = ancestry[#ancestry] - local storybook = storybookByNodeId.current[root.id] - - if storybook then - props.onStoryChanged(selectedNode.instance, storybook) - setLastOpenedStory(selectedNode.instance) + useEffect( + function() + if selectedNode and selectedNode ~= prevSelectedNode then + if props.onStoryChanged then + if selectedNode then + if + selectedNode.icon == TreeView.TreeNodeIcon.Story + and selectedNode.instance + and selectedNode.instance:IsA("ModuleScript") + then + local ancestry = TreeView.getAncestry(selectedNode) + local root = ancestry[#ancestry] + local storybook = storybookByNodeId.current[root.id] + + if storybook then + props.onStoryChanged(selectedNode.instance, storybook) + setLastOpenedStory(selectedNode.instance) + end + end + else + props.onStoryChanged(nil, nil) + end + end + + if props.onShowErrorPage then + if selectedNode.icon == TreeView.TreeNodeIcon.Alert then + local unavailableStorybook = unavailableStorybookByNodeId.current[selectedNode.id] + if unavailableStorybook then + props.onShowErrorPage(unavailableStorybook) + end end end - else - props.onStoryChanged(nil, nil) end - end - end, { props.onStoryChanged, selectedNode, prevSelectedNode, setLastOpenedStory } :: { unknown }) + end, + { props.onShowErrorPage, props.onStoryChanged, selectedNode, prevSelectedNode, setLastOpenedStory } :: { unknown } + ) return React.createElement(TreeView.TreeView, { layoutOrder = props.layoutOrder, diff --git a/src/TreeView/types.luau b/src/TreeView/types.luau index 7f94bd48..460db899 100644 --- a/src/TreeView/types.luau +++ b/src/TreeView/types.luau @@ -1,6 +1,6 @@ local types = {} -export type TreeNodeIcon = "none" | "story" | "storybook" | "folder" | "star" +export type TreeNodeIcon = "none" | "story" | "storybook" | "folder" | "star" | "alert" types.TreeNodeIcon = { None = "none" :: "none", @@ -8,6 +8,7 @@ types.TreeNodeIcon = { Storybook = "storybook" :: "storybook", Folder = "folder" :: "folder", Star = "star" :: "star", + Alert = "alert" :: "alert", } export type PartialTreeNode = { diff --git a/src/TreeView/useTreeNodeIcon.luau b/src/TreeView/useTreeNodeIcon.luau index a2fc1da7..487ce428 100644 --- a/src/TreeView/useTreeNodeIcon.luau +++ b/src/TreeView/useTreeNodeIcon.luau @@ -21,6 +21,8 @@ local function useTreeNodeIcon(icon: TreeNodeIcon): (Sprite, Color3) return assets.Folder, theme.directory elseif icon == types.TreeNodeIcon.Star then return assets.Star, theme.star + elseif icon == types.TreeNodeIcon.Alert then + return assets.Alert, theme.alert else return assets.Folder, theme.textFaded end From f58271e1a8ee5f80342394651f49fb49080c7285 Mon Sep 17 00:00:00 2001 From: Marin Minnerly <me@vocksel.com> Date: Wed, 18 Dec 2024 14:28:26 -0800 Subject: [PATCH 62/79] Flexy sidebar --- src/Panels/Sidebar.luau | 49 ++++++++++++++++++++++------------------- 1 file changed, 26 insertions(+), 23 deletions(-) diff --git a/src/Panels/Sidebar.luau b/src/Panels/Sidebar.luau index c9dead18..b622e5d5 100644 --- a/src/Panels/Sidebar.luau +++ b/src/Panels/Sidebar.luau @@ -31,11 +31,6 @@ type Props = { local function Sidebar(props: Props) local theme = useTheme() - local headerHeight, setHeaderHeight = React.useState(0) - local onHeaderSizeChanged = React.useCallback(function(rbx: Frame) - setHeaderHeight(rbx.AbsoluteSize.Y) - end, { setHeaderHeight }) - local search: string?, setSearch = React.useState(nil :: string?) local onSearchChanged = React.useCallback(function(value: string) if value == "" then @@ -53,6 +48,7 @@ local function Sidebar(props: Props) }, { UIListLayout = e("UIListLayout", { Padding = theme.padding, + VerticalFlex = Enum.UIFlexAlignment.Fill, SortOrder = Enum.SortOrder.LayoutOrder, }), @@ -68,42 +64,49 @@ local function Sidebar(props: Props) BackgroundTransparency = 1, LayoutOrder = nextLayoutOrder(), Size = UDim2.fromScale(1, 0), - [React.Change.AbsoluteSize] = onHeaderSizeChanged, }, { + FlexItem = React.createElement("UIFlexItem", { + FlexMode = Enum.UIFlexMode.Grow, + }), + UIListLayout = e("UIListLayout", { Padding = theme.paddingLarge, SortOrder = Enum.SortOrder.LayoutOrder, }), Branding = e(Branding, { - layoutOrder = 0, + layoutOrder = nextLayoutOrder(), }), Searchbar = e(Searchbar, { - layoutOrder = 1, + layoutOrder = nextLayoutOrder(), onSearchChanged = onSearchChanged, }), - }), - --if #props.storybooks.available == 0 - CreateStorybook = React.createElement(Button, { - layoutOrder = nextLayoutOrder(), - text = "Create Storybook", - onClick = function() - local source = getMostLikelyProjectSources()[1] - - if source then - createOnboardingStorybook(source.Name, source) - else - createOnboardingStorybook(string.gsub(game.Name, "%.", "_"), ReplicatedStorage) - end - end, + --if #props.storybooks.available == 0 + CreateStorybook = React.createElement(Button, { + layoutOrder = nextLayoutOrder(), + text = "Create Storybook", + onClick = function() + local source = getMostLikelyProjectSources()[1] + + if source then + createOnboardingStorybook(source.Name, source) + else + createOnboardingStorybook(string.gsub(game.Name, "%.", "_"), ReplicatedStorage) + end + end, + }), }), ScrollingFrame = e(ScrollingFrame, { LayoutOrder = nextLayoutOrder(), - Size = UDim2.fromScale(1, 1) - UDim2.fromOffset(0, headerHeight), + Size = UDim2.fromScale(1, 1), }, { + FlexItem = React.createElement("UIFlexItem", { + FlexMode = Enum.UIFlexMode.Shrink, + }), + StorybookTreeView = e(StorybookTreeView, { searchTerm = search, storybooks = props.storybooks, From 7185df800e457979f2c2608ede0d026dbb9ef40c Mon Sep 17 00:00:00 2001 From: Marin Minnerly <me@vocksel.com> Date: Wed, 18 Dec 2024 14:32:03 -0800 Subject: [PATCH 63/79] Fix stories not toggling --- src/Storybook/StorybookTreeView.luau | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Storybook/StorybookTreeView.luau b/src/Storybook/StorybookTreeView.luau index 49d9ea06..e9bd4bfe 100644 --- a/src/Storybook/StorybookTreeView.luau +++ b/src/Storybook/StorybookTreeView.luau @@ -131,7 +131,7 @@ local function StorybookTreeView(props: Props) useEffect( function() - if selectedNode and selectedNode ~= prevSelectedNode then + if selectedNode ~= prevSelectedNode then if props.onStoryChanged then if selectedNode then if @@ -154,7 +154,7 @@ local function StorybookTreeView(props: Props) end if props.onShowErrorPage then - if selectedNode.icon == TreeView.TreeNodeIcon.Alert then + if selectedNode and selectedNode.icon == TreeView.TreeNodeIcon.Alert then local unavailableStorybook = unavailableStorybookByNodeId.current[selectedNode.id] if unavailableStorybook then props.onShowErrorPage(unavailableStorybook) From dba040164d9b3f7efcd5dc022ab11b69a2c44789 Mon Sep 17 00:00:00 2001 From: Marin Minnerly <me@vocksel.com> Date: Wed, 18 Dec 2024 16:12:49 -0800 Subject: [PATCH 64/79] Show more comprehensive details for storybook errors --- src/Common/CodeBlock.luau | 79 +++++++++++++++++ src/Navigation/Screen.luau | 6 +- src/Storybook/StoryError.luau | 32 +------ src/Storybook/StorybookError.luau | 112 ++++++++++++++++++++++++ src/Storybook/StorybookError.story.luau | 32 +++++++ 5 files changed, 230 insertions(+), 31 deletions(-) create mode 100644 src/Common/CodeBlock.luau create mode 100644 src/Storybook/StorybookError.luau create mode 100644 src/Storybook/StorybookError.story.luau diff --git a/src/Common/CodeBlock.luau b/src/Common/CodeBlock.luau new file mode 100644 index 00000000..5172303b --- /dev/null +++ b/src/Common/CodeBlock.luau @@ -0,0 +1,79 @@ +local React = require("@pkg/React") +local Sift = require("@pkg/Sift") + +local SelectableTextLabel = require("@root/Forms/SelectableTextLabel") +local nextLayoutOrder = require("@root/Common/nextLayoutOrder") +local useTheme = require("@root/Common/useTheme") + +local useMemo = React.useMemo + +local function getLineNumbers(str: string): string + return Sift.List.reduce(str:split("\n"), function(accumulator, _item, index) + return if index == 1 then tostring(index) else `{accumulator}\n{index}` + end, "") +end + +export type Props = { + source: string, + sourceColor: Color3?, + layoutOrder: number?, +} + +local function StoryError(props: Props) + local theme = useTheme() + + local sourceColor = useMemo(function() + return if props.sourceColor then props.sourceColor else theme.text + end, { props.sourceColor }) + + return React.createElement("Frame", { + AutomaticSize = Enum.AutomaticSize.XY, + BackgroundTransparency = 0.5, + BorderSizePixel = 0, + BackgroundColor3 = theme.sidebar, + LayoutOrder = props.layoutOrder, + }, { + Layout = React.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder, + FillDirection = Enum.FillDirection.Horizontal, + Padding = theme.padding, + }), + + BorderRadius = React.createElement("UICorner", { + CornerRadius = theme.corner, + }), + + Padding = React.createElement("UIPadding", { + PaddingTop = theme.padding, + PaddingRight = theme.padding, + PaddingBottom = theme.padding, + PaddingLeft = theme.padding, + }), + + LineNumbers = React.createElement("TextLabel", { + LayoutOrder = nextLayoutOrder(), + AutomaticSize = Enum.AutomaticSize.XY, + Text = getLineNumbers(props.source), + TextSize = theme.textSize, + LineHeight = 1, + BackgroundTransparency = 1, + Font = Enum.Font.RobotoMono, + TextColor3 = theme.textFaded, + TextXAlignment = Enum.TextXAlignment.Right, + }), + + SourceCode = React.createElement(SelectableTextLabel, { + LayoutOrder = nextLayoutOrder(), + Size = UDim2.fromScale(1, 0), + AutomaticSize = Enum.AutomaticSize.Y, + Text = props.source, + TextColor3 = sourceColor, + TextSize = theme.textSize, + TextWrapped = false, + LineHeight = 1, + Font = Enum.Font.RobotoMono, + }), + }) +end + +return StoryError diff --git a/src/Navigation/Screen.luau b/src/Navigation/Screen.luau index 682a72b3..e792b1ae 100644 --- a/src/Navigation/Screen.luau +++ b/src/Navigation/Screen.luau @@ -6,7 +6,7 @@ local NavigationContext = require("@root/Navigation/NavigationContext") local NoStorySelected = require("@root/Storybook/NoStorySelected") local SettingsView = require("@root/UserSettings/SettingsView") local StoryCanvas = require("@root/Storybook/StoryCanvas") -local StoryError = require("@root/Storybook/StoryError") +local StorybookError = require("@root/Storybook/StorybookError") local useMemo = React.useMemo @@ -29,8 +29,8 @@ local function Screen(props: Props) storybook = props.storybook, }) elseif props.unavailableStorybook then - return React.createElement(StoryError, { - err = `Failed to load {props.unavailableStorybook.storybook.name}\n\n{props.unavailableStorybook.problem}`, + return React.createElement(StorybookError, { + unavailableStorybook = props.unavailableStorybook, }) else return React.createElement(NoStorySelected) diff --git a/src/Storybook/StoryError.luau b/src/Storybook/StoryError.luau index 75a15d86..646c5213 100644 --- a/src/Storybook/StoryError.luau +++ b/src/Storybook/StoryError.luau @@ -1,8 +1,7 @@ local React = require("@pkg/React") -local Sift = require("@pkg/Sift") +local CodeBlock = require("@root/Common/CodeBlock") local ScrollingFrame = require("@root/Common/ScrollingFrame") -local SelectableTextLabel = require("@root/Forms/SelectableTextLabel") local useTheme = require("@root/Common/useTheme") export type Props = { @@ -13,10 +12,6 @@ export type Props = { local function StoryError(props: Props) local theme = useTheme() - local lineNumbers = Sift.List.reduce(props.err:split("\n"), function(accumulator, _item, index) - return if index == 1 then tostring(index) else `{accumulator}\n{index}` - end, "") - return React.createElement(ScrollingFrame, { ScrollingDirection = Enum.ScrollingDirection.XY, LayoutOrder = props.layoutOrder, @@ -34,28 +29,9 @@ local function StoryError(props: Props) PaddingLeft = theme.paddingSmall, }), - LineNumbers = React.createElement("TextLabel", { - LayoutOrder = 1, - AutomaticSize = Enum.AutomaticSize.XY, - Text = lineNumbers, - TextSize = theme.textSize, - LineHeight = 1, - BackgroundTransparency = 1, - Font = Enum.Font.RobotoMono, - TextColor3 = theme.textFaded, - TextXAlignment = Enum.TextXAlignment.Right, - }), - - ErrorMessage = React.createElement(SelectableTextLabel, { - LayoutOrder = 2, - Size = UDim2.fromScale(1, 0), - AutomaticSize = Enum.AutomaticSize.Y, - Text = props.err, - TextColor3 = theme.alert, - TextSize = theme.textSize, - TextWrapped = false, - LineHeight = 1, - Font = Enum.Font.RobotoMono, + CodeBlock = React.createElement(CodeBlock, { + source = props.err, + sourceColor = theme.alert, }), }) end diff --git a/src/Storybook/StorybookError.luau b/src/Storybook/StorybookError.luau new file mode 100644 index 00000000..952ef40f --- /dev/null +++ b/src/Storybook/StorybookError.luau @@ -0,0 +1,112 @@ +local React = require("@pkg/React") +local Sift = require("@pkg/Sift") +local Storyteller = require("@pkg/Storyteller") + +local CodeBlock = require("@root/Common/CodeBlock") +local ScrollingFrame = require("@root/Common/ScrollingFrame") +local nextLayoutOrder = require("@root/Common/nextLayoutOrder") +local useTheme = require("@root/Common/useTheme") + +type UnavailableStorybook = Storyteller.UnavailableStorybook + +export type Props = { + unavailableStorybook: UnavailableStorybook, + layoutOrder: number?, +} + +local function StoryError(props: Props) + local theme = useTheme() + + local storybookSource = props.unavailableStorybook.storybook.source.Source + + return React.createElement(ScrollingFrame, { + ScrollingDirection = Enum.ScrollingDirection.XY, + LayoutOrder = props.layoutOrder, + }, { + Layout = React.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder, + FillDirection = Enum.FillDirection.Vertical, + Padding = theme.paddingLarge, + }), + + Padding = React.createElement("UIPadding", { + PaddingTop = theme.paddingLarge, + PaddingRight = theme.paddingLarge, + PaddingBottom = theme.paddingLarge, + PaddingLeft = theme.paddingLarge, + }), + + MainText = React.createElement("TextLabel", { + LayoutOrder = nextLayoutOrder(), + Text = `Failed to load {props.unavailableStorybook.storybook.name}`, + AutomaticSize = Enum.AutomaticSize.XY, + BackgroundTransparency = 1, + Font = theme.font, + TextColor3 = theme.text, + TextSize = theme.textSize, + TextXAlignment = Enum.TextXAlignment.Left, + TextYAlignment = Enum.TextYAlignment.Center, + }), + + Problem = React.createElement("Frame", { + AutomaticSize = Enum.AutomaticSize.XY, + BackgroundTransparency = 1, + LayoutOrder = nextLayoutOrder(), + }, { + Layout = React.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder, + FillDirection = Enum.FillDirection.Vertical, + Padding = theme.padding, + }), + + Title = React.createElement("TextLabel", { + LayoutOrder = nextLayoutOrder(), + Text = "Error", + AutomaticSize = Enum.AutomaticSize.XY, + BackgroundTransparency = 1, + Font = theme.headerFont, + TextColor3 = theme.text, + TextSize = theme.headerTextSize, + TextXAlignment = Enum.TextXAlignment.Left, + TextYAlignment = Enum.TextYAlignment.Center, + }), + + CodeBlock = React.createElement(CodeBlock, { + source = props.unavailableStorybook.problem, + sourceColor = theme.alert, + layoutOrder = nextLayoutOrder(), + }), + }), + + StorybookSource = React.createElement("Frame", { + AutomaticSize = Enum.AutomaticSize.XY, + BackgroundTransparency = 1, + LayoutOrder = nextLayoutOrder(), + }, { + Layout = React.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder, + FillDirection = Enum.FillDirection.Vertical, + Padding = theme.padding, + }), + + Title = React.createElement("TextLabel", { + LayoutOrder = nextLayoutOrder(), + Text = "Storybook Source", + AutomaticSize = Enum.AutomaticSize.XY, + BackgroundTransparency = 1, + Font = theme.headerFont, + TextColor3 = theme.text, + TextSize = theme.headerTextSize, + TextXAlignment = Enum.TextXAlignment.Left, + TextYAlignment = Enum.TextYAlignment.Center, + }), + + CodeBlock = React.createElement(CodeBlock, { + source = storybookSource, + layoutOrder = nextLayoutOrder(), + }), + }), + }) +end + +return StoryError diff --git a/src/Storybook/StorybookError.story.luau b/src/Storybook/StorybookError.story.luau new file mode 100644 index 00000000..e1361fd7 --- /dev/null +++ b/src/Storybook/StorybookError.story.luau @@ -0,0 +1,32 @@ +local React = require("@pkg/React") +local Storyteller = require("@pkg/Storyteller") + +local ContextProviders = require("@root/Common/ContextProviders") +local MockPlugin = require("@root/Testing/MockPlugin") +local StorybookError = require("@root/Storybook/StorybookError") + +type UnavailableStorybook = Storyteller.UnavailableStorybook + +return { + summary = "Component for displaying error messages to the user", + story = function() + + local storybookModule = script.Parent.Parent["init.storybook"] + local unavailableStorybook: UnavailableStorybook = { + problem = "Something went wrong!", + storybook = { + name = storybookModule.Name, + source = storybookModule, + loader = {} :: any, + } + } + + return React.createElement(ContextProviders, { + plugin = MockPlugin.new() :: , + }, { + StorybookError = React.createElement(StorybookError, { + unavailableStorybook =unavailableStorybook + }), + }) + end, +} From 1d024a8dbd20971702af02c4acf10faf24303265 Mon Sep 17 00:00:00 2001 From: Marin Minnerly <me@vocksel.com> Date: Wed, 18 Dec 2024 16:19:17 -0800 Subject: [PATCH 65/79] Reset the unavailable story between nodes --- src/Plugin/PluginApp.luau | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/Plugin/PluginApp.luau b/src/Plugin/PluginApp.luau index a7c4cf91..db4bb876 100644 --- a/src/Plugin/PluginApp.luau +++ b/src/Plugin/PluginApp.luau @@ -30,10 +30,8 @@ local function App() local onStoryChanged = React.useCallback(function(newStoryModule: ModuleScript?, newStorybook: LoadedStorybook?) navigation.navigateTo("Home") - setStoryModule(function(prev: ModuleScript?) - return if prev ~= newStoryModule then newStoryModule else nil - end) - + setUnavailableStorybook(nil) + setStoryModule(newStoryModule) setStorybook(newStorybook) end, { navigation.navigateTo } :: { unknown }) From eb8262476e4f618ceb7cc0affb0cf3e1d750accd Mon Sep 17 00:00:00 2001 From: Marin Minnerly <me@vocksel.com> Date: Wed, 18 Dec 2024 16:25:37 -0800 Subject: [PATCH 66/79] Display a highlight when hovering a drag handle --- src/Panels/DragHandle.luau | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/src/Panels/DragHandle.luau b/src/Panels/DragHandle.luau index f8765ff4..19c6b2ee 100644 --- a/src/Panels/DragHandle.luau +++ b/src/Panels/DragHandle.luau @@ -1,9 +1,11 @@ local RunService = game:GetService("RunService") -local PluginContext = require("@root/Plugin/PluginContext") local React = require("@pkg/React") local Sift = require("@pkg/Sift") + +local PluginContext = require("@root/Plugin/PluginContext") local types = require("@root/Panels/types") +local useTheme = require("@root/Common/useTheme") local defaultProps = { size = 8, -- px @@ -21,6 +23,7 @@ type InternalProps = Props & typeof(defaultProps) local function DragHandle(providedProps: Props) local props: InternalProps = Sift.Dictionary.merge(defaultProps, providedProps) + local theme = useTheme() local plugin = React.useContext(PluginContext.Context) local isDragging, setIsDragging = React.useState(false) @@ -28,12 +31,14 @@ local function DragHandle(providedProps: Props) local mouseInput: InputObject?, setMouseInput = React.useState(nil :: InputObject?) local getHandleProperties = React.useCallback(function() - local size: UDim2 + local hitboxSize: UDim2 + local highlightSize: UDim2 local position: UDim2 local anchorPoint: Vector2 if props.handle == "Right" or props.handle == "Left" then - size = UDim2.new(0, props.size, 1, 0) + hitboxSize = UDim2.new(0, props.size, 1, 0) + highlightSize = UDim2.new(0, props.size / 4, 1, 0) if props.handle == "Right" then position = UDim2.fromScale(1, 0) @@ -43,7 +48,8 @@ local function DragHandle(providedProps: Props) anchorPoint = Vector2.new(0, 0) end elseif props.handle == "Top" or props.handle == "Bottom" then - size = UDim2.new(1, 0, 0, props.size) + hitboxSize = UDim2.new(1, 0, 0, props.size) + highlightSize = UDim2.new(1, 0, 0, props.size / 4) if props.handle == "Bottom" then position = UDim2.fromScale(0, 1) @@ -54,7 +60,7 @@ local function DragHandle(providedProps: Props) end end - return size, position, anchorPoint + return hitboxSize, highlightSize, position, anchorPoint end, { props.handle, props.size } :: { unknown }) local onInputBegan = React.useCallback(function(_rbx, input: InputObject) @@ -85,7 +91,7 @@ local function DragHandle(providedProps: Props) setIsHovered(false) end, {}) - local size, position, anchorPoint = getHandleProperties() + local hitboxSize, highlightSize, position, anchorPoint = getHandleProperties() React.useEffect(function(): any if mouseInput and isDragging then @@ -125,7 +131,7 @@ local function DragHandle(providedProps: Props) end, { plugin, isDragging, isHovered } :: { unknown }) return React.createElement("ImageButton", { - Size = size, + Size = hitboxSize, Position = position, AnchorPoint = anchorPoint, BackgroundTransparency = 1, @@ -133,6 +139,13 @@ local function DragHandle(providedProps: Props) [React.Event.InputEnded] = onInputEnded, [React.Event.MouseEnter] = onMouseEnter :: any, [React.Event.MouseLeave] = onMouseLeave :: any, + }, { + Highlght = React.createElement("Frame", { + Size = highlightSize, + BorderSizePixel = 0, + BackgroundTransparency = if isHovered or isDragging then 0 else 1, + BackgroundColor3 = theme.selection, + }), }) end From 3e391ca1ed31b3dc430a693f00e52a188ff817f4 Mon Sep 17 00:00:00 2001 From: Marin Minnerly <me@vocksel.com> Date: Thu, 19 Dec 2024 09:18:32 -0800 Subject: [PATCH 67/79] FileSyncService doesn't exist but InternalSyncService does --- src/RobloxInternal/getInternalSyncItems.luau | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/RobloxInternal/getInternalSyncItems.luau b/src/RobloxInternal/getInternalSyncItems.luau index 3751541a..a93e1099 100644 --- a/src/RobloxInternal/getInternalSyncItems.luau +++ b/src/RobloxInternal/getInternalSyncItems.luau @@ -1,15 +1,15 @@ local tryGetService = require("@root/RobloxInternal/tryGetService") -- selene: allow(incorrect_standard_library_use) -type FileSyncService = typeof(game:GetService("FileSyncService")) +type InternalSyncService = typeof(game:GetService("InternalSyncService")) -local FileSyncService: FileSyncService? = tryGetService("FileSyncService") +local InternalSyncService: InternalSyncService? = tryGetService("InternalSyncService") local function getInternalSyncItems(): { InternalSyncItem } local internalSyncItems: { InternalSyncItem } = {} - if FileSyncService then - for _, child in FileSyncService:GetChildren() do + if InternalSyncService then + for _, child in InternalSyncService:GetChildren() do if child:IsA("InternalSyncItem") then table.insert(internalSyncItems, child) end From 5823ece29cb3b3394024ed3560292f3fd6f7d237 Mon Sep 17 00:00:00 2001 From: Marin Minnerly <me@vocksel.com> Date: Thu, 19 Dec 2024 09:21:47 -0800 Subject: [PATCH 68/79] Consolidate modules for Roblox internal work --- src/Common/getInstanceFromFullName.luau | 2 +- src/Permissions/tryGetService.luau | 27 ------------------- .../canAccess.luau | 0 src/RobloxInternal/tryGetService.luau | 10 +++++-- 4 files changed, 9 insertions(+), 30 deletions(-) delete mode 100644 src/Permissions/tryGetService.luau rename src/{Permissions => RobloxInternal}/canAccess.luau (100%) diff --git a/src/Common/getInstanceFromFullName.luau b/src/Common/getInstanceFromFullName.luau index e52e9cbb..d11dd606 100644 --- a/src/Common/getInstanceFromFullName.luau +++ b/src/Common/getInstanceFromFullName.luau @@ -8,7 +8,7 @@ local Sift = require("@pkg/Sift") -local tryGetService = require("@root/Permissions/tryGetService") +local tryGetService = require("@root/RobloxInternal/tryGetService") local PATH_SEPERATOR = "." diff --git a/src/Permissions/tryGetService.luau b/src/Permissions/tryGetService.luau deleted file mode 100644 index f6c050a3..00000000 --- a/src/Permissions/tryGetService.luau +++ /dev/null @@ -1,27 +0,0 @@ -local canAccess = require("@root/Permissions/canAccess") - -local function tryGetService(serviceName: string): Instance? - local service - - pcall(function() - service = game:GetService(serviceName) - end) - - if service then - return service - end - - -- Some services cannot be retrieved by GetService but still exist in the DM - -- and can be retrieved by name. - pcall(function() - service = game:FindFirstChild(serviceName) - end) - - if canAccess(service) then - return service - end - - return nil -end - -return tryGetService diff --git a/src/Permissions/canAccess.luau b/src/RobloxInternal/canAccess.luau similarity index 100% rename from src/Permissions/canAccess.luau rename to src/RobloxInternal/canAccess.luau diff --git a/src/RobloxInternal/tryGetService.luau b/src/RobloxInternal/tryGetService.luau index d2dda643..10d0f873 100644 --- a/src/RobloxInternal/tryGetService.luau +++ b/src/RobloxInternal/tryGetService.luau @@ -1,4 +1,6 @@ -local function tryGetService(serviceName: string): Instance +local canAccess = require("@root/RobloxInternal/canAccess") + +local function tryGetService(serviceName: string): Instance? local service pcall(function() @@ -15,7 +17,11 @@ local function tryGetService(serviceName: string): Instance service = game:FindFirstChild(serviceName) end) - return service + if canAccess(service) then + return service + end + + return nil end return tryGetService From cb7932044e17aeaf8db34bb3eb185bf325e2a2fe Mon Sep 17 00:00:00 2001 From: Marin Minnerly <me@vocksel.com> Date: Thu, 19 Dec 2024 09:28:34 -0800 Subject: [PATCH 69/79] Fix LocalStorageContext types and use a constant storage key --- src/Common/ContextProviders.luau | 4 +++- src/Plugin/LocalStorageContext.luau | 22 +++++++++------------- 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/src/Common/ContextProviders.luau b/src/Common/ContextProviders.luau index 4d862c5e..90115356 100644 --- a/src/Common/ContextProviders.luau +++ b/src/Common/ContextProviders.luau @@ -21,7 +21,9 @@ local function ContextProviders(props: Props) React.createElement(NavigationContext.Provider, { defaultScreen = "Home", }), - React.createElement(LocalStorageContext.Provider), + React.createElement(LocalStorageContext.Provider, { + storageKey = "FlipbookInternal", + }), React.createElement(SettingsContext.Provider), React.createElement(TreeView.TreeViewProvider), }, diff --git a/src/Plugin/LocalStorageContext.luau b/src/Plugin/LocalStorageContext.luau index ddd9b9f0..7c5deeae 100644 --- a/src/Plugin/LocalStorageContext.luau +++ b/src/Plugin/LocalStorageContext.luau @@ -21,22 +21,18 @@ export type LocalStorageContext = { set: (key: string, value: unknown) -> (), } -local LocalStorageContext = React.createContext({}) +local LocalStorageContext = React.createContext(nil :: LocalStorageContext?) export type Props = { - storageKey: string?, + storageKey: string, children: React.Node, } local function LocalStorageProvider(props: Props) local plugin = useContext(PluginContext.Context) - local storageKey = useMemo(function() - return if props.storageKey then props.storageKey else `{plugin.Name}LocalStorage` - end, { props.storageKey, plugin }) - local loadFromDisk = useCallback(function(): LocalStorage - local data = plugin:GetSetting(storageKey) + local data = plugin:GetSetting(props.storageKey) if data then local json = HttpService:JSONDecode(data) if json then @@ -44,7 +40,7 @@ local function LocalStorageProvider(props: Props) end end return {} - end, { plugin, storageKey }) + end, { plugin, props.storageKey } :: { unknown }) local storage, setStorage = useState(loadFromDisk) local prevStorage = usePrevious(storage) @@ -52,15 +48,15 @@ local function LocalStorageProvider(props: Props) local saveToDisk = useCallback(function() local data = HttpService:JSONEncode(storage) if data then - plugin:SetSetting(storageKey, data) + plugin:SetSetting(props.storageKey, data) end - end, { plugin, storageKey, storage }) + end, { plugin, props.storageKey, storage } :: { unknown }) local get = useCallback(function(key: string) return storage[key] end, { storage }) - local set = useCallback(function(key: string, value: unknown) + local set = useCallback(function(key: string, value: any) setStorage(function(prev) return Sift.Dictionary.join(prev, { [key] = if typeof(value) == "function" then value(prev[key]) else value, @@ -72,7 +68,7 @@ local function LocalStorageProvider(props: Props) if storage and storage ~= prevStorage then saveToDisk() end - end, { storage, prevStorage, saveToDisk }) + end, { storage, prevStorage, saveToDisk } :: { unknown }) local context: LocalStorageContext = { get = get, @@ -84,7 +80,7 @@ local function LocalStorageProvider(props: Props) }, props.children) end -local function useLocalStorage(): TreeViewContext +local function useLocalStorage(): LocalStorageContext local context = useContext(LocalStorageContext) if not context then local contextName = script.Name From d3e143aac83baf1a9f4bcf9dc066ad73115dafb3 Mon Sep 17 00:00:00 2001 From: Marin Minnerly <me@vocksel.com> Date: Thu, 19 Dec 2024 09:44:03 -0800 Subject: [PATCH 70/79] Fix unnecessary re-renders for pinned instances --- src/Plugin/LocalStorageContext.luau | 12 ++++++------ src/TreeView/usePinnedInstances.luau | 12 ++++++++---- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/src/Plugin/LocalStorageContext.luau b/src/Plugin/LocalStorageContext.luau index 7c5deeae..bd049b0d 100644 --- a/src/Plugin/LocalStorageContext.luau +++ b/src/Plugin/LocalStorageContext.luau @@ -9,7 +9,7 @@ local usePrevious = require("@root/Common/usePrevious") local useCallback = React.useCallback local useContext = React.useContext local useEffect = React.useEffect -local useMemo = React.useMemo +local useRef = React.useRef local useState = React.useState export type LocalStorage = { @@ -56,10 +56,10 @@ local function LocalStorageProvider(props: Props) return storage[key] end, { storage }) - local set = useCallback(function(key: string, value: any) + local set = useCallback(function(key: string, value: unknown?) setStorage(function(prev) return Sift.Dictionary.join(prev, { - [key] = if typeof(value) == "function" then value(prev[key]) else value, + [key] = if value == nil then Sift.None else value, }) end) end, { storage }) @@ -70,13 +70,13 @@ local function LocalStorageProvider(props: Props) end end, { storage, prevStorage, saveToDisk } :: { unknown }) - local context: LocalStorageContext = { + local context: LocalStorageContext = useRef({ get = get, set = set, - } + }) return React.createElement(LocalStorageContext.Provider, { - value = context, + value = context.current, }, props.children) end diff --git a/src/TreeView/usePinnedInstances.luau b/src/TreeView/usePinnedInstances.luau index 0ca670dd..22b8a74b 100644 --- a/src/TreeView/usePinnedInstances.luau +++ b/src/TreeView/usePinnedInstances.luau @@ -3,6 +3,7 @@ local Sift = require("@pkg/Sift") local LocalStorageContext = require("@root/Plugin/LocalStorageContext") local getInstanceFromFullName = require("@root/Common/getInstanceFromFullName") +local usePrevious = require("@root/Common/usePrevious") local useCallback = React.useCallback local useState = React.useState @@ -21,10 +22,7 @@ local function usePinnedInstances(): (ModuleScript?, (storyModule: ModuleScript? local pinnedPaths, setPinnedPaths = useState(function() return localStorage.get(PINNED_INSTANCES_KEY) or {} end) - - useEffect(function() - localStorage.set(PINNED_INSTANCES_KEY, pinnedPaths) - end, { pinnedPaths }) + local prevPinnedPaths = usePrevious(pinnedPaths) local pin = useCallback(function(instance: Instance) setPinnedPaths(function(prev) @@ -65,6 +63,12 @@ local function usePinnedInstances(): (ModuleScript?, (storyModule: ModuleScript? end end, { isPinned, unpin, pin }) + useEffect(function() + if prevPinnedPaths and pinnedPaths ~= prevPinnedPaths then + localStorage.set(PINNED_INSTANCES_KEY, pinnedPaths) + end + end, { localStorage, pinnedPaths, prevPinnedPaths }) + return { pin = pin, unpin = unpin, From 914535b01233c1c6faf2b791ec1e776520572021 Mon Sep 17 00:00:00 2001 From: Marin Minnerly <me@vocksel.com> Date: Thu, 19 Dec 2024 09:44:17 -0800 Subject: [PATCH 71/79] When no story has been opened, first click won't actually open it --- src/Storybook/StorybookTreeView.luau | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Storybook/StorybookTreeView.luau b/src/Storybook/StorybookTreeView.luau index e9bd4bfe..de85104f 100644 --- a/src/Storybook/StorybookTreeView.luau +++ b/src/Storybook/StorybookTreeView.luau @@ -150,6 +150,7 @@ local function StorybookTreeView(props: Props) end else props.onStoryChanged(nil, nil) + setLastOpenedStory(nil) end end From d130e11962998f5dd69d5a5fd479dcfdce64e7e4 Mon Sep 17 00:00:00 2001 From: Marin Minnerly <me@vocksel.com> Date: Wed, 18 Dec 2024 20:13:47 -0800 Subject: [PATCH 72/79] Add untested code block highlighting --- src/Common/CodeBlock.luau | 13 +++++++++++++ wally.toml | 1 + 2 files changed, 14 insertions(+) diff --git a/src/Common/CodeBlock.luau b/src/Common/CodeBlock.luau index 5172303b..7ad00dab 100644 --- a/src/Common/CodeBlock.luau +++ b/src/Common/CodeBlock.luau @@ -1,3 +1,4 @@ +local Highlighter = require("@pkg/Highlighter") local React = require("@pkg/React") local Sift = require("@pkg/Sift") @@ -6,6 +7,8 @@ local nextLayoutOrder = require("@root/Common/nextLayoutOrder") local useTheme = require("@root/Common/useTheme") local useMemo = React.useMemo +local useRef = React.useRef +local useEffect = React.useEffect local function getLineNumbers(str: string): string return Sift.List.reduce(str:split("\n"), function(accumulator, _item, index) @@ -21,11 +24,20 @@ export type Props = { local function StoryError(props: Props) local theme = useTheme() + local ref = useRef(nil :: TextBox?) local sourceColor = useMemo(function() return if props.sourceColor then props.sourceColor else theme.text end, { props.sourceColor }) + useEffect(function() + Highlighter.matchStudioSettings() + + Highlighter.highlight({ + textObject = ref.current, + }) + end, {}) + return React.createElement("Frame", { AutomaticSize = Enum.AutomaticSize.XY, BackgroundTransparency = 0.5, @@ -72,6 +84,7 @@ local function StoryError(props: Props) TextWrapped = false, LineHeight = 1, Font = Enum.Font.RobotoMono, + ref = ref, }), }) end diff --git a/wally.toml b/wally.toml index 8189576d..96fa8332 100644 --- a/wally.toml +++ b/wally.toml @@ -13,6 +13,7 @@ ReactRoblox = "jsdotlua/react-roblox@17.0.2" ReactSpring = "chriscerie/react-spring@2.0.0" Sift = "csqrl/sift@0.0.8" t = "osyrisrblx/t@3.0.0" +Highlighter = "boatbomber/highlighter@0.9.0" # dev dependencies Roact = "roblox/roact@1.4.4" From 30f2f1a216057d9112bc360efe61859a981a7660 Mon Sep 17 00:00:00 2001 From: Marin Minnerly <me@vocksel.com> Date: Thu, 19 Dec 2024 11:12:42 -0800 Subject: [PATCH 73/79] Syntax highlighting works! --- src/Common/CodeBlock.luau | 32 ++++++++++++++++--------------- src/Storybook/StorybookError.luau | 5 ++--- 2 files changed, 19 insertions(+), 18 deletions(-) diff --git a/src/Common/CodeBlock.luau b/src/Common/CodeBlock.luau index 7ad00dab..63fd5263 100644 --- a/src/Common/CodeBlock.luau +++ b/src/Common/CodeBlock.luau @@ -7,8 +7,6 @@ local nextLayoutOrder = require("@root/Common/nextLayoutOrder") local useTheme = require("@root/Common/useTheme") local useMemo = React.useMemo -local useRef = React.useRef -local useEffect = React.useEffect local function getLineNumbers(str: string): string return Sift.List.reduce(str:split("\n"), function(accumulator, _item, index) @@ -22,21 +20,25 @@ export type Props = { layoutOrder: number?, } -local function StoryError(props: Props) +local function CodeBlock(props: Props) local theme = useTheme() - local ref = useRef(nil :: TextBox?) local sourceColor = useMemo(function() return if props.sourceColor then props.sourceColor else theme.text end, { props.sourceColor }) - useEffect(function() - Highlighter.matchStudioSettings() - - Highlighter.highlight({ - textObject = ref.current, - }) - end, {}) + local source = useMemo(function() + if props.sourceColor then + return props.source + else + return table.concat( + Highlighter.buildRichTextLines({ + src = props.source, + }), + "\n" + ) + end + end, { props.source }) return React.createElement("Frame", { AutomaticSize = Enum.AutomaticSize.XY, @@ -65,7 +67,7 @@ local function StoryError(props: Props) LineNumbers = React.createElement("TextLabel", { LayoutOrder = nextLayoutOrder(), AutomaticSize = Enum.AutomaticSize.XY, - Text = getLineNumbers(props.source), + Text = getLineNumbers(source), TextSize = theme.textSize, LineHeight = 1, BackgroundTransparency = 1, @@ -75,18 +77,18 @@ local function StoryError(props: Props) }), SourceCode = React.createElement(SelectableTextLabel, { + RichText = true, LayoutOrder = nextLayoutOrder(), Size = UDim2.fromScale(1, 0), AutomaticSize = Enum.AutomaticSize.Y, - Text = props.source, + Text = source, TextColor3 = sourceColor, TextSize = theme.textSize, TextWrapped = false, LineHeight = 1, Font = Enum.Font.RobotoMono, - ref = ref, }), }) end -return StoryError +return CodeBlock diff --git a/src/Storybook/StorybookError.luau b/src/Storybook/StorybookError.luau index 952ef40f..c2ea1b6e 100644 --- a/src/Storybook/StorybookError.luau +++ b/src/Storybook/StorybookError.luau @@ -1,5 +1,4 @@ local React = require("@pkg/React") -local Sift = require("@pkg/Sift") local Storyteller = require("@pkg/Storyteller") local CodeBlock = require("@root/Common/CodeBlock") @@ -14,7 +13,7 @@ export type Props = { layoutOrder: number?, } -local function StoryError(props: Props) +local function StorybookError(props: Props) local theme = useTheme() local storybookSource = props.unavailableStorybook.storybook.source.Source @@ -109,4 +108,4 @@ local function StoryError(props: Props) }) end -return StoryError +return StorybookError From 4f91e5209a22eb94dfa36d4b77f218b4e4c546dc Mon Sep 17 00:00:00 2001 From: Marin Minnerly <me@vocksel.com> Date: Fri, 20 Dec 2024 12:28:05 -0800 Subject: [PATCH 74/79] Better errors for stories --- src/Storybook/StoryError.luau | 93 +++++++++++++++++++++++++++++---- src/Storybook/StoryPreview.luau | 3 +- src/Storybook/StoryView.luau | 1 + 3 files changed, 87 insertions(+), 10 deletions(-) diff --git a/src/Storybook/StoryError.luau b/src/Storybook/StoryError.luau index 646c5213..19596cd7 100644 --- a/src/Storybook/StoryError.luau +++ b/src/Storybook/StoryError.luau @@ -1,38 +1,113 @@ local React = require("@pkg/React") +local Storyteller = require("@pkg/Storyteller") local CodeBlock = require("@root/Common/CodeBlock") local ScrollingFrame = require("@root/Common/ScrollingFrame") +local nextLayoutOrder = require("@root/Common/nextLayoutOrder") local useTheme = require("@root/Common/useTheme") +type LoadedStory<T> = Storyteller.LoadedStory<T> + export type Props = { err: string, + storyModule: ModuleScript, layoutOrder: number?, } local function StoryError(props: Props) local theme = useTheme() + local storySource = if props.storyModule then props.storyModule.Source else nil + return React.createElement(ScrollingFrame, { ScrollingDirection = Enum.ScrollingDirection.XY, LayoutOrder = props.layoutOrder, }, { Layout = React.createElement("UIListLayout", { SortOrder = Enum.SortOrder.LayoutOrder, - FillDirection = Enum.FillDirection.Horizontal, - Padding = theme.padding, + FillDirection = Enum.FillDirection.Vertical, + Padding = theme.paddingLarge, }), Padding = React.createElement("UIPadding", { - PaddingTop = theme.paddingSmall, - PaddingRight = theme.paddingSmall, - PaddingBottom = theme.paddingSmall, - PaddingLeft = theme.paddingSmall, + PaddingTop = theme.paddingLarge, + PaddingRight = theme.paddingLarge, + PaddingBottom = theme.paddingLarge, + PaddingLeft = theme.paddingLarge, }), - CodeBlock = React.createElement(CodeBlock, { - source = props.err, - sourceColor = theme.alert, + MainText = React.createElement("TextLabel", { + LayoutOrder = nextLayoutOrder(), + Text = `Failed to load {props.storyModule.Name}`, + AutomaticSize = Enum.AutomaticSize.XY, + BackgroundTransparency = 1, + Font = theme.font, + TextColor3 = theme.text, + TextSize = theme.textSize, + TextXAlignment = Enum.TextXAlignment.Left, + TextYAlignment = Enum.TextYAlignment.Center, }), + + Problem = React.createElement("Frame", { + AutomaticSize = Enum.AutomaticSize.XY, + BackgroundTransparency = 1, + LayoutOrder = nextLayoutOrder(), + }, { + Layout = React.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder, + FillDirection = Enum.FillDirection.Vertical, + Padding = theme.padding, + }), + + Title = React.createElement("TextLabel", { + LayoutOrder = nextLayoutOrder(), + Text = "Error", + AutomaticSize = Enum.AutomaticSize.XY, + BackgroundTransparency = 1, + Font = theme.headerFont, + TextColor3 = theme.text, + TextSize = theme.headerTextSize, + TextXAlignment = Enum.TextXAlignment.Left, + TextYAlignment = Enum.TextYAlignment.Center, + }), + + CodeBlock = React.createElement(CodeBlock, { + source = props.err, + sourceColor = theme.alert, + layoutOrder = nextLayoutOrder(), + }), + }), + + StorySource = if storySource + then React.createElement("Frame", { + AutomaticSize = Enum.AutomaticSize.XY, + BackgroundTransparency = 1, + LayoutOrder = nextLayoutOrder(), + }, { + Layout = React.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder, + FillDirection = Enum.FillDirection.Vertical, + Padding = theme.padding, + }), + + Title = React.createElement("TextLabel", { + LayoutOrder = nextLayoutOrder(), + Text = "Story Source", + AutomaticSize = Enum.AutomaticSize.XY, + BackgroundTransparency = 1, + Font = theme.headerFont, + TextColor3 = theme.text, + TextSize = theme.headerTextSize, + TextXAlignment = Enum.TextXAlignment.Left, + TextYAlignment = Enum.TextYAlignment.Center, + }), + + CodeBlock = React.createElement(CodeBlock, { + source = storySource, + layoutOrder = nextLayoutOrder(), + }), + }) + else nil, }) end diff --git a/src/Storybook/StoryPreview.luau b/src/Storybook/StoryPreview.luau index 2e3c3cbd..db32a160 100644 --- a/src/Storybook/StoryPreview.luau +++ b/src/Storybook/StoryPreview.luau @@ -76,9 +76,10 @@ local StoryPreview = React.forwardRef(function(providedProps: Props, ref: any) end end, { props.story, props.isMountedInViewport } :: { unknown }) - if err then + if err and props.story then return e(StoryError, { layoutOrder = props.layoutOrder, + storyModule = props.story.source, err = err, }) else diff --git a/src/Storybook/StoryView.luau b/src/Storybook/StoryView.luau index 6f8c6af1..e7e74340 100644 --- a/src/Storybook/StoryView.luau +++ b/src/Storybook/StoryView.luau @@ -91,6 +91,7 @@ local function StoryView(props: Props) BackgroundTransparency = 1, }, { Error = storyErr and e(StoryError, { + storyModule = props.story, err = storyErr, }), From 984e87e8e68e66bb1b0f196a52e43c2a88c40ca2 Mon Sep 17 00:00:00 2001 From: Marin Minnerly <me@vocksel.com> Date: Fri, 20 Dec 2024 12:32:43 -0800 Subject: [PATCH 75/79] Make sure we have access to services --- src/RobloxInternal/tryGetService.luau | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/RobloxInternal/tryGetService.luau b/src/RobloxInternal/tryGetService.luau index 10d0f873..23a4e7cb 100644 --- a/src/RobloxInternal/tryGetService.luau +++ b/src/RobloxInternal/tryGetService.luau @@ -1,13 +1,13 @@ local canAccess = require("@root/RobloxInternal/canAccess") local function tryGetService(serviceName: string): Instance? - local service + local service: Instance? pcall(function() service = game:GetService(serviceName) end) - if service then + if service and canAccess(service) then return service end @@ -17,7 +17,7 @@ local function tryGetService(serviceName: string): Instance? service = game:FindFirstChild(serviceName) end) - if canAccess(service) then + if service and canAccess(service) then return service end From 72ed25339d633e995cc9f5fe69a83e797e90420e Mon Sep 17 00:00:00 2001 From: Marin Minnerly <me@vocksel.com> Date: Fri, 20 Dec 2024 12:33:29 -0800 Subject: [PATCH 76/79] I think I was mistaken about the second case --- src/RobloxInternal/tryGetService.luau | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/src/RobloxInternal/tryGetService.luau b/src/RobloxInternal/tryGetService.luau index 23a4e7cb..3f0ed92e 100644 --- a/src/RobloxInternal/tryGetService.luau +++ b/src/RobloxInternal/tryGetService.luau @@ -9,19 +9,9 @@ local function tryGetService(serviceName: string): Instance? if service and canAccess(service) then return service + else + return nil end - - -- Some services cannot be retrieved by GetService but still exist in the DM - -- and can be retrieved by name. - pcall(function() - service = game:FindFirstChild(serviceName) - end) - - if service and canAccess(service) then - return service - end - - return nil end return tryGetService From 6caae9eccc8acfad378e5c75628016973b4902cd Mon Sep 17 00:00:00 2001 From: Marin Minnerly <me@vocksel.com> Date: Fri, 20 Dec 2024 12:44:29 -0800 Subject: [PATCH 77/79] Use a new function pair for saving and loading paths --- src/Common/getInstanceFromFullName.luau | 64 ------------------- src/Common/getInstanceFromPath.luau | 44 +++++++++++++ ...pec.luau => getInstanceFromPath.spec.luau} | 25 ++++---- src/Common/getInstancePath.luau | 18 ++++++ src/Common/getInstancePath.spec.luau | 48 ++++++++++++++ src/Plugin/LocalStorageContext.luau | 34 +++++----- src/Storybook/useLastOpenedStory.luau | 13 ++-- src/TreeView/usePinnedInstances.luau | 11 ++-- 8 files changed, 150 insertions(+), 107 deletions(-) delete mode 100644 src/Common/getInstanceFromFullName.luau create mode 100644 src/Common/getInstanceFromPath.luau rename src/Common/{getInstanceFromFullName.spec.luau => getInstanceFromPath.spec.luau} (62%) create mode 100644 src/Common/getInstancePath.luau create mode 100644 src/Common/getInstancePath.spec.luau diff --git a/src/Common/getInstanceFromFullName.luau b/src/Common/getInstanceFromFullName.luau deleted file mode 100644 index d11dd606..00000000 --- a/src/Common/getInstanceFromFullName.luau +++ /dev/null @@ -1,64 +0,0 @@ ---[[ - Gets an instance based off the result of GetFullName(). - - This is used in conjunction with debug.info() to locate the calling script. - - Returns nil if the instance is outside the DataModel. -]] - -local Sift = require("@pkg/Sift") - -local tryGetService = require("@root/RobloxInternal/tryGetService") - -local PATH_SEPERATOR = "." - -local function getInstanceFromFullName(fullName: string): Instance? - local parts = fullName:split(PATH_SEPERATOR) - local serviceName = table.remove(parts, 1) - - if serviceName then - -- This function only works for instances in the DataModel. As such, the - -- first part of the path will always be a service, so if we can't find - -- one we exit out and return nil - local current = tryGetService(serviceName) - - if current then - while #parts > 0 do - -- Keep around a copy of the `parts` array. We are going to concat this - -- into new paths, and incrementally remove from the right to narrow - -- down the file path. - local tempParts = Sift.Array.copy(parts) - - -- The result of GetFullName() uses dots to separate paths, but we also - -- use dots in our file names (e.g. with spec and story files). As such, - -- this block will look ahead to see if multiple parts are actually a - -- single filename. - for _ = 1, #tempParts do - local name = table.concat(tempParts, PATH_SEPERATOR) - local found = current:FindFirstChild(name) - - if found then - current = found - parts = Sift.List.shift(parts, #name:split(PATH_SEPERATOR)) - break - else - if #tempParts == 1 then - -- This fixes a crash when searching for paths that - -- no longer exist - return nil - else - -- Reduce from the right until we find the next instance - tempParts = Sift.List.pop(tempParts) - end - end - end - end - - return current - end - end - - return nil -end - -return getInstanceFromFullName diff --git a/src/Common/getInstanceFromPath.luau b/src/Common/getInstanceFromPath.luau new file mode 100644 index 00000000..a28c0d7d --- /dev/null +++ b/src/Common/getInstanceFromPath.luau @@ -0,0 +1,44 @@ +--[[ + Gets an instance based off the result of getInstancePath. + + The reason we use this over GetFullName is because it uses a dot (.) as the + path separator which makes it difficult to disambiguate instances stories + and test files (Foo.story and Foo.spec, respectively) + + Returns `nil` if the instance is outside the DataModel or otherwise cannot + be found. +]] + +local tryGetService = require("@root/RobloxInternal/tryGetService") + +local PATH_SEPERATOR = "/" + +local function getInstanceFromPath(path: string): Instance? + local parts = path:split(PATH_SEPERATOR) + local serviceName = parts[1] + + if serviceName then + -- This function only works for instances in the DataModel. As such, the + -- first part of the path will always be a service, so if we can't find + -- one we exit out and return nil + local current = tryGetService(serviceName) + + if current then + for i = 2, #parts do + local found = current:FindFirstChild(parts[i]) + + if found then + current = found + else + return nil + end + end + + return current + end + end + + return nil +end + +return getInstanceFromPath diff --git a/src/Common/getInstanceFromFullName.spec.luau b/src/Common/getInstanceFromPath.spec.luau similarity index 62% rename from src/Common/getInstanceFromFullName.spec.luau rename to src/Common/getInstanceFromPath.spec.luau index d110cbda..a343e4a4 100644 --- a/src/Common/getInstanceFromFullName.spec.luau +++ b/src/Common/getInstanceFromPath.spec.luau @@ -3,7 +3,8 @@ local ReplicatedStorage = game:GetService("ReplicatedStorage") local JestGlobals = require("@pkg/JestGlobals") local newFolder = require("@root/Testing/newFolder") -local getInstanceFromFullName = require("./getInstanceFromFullName") +local getInstanceFromPath = require("./getInstanceFromPath") +local getInstancePath = require("./getInstancePath") local expect = JestGlobals.expect local test = JestGlobals.test @@ -18,7 +19,7 @@ afterEach(function() end) test("gets services", function() - local path = getInstanceFromFullName("ReplicatedStorage") + local path = getInstanceFromPath("ReplicatedStorage") expect(path).toBe(ReplicatedStorage) end) @@ -32,8 +33,7 @@ test("works on nested instances", function() }) folder.Parent = ReplicatedStorage - local path = getInstanceFromFullName(module:GetFullName()) - expect(path).toBe(module) + expect(getInstanceFromPath(getInstancePath(module))).toBe(module) end) test("works with spec files", function() @@ -46,7 +46,7 @@ test("works with spec files", function() }) folder.Parent = ReplicatedStorage - local path = getInstanceFromFullName(module:GetFullName()) + local path = getInstanceFromPath(module:GetFullName()) expect(path).toBe(module) end) @@ -61,18 +61,17 @@ test("finds spec files BEFORE the module it is associated with", function() }) folder.Parent = ReplicatedStorage - local path = getInstanceFromFullName(module:GetFullName()) - expect(path).toBe(module) + expect(getInstanceFromPath(getInstancePath(module))).toBe(module) end) test("returns nil if the first part of the path is not a service", function() - expect(getInstanceFromFullName("Part")).toBeUndefined() + expect(getInstanceFromPath("Part")).toBeUndefined() end) test("returns nil if the path does not exist", function() - expect(getInstanceFromFullName("Foo.story")).toBeUndefined() - expect(getInstanceFromFullName("Path.To.Foo.story")).toBeUndefined() - expect(getInstanceFromFullName("ReplicatedStorage.Foo.Bar.Baz")).toBeUndefined() - expect(getInstanceFromFullName("ReplicatedStorage.Sample.story")).toBeUndefined() - expect(getInstanceFromFullName("ReplicatedStorage.Sample.spec")).toBeUndefined() + expect(getInstanceFromPath("Foo.story")).toBeUndefined() + expect(getInstanceFromPath("Path/To/Foo.story")).toBeUndefined() + expect(getInstanceFromPath("ReplicatedStorage/Foo/Bar/Baz")).toBeUndefined() + expect(getInstanceFromPath("ReplicatedStorage/Sample.story")).toBeUndefined() + expect(getInstanceFromPath("ReplicatedStorage/Sample.spec")).toBeUndefined() end) diff --git a/src/Common/getInstancePath.luau b/src/Common/getInstancePath.luau new file mode 100644 index 00000000..082df3e6 --- /dev/null +++ b/src/Common/getInstancePath.luau @@ -0,0 +1,18 @@ +local PATH_SEPARATOR = "/" + +local function getInstancePath(instance: Instance, pathSeparator: string?): string + pathSeparator = if pathSeparator then pathSeparator else PATH_SEPARATOR + assert(pathSeparator, "Luau") + + local path = {} + local current = instance + + while current and current.Parent ~= nil do + table.insert(path, 1, current.Name) + current = current.Parent + end + + return table.concat(path, "/") +end + +return getInstancePath diff --git a/src/Common/getInstancePath.spec.luau b/src/Common/getInstancePath.spec.luau new file mode 100644 index 00000000..1ee401ba --- /dev/null +++ b/src/Common/getInstancePath.spec.luau @@ -0,0 +1,48 @@ +local ReplicatedStorage = game:GetService("ReplicatedStorage") + +local JestGlobals = require("@pkg/JestGlobals") +local newFolder = require("@root/Testing/newFolder") + +local getInstancePath = require("./getInstancePath") + +local expect = JestGlobals.expect +local test = JestGlobals.test +local afterEach = JestGlobals.afterEach + +local folder: Folder + +afterEach(function() + if folder then + folder:Destroy() + end +end) + +test("services are treated as the root", function() + expect(getInstancePath(ReplicatedStorage)).toBe("ReplicatedStorage") +end) + +test("works on nested instances", function() + local module = Instance.new("ModuleScript") + + folder = newFolder({ + foo = newFolder({ + bar = module, + }), + }) + folder.Parent = ReplicatedStorage + + expect(getInstancePath(module)).toBe("ReplicatedStorage/Folder/foo/bar") +end) + +test("works with spec files", function() + local module = Instance.new("ModuleScript") + + folder = newFolder({ + foo = newFolder({ + ["bar.spec"] = module, + }), + }) + folder.Parent = ReplicatedStorage + + expect(getInstancePath(module)).toBe("ReplicatedStorage/Folder/foo/bar.spec") +end) diff --git a/src/Plugin/LocalStorageContext.luau b/src/Plugin/LocalStorageContext.luau index bd049b0d..0f705cb6 100644 --- a/src/Plugin/LocalStorageContext.luau +++ b/src/Plugin/LocalStorageContext.luau @@ -4,12 +4,9 @@ local React = require("@pkg/React") local Sift = require("@pkg/Sift") local PluginContext = require("@root/Plugin/PluginContext") -local usePrevious = require("@root/Common/usePrevious") local useCallback = React.useCallback local useContext = React.useContext -local useEffect = React.useEffect -local useRef = React.useRef local useState = React.useState export type LocalStorage = { @@ -42,15 +39,16 @@ local function LocalStorageProvider(props: Props) return {} end, { plugin, props.storageKey } :: { unknown }) - local storage, setStorage = useState(loadFromDisk) - local prevStorage = usePrevious(storage) - - local saveToDisk = useCallback(function() - local data = HttpService:JSONEncode(storage) + local saveToDisk = useCallback(function(newStorage: { [any]: any }) + local data = HttpService:JSONEncode(newStorage) if data then plugin:SetSetting(props.storageKey, data) end - end, { plugin, props.storageKey, storage } :: { unknown }) + end, { plugin, props.storageKey } :: { unknown }) + + local storage, setStorage = useState(function() + return loadFromDisk() + end) local get = useCallback(function(key: string) return storage[key] @@ -58,25 +56,23 @@ local function LocalStorageProvider(props: Props) local set = useCallback(function(key: string, value: unknown?) setStorage(function(prev) - return Sift.Dictionary.join(prev, { + local new = Sift.Dictionary.join(prev, { [key] = if value == nil then Sift.None else value, }) + + saveToDisk(new) + + return new end) end, { storage }) - useEffect(function() - if storage and storage ~= prevStorage then - saveToDisk() - end - end, { storage, prevStorage, saveToDisk } :: { unknown }) - - local context: LocalStorageContext = useRef({ + local context: LocalStorageContext = { get = get, set = set, - }) + } return React.createElement(LocalStorageContext.Provider, { - value = context.current, + value = context, }, props.children) end diff --git a/src/Storybook/useLastOpenedStory.luau b/src/Storybook/useLastOpenedStory.luau index b59457f5..8165ca0f 100644 --- a/src/Storybook/useLastOpenedStory.luau +++ b/src/Storybook/useLastOpenedStory.luau @@ -2,7 +2,8 @@ local React = require("@pkg/React") local LocalStorageContext = require("@root/Plugin/LocalStorageContext") local SettingsContext = require("@root/UserSettings/SettingsContext") -local getInstanceFromFullName = require("@root/Common/getInstanceFromFullName") +local getInstanceFromPath = require("@root/Common/getInstanceFromPath") +local getInstancePath = require("@root/Common/getInstancePath") local useCallback = React.useCallback local useMemo = React.useMemo @@ -15,8 +16,8 @@ local function useLastOpenedStory(): (ModuleScript?, (storyModule: ModuleScript? local settingsContext = SettingsContext.use() local setLastOpenedStory = useCallback(function(storyModule: ModuleScript?) - localStorage.set(LAST_OPENED_STORY_PATH_KEY, if storyModule then storyModule:GetFullName() else nil) - end, { localStorage }) + localStorage.set(LAST_OPENED_STORY_PATH_KEY, if storyModule then getInstancePath(storyModule) else nil) + end, { localStorage.set }) local lastOpenedStory = useMemo(function(): ModuleScript? local rememberLastOpenedStory = settingsContext.getSetting(REMEMBER_LAST_OPENED_STORY_KEY) @@ -27,8 +28,8 @@ local function useLastOpenedStory(): (ModuleScript?, (storyModule: ModuleScript? local lastOpenedStoryPath = localStorage.get(LAST_OPENED_STORY_PATH_KEY) - if lastOpenedStoryPath then - local instance = getInstanceFromFullName(lastOpenedStoryPath) + if lastOpenedStoryPath and typeof(lastOpenedStoryPath) == "string" then + local instance = getInstanceFromPath(lastOpenedStoryPath) if instance and instance:IsA("ModuleScript") then return instance @@ -36,7 +37,7 @@ local function useLastOpenedStory(): (ModuleScript?, (storyModule: ModuleScript? end return nil - end, { settingsContext, localStorage }) + end, { settingsContext, localStorage.get }) return lastOpenedStory, setLastOpenedStory end diff --git a/src/TreeView/usePinnedInstances.luau b/src/TreeView/usePinnedInstances.luau index 22b8a74b..6644329d 100644 --- a/src/TreeView/usePinnedInstances.luau +++ b/src/TreeView/usePinnedInstances.luau @@ -2,7 +2,8 @@ local React = require("@pkg/React") local Sift = require("@pkg/Sift") local LocalStorageContext = require("@root/Plugin/LocalStorageContext") -local getInstanceFromFullName = require("@root/Common/getInstanceFromFullName") +local getInstanceFromPath = require("@root/Common/getInstanceFromPath") +local getInstancePath = require("@root/Common/getInstancePath") local usePrevious = require("@root/Common/usePrevious") local useCallback = React.useCallback @@ -26,14 +27,14 @@ local function usePinnedInstances(): (ModuleScript?, (storyModule: ModuleScript? local pin = useCallback(function(instance: Instance) setPinnedPaths(function(prev) - return Sift.List.append(prev, instance:GetFullName()) + return Sift.List.append(prev, getInstancePath(instance)) end) end, {}) local unpin = useCallback(function(instance: Instance) setPinnedPaths(function(prev) return Sift.List.filter(prev, function(pinnedPath) - return pinnedPath ~= instance:GetFullName() + return pinnedPath ~= getInstancePath(instance) end) end) end, {}) @@ -44,7 +45,7 @@ local function usePinnedInstances(): (ModuleScript?, (storyModule: ModuleScript? for _, pinnedPath in pinnedPaths do table.insert(pinnedInstances, { path = pinnedPath, - instance = getInstanceFromFullName(pinnedPath), + instance = getInstanceFromPath(pinnedPath), }) end @@ -67,7 +68,7 @@ local function usePinnedInstances(): (ModuleScript?, (storyModule: ModuleScript? if prevPinnedPaths and pinnedPaths ~= prevPinnedPaths then localStorage.set(PINNED_INSTANCES_KEY, pinnedPaths) end - end, { localStorage, pinnedPaths, prevPinnedPaths }) + end, { localStorage.set, pinnedPaths, prevPinnedPaths }) return { pin = pin, From d76e4aaf2e682693a2d86a623e1dd46f7587af7a Mon Sep 17 00:00:00 2001 From: Marin Minnerly <me@vocksel.com> Date: Fri, 20 Dec 2024 16:35:18 -0800 Subject: [PATCH 78/79] Add LuauPolyfill to fix build issues --- wally.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/wally.toml b/wally.toml index 96fa8332..a73e9495 100644 --- a/wally.toml +++ b/wally.toml @@ -23,3 +23,4 @@ JestGlobals = "jsdotlua/jest-globals@3.6.1-rc.2" # Storyteller dependencies Prospector = "egomoose/prospector@1.1.0" Janitor = "howmanysmall/janitor@1.13.15" +LuauPolyfill = "jsdotlua/luau-polyfill@1.2.7" From b8e5bc9b0903e19f4feef6d4ccd4caf057d60860 Mon Sep 17 00:00:00 2001 From: Marin Minnerly <me@vocksel.com> Date: Sun, 29 Dec 2024 18:56:41 -0800 Subject: [PATCH 79/79] Take a pass over instance path functions --- src/Common/getInstanceFromPath.luau | 8 ++++---- src/Common/getInstanceFromPath.spec.luau | 2 +- src/Common/getInstancePath.luau | 5 ++--- src/Common/getInstancePath.spec.luau | 17 +++++++++++++++-- 4 files changed, 22 insertions(+), 10 deletions(-) diff --git a/src/Common/getInstanceFromPath.luau b/src/Common/getInstanceFromPath.luau index a28c0d7d..65e7969f 100644 --- a/src/Common/getInstanceFromPath.luau +++ b/src/Common/getInstanceFromPath.luau @@ -1,9 +1,9 @@ --[[ - Gets an instance based off the result of getInstancePath. + Gets an instance based off the result of `getInstancePath`. - The reason we use this over GetFullName is because it uses a dot (.) as the - path separator which makes it difficult to disambiguate instances stories - and test files (Foo.story and Foo.spec, respectively) + The reason we don't use GetFullName is because it uses a dot (.) as the path + separator which makes it difficult to disambiguate stories and test files + (Foo.story and Foo.spec, respectively) Returns `nil` if the instance is outside the DataModel or otherwise cannot be found. diff --git a/src/Common/getInstanceFromPath.spec.luau b/src/Common/getInstanceFromPath.spec.luau index a343e4a4..d1f835a3 100644 --- a/src/Common/getInstanceFromPath.spec.luau +++ b/src/Common/getInstanceFromPath.spec.luau @@ -46,7 +46,7 @@ test("works with spec files", function() }) folder.Parent = ReplicatedStorage - local path = getInstanceFromPath(module:GetFullName()) + local path = getInstanceFromPath(getInstancePath(module)) expect(path).toBe(module) end) diff --git a/src/Common/getInstancePath.luau b/src/Common/getInstancePath.luau index 082df3e6..e071b7c0 100644 --- a/src/Common/getInstancePath.luau +++ b/src/Common/getInstancePath.luau @@ -1,8 +1,7 @@ local PATH_SEPARATOR = "/" local function getInstancePath(instance: Instance, pathSeparator: string?): string - pathSeparator = if pathSeparator then pathSeparator else PATH_SEPARATOR - assert(pathSeparator, "Luau") + local separator = if pathSeparator then pathSeparator else PATH_SEPARATOR local path = {} local current = instance @@ -12,7 +11,7 @@ local function getInstancePath(instance: Instance, pathSeparator: string?): stri current = current.Parent end - return table.concat(path, "/") + return table.concat(path, separator) end return getInstancePath diff --git a/src/Common/getInstancePath.spec.luau b/src/Common/getInstancePath.spec.luau index 1ee401ba..77b93f9f 100644 --- a/src/Common/getInstancePath.spec.luau +++ b/src/Common/getInstancePath.spec.luau @@ -31,7 +31,7 @@ test("works on nested instances", function() }) folder.Parent = ReplicatedStorage - expect(getInstancePath(module)).toBe("ReplicatedStorage/Folder/foo/bar") + expect(getInstancePath(module)).toBe("ReplicatedStorage/Root/foo/bar") end) test("works with spec files", function() @@ -44,5 +44,18 @@ test("works with spec files", function() }) folder.Parent = ReplicatedStorage - expect(getInstancePath(module)).toBe("ReplicatedStorage/Folder/foo/bar.spec") + expect(getInstancePath(module)).toBe("ReplicatedStorage/Root/foo/bar.spec") +end) + +test("path separator can be changed", function() + local module = Instance.new("ModuleScript") + + folder = newFolder({ + foo = newFolder({ + bar = module, + }), + }) + folder.Parent = ReplicatedStorage + + expect(getInstancePath(module, " > ")).toBe("ReplicatedStorage > Root > foo > bar") end)