diff --git a/plugin/src/applyTheme.lua b/plugin/src/applyTheme.lua new file mode 100644 index 0000000..7764cf9 --- /dev/null +++ b/plugin/src/applyTheme.lua @@ -0,0 +1,42 @@ +local Root = script:FindFirstAncestor("rbxtheme") + +local getThemeColors = require(Root.getThemeColors) +local types = require(Root.types) + +local function applyTheme(theme: types.ExtensionTheme, studio: Studio): string? + local colors = getThemeColors(theme) + local problems = {} + + if colors and colors.found then + for name, color in colors.found do + -- Discard the alpha component of the hexcode + if #color - 1 > 6 then + local colorNoAlpha = color:sub(1, 7) + + -- TODO: Blend colors with the background to get rid of the + -- alpha component. That way we don't need to warn the user + table.insert(problems, `{name} uses unsupported alpha value. Truncating {color} to {colorNoAlpha}`) + + color = colorNoAlpha + end + + local success, result = pcall(function() + studio[name] = Color3.fromHex(color) + end) + + if not success then + table.insert(problems, `Failed to set {name}: {result}`) + end + end + end + + if #problems > 0 then + local problemsStr = "" + for index, problem in problems do + problemsStr ..= `{index}. {problem}\n` + end + return `{theme.name} was applied, but not all colors were set:\n{problemsStr}` + end +end + +return applyTheme diff --git a/plugin/src/components/App.lua b/plugin/src/components/App.lua index 01c75d4..f06b68b 100644 --- a/plugin/src/components/App.lua +++ b/plugin/src/components/App.lua @@ -1,53 +1,55 @@ local Root = script:FindFirstAncestor("rbxtheme") local React = require(Root.Packages.React) -local fetchVisualStudioExtensions = require(Root.fetchVisualStudioExtensions) +local Sift = require(Root.Packages.Sift) +local types = require(Root.types) +local Home = require(Root.Components.Home) +local ThemeDetails = require(Root.Components.ThemeDetails) -type VsMarketplaceExtension = fetchVisualStudioExtensions.VsMarketplaceExtension - -local useEffect = React.useState +local useCallback = React.useCallback local useState = React.useState +type PublishedExtension = types.PublishedExtension +type ExtensionTheme = types.ExtensionTheme + +export type View = "Home" | "ThemeDetails" + export type Props = { plugin: Plugin, } local function App(_props: Props) - local extensions, setExtensions = useState({} :: { VsMarketplaceExtension }) - - useEffect(function() - fetchVisualStudioExtensions({ - searchTerm = "theme", - }):andThen(function(newExtensions) - print("extensions", newExtensions) - setExtensions(newExtensions) - end) + local view, setView = useState("Home" :: View) + local viewParams, setViewParams = useState({}) + + local onBack = useCallback(function() + setViewParams({}) + setView("Home") + end, {}) + + local onViewExtension = useCallback(function(extension: PublishedExtension, themes: { ExtensionTheme }) + setViewParams({ + extension = extension, + themes = themes, + }) + setView("ThemeDetails") end, {}) - return React.createElement("Frame", { - Size = UDim2.fromScale(1, 1), - BackgroundTransparency = 1, - }, { - Layout = React.createElement("UIListLayout", { - SortOrder = Enum.SortOrder.LayoutOrder, - }), - - SearchForm = React.createElement("Frame", { - LayoutOrder = 1, - Size = UDim2.fromScale(1, 1), - BackgroundTransparency = 1, - }, { - Input = React.createElement("TextBox", { - PlaceholderText = "Search themes...", - }), - - ErrorMessage = React.createElement("TextLabel", {}), - }), - - Extensions = React.createElement("TextLabel", { - LayoutOrder = 2, - Text = tostring(extensions), - }), + return React.createElement("Folder", nil, { + Home = if view == "Home" + then React.createElement(Home, { + onViewExtension = onViewExtension, + }) + else nil, + + ThemeDetails = if view == "ThemeDetails" + then React.createElement( + ThemeDetails, + Sift.Dictionary.join(viewParams, { + onBack = onBack, + }) + ) + else nil, }) end diff --git a/plugin/src/components/App.story.lua b/plugin/src/components/App.story.lua deleted file mode 100644 index 023c30e..0000000 --- a/plugin/src/components/App.story.lua +++ /dev/null @@ -1,14 +0,0 @@ -local Root = script:FindFirstAncestor("rbxtheme") - -local React = require(Root.Packages.React) -local App = require(script.Parent.App) - -local mockPlugin = {} - -return { - story = function() - return React.createElement(App, { - plugin = mockPlugin, - }) - end, -} diff --git a/plugin/src/components/ExtensionsList.lua b/plugin/src/components/ExtensionsList.lua new file mode 100644 index 0000000..fb96c71 --- /dev/null +++ b/plugin/src/components/ExtensionsList.lua @@ -0,0 +1,123 @@ +local Root = script:FindFirstAncestor("rbxtheme") + +local React = require(Root.Packages.React) +local types = require(Root.types) +local getLayoutOrder = require(Root.Components.getLayoutOrder) + +type PublishedExtension = types.PublishedExtension + +export type Props = { + extensions: { PublishedExtension }, + onView: (extension: PublishedExtension) -> (), + LayoutOrder: number?, +} + +local ACTION_BUTTON_WIDTH = 120 +local PADDING = UDim.new(0, 8) + +local function ExtensionsList(props: Props) + local children = { + Layout = React.createElement("UIListLayout", { + Padding = PADDING, + SortOrder = Enum.SortOrder.LayoutOrder, + }), + } + + for i, extension in props.extensions do + local isEven = i % 2 == 0 + local latestVersion = if extension.versions then extension.versions[1] else nil + + children[extension.extensionName] = React.createElement("Frame", { + LayoutOrder = i, + BackgroundTransparency = if isEven then 1 else 0.2, + BackgroundColor3 = Color3.fromRGB(100, 100, 100), + BorderSizePixel = 0, + AutomaticSize = Enum.AutomaticSize.Y, + Size = UDim2.fromScale(1, 0), + }, { + Layout = React.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder, + FillDirection = Enum.FillDirection.Horizontal, + VerticalAlignment = Enum.VerticalAlignment.Center, + }), + + Padding = React.createElement("UIPadding", { + PaddingTop = PADDING, + PaddingRight = PADDING, + PaddingBottom = PADDING, + PaddingLeft = PADDING, + }), + + Main = React.createElement("Frame", { + LayoutOrder = getLayoutOrder(), + Size = UDim2.fromScale(1, 0) - UDim2.fromOffset(ACTION_BUTTON_WIDTH, 0), + AutomaticSize = Enum.AutomaticSize.Y, + BackgroundTransparency = 1, + }, { + Layout = React.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder, + Padding = PADDING, + }), + + Name = React.createElement("TextLabel", { + LayoutOrder = getLayoutOrder(), + AutomaticSize = Enum.AutomaticSize.XY, + BackgroundTransparency = 1, + Text = `{extension.displayName} v{latestVersion.version}`, + TextSize = 16, + Font = Enum.Font.GothamMedium, + TextXAlignment = Enum.TextXAlignment.Left, + TextYAlignment = Enum.TextYAlignment.Top, + TextColor3 = Color3.fromRGB(255, 255, 255), + TextTruncate = Enum.TextTruncate.AtEnd, + }), + + Publisher = React.createElement("TextLabel", { + LayoutOrder = getLayoutOrder(), + Text = extension.publisher.publisherName, + TextSize = 14, + TextColor3 = Color3.fromRGB(200, 200, 200), + AutomaticSize = Enum.AutomaticSize.XY, + BackgroundTransparency = 1, + Font = Enum.Font.GothamMedium, + TextXAlignment = Enum.TextXAlignment.Left, + TextYAlignment = Enum.TextYAlignment.Top, + }), + }), + + Action = React.createElement("TextButton", { + LayoutOrder = getLayoutOrder(), + Text = "View", + TextSize = 14, + TextColor3 = Color3.fromRGB(255, 255, 255), + BorderSizePixel = 0, + Font = Enum.Font.GothamMedium, + Size = UDim2.fromOffset(ACTION_BUTTON_WIDTH, 0), + AutomaticSize = Enum.AutomaticSize.Y, + [React.Event.Activated] = function() + props.onView(extension) + end, + }, { + Padding = React.createElement("UIPadding", { + PaddingTop = PADDING, + PaddingRight = PADDING, + PaddingBottom = PADDING, + PaddingLeft = PADDING, + }), + + Corner = React.createElement("UICorner", { + CornerRadius = PADDING, + }), + }), + }) + end + + return React.createElement("Frame", { + LayoutOrder = props.LayoutOrder, + AutomaticSize = Enum.AutomaticSize.Y, + Size = UDim2.fromScale(1, 0), + BackgroundTransparency = 1, + }, children) +end + +return ExtensionsList diff --git a/plugin/src/components/Home.lua b/plugin/src/components/Home.lua new file mode 100644 index 0000000..938900b --- /dev/null +++ b/plugin/src/components/Home.lua @@ -0,0 +1,141 @@ +local Root = script:FindFirstAncestor("rbxtheme") + +local React = require(Root.Packages.React) +local fetchVisualStudioExtensions = require(Root.fetchVisualStudioExtensions) +local fetchExtensionThemes = require(Root.fetchExtensionThemes) +local types = require(Root.types) +local LoadingSpinner = require(Root.Components.LoadingSpinner) +local ExtensionsList = require(Root.Components.ExtensionsList) +local getLayoutOrder = require(Root.Components.getLayoutOrder) + +type PublishedExtension = types.PublishedExtension +type ExtensionTheme = types.ExtensionTheme + +local useCallback = React.useCallback +local useEffect = React.useEffect +local useState = React.useState + +local PADDING = UDim.new(0, 8) + +export type Props = { + onViewExtension: (extension: PublishedExtension, themes: { Theme }) -> (), +} + +local function Home(props: Props) + local isLoading, setIsLoading = useState(true) + local extensions, setExtensions = useState({} :: { PublishedExtension }) + local searchTerm, setSearchTerm = useState("") + + local onView = useCallback(function(extension: PublishedExtension) + local latestVersion = extension.versions[1] + + if latestVersion then + fetchExtensionThemes(extension, latestVersion.version) + :andThen(function(themes) + props.onViewExtension(extension, themes) + end) + :catch(function(err) + warn("ERR:", err) + end) + else + warn("No latest version found for extension {extension.displayName}") + end + end, {}) + + local onSearch = useCallback(function(rbx: TextBox, enterPressed: boolean) + if enterPressed then + setSearchTerm(rbx.Text) + end + end, {}) + + useEffect(function() + setIsLoading(true) + fetchVisualStudioExtensions({ + -- page = page, -- TODO: Increment the page when scrolling to the bottom of the list + pageSize = 20, + searchTerm = if searchTerm ~= "" then searchTerm else "theme", + }) + :andThen(function(newExtensions) + setExtensions(newExtensions) + end) + :finally(function() + setIsLoading(false) + end) + end, { searchTerm }) + + return React.createElement("ScrollingFrame", { + Size = UDim2.fromScale(1, 1), + BackgroundTransparency = 1, + CanvasSize = UDim2.fromScale(0, 0), + AutomaticCanvasSize = Enum.AutomaticSize.Y, + }, { + Layout = React.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder, + Padding = PADDING, + }), + + Padding = React.createElement("UIPadding", { + PaddingTop = PADDING, + PaddingRight = PADDING, + PaddingBottom = PADDING, + PaddingLeft = PADDING, + }), + + SearchForm = React.createElement("Frame", { + LayoutOrder = getLayoutOrder(), + Size = UDim2.fromScale(1, 0), + AutomaticSize = Enum.AutomaticSize.Y, + BackgroundTransparency = 1, + }, { + Input = React.createElement("TextBox", { + Text = searchTerm, + PlaceholderText = "Search themes...", + Size = UDim2.fromScale(1, 0), + TextColor3 = Color3.fromRGB(255, 255, 255), + PlaceholderColor3 = Color3.fromRGB(200, 200, 200), + TextSize = 16, + Font = Enum.Font.Gotham, + AutomaticSize = Enum.AutomaticSize.Y, + BorderSizePixel = 0, + BackgroundColor3 = Color3.fromRGB(100, 100, 100), + [React.Event.FocusLost] = onSearch, + }, { + Padding = React.createElement("UIPadding", { + PaddingTop = PADDING, + PaddingRight = PADDING, + PaddingBottom = PADDING, + PaddingLeft = PADDING, + }), + }), + }), + + ExtensionsListWrapper = React.createElement( + "Frame", + { + Size = UDim2.fromScale(1, 0), + AutomaticSize = Enum.AutomaticSize.Y, + BackgroundTransparency = 1, + LayoutOrder = getLayoutOrder(), + }, + if isLoading + then { + Layout = React.createElement("UIListLayout", { + HorizontalAlignment = Enum.HorizontalAlignment.Center, + VerticalAlignment = Enum.VerticalAlignment.Center, + }), + + LoadingSpinner = React.createElement(LoadingSpinner), + } + else { + ExtensionList = if not isLoading + then React.createElement(ExtensionsList, { + extensions = extensions, + onView = onView, + }) + else nil, + } + ), + }) +end + +return Home diff --git a/plugin/src/components/LoadingSpinner.lua b/plugin/src/components/LoadingSpinner.lua new file mode 100644 index 0000000..5ed9fbf --- /dev/null +++ b/plugin/src/components/LoadingSpinner.lua @@ -0,0 +1,41 @@ +local Root = script:FindFirstAncestor("rbxtheme") + +local React = require(Root.Packages.React) +local Sift = require(Root.Packages.Sift) + +local useClock = require(Root.Components.useClock) + +export type Props = { + speed: number?, + Size: UDim2?, +} + +local defaultProps = { + speed = 1, + Size = UDim2.fromOffset(64, 64), +} + +local SPEED_MULTIPLIER = 200 + +type InternalProps = Props & typeof(defaultProps) + +local function LoadingSpinner(providedProps: Props) + local props: InternalProps = Sift.Dictionary.join(defaultProps, providedProps) + local clock = useClock() + + return React.createElement("Frame", { + Size = props.Size, + BackgroundTransparency = 1, + }, { + Spinner = React.createElement("ImageLabel", { + Image = "rbxasset://textures/DarkThemeLoadingCircle.png", + Size = UDim2.fromScale(1, 1), + BackgroundTransparency = 1, + Rotation = clock:map(function(n) + return n * (props.speed * SPEED_MULTIPLIER) + end), + }), + }) +end + +return LoadingSpinner diff --git a/plugin/src/components/ThemeDetails.lua b/plugin/src/components/ThemeDetails.lua new file mode 100644 index 0000000..7575bd4 --- /dev/null +++ b/plugin/src/components/ThemeDetails.lua @@ -0,0 +1,234 @@ +local Root = script:FindFirstAncestor("rbxtheme") + +local React = require(Root.Packages.React) +local Sift = require(Root.Packages.Sift) +local types = require(Root.types) +local applyTheme = require(Root.applyTheme) +local getLayoutOrder = require(Root.Components.getLayoutOrder) + +local useCallback = React.useCallback + +local ACTION_BUTTON_WIDTH = 120 +local PADDING = UDim.new(0, 8) + +export type Props = { + extension: types.PublishedExtension, + themes: { types.ExtensionTheme }, + onBack: () -> (), + studio: Studio?, +} + +local defaultProps = { + studio = settings().Studio, +} + +type InternalProps = Props & typeof(defaultProps) + +local function getThemeKey(theme: types.ExtensionTheme) + return theme.uuid or theme.name or tostring(theme) +end + +local function ThemeDetails(providedProps: Props) + local props: InternalProps = Sift.Dictionary.join(defaultProps, providedProps) + + local latestVersion = if props.extension.versions then props.extension.versions[1] else nil + local themeVariants = { + Layout = React.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder, + Padding = PADDING, + }), + } + + -- TODO: After applying a theme, ask if the user want's to switch their + -- Studio theme (dark/light) to match + local onApplyTheme = useCallback(function(theme: types.ExtensionTheme) + local err = applyTheme(theme, props.studio) + if err then + warn(err) + end + end, {}) + + for i, theme in props.themes do + local key = getThemeKey(theme) + local isEven = i % 2 == 0 + + themeVariants[key] = React.createElement("Frame", { + LayoutOrder = i, + BackgroundTransparency = if isEven then 1 else 0.2, + BackgroundColor3 = Color3.fromRGB(100, 100, 100), + BorderSizePixel = 0, + AutomaticSize = Enum.AutomaticSize.Y, + Size = UDim2.fromScale(1, 0), + }, { + Layout = React.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder, + FillDirection = Enum.FillDirection.Horizontal, + VerticalAlignment = Enum.VerticalAlignment.Center, + }), + + Padding = React.createElement("UIPadding", { + PaddingTop = PADDING, + PaddingRight = PADDING, + PaddingBottom = PADDING, + PaddingLeft = PADDING, + }), + + Main = React.createElement("Frame", { + LayoutOrder = getLayoutOrder(), + Size = UDim2.fromScale(1, 0) - UDim2.fromOffset(ACTION_BUTTON_WIDTH, 0), + AutomaticSize = Enum.AutomaticSize.Y, + BackgroundTransparency = 1, + }, { + Layout = React.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder, + Padding = PADDING, + }), + + Name = React.createElement("TextLabel", { + LayoutOrder = getLayoutOrder(), + AutomaticSize = Enum.AutomaticSize.XY, + BackgroundTransparency = 1, + Text = theme.name, + TextSize = 16, + Font = Enum.Font.GothamMedium, + TextXAlignment = Enum.TextXAlignment.Left, + TextYAlignment = Enum.TextYAlignment.Top, + TextColor3 = Color3.fromRGB(255, 255, 255), + TextTruncate = Enum.TextTruncate.AtEnd, + }), + }), + + Action = React.createElement("TextButton", { + LayoutOrder = getLayoutOrder(), + Text = "Use Theme", + TextSize = 14, + TextColor3 = Color3.fromRGB(255, 255, 255), + BorderSizePixel = 0, + Font = Enum.Font.GothamMedium, + Size = UDim2.fromOffset(ACTION_BUTTON_WIDTH, 0), + AutomaticSize = Enum.AutomaticSize.Y, + BackgroundColor3 = Color3.fromRGB(143, 186, 86), + [React.Event.Activated] = function() + onApplyTheme(theme) + end, + }, { + Padding = React.createElement("UIPadding", { + PaddingTop = PADDING, + PaddingRight = PADDING, + PaddingBottom = PADDING, + PaddingLeft = PADDING, + }), + + Corner = React.createElement("UICorner", { + CornerRadius = PADDING, + }), + }), + }) + end + + return React.createElement("Frame", { + Size = UDim2.fromScale(1, 0), + AutomaticSize = Enum.AutomaticSize.Y, + BackgroundTransparency = 1, + }, { + Layout = React.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder, + Padding = PADDING, + }), + + Padding = React.createElement("UIPadding", { + PaddingTop = PADDING, + PaddingRight = PADDING, + PaddingBottom = PADDING, + PaddingLeft = PADDING, + }), + + Back = React.createElement("TextButton", { + LayoutOrder = getLayoutOrder(), + Text = "< Back", + AutomaticSize = Enum.AutomaticSize.XY, + BackgroundTransparency = 1, + TextSize = 16, + Font = Enum.Font.Gotham, + TextXAlignment = Enum.TextXAlignment.Left, + TextYAlignment = Enum.TextYAlignment.Top, + TextColor3 = Color3.fromRGB(255, 255, 255), + TextTruncate = Enum.TextTruncate.AtEnd, + [React.Event.Activated] = props.onBack, + }), + + Header = React.createElement("Frame", { + LayoutOrder = getLayoutOrder(), + AutomaticSize = Enum.AutomaticSize.Y, + Size = UDim2.fromScale(1, 0), + BackgroundTransparency = 1, + }, { + Layout = React.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder, + Padding = PADDING, + }), + + Primary = React.createElement("Frame", { + LayoutOrder = getLayoutOrder(), + AutomaticSize = Enum.AutomaticSize.Y, + Size = UDim2.fromScale(1, 0), + BackgroundTransparency = 1, + }, { + Layout = React.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder, + FillDirection = Enum.FillDirection.Horizontal, + Padding = PADDING, + }), + + Name = React.createElement("TextLabel", { + LayoutOrder = getLayoutOrder(), + AutomaticSize = Enum.AutomaticSize.XY, + BackgroundTransparency = 1, + Text = props.extension.displayName, + TextSize = 16, + Font = Enum.Font.Gotham, + TextXAlignment = Enum.TextXAlignment.Left, + TextYAlignment = Enum.TextYAlignment.Top, + TextColor3 = Color3.fromRGB(255, 255, 255), + TextTruncate = Enum.TextTruncate.AtEnd, + }), + + Version = if latestVersion + then React.createElement("TextLabel", { + LayoutOrder = getLayoutOrder(), + AutomaticSize = Enum.AutomaticSize.XY, + BackgroundTransparency = 1, + Text = `v{latestVersion.version}`, + TextSize = 16, + Font = Enum.Font.Gotham, + TextXAlignment = Enum.TextXAlignment.Left, + TextYAlignment = Enum.TextYAlignment.Top, + TextColor3 = Color3.fromRGB(255, 255, 255), + TextTruncate = Enum.TextTruncate.AtEnd, + }) + else nil, + }), + + Publisher = React.createElement("TextLabel", { + LayoutOrder = getLayoutOrder(), + Text = props.extension.publisher.publisherName, + TextSize = 14, + TextColor3 = Color3.fromRGB(200, 200, 200), + AutomaticSize = Enum.AutomaticSize.XY, + BackgroundTransparency = 1, + Font = Enum.Font.Gotham, + TextXAlignment = Enum.TextXAlignment.Left, + TextYAlignment = Enum.TextYAlignment.Top, + }), + }), + + Variants = React.createElement("Frame", { + LayoutOrder = getLayoutOrder(), + Size = UDim2.fromScale(1, 0), + BackgroundTransparency = 1, + AutomaticSize = Enum.AutomaticSize.Y, + }, themeVariants), + }) +end + +return ThemeDetails diff --git a/plugin/src/components/getLayoutOrder.lua b/plugin/src/components/getLayoutOrder.lua new file mode 100644 index 0000000..0dbecd4 --- /dev/null +++ b/plugin/src/components/getLayoutOrder.lua @@ -0,0 +1,8 @@ +local nextOrder = 0 + +local function getLayoutOrder() + nextOrder += 1 + return nextOrder +end + +return getLayoutOrder diff --git a/plugin/src/components/useClock.lua b/plugin/src/components/useClock.lua new file mode 100644 index 0000000..91d9bd3 --- /dev/null +++ b/plugin/src/components/useClock.lua @@ -0,0 +1,28 @@ +local Root = script:FindFirstAncestor("rbxtheme") + +local RunService = game:GetService("RunService") + +local React = require(Root.Packages.React) + +local useEffect = React.useState +local useBinding = React.useBinding + +type Binding = React.Binding + +local function useClock(): Binding + local clockBinding, setClockBinding = useBinding(0) + + useEffect(function() + local stepConnection = RunService.PostSimulation:Connect(function(delta) + setClockBinding(clockBinding:getValue() + delta) + end) + + return function() + stepConnection:Disconnect() + end + end, {}) + + return clockBinding +end + +return useClock diff --git a/plugin/src/init.server.lua b/plugin/src/init.server.lua index 149fe6f..5641e34 100644 --- a/plugin/src/init.server.lua +++ b/plugin/src/init.server.lua @@ -1,7 +1,7 @@ local React = require(script.Packages.React) local ReactRoblox = require(script.Packages.ReactRoblox) local constants = require(script.constants) -local App = require(script.components.App) +local App = require(script.Components.App) local info = DockWidgetPluginGuiInfo.new(Enum.InitialDockState.Float, false, false, 400, 400, 100, 100) local widget = plugin:CreateDockWidgetPluginGui(constants.PLUGIN_NAME, info) diff --git a/plugin/src/types.lua b/plugin/src/types.lua index e4adfb6..e743d95 100644 --- a/plugin/src/types.lua +++ b/plugin/src/types.lua @@ -182,4 +182,17 @@ export type ExtensionQueryResult = { results: { ExtensionFilterResult }, } +type TokenColor = { + scope: { string }, + settings: { [string]: string }, +} + +export type ExtensionTheme = { + colors: { [string]: string }, + name: string, + tokenColors: { TokenColor }, + semanticHighlighting: boolean?, + type: string?, +} + return nil