diff --git a/packages/cyberstorm/src/components/NewTabs/Tabs.module.css b/packages/cyberstorm/src/components/NewTabs/Tabs.module.css new file mode 100644 index 000000000..a0a2814a3 --- /dev/null +++ b/packages/cyberstorm/src/components/NewTabs/Tabs.module.css @@ -0,0 +1,54 @@ +.root { + display: flex; + flex-direction: column; + gap: var(--space--32); +} + +.buttons { + display: flex; + overflow: auto; +} + +.button { + display: flex; + flex: none; + flex-direction: row; + gap: var(--space--12); + align-items: center; + justify-content: center; + + padding: var(--space--12) var(--space--16); + border-bottom: 3px solid var(--color-purple--5); + color: var(--tab-color); + background-color: transparent; + + --tab-color: var(--color-text--default); +} + +.button.active { + border-color: var(--tab-color); + + --tab-color: var(--color-green--5); +} + +.button:disabled { + --tab-color: var(--color-border--highlight); +} + +.icon { + color: var(--color-border--highlight); +} + +.button.active .icon { + color: var(--tab-color); +} + +.button:not(.active, :disabled):hover > .icon { + color: var(--color-text--tertiary); +} + +.label { + font-weight: var(--font-weight-bold); + font-size: var(--font-size--l); + line-height: 1.3; +} diff --git a/packages/cyberstorm/src/components/NewTabs/Tabs.tsx b/packages/cyberstorm/src/components/NewTabs/Tabs.tsx new file mode 100644 index 000000000..a23395695 --- /dev/null +++ b/packages/cyberstorm/src/components/NewTabs/Tabs.tsx @@ -0,0 +1,148 @@ +"use client"; +import { IconDefinition } from "@fortawesome/fontawesome-svg-core"; +import React, { PropsWithChildren, startTransition, useState } from "react"; + +import styles from "./Tabs.module.css"; +import { Icon } from "../Icon/Icon"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { classnames } from "../../utils/utils"; + +interface Props extends PropsWithChildren { + /** + * Name prop of the Tab that should be shown by default. + * If omitted, the first Tab acts as the default Tab. + */ + defaultActive?: string; +} + +/** + * Wrapper components for Tab components. + * + * Validates the passed Tab components and handles switching between + * them. + */ +const Tabs = (props: Props) => { + const { defaultActive } = props; + + // Sanity checking the child components. + const children = React.Children.toArray(props.children); + const seenNames: string[] = []; + + children.forEach((child) => { + if (!React.isValidElement(child) || child.type !== Tab) { + throw new Error("Tabs component only allows Tab child components"); + } + if (seenNames.includes(child.props.name)) { + throw new Error(`Non-unique Tab.name "${child.props.name}" in Tabs`); + } + seenNames.push(child.props.name); + }); + + if (defaultActive && !seenNames.includes(defaultActive)) { + throw new Error( + `Tabs.defaultActive "${defaultActive}" matches no child Tab.name` + ); + } + + const [activeTab, setActiveTab] = useState(defaultActive || seenNames[0]); + + // Throw away the ease-of-use Tab components and render internal + // components instead. + const buttons: JSX.Element[] = []; + const contents: JSX.Element[] = []; + + children.forEach((child) => { + if (React.isValidElement(child)) { + buttons.push( + setActiveTab(child.props.name)} + {...child.props} + /> + ); + contents.push( + + {child.props.children} + + ); + } + }); + + return ( +
+
{buttons}
+
{contents}
+
+ ); +}; + +Tabs.displayName = "Tabs"; + +interface TabProps extends PropsWithChildren { + /** Unique identifier for the Tab in the context of Tabs */ + name: string; + /** Text shown on the Tab activator */ + label: string; + /** + * Prevents user from selecting the Tab. + * + * Note that if default Tab is disabled, the contents will be shown + * initially, but user can't reselect the Tab once they leave it. + */ + disabled?: boolean; + /** Icon shown on the Tab activator */ + icon?: IconDefinition; +} + +const Tab = (props: TabProps) => props && null; + +Tab.displayName = "Tab"; + +export default Object.assign(Tabs, { Tab: Tab }); + +interface InternalTabButtonProps { + label: string; + active: boolean; + setActive: () => void; + disabled?: boolean; + icon?: IconDefinition; +} + +/** Only for internal use by Tabs and thus not exported. */ +const InternalTabButton = (props: InternalTabButtonProps) => { + const { active, disabled = false, icon, label, setActive } = props; + + return ( + + ); +}; + +InternalTabButton.displayName = "InternalTabButton"; + +interface InternalTabContentProps extends PropsWithChildren { + active: boolean; +} + +/** Only for internal use by Tabs and thus not exported. */ +const InternalTabContent = (props: InternalTabContentProps) => { + return props.active ? <>{props.children} : <>; +}; + +InternalTabContent.displayName = "InternalTabContent"; diff --git a/packages/cyberstorm/src/index.ts b/packages/cyberstorm/src/index.ts index b5e488ef9..7231055c0 100644 --- a/packages/cyberstorm/src/index.ts +++ b/packages/cyberstorm/src/index.ts @@ -53,6 +53,7 @@ export type { MenuItemProps } from "./components/MenuItem/"; export { MetaItem, type MetaItemProps } from "./components/MetaItem/MetaItem"; export { MetaInfoItem } from "./components/MetaInfoItem/MetaInfoItem"; export { MetaInfoItemList } from "./components/MetaInfoItemList/MetaInfoItemList"; +export { default as NewTabs } from "./components/NewTabs/Tabs"; export { Switch, type SwitchProps } from "./components/Switch/Switch"; export { PackageCard } from "./components/PackageCard/PackageCard"; export {