diff --git a/.changeset/nervous-hounds-sleep.md b/.changeset/nervous-hounds-sleep.md
new file mode 100644
index 0000000000..530ff348f7
--- /dev/null
+++ b/.changeset/nervous-hounds-sleep.md
@@ -0,0 +1,5 @@
+---
+'@backstage/frontend-app-api': patch
+---
+
+Make themes configurable through extensions, and switched default themes to use extensions instead.
diff --git a/.changeset/silly-swans-invent.md b/.changeset/silly-swans-invent.md
new file mode 100644
index 0000000000..68b094aba7
--- /dev/null
+++ b/.changeset/silly-swans-invent.md
@@ -0,0 +1,5 @@
+---
+'@backstage/frontend-plugin-api': patch
+---
+
+Added `createThemeExtension` and `coreExtensionData.theme`.
diff --git a/packages/frontend-app-api/package.json b/packages/frontend-app-api/package.json
index 0982cb0157..6c45e9239a 100644
--- a/packages/frontend-app-api/package.json
+++ b/packages/frontend-app-api/package.json
@@ -25,7 +25,8 @@
"devDependencies": {
"@backstage/cli": "workspace:^",
"@backstage/test-utils": "workspace:^",
- "@testing-library/jest-dom": "^5.10.1"
+ "@testing-library/jest-dom": "^5.10.1",
+ "@testing-library/react": "^12.1.3"
},
"configSchema": "config.d.ts",
"files": [
@@ -39,8 +40,10 @@
"@backstage/core-plugin-api": "workspace:^",
"@backstage/frontend-plugin-api": "workspace:^",
"@backstage/plugin-graphiql": "workspace:^",
+ "@backstage/theme": "workspace:^",
"@backstage/types": "workspace:^",
"@material-ui/core": "^4.12.4",
+ "@material-ui/icons": "^4.11.3",
"@types/react": "^16.13.1 || ^17.0.0",
"lodash": "^4.17.21"
},
diff --git a/packages/frontend-app-api/src/extensions/themes.tsx b/packages/frontend-app-api/src/extensions/themes.tsx
new file mode 100644
index 0000000000..ea11b6a638
--- /dev/null
+++ b/packages/frontend-app-api/src/extensions/themes.tsx
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2023 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import React from 'react';
+import {
+ UnifiedThemeProvider,
+ themes as builtinThemes,
+} from '@backstage/theme';
+import DarkIcon from '@material-ui/icons/Brightness2';
+import LightIcon from '@material-ui/icons/WbSunny';
+import { createThemeExtension } from '@backstage/frontend-plugin-api';
+
+export const LightTheme = createThemeExtension({
+ id: 'light',
+ title: 'Light Theme',
+ variant: 'light',
+ icon: ,
+ Provider: ({ children }) => (
+
+ ),
+});
+
+export const DarkTheme = createThemeExtension({
+ id: 'dark',
+ title: 'Dark Theme',
+ variant: 'dark',
+ icon: ,
+ Provider: ({ children }) => (
+
+ ),
+});
diff --git a/packages/frontend-app-api/src/wiring/createApp.test.tsx b/packages/frontend-app-api/src/wiring/createApp.test.tsx
index 903b044ed3..f6172f5a4b 100644
--- a/packages/frontend-app-api/src/wiring/createApp.test.tsx
+++ b/packages/frontend-app-api/src/wiring/createApp.test.tsx
@@ -18,10 +18,11 @@ import {
createExtension,
createPageExtension,
createPlugin,
+ createThemeExtension,
} from '@backstage/frontend-plugin-api';
-import { createInstances } from './createApp';
-
-import { MockConfigApi } from '@backstage/test-utils';
+import { createApp, createInstances } from './createApp';
+import { screen } from '@testing-library/react';
+import { MockConfigApi, renderWithEffects } from '@backstage/test-utils';
import React from 'react';
import { createRouteRef } from '@backstage/core-plugin-api';
@@ -104,3 +105,33 @@ describe('createInstances', () => {
);
});
});
+
+describe('createApp', () => {
+ it('should allow themes to be installed', async () => {
+ const app = createApp({
+ configLoader: async () =>
+ new MockConfigApi({
+ app: {
+ extensions: [{ 'themes.light': false }, { 'themes.dark': false }],
+ },
+ }),
+ plugins: [
+ createPlugin({
+ id: 'test',
+ extensions: [
+ createThemeExtension({
+ id: 'derp',
+ title: 'Derp',
+ variant: 'dark',
+ Provider: () =>
Derp
,
+ }),
+ ],
+ }),
+ ],
+ });
+
+ await renderWithEffects(app.createRoot());
+
+ await expect(screen.findByText('Derp')).resolves.toBeInTheDocument();
+ });
+});
diff --git a/packages/frontend-app-api/src/wiring/createApp.tsx b/packages/frontend-app-api/src/wiring/createApp.tsx
index dc7db79022..deb19d919d 100644
--- a/packages/frontend-app-api/src/wiring/createApp.tsx
+++ b/packages/frontend-app-api/src/wiring/createApp.tsx
@@ -50,6 +50,7 @@ import {
attachComponentData,
useRouteRef,
identityApiRef,
+ AppTheme,
} from '@backstage/core-plugin-api';
import { getAvailablePlugins } from './discovery';
import {
@@ -77,10 +78,10 @@ import {
apis as defaultApis,
components as defaultComponents,
icons as defaultIcons,
- themes as defaultThemes,
} from '../../../app-defaults/src/defaults';
import { BrowserRouter, Route } from 'react-router-dom';
import { SidebarItem } from '@backstage/core-components';
+import { DarkTheme, LightTheme } from '../extensions/themes';
/** @public */
export interface ExtensionTreeNode {
@@ -171,7 +172,14 @@ export function createInstances(options: {
plugins: BackstagePlugin[];
config: Config;
}) {
- const builtinExtensions = [Core, CoreRoutes, CoreNav, CoreLayout];
+ const builtinExtensions = [
+ Core,
+ CoreRoutes,
+ CoreNav,
+ CoreLayout,
+ LightTheme,
+ DarkTheme,
+ ];
// pull in default extension instance from discovered packages
// apply config to adjust default extension instances and add more
@@ -375,6 +383,12 @@ function createApiHolder(
?.map(e => e.getData(coreExtensionData.apiFactory))
.filter((x): x is AnyApiFactory => !!x) ?? [];
+ const themeExtensions =
+ coreExtension.attachments
+ .get('themes')
+ ?.map(e => e.getData(coreExtensionData.theme))
+ .filter((x): x is AppTheme => !!x) ?? [];
+
for (const factory of [...defaultApis, ...pluginApis]) {
factoryRegistry.register('default', factory);
}
@@ -422,7 +436,7 @@ function createApiHolder(
api: appThemeApiRef,
deps: {},
// TODO: add extension for registering themes
- factory: () => AppThemeSelector.createWithStorage(defaultThemes),
+ factory: () => AppThemeSelector.createWithStorage(themeExtensions),
});
factoryRegistry.register('static', {
diff --git a/packages/frontend-plugin-api/api-report.md b/packages/frontend-plugin-api/api-report.md
index 5855976bc4..e28d7d5993 100644
--- a/packages/frontend-plugin-api/api-report.md
+++ b/packages/frontend-plugin-api/api-report.md
@@ -7,6 +7,7 @@
import { AnyApiFactory } from '@backstage/core-plugin-api';
import { AnyApiRef } from '@backstage/core-plugin-api';
+import { AppTheme } from '@backstage/core-plugin-api';
import { IconComponent } from '@backstage/core-plugin-api';
import { JsonObject } from '@backstage/types';
import { JSX as JSX_2 } from 'react';
@@ -71,6 +72,7 @@ export const coreExtensionData: {
apiFactory: ConfigurableExtensionDataRef;
routeRef: ConfigurableExtensionDataRef;
navTarget: ConfigurableExtensionDataRef;
+ theme: ConfigurableExtensionDataRef;
};
// @public (undocumented)
@@ -199,6 +201,9 @@ export function createSchemaFromZod(
schemaCreator: (zImpl: typeof z) => ZodSchema,
): PortableSchema;
+// @public (undocumented)
+export function createThemeExtension(theme: AppTheme): Extension;
+
// @public (undocumented)
export interface Extension {
// (undocumented)
diff --git a/packages/frontend-plugin-api/src/extensions/createThemeExtension.ts b/packages/frontend-plugin-api/src/extensions/createThemeExtension.ts
new file mode 100644
index 0000000000..5c13dd199e
--- /dev/null
+++ b/packages/frontend-plugin-api/src/extensions/createThemeExtension.ts
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2023 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { createExtension, coreExtensionData } from '../wiring';
+import { AppTheme } from '@backstage/core-plugin-api';
+
+/** @public */
+export function createThemeExtension(theme: AppTheme) {
+ return createExtension({
+ id: `themes.${theme.id}`,
+ at: 'core/themes',
+ output: {
+ theme: coreExtensionData.theme,
+ },
+ factory({ bind }) {
+ bind({ theme });
+ },
+ });
+}
diff --git a/packages/frontend-plugin-api/src/extensions/index.ts b/packages/frontend-plugin-api/src/extensions/index.ts
index 0ec0cdf652..00cf4f919c 100644
--- a/packages/frontend-plugin-api/src/extensions/index.ts
+++ b/packages/frontend-plugin-api/src/extensions/index.ts
@@ -17,3 +17,4 @@
export { createApiExtension } from './createApiExtension';
export { createPageExtension } from './createPageExtension';
export { createNavItemExtension } from './createNavItemExtension';
+export { createThemeExtension } from './createThemeExtension';
diff --git a/packages/frontend-plugin-api/src/wiring/coreExtensionData.ts b/packages/frontend-plugin-api/src/wiring/coreExtensionData.ts
index 079d5bcb18..6913501907 100644
--- a/packages/frontend-plugin-api/src/wiring/coreExtensionData.ts
+++ b/packages/frontend-plugin-api/src/wiring/coreExtensionData.ts
@@ -17,6 +17,7 @@
import { JSX } from 'react';
import {
AnyApiFactory,
+ AppTheme,
IconComponent,
RouteRef,
} from '@backstage/core-plugin-api';
@@ -36,4 +37,5 @@ export const coreExtensionData = {
apiFactory: createExtensionDataRef('core.api.factory'),
routeRef: createExtensionDataRef('core.routing.ref'),
navTarget: createExtensionDataRef('core.nav.target'),
+ theme: createExtensionDataRef('core.theme'),
};
diff --git a/yarn.lock b/yarn.lock
index f7e19c3179..3dd3f6315b 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -4307,9 +4307,12 @@ __metadata:
"@backstage/frontend-plugin-api": "workspace:^"
"@backstage/plugin-graphiql": "workspace:^"
"@backstage/test-utils": "workspace:^"
+ "@backstage/theme": "workspace:^"
"@backstage/types": "workspace:^"
"@material-ui/core": ^4.12.4
+ "@material-ui/icons": ^4.11.3
"@testing-library/jest-dom": ^5.10.1
+ "@testing-library/react": ^12.1.3
"@types/react": ^16.13.1 || ^17.0.0
lodash: ^4.17.21
peerDependencies: