diff --git a/.changeset/silent-maps-attend.md b/.changeset/silent-maps-attend.md new file mode 100644 index 000000000..59abdab0e --- /dev/null +++ b/.changeset/silent-maps-attend.md @@ -0,0 +1,5 @@ +--- +"@cloudoperators/juno-ui-components": minor +--- + +Migrate AppShellProvider and CodeBlock to TypeScript diff --git a/packages/ui-components/src/components/AppShellProvider/AppShellProvider.component.tsx b/packages/ui-components/src/components/AppShellProvider/AppShellProvider.component.tsx new file mode 100644 index 000000000..d3e9588ca --- /dev/null +++ b/packages/ui-components/src/components/AppShellProvider/AppShellProvider.component.tsx @@ -0,0 +1,58 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from "react" + +import { StyleProvider } from "../StyleProvider" +import { ShadowRoot } from "../ShadowRoot" +import { PortalProvider } from "../PortalProvider" +import { DEFAULT_THEME_NAME } from "../StyleProvider/StyleProvider.component" + +const Wrapper = ({ children, shadowRoot, shadowRootMode }: WrapperProps) => { + return shadowRoot ? ( + + <>{children} + + ) : ( + children + ) +} + +/** + * This provider acts as a wrapper for Juno apps. It renders a StyleProvider and PortalProvider + */ +export const AppShellProvider = ({ + shadowRoot = true, + shadowRootMode = "open", + stylesWrapper = "inline", + theme = DEFAULT_THEME_NAME, + children, +}: AppShellProviderProps) => { + return ( + + + {children} + + + ) +} + +export type AppShellStyleWrapper = "head" | "inline" + +interface WrapperProps { + /** React nodes or a collection of React nodes to be rendered as content. */ + children?: React.ReactNode + /** Whether the app is rendered inside a ShadowRoot. Only choose false if the app is meant to run as a stand-alone application. */ + shadowRoot?: boolean + /** Shadow root mode */ + shadowRootMode?: ShadowRootMode +} + +export interface AppShellProviderProps extends WrapperProps { + /** Where app stylesheets are imported. This is only relevant if shadowRoot === false. If you use a ShadowRoot the styles must be inline. */ + stylesWrapper?: AppShellStyleWrapper + /** theme: theme-dark or theme-light */ + theme?: "theme-dark" | "theme-light" +} diff --git a/packages/ui-components/src/components/AppShellProvider/AppShellProvider.stories.js b/packages/ui-components/src/components/AppShellProvider/AppShellProvider.stories.tsx similarity index 89% rename from packages/ui-components/src/components/AppShellProvider/AppShellProvider.stories.js rename to packages/ui-components/src/components/AppShellProvider/AppShellProvider.stories.tsx index ac979fb77..4194deae7 100644 --- a/packages/ui-components/src/components/AppShellProvider/AppShellProvider.stories.js +++ b/packages/ui-components/src/components/AppShellProvider/AppShellProvider.stories.tsx @@ -4,9 +4,9 @@ */ import React from "react" -import { AppShellProvider } from "." -import { CodeBlock } from "../CodeBlock/index.js" -import { Message } from "../Message/Message.component" +import { Message } from "../Message" +import { AppShellProvider, AppShellProviderProps } from "./AppShellProvider.component" +import { CodeBlock } from "../CodeBlock" export default { title: "Layout/AppShellProvider", @@ -18,7 +18,7 @@ export default { }, } -const Template = (args) => {args.children} +const Template = (args: AppShellProviderProps) => {args.children} export const Default = { render: Template, diff --git a/packages/ui-components/src/components/AppShellProvider/AppShellProvider.test.tsx b/packages/ui-components/src/components/AppShellProvider/AppShellProvider.test.tsx new file mode 100644 index 000000000..3b6a7f4a3 --- /dev/null +++ b/packages/ui-components/src/components/AppShellProvider/AppShellProvider.test.tsx @@ -0,0 +1,20 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as React from "react" +import { render } from "@testing-library/react" +import { AppShellProvider } from "./AppShellProvider.component" + +describe("AppShellProvider", () => { + test("renders an AppShellProvider wrapper div with 'theme-dark' theme class by default", () => { + render() + expect(document.querySelector(".juno-app-body")).toHaveClass("theme-dark") + }) + + test("renders an AppShellProvider wrapper div with theme as passed", () => { + render() + expect(document.querySelector("div.juno-app-body")).toHaveClass("theme-light") + }) +}) diff --git a/packages/ui-components/src/components/AppShellProvider/index.js b/packages/ui-components/src/components/AppShellProvider/index.js index 3af34f51c..f102c1979 100644 --- a/packages/ui-components/src/components/AppShellProvider/index.js +++ b/packages/ui-components/src/components/AppShellProvider/index.js @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export { AppShellProvider } from "./AppShellProvider.component" +export { AppShellProvider } from "../../deprecated_js/AppShellProvider/AppShellProvider.component" diff --git a/packages/ui-components/src/components/CodeBlock/CodeBlock.component.js b/packages/ui-components/src/components/CodeBlock/CodeBlock.component.tsx similarity index 77% rename from packages/ui-components/src/components/CodeBlock/CodeBlock.component.js rename to packages/ui-components/src/components/CodeBlock/CodeBlock.component.tsx index cef3ef0ae..1561d3832 100644 --- a/packages/ui-components/src/components/CodeBlock/CodeBlock.component.js +++ b/packages/ui-components/src/components/CodeBlock/CodeBlock.component.tsx @@ -3,24 +3,23 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { useState, useRef } from "react" -import PropTypes from "prop-types" -import { JsonViewer } from "../../deprecated_js/JsonViewer/JsonViewer.component" -import { Icon } from "../../deprecated_js/Icon/index" +import React, { useState, useRef, useCallback } from "react" +import { JsonViewer } from "../JsonViewer" +import { Icon } from "../Icon" const wrapperStyles = ` jn-bg-theme-code-block jn-rounded ` -const preStyles = (wrap) => { +const preStyles = (wrap: boolean) => { return ` jn-p-6 ${wrap ? "jn-break-words jn-break-all jn-whitespace-pre-wrap" : "jn-overflow-x-auto"} ` } -const sizeStyles = (size) => { +const sizeStyles = (size: CodeBlockSize) => { switch (size) { case "small": return ` @@ -117,23 +116,33 @@ export const CodeBlock = ({ lang = "", className = "", ...props -}) => { +}: CodeBlockProps) => { const [isCopied, setIsCopied] = useState(false) - const timeoutRef = React.useRef(null) + const timeoutRef = React.useRef(null) React.useEffect(() => { - return () => clearTimeout(timeoutRef.current) // clear when component is unmounted + return () => { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current) + } + } // clear when component is unmounted }, []) - const theCode = useRef(null) + const theCode = useRef(null) - const handleCopyClick = () => { - const textToCopy = lang === "json" ? JSON.stringify(content || children) : theCode.current.textContent - navigator.clipboard.writeText(textToCopy) + const handleCopyClick = useCallback(() => { + const textToCopy = lang === "json" ? JSON.stringify(content || children) : theCode.current?.textContent + if (textToCopy) { + navigator.clipboard.writeText(textToCopy).catch(() => { + console.warn("Cannot copy text to clipboard") + }) + } setIsCopied(true) - clearTimeout(timeoutRef.current) // clear any possibly existing Refs - timeoutRef.current = setTimeout(() => setIsCopied(false), 1000) - } + if (timeoutRef.current) { + clearTimeout(timeoutRef.current) // clear any possibly existing Refs + } + timeoutRef.current = window.setTimeout(() => setIsCopied(false), 1000) + }, [content, children, lang]) return (
- {content || children} + {(content || children) as React.ReactNode} )} @@ -170,21 +179,23 @@ export const CodeBlock = ({ ) } -CodeBlock.propTypes = { +type CodeBlockSize = "auto" | "small" | "medium" | "large" + +export interface CodeBlockProps { /** The content to render. Will override children if passed. */ - content: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), + content?: string | object /** The children to render. Will be overridden by content prop if passed as well. */ - children: PropTypes.oneOfType([PropTypes.node, PropTypes.object]), + children?: React.ReactNode | object /** Pass at title to render. Will look like a single tab. */ - heading: PropTypes.string, + heading?: string /** Set whether the code should wrap or not. Default is true. */ - wrap: PropTypes.bool, + wrap?: boolean /** Set the size of the CodeBlock. Default is "auto" */ - size: PropTypes.oneOf(["auto", "small", "medium", "large"]), + size?: CodeBlockSize /** Render a button to copy the code to the clipboard. Defaults to true */ - copy: PropTypes.bool, + copy?: boolean /** Pass a lang prop. Passing "json" will render a fully-featured JsonView. Will also add a data-lang-attribute to the codeblock */ - lang: PropTypes.string, + lang?: string /** Add a custom className to the wrapper of the CodeBlock */ - className: PropTypes.string, + className?: string } diff --git a/packages/ui-components/src/components/CodeBlock/CodeBlock.stories.js b/packages/ui-components/src/components/CodeBlock/CodeBlock.stories.tsx similarity index 91% rename from packages/ui-components/src/components/CodeBlock/CodeBlock.stories.js rename to packages/ui-components/src/components/CodeBlock/CodeBlock.stories.tsx index 5df9b1520..b684fc7e1 100644 --- a/packages/ui-components/src/components/CodeBlock/CodeBlock.stories.js +++ b/packages/ui-components/src/components/CodeBlock/CodeBlock.stories.tsx @@ -4,12 +4,11 @@ */ import React from "react" -import PropTypes from "prop-types" -import { CodeBlock } from "./index.js" -import { Tabs } from "../../deprecated_js/Tabs/index.js" -import { TabList } from "../../deprecated_js/TabList/index.js" -import { Tab } from "../../deprecated_js/Tab/index.js" -import { TabPanel } from "../../deprecated_js/TabPanel/index.js" +import { CodeBlock } from "." +import { Tabs } from "../Tabs" +import { TabList } from "../TabList" +import { Tab } from "../Tab" +import { TabPanel } from "../TabPanel" const TabStory = { args: { @@ -32,7 +31,7 @@ export default { }, } -const TabsTemplate = ({ tabs, codeBlocks }) => ( +const TabsTemplate = ({ tabs, codeBlocks }: TabsTemplateProps) => ( {tabs.map((tab, t) => ( @@ -47,9 +46,9 @@ const TabsTemplate = ({ tabs, codeBlocks }) => ( ) -TabsTemplate.propTypes = { - tabs: PropTypes.array, - codeBlocks: PropTypes.array, +interface TabsTemplateProps { + tabs: (typeof Tab)[] + codeBlocks: (typeof CodeBlock)[] } export const Default = { diff --git a/packages/ui-components/src/components/CodeBlock/CodeBlock.test.js b/packages/ui-components/src/components/CodeBlock/CodeBlock.test.tsx similarity index 85% rename from packages/ui-components/src/components/CodeBlock/CodeBlock.test.js rename to packages/ui-components/src/components/CodeBlock/CodeBlock.test.tsx index 752b09782..05c65fee8 100644 --- a/packages/ui-components/src/components/CodeBlock/CodeBlock.test.js +++ b/packages/ui-components/src/components/CodeBlock/CodeBlock.test.tsx @@ -5,31 +5,31 @@ import * as React from "react" import { render, screen } from "@testing-library/react" -import { CodeBlock } from "./index" +import { CodeBlock } from "." describe("CodeBlock", () => { - test("renders a CodeBlock with content as passed", async () => { + test("renders a CodeBlock with content as passed", () => { render() expect(screen.getByTestId("codeblock")).toBeInTheDocument() expect(screen.getByTestId("codeblock")).toHaveClass("juno-code-block") expect(screen.getByTestId("codeblock")).toHaveTextContent("some example code") }) - test("renders a CodeBlock with children as passed", async () => { + test("renders a CodeBlock with children as passed", () => { render({"some children here"}) expect(screen.getByTestId("codeblock")).toBeInTheDocument() expect(screen.getByTestId("codeblock")).toHaveClass("juno-code-block") expect(screen.getByTestId("codeblock")).toHaveTextContent("some children here") }) - test("renders a CodeBlock with a lang attribute as passed", async () => { + test("renders a CodeBlock with a lang attribute as passed", () => { render() expect(screen.getByTestId("codeblock")).toBeInTheDocument() expect(screen.getByTestId("codeblock")).toHaveClass("juno-code-block") expect(screen.getByTestId("codeblock")).toHaveAttribute("data-lang", "javascript") }) - test("renders a wrapping CodeBlock by default", async () => { + test("renders a wrapping CodeBlock by default", () => { render() expect(screen.getByTestId("codeblock")).toBeInTheDocument() expect(screen.getByTestId("codeblock")).toHaveClass("juno-code-block") @@ -39,7 +39,7 @@ describe("CodeBlock", () => { expect(document.querySelector("pre")).not.toHaveClass("jn-overflow-x-auto") }) - test("renders a non-wrapping CodeBlock as passed", async () => { + test("renders a non-wrapping CodeBlock as passed", () => { render() expect(screen.getByTestId("codeblock")).toBeInTheDocument() expect(screen.getByTestId("codeblock")).toHaveClass("juno-code-block") @@ -49,7 +49,7 @@ describe("CodeBlock", () => { expect(document.querySelector("pre")).toHaveClass("jn-overflow-x-auto") }) - test("renders a CodeBlock without height restrictions by default", async () => { + test("renders a CodeBlock without height restrictions by default", () => { render() expect(document.querySelector("pre")).toBeInTheDocument() expect(document.querySelector("pre")).not.toHaveClass("juno-codeblock-pre-small") @@ -57,7 +57,7 @@ describe("CodeBlock", () => { expect(document.querySelector("pre")).not.toHaveClass("juno-codeblock-pre-large") }) - test("renders a small sized CodeBlock as passed", async () => { + test("renders a small sized CodeBlock as passed", () => { render() expect(document.querySelector("pre")).toBeInTheDocument() expect(document.querySelector("pre")).toHaveClass("juno-codeblock-pre-small") @@ -65,7 +65,7 @@ describe("CodeBlock", () => { expect(document.querySelector("pre")).not.toHaveClass("juno-codeblock-pre-large") }) - test("renders a medium sized CodeBlock as passed", async () => { + test("renders a medium sized CodeBlock as passed", () => { render() expect(document.querySelector("pre")).toBeInTheDocument() expect(document.querySelector("pre")).not.toHaveClass("juno-codeblock-pre-small") @@ -73,14 +73,14 @@ describe("CodeBlock", () => { expect(document.querySelector("pre")).not.toHaveClass("juno-codeblock-pre-large") }) - test("renders a heading as passed", async () => { + test("renders a heading as passed", () => { render() expect(screen.getByTestId("codeblock")).toBeInTheDocument() expect(document.querySelector(".juno-codeblock-heading")).toBeInTheDocument() expect(document.querySelector(".juno-codeblock-heading")).toHaveTextContent("Look, a CodeBlock!") }) - test("renders a JSONView as passed", async () => { + test("renders a JSONView as passed", () => { const testJson = { someKey: "some value", someOtherKey: 12, @@ -92,7 +92,7 @@ describe("CodeBlock", () => { expect(document.querySelector("[data-json-viewer]")).toBeInTheDocument() }) - test("renders a JSONView as passed with children", async () => { + test("renders a JSONView as passed with children", () => { const testObj = { someKey: "some value", someOtherKey: 12, @@ -108,18 +108,18 @@ describe("CodeBlock", () => { expect(document.querySelector("[data-json-viewer]")).toBeInTheDocument() }) - test("renders a CodeBlock with a Copy button by default", async () => { + test("renders a CodeBlock with a Copy button by default", () => { render() expect(screen.getByRole("button", { name: "contentCopy" })).toBeInTheDocument() }) - test("renders a CodeBlock with className as passed", async () => { + test("renders a CodeBlock with className as passed", () => { render() expect(screen.getByTestId("codeblock")).toBeInTheDocument() expect(screen.getByTestId("codeblock")).toHaveClass("my-class") }) - test("renders a CodeBlock with all props as passed", async () => { + test("renders a CodeBlock with all props as passed", () => { render() expect(screen.getByTestId("codeblock")).toBeInTheDocument() expect(screen.getByTestId("codeblock")).toHaveAttribute("data-lolol", "code-lang-js") diff --git a/packages/ui-components/src/components/CodeBlock/index.js b/packages/ui-components/src/components/CodeBlock/index.ts similarity index 71% rename from packages/ui-components/src/components/CodeBlock/index.js rename to packages/ui-components/src/components/CodeBlock/index.ts index 70d649657..c3b9ee86c 100644 --- a/packages/ui-components/src/components/CodeBlock/index.js +++ b/packages/ui-components/src/components/CodeBlock/index.ts @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export { CodeBlock } from "./CodeBlock.component.js" +export { CodeBlock } from "./CodeBlock.component" diff --git a/packages/ui-components/src/components/ComboBox/ComboBox.test.js b/packages/ui-components/src/components/ComboBox/ComboBox.test.js index d165de214..26f85447e 100644 --- a/packages/ui-components/src/components/ComboBox/ComboBox.test.js +++ b/packages/ui-components/src/components/ComboBox/ComboBox.test.js @@ -7,7 +7,7 @@ import * as React from "react" import { cleanup, render, screen, waitFor } from "@testing-library/react" import userEvent from "@testing-library/user-event" import { ComboBox } from "./index" -import { AppShellProvider } from "../AppShellProvider/index" +import { AppShellProvider } from "../../deprecated_js/AppShellProvider/index" import { ComboBoxOption } from "../ComboBoxOption/index" const mockOnBlur = jest.fn() diff --git a/packages/ui-components/src/components/ComboBoxOption/ComboBoxOption.test.js b/packages/ui-components/src/components/ComboBoxOption/ComboBoxOption.test.js index 8aac13416..06e2cfc65 100644 --- a/packages/ui-components/src/components/ComboBoxOption/ComboBoxOption.test.js +++ b/packages/ui-components/src/components/ComboBoxOption/ComboBoxOption.test.js @@ -5,7 +5,7 @@ import * as React from "react" import { cleanup, render, screen, waitFor } from "@testing-library/react" -import { AppShellProvider } from "../AppShellProvider" +import { AppShellProvider } from "../../deprecated_js/AppShellProvider" import userEvent from "@testing-library/user-event" import { ComboBox } from "../ComboBox/ComboBox.component" import { ComboBoxOption } from "../ComboBoxOption/ComboBoxOption.component" diff --git a/packages/ui-components/src/components/DateTimePicker/DateTimePicker.test.js b/packages/ui-components/src/components/DateTimePicker/DateTimePicker.test.js index 0c5f91e98..c0bcef8da 100644 --- a/packages/ui-components/src/components/DateTimePicker/DateTimePicker.test.js +++ b/packages/ui-components/src/components/DateTimePicker/DateTimePicker.test.js @@ -7,7 +7,7 @@ import React from "react" import { cleanup, render, screen, waitFor } from "@testing-library/react" import userEvent from "@testing-library/user-event" import { DateTimePicker } from "./index" -import { AppShellProvider } from "../AppShellProvider" +import { AppShellProvider } from "../../deprecated_js/AppShellProvider" const mockOnOpen = jest.fn() const mockOnClear = jest.fn() diff --git a/packages/ui-components/src/components/JsonViewer/JsonViewer.component.tsx b/packages/ui-components/src/components/JsonViewer/JsonViewer.component.tsx index 921a5a91c..055fbfa23 100644 --- a/packages/ui-components/src/components/JsonViewer/JsonViewer.component.tsx +++ b/packages/ui-components/src/components/JsonViewer/JsonViewer.component.tsx @@ -410,7 +410,7 @@ type ThemeType = "dark" | "light" export interface JsonViewerProps extends Omit, "data"> { /** Pass a valid json. Required. */ // data: PropTypes.object.isRequired, - data: object | object[] + data: string | object | object[] /** pass a styles object */ style?: object /** show toolbar */ diff --git a/packages/ui-components/src/components/StyleProvider/StyleProvider.component.tsx b/packages/ui-components/src/components/StyleProvider/StyleProvider.component.tsx index 61b146393..66db9c1d2 100644 --- a/packages/ui-components/src/components/StyleProvider/StyleProvider.component.tsx +++ b/packages/ui-components/src/components/StyleProvider/StyleProvider.component.tsx @@ -29,7 +29,7 @@ export interface StyleContextProps { const StylesContext = createContext(undefined) const APP_BODY_CSS_CLASS_NAME = "juno-app-body" -const DEFAULT_THEME_NAME = "theme-dark" +export const DEFAULT_THEME_NAME = "theme-dark" /** * Component that inserts the ui styles and manages theming and styles. @@ -149,7 +149,7 @@ export interface StyleProviderProps { /** What element to render as a wrapper, respectively where to render the StyleProvider. */ stylesWrapper?: StyleProviderStylesWrapper /** The name of the theme to render. */ - theme?: string + theme?: string | null /** The mode of the shadowRoot. Only relevant when `stylesWrapper="shadowRoot"`. */ shadowRootMode?: ShadowRootMode } diff --git a/packages/ui-components/src/components/AppShellProvider/AppShellProvider.component.js b/packages/ui-components/src/deprecated_js/AppShellProvider/AppShellProvider.component.js similarity index 94% rename from packages/ui-components/src/components/AppShellProvider/AppShellProvider.component.js rename to packages/ui-components/src/deprecated_js/AppShellProvider/AppShellProvider.component.js index f86e0e05f..f275fbaae 100644 --- a/packages/ui-components/src/components/AppShellProvider/AppShellProvider.component.js +++ b/packages/ui-components/src/deprecated_js/AppShellProvider/AppShellProvider.component.js @@ -6,7 +6,7 @@ import React from "react" import PropTypes from "prop-types" -import { StyleProvider } from "../../deprecated_js/StyleProvider/StyleProvider.component" +import { StyleProvider } from "../StyleProvider/StyleProvider.component" import { ShadowRoot } from "../../deprecated_js/ShadowRoot/ShadowRoot.component" import { PortalProvider } from "../../deprecated_js/PortalProvider/PortalProvider.component" diff --git a/packages/ui-components/src/components/AppShellProvider/AppShellProvider.test.js b/packages/ui-components/src/deprecated_js/AppShellProvider/AppShellProvider.test.js similarity index 100% rename from packages/ui-components/src/components/AppShellProvider/AppShellProvider.test.js rename to packages/ui-components/src/deprecated_js/AppShellProvider/AppShellProvider.test.js diff --git a/packages/ui-components/src/deprecated_js/AppShellProvider/index.js b/packages/ui-components/src/deprecated_js/AppShellProvider/index.js new file mode 100644 index 000000000..3af34f51c --- /dev/null +++ b/packages/ui-components/src/deprecated_js/AppShellProvider/index.js @@ -0,0 +1,6 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export { AppShellProvider } from "./AppShellProvider.component" diff --git a/packages/ui-components/src/index.js b/packages/ui-components/src/index.js index f83a13d6f..4b18cf042 100644 --- a/packages/ui-components/src/index.js +++ b/packages/ui-components/src/index.js @@ -7,7 +7,7 @@ import "./global.scss" export { AppBody } from "./components/AppBody/index.js" export { AppIntro } from "./components/AppIntro/index.js" export { AppShell } from "./components/AppShell/index.js" -export { AppShellProvider } from "./components/AppShellProvider/index.js" +export { AppShellProvider } from "./deprecated_js/AppShellProvider/index" export { Badge } from "./components/Badge" export { Box } from "./components/Box" export { Breadcrumb } from "./components/Breadcrumb" @@ -18,7 +18,7 @@ export { Checkbox } from "./components/Checkbox/index" export { CheckboxRow } from "./components/CheckboxRow/index.js" export { CheckboxGroup } from "./components/CheckboxGroup/index.js" export { Code } from "./components/Code/index.js" -export { CodeBlock } from "./components/CodeBlock/index.js" +export { CodeBlock } from "./components/CodeBlock/index" export { ComboBox } from "./components/ComboBox/index.js" export { ComboBoxOption } from "./components/ComboBoxOption/index.js" export { ContentArea } from "./components/ContentArea/index.js"