diff --git a/.changeset/hip-dancers-fix.md b/.changeset/hip-dancers-fix.md new file mode 100644 index 00000000..706838f0 --- /dev/null +++ b/.changeset/hip-dancers-fix.md @@ -0,0 +1,6 @@ +--- +"@open-pioneer/runtime": minor +--- + +Implement support for custom chakra themes via the `theme` parameter in `createCustomElement()`. +`theme` from `@open-pioneer/base-theme` is used as default when no other theme is configured. diff --git a/.changeset/slow-walls-cheat.md b/.changeset/slow-walls-cheat.md new file mode 100644 index 00000000..c0af9321 --- /dev/null +++ b/.changeset/slow-walls-cheat.md @@ -0,0 +1,5 @@ +--- +"@open-pioneer/base-theme": minor +--- + +Initial release. diff --git a/.changeset/twenty-donuts-kneel.md b/.changeset/twenty-donuts-kneel.md new file mode 100644 index 00000000..af201126 --- /dev/null +++ b/.changeset/twenty-donuts-kneel.md @@ -0,0 +1,5 @@ +--- +"@open-pioneer/chakra-integration": minor +--- + +Add theme parameter and experimental base theme. diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 390cb67e..4dba6269 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -171,6 +171,17 @@ importers: specifier: ^0.31.4 version: 0.31.4(jsdom@22.1.0)(sass@1.63.6) + src/packages/base-theme: + dependencies: + '@open-pioneer/chakra-integration': + specifier: workspace:^ + version: link:../chakra-integration + devDependencies: + core-packages: + specifier: workspace:^ + version: link:../../.. + publishDirectory: dist + src/packages/chakra-integration: dependencies: '@chakra-ui/react': @@ -240,6 +251,9 @@ importers: '@formatjs/intl': specifier: ^2.7.2 version: 2.9.0(typescript@5.1.6) + '@open-pioneer/base-theme': + specifier: workspace:^ + version: link:../base-theme '@open-pioneer/chakra-integration': specifier: workspace:^ version: link:../chakra-integration @@ -324,6 +338,9 @@ importers: src/samples/chakra-sample/chakra-app: dependencies: + '@open-pioneer/base-theme': + specifier: workspace:^ + version: link:../../../packages/base-theme '@open-pioneer/chakra-integration': specifier: workspace:^ version: link:../../../packages/chakra-integration @@ -1823,6 +1840,7 @@ packages: /@emotion/memoize@0.7.4: resolution: {integrity: sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==} + requiresBuild: true dev: false optional: true diff --git a/src/packages/base-theme/CHANGELOG.md b/src/packages/base-theme/CHANGELOG.md new file mode 100644 index 00000000..7ecfde6b --- /dev/null +++ b/src/packages/base-theme/CHANGELOG.md @@ -0,0 +1 @@ +# @open-pioneer/base-theme diff --git a/src/packages/base-theme/LICENSE b/src/packages/base-theme/LICENSE new file mode 100644 index 00000000..d6456956 --- /dev/null +++ b/src/packages/base-theme/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. diff --git a/src/packages/base-theme/README.md b/src/packages/base-theme/README.md new file mode 100644 index 00000000..ace0e893 --- /dev/null +++ b/src/packages/base-theme/README.md @@ -0,0 +1,10 @@ +# @open-pioneer/base-theme + +Provides the default open pioneer trails chakra theme. + +```js +import { theme as baseTheme } from "@open-pioneer/base-theme"; +``` + +> NOTE: This package is still under active development. +> There will be breaking changes. diff --git a/src/packages/base-theme/build.config.mjs b/src/packages/base-theme/build.config.mjs new file mode 100644 index 00000000..015464d5 --- /dev/null +++ b/src/packages/base-theme/build.config.mjs @@ -0,0 +1,7 @@ +// SPDX-FileCopyrightText: con terra GmbH and contributors +// SPDX-License-Identifier: Apache-2.0 +import { defineBuildConfig } from "@open-pioneer/build-support"; + +export default defineBuildConfig({ + entryPoints: ["index"] +}); diff --git a/src/packages/base-theme/index.ts b/src/packages/base-theme/index.ts new file mode 100644 index 00000000..c0176685 --- /dev/null +++ b/src/packages/base-theme/index.ts @@ -0,0 +1,3 @@ +// SPDX-FileCopyrightText: con terra GmbH and contributors +// SPDX-License-Identifier: Apache-2.0 +export { theme } from "./theme"; diff --git a/src/packages/base-theme/package.json b/src/packages/base-theme/package.json new file mode 100644 index 00000000..0477d5b5 --- /dev/null +++ b/src/packages/base-theme/package.json @@ -0,0 +1,19 @@ +{ + "name": "@open-pioneer/base-theme", + "version": "0.0.1", + "main": "index.ts", + "license": "Apache-2.0", + "scripts": { + "build": "build-pioneer-package" + }, + "peerDependencies": { + "@open-pioneer/chakra-integration": "workspace:^" + }, + "devDependencies": { + "core-packages": "workspace:^" + }, + "publishConfig": { + "directory": "dist", + "linkDirectory": false + } +} diff --git a/src/packages/base-theme/theme.ts b/src/packages/base-theme/theme.ts new file mode 100644 index 00000000..f4bf5c4e --- /dev/null +++ b/src/packages/base-theme/theme.ts @@ -0,0 +1,23 @@ +// SPDX-FileCopyrightText: con terra GmbH and contributors +// SPDX-License-Identifier: Apache-2.0 +import { extendTheme } from "@open-pioneer/chakra-integration"; + +/** + * Base theme for open pioneer trails applications. + * + * All custom themes should extend this theme: + * + * ```ts + * import { extendTheme } from "@open-pioneer/chakra-integration"; + * import { theme as baseTheme } from "@open-pioneer/base-theme"; + * + * export const theme = extendTheme({ + * // Your overrides + * }, baseTheme); + * ``` + * + * NOTE: this API is still _experimental_. + * + * @experimental + */ +export const theme = extendTheme({}); diff --git a/src/packages/base-theme/typedoc.json b/src/packages/base-theme/typedoc.json new file mode 100644 index 00000000..9900c8bd --- /dev/null +++ b/src/packages/base-theme/typedoc.json @@ -0,0 +1,4 @@ +{ + "extends": ["core-packages/typedoc.base.json"], + "entryPoints": ["./index.tsx"] +} diff --git a/src/packages/chakra-integration/Provider.tsx b/src/packages/chakra-integration/Provider.tsx index 30c57b0f..e8b17965 100644 --- a/src/packages/chakra-integration/Provider.tsx +++ b/src/packages/chakra-integration/Provider.tsx @@ -4,18 +4,18 @@ import { CSSReset, DarkMode, EnvironmentProvider, - extendTheme, GlobalStyle, LightMode, - theme as baseTheme, ThemeProvider, ToastOptionProvider, ToastProvider, - ToastProviderProps + ToastProviderProps, + extendTheme, + theme as chakraBaseTheme } from "@chakra-ui/react"; import createCache, { EmotionCache } from "@emotion/cache"; import { CacheProvider, Global } from "@emotion/react"; -import { FC, PropsWithChildren, useEffect, useRef } from "react"; +import { FC, PropsWithChildren, RefObject, useEffect, useMemo, useRef } from "react"; import { PortalRootProvider } from "./PortalFix"; export type CustomChakraProviderProps = PropsWithChildren<{ @@ -31,6 +31,11 @@ export type CustomChakraProviderProps = PropsWithChildren<{ * Configures the color mode of the application. */ colorMode?: "light" | "dark"; + + /** + * Chakra theming object. + */ + theme?: Record; }>; // todo min-height vs height @@ -49,17 +54,6 @@ const defaultStyles = ` font-feature-settings: 'kern'; }`; -const theme = extendTheme({ - styles: { - global: { - // Apply the same styles to the application root node that chakra would usually apply to the html and body. - ".chakra-host": - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (baseTheme.styles.global as Record).body - } - } -}); - // https://github.dev/chakra-ui/chakra-ui/blob/80971001d7b77d02d5f037487a37237ded315480/packages/components/color-mode/src/color-mode.utils.ts#L3-L6 const colorModeClassnames = { light: "chakra-ui-light", @@ -70,7 +64,8 @@ const colorModeClassnames = { export const CustomChakraProvider: FC = ({ container, colorMode, - children + children, + theme: themeProp }) => { /* Chakra integration internals: @@ -108,13 +103,9 @@ export const CustomChakraProvider: FC = ({ */ - const cacheRef = useRef(); - if (!cacheRef.current) { - cacheRef.current = createCache({ - key: "css", - container: container - }); - } + const cache = useEmotionCache(container); + + const theme = useMemo(() => wrapTheme(themeProp), [themeProp]); const chakraHost = useRef(null); const toastOptions: ToastProviderProps = { @@ -123,27 +114,11 @@ export const CustomChakraProvider: FC = ({ } }; - const mode = colorMode ?? "light"; - useEffect(() => { - const host = chakraHost.current; - if (!host) { - return; - } - - // https://github.dev/chakra-ui/chakra-ui/blob/80971001d7b77d02d5f037487a37237ded315480/packages/components/color-mode/src/color-mode.utils.ts#L16-L25 - const className = colorModeClassnames[mode]; - host.classList.add(className); - host.dataset.theme = mode; - return () => { - host.classList.remove(className); - host.dataset.theme = undefined; - }; - }, [mode]); - const ColorMode = mode === "light" ? LightMode : DarkMode; + const ColorMode = useSyncedColorMode(chakraHost, colorMode); return (
- + @@ -163,3 +138,60 @@ export const CustomChakraProvider: FC = ({
); }; + +/** + * Computes the correct color mode and returns a component that will apply that mode. + * + * The current color mode is automatically propagates as a css class on the chakra host element. + */ +function useSyncedColorMode( + chakraHost: RefObject, + colorMode: "light" | "dark" | undefined +) { + const mode = colorMode ?? "light"; + useEffect(() => { + const host = chakraHost.current; + if (!host) { + return; + } + + // https://github.dev/chakra-ui/chakra-ui/blob/80971001d7b77d02d5f037487a37237ded315480/packages/components/color-mode/src/color-mode.utils.ts#L16-L25 + const className = colorModeClassnames[mode]; + host.classList.add(className); + host.dataset.theme = mode; + return () => { + host.classList.remove(className); + host.dataset.theme = undefined; + }; + }, [chakraHost, mode]); + const ColorMode = mode === "light" ? LightMode : DarkMode; + return ColorMode; +} + +function wrapTheme(theme: Record = chakraBaseTheme): Record { + return extendTheme( + { + styles: { + //add global css styles here + global: { + // Apply the same styles to the application root node that chakra would usually apply to the html and body. + ".chakra-host": + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (chakraBaseTheme.styles.global as Record).body + } + } + }, + theme + ); +} + +function useEmotionCache(container: Node): EmotionCache { + const cacheRef = useRef(); + if (!cacheRef.current) { + cacheRef.current = createCache({ + key: "css", + container: container + }); + } + return cacheRef.current; +} diff --git a/src/packages/runtime/CustomElement.ts b/src/packages/runtime/CustomElement.ts index 221f35c2..d15d1890 100644 --- a/src/packages/runtime/CustomElement.ts +++ b/src/packages/runtime/CustomElement.ts @@ -70,6 +70,11 @@ export interface CustomElementOptions { * Defaults to `false` in production mode and `true` during development to make testing easier. */ openShadowRoot?: boolean; + + /** + * Chakra theming object. + */ + theme?: Record; } /** @@ -315,6 +320,7 @@ class ElementState { this.reactIntegration = new ReactIntegration({ rootNode: container, container: shadowRoot, + theme: options.theme, serviceLayer, packages }); @@ -326,7 +332,7 @@ class ElementState { } private render() { - this.reactIntegration?.render(this.options.component ?? emptyComponent, {}); + this.reactIntegration?.render(this.options.component ?? emptyComponent); } private initStyles() { diff --git a/src/packages/runtime/package.json b/src/packages/runtime/package.json index c485a558..bfd4165b 100644 --- a/src/packages/runtime/package.json +++ b/src/packages/runtime/package.json @@ -8,6 +8,7 @@ }, "peerDependencies": { "@formatjs/intl": "^2.7.2", + "@open-pioneer/base-theme": "workspace:^", "@open-pioneer/chakra-integration": "workspace:^", "@open-pioneer/core": "workspace:^", "@open-pioneer/runtime-react-support": "workspace:^", diff --git a/src/packages/runtime/react-integration/ReactIntegration.test.ts b/src/packages/runtime/react-integration/ReactIntegration.test.ts index 46aca5b5..c98cd51f 100644 --- a/src/packages/runtime/react-integration/ReactIntegration.test.ts +++ b/src/packages/runtime/react-integration/ReactIntegration.test.ts @@ -6,17 +6,18 @@ import { createElement } from "react"; import { beforeEach, expect, it } from "vitest"; import { usePropertiesInternal, useServiceInternal, useServicesInternal } from "./hooks"; -import { findByText } from "@testing-library/dom"; +import { findByTestId, findByText } from "@testing-library/dom"; +import { act } from "@testing-library/react"; import { Service, ServiceConstructor } from "../Service"; // eslint-disable-next-line import/no-relative-packages import { UIWithProperties, UIWithService, UIWithServices } from "./test-data/test-package/UI"; import { ServiceLayer } from "../service-layer/ServiceLayer"; import { ReactIntegration } from "./ReactIntegration"; -import { act } from "react-dom/test-utils"; import { PackageRepr } from "../service-layer/PackageRepr"; import { createConstructorFactory, ServiceRepr } from "../service-layer/ServiceRepr"; import { InterfaceSpec, ReferenceSpec } from "../service-layer/InterfaceSpec"; import { createEmptyI18n, PackageIntl } from "../i18n"; +import { useTheme } from "@open-pioneer/chakra-integration"; interface TestProvider { value: string; @@ -46,7 +47,7 @@ it("should allow access to service via react hook", async () => { }); act(() => { - integration.render(TestComponent, {}); + integration.render(TestComponent); }); const node = await findByText(wrapper, "Hello TEST"); @@ -65,7 +66,7 @@ it("should get error when using undefined service", async () => { expect(() => { act(() => { - integration.render(TestComponent, {}); + integration.render(TestComponent); }); }).toThrowErrorMatchingSnapshot(); }); @@ -92,7 +93,7 @@ it("should allow access to service with qualifier via react hook", async () => { }); act(() => { - integration.render(TestComponent, {}); + integration.render(TestComponent); }); const node = await findByText(wrapper, "Hello TEST"); @@ -122,7 +123,7 @@ it("should deny access to service when the qualifier does not match", async () = expect(() => { act(() => { - integration.render(TestComponent, {}); + integration.render(TestComponent); }); }).toThrowErrorMatchingSnapshot(); }); @@ -165,7 +166,7 @@ it("should allow access to all services via react hook", async () => { }); act(() => { - integration.render(TestComponent, {}); + integration.render(TestComponent); }); const node = await findByText(wrapper, /^Joined Values:/); @@ -188,7 +189,7 @@ it("should deny access to all services if declaration is missing", async () => { expect(() => { act(() => { - integration.render(TestComponent, {}); + integration.render(TestComponent); }); }).toThrowErrorMatchingSnapshot(); }); @@ -206,7 +207,7 @@ it("should be able to read properties from react component", async () => { }); act(() => { - integration.render(TestComponent, {}); + integration.render(TestComponent); }); const node = await findByText(wrapper, "Hello USER"); @@ -230,7 +231,7 @@ it("should provide the autogenerated useService hook", async () => { }); act(() => { - integration.render(UIWithService, {}); + integration.render(UIWithService); }); const node = await findByText(wrapper, /^Test-UI:/); @@ -261,7 +262,7 @@ it("should provide the autogenerated useServices hook", async () => { }); act(() => { - integration.render(UIWithServices, {}); + integration.render(UIWithServices); }); const node = await findByText(wrapper, /^Test-UI:/); @@ -278,7 +279,7 @@ it("should provide the autogenerated useProperties hook", async () => { }); act(() => { - integration.render(UIWithProperties, {}); + integration.render(UIWithProperties); }); const node = await findByText(wrapper, /^Test-UI:/); @@ -297,11 +298,42 @@ it("should throw error when requesting properties from an unknown package", asyn expect(() => { act(() => { - integration.render(TestComponent, {}); + integration.render(TestComponent); }); }).toThrowErrorMatchingSnapshot(); }); +it("should apply the configured chakra theme", async () => { + const testTheme = { + colors: { + dummyColor: "#123456" + } + }; + const { integration, wrapper } = createIntegration({ + disablePackage: true, + theme: testTheme + }); + + function TestComponent() { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const theme = useTheme() as any; + return createElement( + "div", + { + "data-testid": "test-div" + }, + `Color: ${theme.colors.dummyColor}` + ); + } + + act(() => { + integration.render(TestComponent); + }); + + const node = await findByTestId(wrapper, "test-div"); + expect(node.textContent).toBe("Color: #123456"); +}); + interface ServiceSpec { name: string; interfaces: InterfaceSpec[]; @@ -320,6 +352,7 @@ function createIntegration(options?: { packageUiReferences?: ReferenceSpec[]; i18n?: PackageIntl; services?: ServiceSpec[]; + theme?: Record; }): TestIntegration { const wrapper = document.createElement("div"); const packages = new Map(); @@ -354,6 +387,7 @@ function createIntegration(options?: { const integration = new ReactIntegration({ container: wrapper, rootNode: wrapper, + theme: options?.theme, packages, serviceLayer }); diff --git a/src/packages/runtime/react-integration/ReactIntegration.tsx b/src/packages/runtime/react-integration/ReactIntegration.tsx index c56eba12..510dd62c 100644 --- a/src/packages/runtime/react-integration/ReactIntegration.tsx +++ b/src/packages/runtime/react-integration/ReactIntegration.tsx @@ -10,16 +10,19 @@ import { PackageRepr } from "../service-layer/PackageRepr"; import { InterfaceSpec, renderInterfaceSpec } from "../service-layer/InterfaceSpec"; import { renderAmbiguousServiceChoices } from "../service-layer/ServiceLookup"; import { CustomChakraProvider } from "@open-pioneer/chakra-integration"; +import { theme as defaultTrailsTheme } from "@open-pioneer/base-theme"; export interface ReactIntegrationOptions { packages: Map; serviceLayer: ServiceLayer; rootNode: HTMLDivElement; container: Node; + theme: Record | undefined; } export class ReactIntegration { private containerNode: Node; + private theme: Record | undefined; private packages: Map; private serviceLayer: ServiceLayer; private root: Root; @@ -27,6 +30,7 @@ export class ReactIntegration { constructor(options: ReactIntegrationOptions) { this.containerNode = options.container; + this.theme = options.theme; this.packages = options.packages; this.serviceLayer = options.serviceLayer; this.root = createRoot(options.rootNode); @@ -85,12 +89,16 @@ export class ReactIntegration { }; } - render(Component: ComponentType, props: Record) { + render(Component: ComponentType) { this.root.render( - + - + diff --git a/src/samples/chakra-sample/chakra-app/SampleUI.tsx b/src/samples/chakra-sample/chakra-app/SampleUI.tsx index 351f6b1c..c885393c 100644 --- a/src/samples/chakra-sample/chakra-app/SampleUI.tsx +++ b/src/samples/chakra-sample/chakra-app/SampleUI.tsx @@ -70,7 +70,7 @@ function LinkComponent() { return ( This is a{" "} - + link to Chakra's Design system @@ -79,13 +79,7 @@ function LinkComponent() { function ComponentStack() { return ( - } - spacing="24px" - align="stretch" - > + } spacing="24px" align="stretch"> @@ -110,7 +104,7 @@ function ComponentStack() { - + @@ -119,7 +113,7 @@ function ComponentStack() { function PortalExample() { return ( - + Portal Example: This is box and displayed here. Scroll/Look down to see the portal that is added at the end of document.body. The Portal is part of this Box. @@ -131,7 +125,7 @@ function PortalExample() { function TooltipExample() { return ( - + ); } @@ -140,7 +134,6 @@ function ToastExample() { const toast = useToast(); return ( + @@ -189,10 +180,10 @@ function AlertDialogExample() { - - @@ -208,9 +199,7 @@ function ModalExample() { return ( <> - + @@ -220,9 +209,7 @@ function ModalExample() { This is a modal text! - + @@ -236,7 +223,7 @@ function DrawerExample() { const btnRef = useRef(null); return ( <> - Cancel - + @@ -270,7 +257,7 @@ function PopoverExample() { <> - + @@ -282,9 +269,7 @@ function PopoverExample() { - + diff --git a/src/samples/chakra-sample/chakra-app/TableExample.tsx b/src/samples/chakra-sample/chakra-app/TableExample.tsx index 72c962ad..a1e102f2 100644 --- a/src/samples/chakra-sample/chakra-app/TableExample.tsx +++ b/src/samples/chakra-sample/chakra-app/TableExample.tsx @@ -14,7 +14,7 @@ import { export function TableExampleComponent() { return ( - +
This is the table cation diff --git a/src/samples/chakra-sample/chakra-app/app.ts b/src/samples/chakra-sample/chakra-app/app.ts index a1084374..70c537ee 100644 --- a/src/samples/chakra-sample/chakra-app/app.ts +++ b/src/samples/chakra-sample/chakra-app/app.ts @@ -2,10 +2,12 @@ // SPDX-License-Identifier: Apache-2.0 import { createCustomElement } from "@open-pioneer/runtime"; import * as appMetadata from "open-pioneer:app"; +import { theme } from "./theme/theme"; import { SampleUI } from "./SampleUI"; const Element = createCustomElement({ component: SampleUI, + theme, appMetadata }); diff --git a/src/samples/chakra-sample/chakra-app/package.json b/src/samples/chakra-sample/chakra-app/package.json index 1a5fa71b..05e321f5 100644 --- a/src/samples/chakra-sample/chakra-app/package.json +++ b/src/samples/chakra-sample/chakra-app/package.json @@ -3,6 +3,7 @@ "version": "0.0.6", "private": true, "dependencies": { + "@open-pioneer/base-theme": "workspace:^", "@open-pioneer/chakra-integration": "workspace:^", "@open-pioneer/runtime": "workspace:^" } diff --git a/src/samples/chakra-sample/chakra-app/theme/theme.ts b/src/samples/chakra-sample/chakra-app/theme/theme.ts new file mode 100644 index 00000000..f4fb9dc1 --- /dev/null +++ b/src/samples/chakra-sample/chakra-app/theme/theme.ts @@ -0,0 +1,76 @@ +// SPDX-FileCopyrightText: con terra GmbH and contributors +// SPDX-License-Identifier: Apache-2.0 +import { extendTheme } from "@open-pioneer/chakra-integration"; +import { theme as baseTheme } from "@open-pioneer/base-theme"; + +export const theme = extendTheme( + { + colors: { + primary: { + 50: "#defffd", + 100: "#b3fffa", + 200: "#86feee", + 300: "#5bfedd", + 400: "#3efec9", + 500: "#32e5a6", + 600: "#23b277", + 700: "#147f4c", + 800: "#004d23", + 900: "#001b0a" + } + }, + fonts: { + heading: "Helvetica" + }, + components: { + Button: { + defaultProps: { + colorScheme: "primary" + }, + variants: { + cancel: { + color: "font_inverse", + bg: "error", + _hover: { backgroundColor: "error_hover" } + } + } + }, + Link: { + baseStyle: { + color: "font_link" + } + }, + Divider: { + baseStyle: { + borderColor: "border" + } + } + }, + semanticTokens: { + colors: { + "background_primary": "primary.300", + "background_secondary": "primary.500", + "placeholder": "primary.100", + "font_primary": "black", + "font_secondary": "grey.500", + "font_inverse": "white", + "font_link": "yellow.300", + "border": "black", + "error": "red.500", + "error_hover": "red.600", + "success": "green.500", + "highlight": "yellow.300", + + // override internal chakra theming variables + "chakra-body-bg": "background_primary", + "chakra-subtle-bg": "background_secondary", + "chakra-body-text": "font_primary", + "chakra-subtle-text": "font_secondary", + "chakra-inverse-text": "font_inverse", + "chakra-border-color": "border", + "chakra-placeholder-color": "placeholder" + } + } + }, + baseTheme +); diff --git a/src/samples/chakra-sample/index.html b/src/samples/chakra-sample/index.html index 0dca8a16..db227dfa 100644 --- a/src/samples/chakra-sample/index.html +++ b/src/samples/chakra-sample/index.html @@ -3,7 +3,7 @@ - Date App Demo + Chakra Demo