diff --git a/img/Star.png b/img/Star.png new file mode 100644 index 00000000..e1bba5da Binary files /dev/null and b/img/Star.png differ diff --git a/img/StarFilled.png b/img/StarFilled.png new file mode 100644 index 00000000..fd0fbb12 Binary files /dev/null and b/img/StarFilled.png differ diff --git a/src/Storybook/StorybookTreeView.luau b/src/Storybook/StorybookTreeView.luau index 13a3c4e5..18dac511 100644 --- a/src/Storybook/StorybookTreeView.luau +++ b/src/Storybook/StorybookTreeView.luau @@ -1,9 +1,12 @@ +local HttpService = game:GetService("HttpService") + local React = require("@pkg/React") local Storyteller = require("@pkg/Storyteller") 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 @@ -26,10 +29,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 = "star", + 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 do local root = createTreeNodesForStorybook(storybook) table.insert(roots, root) 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..f6cb4ba2 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,24 @@ local function TreeNode(props: Props) }), }), + Pin = if props.node.instance and (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 = if pinning.isPinned(props.node.instance) then assets.StarFilled else assets.Star, + color = theme.text, + size = UDim2.fromOffset(16, 16), + }), + }) + else nil, + 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/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 new file mode 100644 index 00000000..0ca670dd --- /dev/null +++ b/src/TreeView/usePinnedInstances.luau @@ -0,0 +1,77 @@ +local React = require("@pkg/React") +local Sift = require("@pkg/Sift") + +local LocalStorageContext = require("@root/Plugin/LocalStorageContext") +local getInstanceFromFullName = require("@root/Common/getInstanceFromFullName") + +local useCallback = React.useCallback +local useState = React.useState +local useEffect = React.useEffect + +local PINNED_INSTANCES_KEY = "pinnedInstancePaths" + +export type PinnedInstance = { + path: string, + instance: Instance?, +} + +local function usePinnedInstances(): (ModuleScript?, (storyModule: ModuleScript?) -> ()) + local localStorage = LocalStorageContext.use() + + local pinnedPaths, setPinnedPaths = useState(function() + return localStorage.get(PINNED_INSTANCES_KEY) or {} + end) + + useEffect(function() + localStorage.set(PINNED_INSTANCES_KEY, pinnedPaths) + end, { pinnedPaths }) + + local pin = useCallback(function(instance: Instance) + setPinnedPaths(function(prev) + return Sift.List.append(prev, instance:GetFullName()) + end) + end, {}) + + local unpin = useCallback(function(instance: Instance) + setPinnedPaths(function(prev) + return Sift.List.filter(prev, function(pinnedPath) + return pinnedPath ~= instance:GetFullName() + end) + end) + end, {}) + + local getPinnedInstances = useCallback(function(): { PinnedInstance } + local pinnedInstances: { PinnedInstance } = {} + + for _, pinnedPath in pinnedPaths do + table.insert(pinnedInstances, { + path = pinnedPath, + instance = getInstanceFromFullName(pinnedPath), + }) + end + + return pinnedInstances + end, { pinnedPaths }) + + local isPinned = useCallback(function(instance: Instance) + return table.find(pinnedPaths, instance:GetFullName()) ~= nil + end, { pinnedPaths }) + + local togglePin = useCallback(function(instance: Instance) + if isPinned(instance) then + unpin(instance) + else + pin(instance) + end + end, { isPinned, unpin, pin }) + + return { + pin = pin, + unpin = unpin, + isPinned = isPinned, + togglePin = togglePin, + getPinnedInstances = getPinnedInstances, + } +end + +return usePinnedInstances 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/assets.luau b/src/assets.luau index d00fe303..45e15646 100644 --- a/src/assets.luau +++ b/src/assets.luau @@ -1,53 +1,63 @@ -- This file was @generated by Tarmac. It is not intended for manual editing. return { ChevronRight = { - Image = "rbxassetid://18940815650", + Image = "rbxassetid://137821613816144", ImageRectOffset = Vector2.new(49, 226), ImageRectSize = Vector2.new(32, 32), }, Component = { - Image = "rbxassetid://18940815650", + Image = "rbxassetid://137821613816144", ImageRectOffset = Vector2.new(0, 275), ImageRectSize = Vector2.new(32, 32), }, Folder = { - Image = "rbxassetid://18940815650", + Image = "rbxassetid://137821613816144", ImageRectOffset = Vector2.new(345, 0), ImageRectSize = Vector2.new(32, 32), }, GitHubMark = { - Image = "rbxassetid://18940815650", + Image = "rbxassetid://137821613816144", ImageRectOffset = Vector2.new(0, 0), ImageRectSize = Vector2.new(230, 225), }, IconLight = { - Image = "rbxassetid://18940815650", + Image = "rbxassetid://137821613816144", ImageRectOffset = Vector2.new(231, 65), ImageRectSize = Vector2.new(42, 42), }, Magnify = { - Image = "rbxassetid://18940815650", + Image = "rbxassetid://137821613816144", ImageRectOffset = Vector2.new(0, 226), ImageRectSize = Vector2.new(48, 48), }, Minify = { - Image = "rbxassetid://18940815650", + Image = "rbxassetid://137821613816144", ImageRectOffset = Vector2.new(296, 0), ImageRectSize = Vector2.new(48, 48), }, Search = { - Image = "rbxassetid://18940815650", + Image = "rbxassetid://137821613816144", ImageRectOffset = Vector2.new(296, 49), ImageRectSize = Vector2.new(32, 32), }, + Star = { + Image = "rbxassetid://137821613816144", + ImageRectOffset = Vector2.new(82, 226), + ImageRectSize = Vector2.new(25, 24), + }, + StarFilled = { + Image = "rbxassetid://137821613816144", + ImageRectOffset = Vector2.new(49, 259), + ImageRectSize = Vector2.new(25, 24), + }, Storybook = { - Image = "rbxassetid://18940815650", + Image = "rbxassetid://137821613816144", ImageRectOffset = Vector2.new(231, 108), ImageRectSize = Vector2.new(32, 32), }, flipbook = { - Image = "rbxassetid://18940815650", + Image = "rbxassetid://137821613816144", ImageRectOffset = Vector2.new(231, 0), ImageRectSize = Vector2.new(64, 64), }, -} +} \ No newline at end of file diff --git a/src/themes.luau b/src/themes.luau index a2b57a8d..db5a6325 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.amber600, github = Color3.fromHex("#333333"), @@ -86,6 +88,7 @@ local Dark: Theme = { story = tailwind.green500, directory = tailwind.purple500, alert = tailwind.rose500, + star = tailwind.amber400, github = Color3.fromHex("#ffffff"), diff --git a/tarmac-manifest.toml b/tarmac-manifest.toml index 9dd6cda0..cb18f728 100644 --- a/tarmac-manifest.toml +++ b/tarmac-manifest.toml @@ -1,59 +1,71 @@ [inputs."img/ChevronRight.png"] hash = "4c91853652b4e0a6317804c54db6397ac0c710c819b282f7be2179e3abdc879c" -id = 18940815650 +id = 137821613816144 slice = [[49, 226], [81, 258]] packable = true [inputs."img/Component.png"] hash = "d51b19d5f35ac87fd8156500828f3e217b6fc420b70fee5d97b3577e4e6a8843" -id = 18940815650 +id = 137821613816144 slice = [[0, 275], [32, 307]] packable = true [inputs."img/Folder.png"] hash = "4c0e80a363d36a4d127d410795cb13eec74d55871b70174636c0c962c242835c" -id = 18940815650 +id = 137821613816144 slice = [[345, 0], [377, 32]] packable = true [inputs."img/GitHubMark.png"] hash = "a0fb973cd9bf0c1123dbe783f7a4093dbcf9b4910d6e385c16a837a04a02306c" -id = 18940815650 +id = 137821613816144 slice = [[0, 0], [230, 225]] packable = true [inputs."img/IconLight.png"] hash = "237c66813c2b5a58ae9c46166f72db8698fbb046922e2473ebaca8f7d7376ce1" -id = 18940815650 +id = 137821613816144 slice = [[231, 65], [273, 107]] packable = true [inputs."img/Magnify.png"] hash = "351db4b6132a9e02d01bc4cdc3c850099dce358673daaa0da66745b675ad38b1" -id = 18940815650 +id = 137821613816144 slice = [[0, 226], [48, 274]] packable = true [inputs."img/Minify.png"] hash = "694db8ce141475fc0f42980d2a6dfd058dfa155348a579fab29ca6dd5684ec14" -id = 18940815650 +id = 137821613816144 slice = [[296, 0], [344, 48]] packable = true [inputs."img/Search.png"] hash = "0ab89c0309f577f3ccae0e03b45292f6e2bca51cc9b8250f56ad756e04231c57" -id = 18940815650 +id = 137821613816144 slice = [[296, 49], [328, 81]] packable = true +[inputs."img/Star.png"] +hash = "ae3c74c88729a8600531b2ade6cbaaa091033fffd0de5145b8462d3a15d0a510" +id = 137821613816144 +slice = [[82, 226], [107, 250]] +packable = true + +[inputs."img/StarFilled.png"] +hash = "d958666976bb5a1ab5672e5a79d393b28fca26fb348f9b07ceecbcaf059e78f3" +id = 137821613816144 +slice = [[49, 259], [74, 283]] +packable = true + [inputs."img/Storybook.png"] hash = "2a4b8bd513a8fa7eebae482943ab3435a659359bd94b24eff1c3dcc6f1244799" -id = 18940815650 +id = 137821613816144 slice = [[231, 108], [263, 140]] packable = true [inputs."img/flipbook.png"] hash = "423e3dcff54fce8824f14d7cadcfb90eef2abecd817df2bee13951bd2674f79a" -id = 18940815650 +id = 137821613816144 slice = [[231, 0], [295, 64]] packable = true