From cf95617dc5c4242d100a4b3182338ab7c4f4421d Mon Sep 17 00:00:00 2001 From: Jimmy Svensson Date: Mon, 12 Feb 2024 13:27:06 +0100 Subject: [PATCH] chore: overhaul example page(#194) --- .vscode/snippets.code-snippets | 51 ++ examples/src/App.tsx | 6 +- examples/src/components/code-block.tsx | 25 - .../navigation-menu/navigation-footer.tsx | 81 +++ .../navigation-menu/navigation-menu-list.tsx | 52 ++ .../navigation-menu/navigation-menu.item.tsx | 56 ++ .../navigation-menu/navigation-menu.tsx | 134 ++++ examples/src/components/page-header.tsx | 66 -- examples/src/components/section-title.tsx | 54 -- .../story/story-code-block-accordion.tsx | 35 + .../src/components/story/story-code-block.tsx | 48 ++ .../story-navigation-menu-item.tsx | 64 ++ .../story-page-navigation.tsx | 47 ++ .../components/story/story-page-header.tsx | 42 ++ examples/src/components/story/story-page.tsx | 146 ++++ .../components/story/story-section-header.tsx | 41 ++ .../src/components/story/story-section.tsx | 60 ++ examples/src/components/story/story.utils.tsx | 39 + examples/src/constants/constants.ts | 3 + examples/src/landingpage/index.tsx | 143 +++- examples/src/landingpage/welcome-card.tsx | 57 ++ .../welcome-image/welcome-image.component.tsx | 18 +- .../welcome-message.component.tsx | 36 - .../welcome-message/welcome-message.styles.ts | 19 - examples/src/layout.tsx | 51 +- examples/src/main-page.tsx | 69 +- examples/src/routing/route-map.tsx | 137 ++-- examples/src/routing/routes.ts | 1 - examples/src/routing/use-scroll-to-anchor.tsx | 26 + examples/src/stories/icon-page.tsx | 25 +- examples/src/stories/password-input-page.tsx | 30 - .../password-input/password-input-example.tsx | 17 + .../password-input/password-input-page.tsx | 56 ++ examples/src/stories/slider-page.tsx | 324 --------- .../slider/examples/custom-example.tsx | 171 +++++ .../slider/examples/disabled-example.tsx | 29 + .../range-slider-with-steps-example.tsx | 31 + .../slider/examples/regular-example.tsx | 17 + .../stories/slider/examples/small-example.tsx | 17 + .../examples/stepping-to-marks-example.tsx | 39 + .../examples/transform-value-example.tsx | 29 + .../with-external-buttons-example.tsx | 93 +++ .../slider/examples/with-marks-example.tsx | 37 + .../slider/examples/with-range-example.tsx | 41 ++ .../slider/examples/with-steps-example.tsx | 31 + examples/src/stories/slider/slider-page.tsx | 158 +++++ examples/src/stories/stepper-page.tsx | 67 -- .../examples/stepper-dialog-example.tsx | 82 +++ .../vertical-stepper-dialog-example.tsx | 82 +++ examples/src/stories/stepper/stepper-page.tsx | 50 ++ .../tab-list-utilities/tab-list-example.tsx | 145 ++++ .../tab-list-utilities/tab-list-styled.tsx | 94 --- .../tab-list-utilities-page.tsx | 135 +--- .../stories/table-utilities/table-example.tsx | 671 ++++++++++++++++++ .../table-utilities/table-utlities-page.tsx | 37 + examples/src/stories/table-utlities-page.tsx | 358 ---------- examples/src/stories/theme-page.tsx | 40 +- .../src/stories/vertical-stepper-page.tsx | 48 -- 58 files changed, 3124 insertions(+), 1437 deletions(-) create mode 100644 .vscode/snippets.code-snippets delete mode 100644 examples/src/components/code-block.tsx create mode 100644 examples/src/components/navigation-menu/navigation-footer.tsx create mode 100644 examples/src/components/navigation-menu/navigation-menu-list.tsx create mode 100644 examples/src/components/navigation-menu/navigation-menu.item.tsx create mode 100644 examples/src/components/navigation-menu/navigation-menu.tsx delete mode 100644 examples/src/components/page-header.tsx delete mode 100644 examples/src/components/section-title.tsx create mode 100644 examples/src/components/story/story-code-block-accordion.tsx create mode 100644 examples/src/components/story/story-code-block.tsx create mode 100644 examples/src/components/story/story-navigation/story-navigation-menu-item.tsx create mode 100644 examples/src/components/story/story-navigation/story-page-navigation.tsx create mode 100644 examples/src/components/story/story-page-header.tsx create mode 100644 examples/src/components/story/story-page.tsx create mode 100644 examples/src/components/story/story-section-header.tsx create mode 100644 examples/src/components/story/story-section.tsx create mode 100644 examples/src/components/story/story.utils.tsx create mode 100644 examples/src/constants/constants.ts create mode 100644 examples/src/landingpage/welcome-card.tsx delete mode 100644 examples/src/landingpage/welcome-message/welcome-message.component.tsx delete mode 100644 examples/src/landingpage/welcome-message/welcome-message.styles.ts create mode 100644 examples/src/routing/use-scroll-to-anchor.tsx delete mode 100644 examples/src/stories/password-input-page.tsx create mode 100644 examples/src/stories/password-input/password-input-example.tsx create mode 100644 examples/src/stories/password-input/password-input-page.tsx delete mode 100644 examples/src/stories/slider-page.tsx create mode 100644 examples/src/stories/slider/examples/custom-example.tsx create mode 100644 examples/src/stories/slider/examples/disabled-example.tsx create mode 100644 examples/src/stories/slider/examples/range-slider-with-steps-example.tsx create mode 100644 examples/src/stories/slider/examples/regular-example.tsx create mode 100644 examples/src/stories/slider/examples/small-example.tsx create mode 100644 examples/src/stories/slider/examples/stepping-to-marks-example.tsx create mode 100644 examples/src/stories/slider/examples/transform-value-example.tsx create mode 100644 examples/src/stories/slider/examples/with-external-buttons-example.tsx create mode 100644 examples/src/stories/slider/examples/with-marks-example.tsx create mode 100644 examples/src/stories/slider/examples/with-range-example.tsx create mode 100644 examples/src/stories/slider/examples/with-steps-example.tsx create mode 100644 examples/src/stories/slider/slider-page.tsx delete mode 100644 examples/src/stories/stepper-page.tsx create mode 100644 examples/src/stories/stepper/examples/stepper-dialog-example.tsx create mode 100644 examples/src/stories/stepper/examples/vertical-stepper-dialog-example.tsx create mode 100644 examples/src/stories/stepper/stepper-page.tsx create mode 100644 examples/src/stories/tab-list-utilities/tab-list-example.tsx delete mode 100644 examples/src/stories/tab-list-utilities/tab-list-styled.tsx create mode 100644 examples/src/stories/table-utilities/table-example.tsx create mode 100644 examples/src/stories/table-utilities/table-utlities-page.tsx delete mode 100644 examples/src/stories/table-utlities-page.tsx delete mode 100644 examples/src/stories/vertical-stepper-page.tsx diff --git a/.vscode/snippets.code-snippets b/.vscode/snippets.code-snippets new file mode 100644 index 00000000..38bdc043 --- /dev/null +++ b/.vscode/snippets.code-snippets @@ -0,0 +1,51 @@ +{ + // General snippets + "Print to console": { + "scope": "javascript,typescript,typescriptreact", + "prefix": "log", + "body": ["console.log('$1', $1);"], + "description": "Log output to console" + }, + + "React component fluent": { + "prefix": "component:fluent", + "scope": "typescriptreact", + "body": [ + "import { makeStyles, mergeClasses } from '@fluentui/react-components'", + "import React from 'react'", + "", + "const componentId = '${1/([A-Z][a-z]*)([A-Z][a-z]*)?([A-Z][a-z]*)?([A-Z][a-z]*)?/${1:/downcase}${2:+-}${2:/downcase}${3:+-}${3:/downcase}${4:+-}${4:/downcase}/}'", + "export const ${1/^(.)/${1:/downcase}/}ClassNames = {", + " root: componentId", + "}", + "", + "const useStyles = makeStyles({", + " root: {}", + "})", + "", + "type TUse${1:}Styles = {", + " test: string", + "}", + "", + "export function use${1:}Styles({test}:TUse${1:}Styles ){", + " const styles = useStyles();", + " const rootStyle = mergeClasses(${1/^(.)/${1:/downcase}/}ClassNames.root, styles.root);", + " return {styles, rootStyle}", + "}", + "", + "type T${1:} = {", + "}", + "", + "export function ${1:}({ ...rest }:T${1:}){", + " const {styles, rootStyle} = use${1:}Styles({test:''})", + "", + " return (", + "
", + " Hello, this is ${1:} Component!", + "
", + " )", + "}" + ], + "description": "React component fluent" + } +} diff --git a/examples/src/App.tsx b/examples/src/App.tsx index 01659771..9521b850 100644 --- a/examples/src/App.tsx +++ b/examples/src/App.tsx @@ -3,11 +3,11 @@ import React, { useMemo } from "react"; import { HashRouter, Route, Routes } from "react-router-dom"; import { useAppContext } from "./context/ApplicationStateProvider"; import { MainPage } from "./main-page"; -import { PageNotFound } from "./routing/page-not-found"; -import { getRouteByGroup, RouteGroup } from "./routing/route-map"; -import { routes } from "./routing/routes"; import { useScrollStaticStyles } from "@axiscommunications/fluent-styles"; import { WelcomePage } from "./landingpage"; +import { getRouteByGroup, RouteGroup } from "./routing/route-map"; +import { routes } from "./routing/routes"; +import { PageNotFound } from "./routing/page-not-found"; export const App = () => { useScrollStaticStyles(); diff --git a/examples/src/components/code-block.tsx b/examples/src/components/code-block.tsx deleted file mode 100644 index 6c25dbf6..00000000 --- a/examples/src/components/code-block.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import SyntaxHighlighter from "react-syntax-highlighter"; -import React from "react"; -import { - Accordion, - AccordionHeader, - AccordionItem, - AccordionPanel, -} from "@fluentui/react-components"; - -export const CodeBlock = ( - { codeString, title }: { codeString: string; title: string } -) => { - return ( - - - {title} - - - {codeString} - - - - - ); -}; diff --git a/examples/src/components/navigation-menu/navigation-footer.tsx b/examples/src/components/navigation-menu/navigation-footer.tsx new file mode 100644 index 00000000..62d9bb53 --- /dev/null +++ b/examples/src/components/navigation-menu/navigation-footer.tsx @@ -0,0 +1,81 @@ +import { + Caption1, + Card, + CardHeader, + Link, + makeStyles, + mergeClasses, + shorthands, + Text, + tokens, +} from "@fluentui/react-components"; +import React from "react"; +import { GitHubUrls } from "../../constants/constants"; + +const componentId = "navigation-footer"; +export const navigationFooterClassNames = { + root: componentId, +}; + +const useStyles = makeStyles({ + root: { + width: "100%", + backgroundImage: + `linear-gradient(90deg,${tokens.colorNeutralBackground1}0%,${tokens.colorNeutralBackground3}95%)`, + ":hover #gitIcon": { + transform: "scale(1.5) rotate(15deg)", + ...shorthands.transition("all", "250ms"), + }, + }, + title: { + ...shorthands.margin(0, 0, "12px"), + }, + description: { + ...shorthands.margin(0, 0, "12px"), + }, + caption: { + color: tokens.colorNeutralForeground3, + }, +}); + +export function useNavigationFooterStyles() { + const styles = useStyles(); + const rootStyle = mergeClasses(navigationFooterClassNames.root, styles.root); + return { styles, rootStyle }; +} + +export function NavigationFooter() { + const { styles, rootStyle } = useNavigationFooterStyles(); + + return ( + + + + + } + header={fluent-components} + description={ + @axiscommunications + } + /> + + ); +} diff --git a/examples/src/components/navigation-menu/navigation-menu-list.tsx b/examples/src/components/navigation-menu/navigation-menu-list.tsx new file mode 100644 index 00000000..567ad200 --- /dev/null +++ b/examples/src/components/navigation-menu/navigation-menu-list.tsx @@ -0,0 +1,52 @@ +import { MenuList } from "@fluentui/react-components"; +import React, { useCallback, useMemo } from "react"; +import { useLocation, useNavigate } from "react-router-dom"; + +import { NavigationMenuItem } from "./navigation-menu.item"; +import { getRouteByCategory, RouteCategory } from "../../routing/route-map"; +import { TRoute } from "../../routing/routes"; + +const componentId = "navigation-menu-list"; +export const navigationMenuListClassNames = { + root: componentId, +}; + +type TNavigationMenuList = { + category: RouteCategory; +}; + +export function NavigationMenuList({ category, ...rest }: TNavigationMenuList) { + const routes = getRouteByCategory(category); + const navigate = useNavigate(); + const { pathname } = useLocation(); + + const goTo = useCallback( + (route: TRoute) => { + navigate(route); + }, + [navigate] + ); + + const renderMenuItems = useMemo( + () => + Array.from(routes.entries()).map((entry) => { + const [key, [route, routeData]] = entry; + return ( + goTo(route)} + selected={route === pathname} + > + {routeData.label} + + ); + }), + [routes] + ); + + return ( + + {renderMenuItems} + + ); +} diff --git a/examples/src/components/navigation-menu/navigation-menu.item.tsx b/examples/src/components/navigation-menu/navigation-menu.item.tsx new file mode 100644 index 00000000..9c8666c6 --- /dev/null +++ b/examples/src/components/navigation-menu/navigation-menu.item.tsx @@ -0,0 +1,56 @@ +import { + makeStyles, + MenuItem, + MenuItemProps, + mergeClasses, + tokens, +} from "@fluentui/react-components"; +import React from "react"; + +const componentId = "navigation-menu-item"; +export const navigationMenuItemClassNames = { + root: componentId, +}; + +const useStyles = makeStyles({ + root: { + backgroundImage: + `linear-gradient(90deg,${tokens.colorNeutralBackground1}0%,${tokens.colorNeutralBackground3}95%)`, + }, + selected: { + backgroundImage: + `linear-gradient(90deg,${tokens.colorNeutralBackground1}100%,${tokens.colorNeutralBackground3}0%)`, + }, +}); + +type TUseNavigationMenuItemStyles = { + selected: boolean; +}; + +export function useNavigationMenuItemStyles( + { selected }: TUseNavigationMenuItemStyles +) { + const styles = useStyles(); + const rootStyle = mergeClasses( + navigationMenuItemClassNames.root, + styles.root, + selected && styles.selected + ); + return { styles, rootStyle }; +} + +type TNavigationMenuItem = { + selected: boolean; +} & MenuItemProps; + +export function NavigationMenuItem( + { children, selected, ...rest }: TNavigationMenuItem +) { + const { rootStyle } = useNavigationMenuItemStyles({ selected }); + + return ( + + {children} + + ); +} diff --git a/examples/src/components/navigation-menu/navigation-menu.tsx b/examples/src/components/navigation-menu/navigation-menu.tsx new file mode 100644 index 00000000..12515a3d --- /dev/null +++ b/examples/src/components/navigation-menu/navigation-menu.tsx @@ -0,0 +1,134 @@ +import { + Accordion, + AccordionHeader, + AccordionItem, + AccordionPanel, + Button, + DrawerBody, + DrawerFooter, + DrawerHeader, + InlineDrawer, + makeStyles, + mergeClasses, + shorthands, + tokens, +} from "@fluentui/react-components"; +import { bundleIcon, HomeFilled, HomeRegular } from "@fluentui/react-icons"; +import React from "react"; +import { useNavigate } from "react-router-dom"; +import { RouteCategory } from "../../routing/route-map"; +import { routes } from "../../routing/routes"; +import { NavigationFooter } from "./navigation-footer"; +import { NavigationMenuList } from "./navigation-menu-list"; + +const HomeIcon = bundleIcon(HomeFilled, HomeRegular); + +const componentId = "navigation-menu"; +export const navigationMenuClassNames = { + root: componentId, +}; + +const useStyles = makeStyles({ + root: { + height: "100%", + width: "300px", + ...shorthands.border(0), + }, + body: { + backgroundImage: + `linear-gradient(to top, ${tokens.colorNeutralBackground3}, ${tokens.colorNeutralBackground3}), + linear-gradient(to top, ${tokens.colorNeutralBackground3}, ${tokens.colorNeutralBackground3}), + linear-gradient(to top, ${tokens.colorNeutralBackground3}, ${tokens.colorNeutralBackground3}), + linear-gradient(to bottom, ${tokens.colorNeutralStroke1}, ${tokens.colorNeutralBackground3})`, + backgroundColor: tokens.colorNeutralBackground3, + backgroundSize: "100% 2px, 100% 2px, 94% 1px, 100% 0px", + ...shorthands.margin(0), + ...shorthands.padding( + tokens.spacingVerticalXS, + tokens.spacingVerticalS, + tokens.spacingVerticalXS, + tokens.spacingVerticalS + ), + ":first-child": { + paddingTop: "unset", + }, + }, + header: { + ...shorthands.margin(0), + ...shorthands.padding( + tokens.spacingVerticalXS, + tokens.spacingVerticalS, + tokens.spacingVerticalXS, + tokens.spacingVerticalS + ), + backgroundColor: tokens.colorNeutralBackground3, + }, + footer: { + ...shorthands.margin(0), + ...shorthands.padding( + tokens.spacingVerticalM, + tokens.spacingVerticalS, + tokens.spacingVerticalXS, + tokens.spacingVerticalS + ), + backgroundColor: tokens.colorNeutralBackground3, + }, +}); + +export function useNavigationMenuStyles() { + const styles = useStyles(); + const rootStyle = mergeClasses(navigationMenuClassNames.root, styles.root); + return { styles, rootStyle }; +} + +export function NavigationMenu({ ...rest }) { + const { styles, rootStyle } = useNavigationMenuStyles(); + const navigate = useNavigate(); + + return ( + + +
+ +
+
+ + + + Misc + + + + + + Components + + + + + + Styles + + + + + + + + + +
+ ); +} diff --git a/examples/src/components/page-header.tsx b/examples/src/components/page-header.tsx deleted file mode 100644 index 1d672376..00000000 --- a/examples/src/components/page-header.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import React, { PropsWithChildren } from "react"; - -import { - makeStyles, - mergeClasses, - shorthands, - Text, - tokens, -} from "@fluentui/react-components"; - -type PageHeaderProps = { - readonly title: string; - readonly borderBottom?: boolean; - readonly className?: string; -}; - -const useStyles = makeStyles({ - root: { - alignItems: "flex-end", - display: "flex", - flexShrink: 0, - minHeight: "40px", - width: "100%", - }, - borderBottom: { - ...shorthands.borderBottom("1px", "solid", tokens.colorNeutralStroke3), - }, - title: { - flexShrink: 0, - alignSelf: "center", - }, - point: { - color: tokens.colorBrandBackground, - }, - content: { - width: "100%", - }, -}); - -export const PageHeader = ({ - title, - borderBottom = true, - children, - className, -}: PropsWithChildren) => { - const styles = useStyles(); - - return ( -
- {title && ( - - {title} - . - - )} -
{children}
-
- ); -}; diff --git a/examples/src/components/section-title.tsx b/examples/src/components/section-title.tsx deleted file mode 100644 index 17aac58d..00000000 --- a/examples/src/components/section-title.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import React, { PropsWithChildren } from "react"; - -import { - makeStyles, - mergeClasses, - Text, - tokens, -} from "@fluentui/react-components"; - -type SectionTitleProps = { - readonly title: string; - readonly className?: string; - readonly withDot?: boolean; -}; - -const useStyles = makeStyles({ - root: { - alignItems: "flex-end", - display: "flex", - flexShrink: 0, - height: "40px", - width: "100%", - }, - title: { - flexShrink: 0, - alignSelf: "center", - }, - point: { - color: tokens.colorBrandBackground, - }, - content: { - width: "100%", - marginLeft: tokens.spacingHorizontalXXXL, - }, -}); - -export const SectionTitle = ({ - title, - children, - withDot = false, - className, -}: PropsWithChildren) => { - const styles = useStyles(); - - return ( -
- - {title} - {withDot && .} - -
{children}
-
- ); -}; diff --git a/examples/src/components/story/story-code-block-accordion.tsx b/examples/src/components/story/story-code-block-accordion.tsx new file mode 100644 index 00000000..29ea253a --- /dev/null +++ b/examples/src/components/story/story-code-block-accordion.tsx @@ -0,0 +1,35 @@ +import { + Accordion, + AccordionHeader, + AccordionItem, + AccordionPanel, + makeStyles, + shorthands, + tokens, +} from "@fluentui/react-components"; +import React from "react"; +import { StoryCodeBlock } from "./story-code-block"; + +const useStyles = makeStyles({ + root: { + backgroundColor: tokens.colorNeutralBackground1, + ...shorthands.borderRadius(tokens.borderRadiusLarge), + }, +}); + +export const StoryCodeBlockAccordion = ( + { codeString, title = "Show code" }: { codeString: string; title?: string } +) => { + const styles = useStyles(); + + return ( + + + {title} + + + + + + ); +}; diff --git a/examples/src/components/story/story-code-block.tsx b/examples/src/components/story/story-code-block.tsx new file mode 100644 index 00000000..a070d60b --- /dev/null +++ b/examples/src/components/story/story-code-block.tsx @@ -0,0 +1,48 @@ +import { Button, makeStyles, tokens } from "@fluentui/react-components"; +import { RectangleLandscapeHintCopyRegular } from "@fluentui/react-icons"; +import React, { useCallback } from "react"; +import SyntaxHighlighter from "react-syntax-highlighter"; + +const useStyles = makeStyles({ + root: { + position: "relative", + }, + copy: { + position: "absolute", + top: tokens.spacingVerticalM, + right: tokens.spacingVerticalM, + }, +}); + +export const StoryCodeBlock = ( + { codeString }: { codeString: string } +) => { + const styles = useStyles(); + + const copyCode = useCallback(async () => { + await copyToClipboard(codeString); + }, []); + + return ( +
+ + + {codeString} + +
+ ); +}; + +async function copyToClipboard(text: string): Promise { + try { + await navigator.clipboard.writeText(text); + } catch (error) { + console.error("Error copying to clipboard:", error); + } +} diff --git a/examples/src/components/story/story-navigation/story-navigation-menu-item.tsx b/examples/src/components/story/story-navigation/story-navigation-menu-item.tsx new file mode 100644 index 00000000..31956cc9 --- /dev/null +++ b/examples/src/components/story/story-navigation/story-navigation-menu-item.tsx @@ -0,0 +1,64 @@ +import { + Button, + makeStyles, + MenuButtonProps, + mergeClasses, + tokens, +} from "@fluentui/react-components"; +import React from "react"; + +const componentId = "navigation-menu-item"; +export const navigationMenuItemClassNames = { + root: componentId, +}; + +const useStyles = makeStyles({ + root: { + backgroundImage: + `linear-gradient(90deg,${tokens.colorNeutralBackground3}0%,${tokens.colorNeutralBackground1}50%,${tokens.colorNeutralBackground3}100%)`, + ":hover": { + backgroundColor: tokens.colorNeutralBackground2, + }, + }, + selected: { + backgroundImage: + `linear-gradient(90deg,${tokens.colorNeutralBackground1}0%,${tokens.colorNeutralBackground1Selected}50%,${tokens.colorNeutralBackground1}100%)`, + }, +}); + +type TUseStoryNavigationMenuItemStyles = { + selected: boolean; +}; + +export function useNavigationMenuItemStyles( + { selected }: TUseStoryNavigationMenuItemStyles +) { + const styles = useStyles(); + const rootStyle = mergeClasses( + navigationMenuItemClassNames.root, + styles.root, + selected && styles.selected + ); + return { styles, rootStyle }; +} + +type TStoryNavigationMenuItem = { + selected: boolean; +} & MenuButtonProps; + +export function StoryNavigationMenuItem( + { children, selected, ...rest }: TStoryNavigationMenuItem +) { + const { rootStyle } = useNavigationMenuItemStyles({ selected }); + + return ( + + ); +} diff --git a/examples/src/components/story/story-navigation/story-page-navigation.tsx b/examples/src/components/story/story-navigation/story-page-navigation.tsx new file mode 100644 index 00000000..634d3bad --- /dev/null +++ b/examples/src/components/story/story-navigation/story-page-navigation.tsx @@ -0,0 +1,47 @@ +import { MenuList } from "@fluentui/react-components"; +import React, { useEffect, useMemo, useState } from "react"; +import { useLocation, useNavigate } from "react-router-dom"; +import { StoryNavigationMenuItem } from "./story-navigation-menu-item"; + +export type TStoryNavigationLink = { + title: string; + anchor: string; +}; + +type TStoryPageNavigation = { + links: TStoryNavigationLink[]; +}; + +export function StoryPageNavigation({ links }: TStoryPageNavigation) { + const { pathname, hash } = useLocation(); + const navigate = useNavigate(); + const [selected, setSelected] = useState("#" + links[0].anchor); + + useEffect(() => { + if (hash) { + setSelected(hash); + } + }, [hash]); + + const renderMenuItems = useMemo( + () => + links.map(({ title, anchor }) => { + return ( + navigate(`${pathname}#${anchor}`)} + > + {title} + + ); + }), + [selected] + ); + + return ( + + {renderMenuItems} + + ); +} diff --git a/examples/src/components/story/story-page-header.tsx b/examples/src/components/story/story-page-header.tsx new file mode 100644 index 00000000..83e746a1 --- /dev/null +++ b/examples/src/components/story/story-page-header.tsx @@ -0,0 +1,42 @@ +import React, { PropsWithChildren } from "react"; + +import { + Caption1, + makeStyles, + Title2, + tokens, +} from "@fluentui/react-components"; + +type PageHeaderProps = { + title: string; +}; + +const useStyles = makeStyles({ + root: { + display: "flex", + justifyItems: "flex-end", + flexDirection: "column", + }, + point: { + color: tokens.colorBrandBackground, + }, +}); + +export const StoryPageHeader = ({ + title, + children, +}: PropsWithChildren) => { + const styles = useStyles(); + + return ( +
+ + {title} + . + + {children} +
+ ); +}; diff --git a/examples/src/components/story/story-page.tsx b/examples/src/components/story/story-page.tsx new file mode 100644 index 00000000..e26ad411 --- /dev/null +++ b/examples/src/components/story/story-page.tsx @@ -0,0 +1,146 @@ +import { + DrawerBody, + DrawerHeader, + InlineDrawer, + Link, + makeStyles, + mergeClasses, + shorthands, + tokens, +} from "@fluentui/react-components"; +import React, { PropsWithChildren } from "react"; +import { StoryPageHeader } from "./story-page-header"; + +const componentId = "story-page"; +export const storyPageClassNames = { + root: componentId, +}; + +const useStyles = makeStyles({ + root: { + height: "100%", + backgroundColor: tokens.colorNeutralBackground3, + display: "grid", + gridTemplateColumns: "1fr min-content", + ...shorthands.padding(0, "15%", 0, "5%"), + }, + header: { + ...shorthands.padding(tokens.spacingHorizontalM, tokens.spacingVerticalL), + }, + headerDescription: { + display: "flex", + flexDirection: "column", + ...shorthands.gap(tokens.spacingHorizontalXXL), + }, + package: { + color: tokens.colorNeutralForeground3, + }, + body: { + overflowY: "auto", + height: "100%", + position: "relative", + display: "flex", + flexDirection: "column", + ...shorthands.gap(tokens.spacingVerticalS), + ...shorthands.padding(tokens.spacingVerticalXXS, tokens.spacingVerticalL), + }, + main: { + overflowY: "auto", + height: "100%", + display: "flex", + flexDirection: "column", + flexGrow: 1, + }, + navigation: { + height: "100%", + width: "220px", + ...shorthands.border(0), + }, + navigationBody: { + backgroundImage: + `linear-gradient(to top, ${tokens.colorNeutralBackground3}, ${tokens.colorNeutralBackground3}), + linear-gradient(to top, ${tokens.colorNeutralBackground3}, ${tokens.colorNeutralBackground3}), + linear-gradient(to top, ${tokens.colorNeutralBackground3}, ${tokens.colorNeutralBackground3}), + linear-gradient(to bottom, ${tokens.colorNeutralStroke1}, ${tokens.colorNeutralBackground3})`, + backgroundColor: tokens.colorNeutralBackground3, + backgroundSize: "100% 2px, 100% 2px, 94% 1px, 100% 0px", + ...shorthands.margin(0), + ...shorthands.padding( + tokens.spacingVerticalXS, + tokens.spacingVerticalS, + tokens.spacingVerticalXS, + tokens.spacingVerticalS + ), + ":first-child": { + paddingTop: "unset", + }, + }, + navigationHeader: { + ...shorthands.margin(0), + ...shorthands.padding( + tokens.spacingVerticalM, + tokens.spacingVerticalS, + tokens.spacingVerticalXS, + tokens.spacingVerticalS + ), + backgroundColor: tokens.colorNeutralBackground3, + }, +}); + +export function useStoryPageStyles() { + const styles = useStyles(); + const rootStyle = mergeClasses(storyPageClassNames.root, styles.root); + return { styles, rootStyle }; +} + +type TStoryPage = { + title: string; + ghPackage: string; + ghUrl?: string; + description?: string; + navigation?: JSX.Element; +}; + +export function StoryPage( + { title, description, ghPackage, ghUrl, navigation, children, ...rest }: + PropsWithChildren +) { + const { styles, rootStyle } = useStoryPageStyles(); + return ( +
+
+
+ +
+
+ {ghUrl + ? ( + + {ghPackage} + + ) + : ghPackage} +
+ {description} +
+
+
+
+ {children} +
+
+ {navigation && ( + + + + {navigation} + + + )} +
+ ); +} diff --git a/examples/src/components/story/story-section-header.tsx b/examples/src/components/story/story-section-header.tsx new file mode 100644 index 00000000..eb6627f1 --- /dev/null +++ b/examples/src/components/story/story-section-header.tsx @@ -0,0 +1,41 @@ +import React, { PropsWithChildren } from "react"; + +import { + Caption2, + makeStyles, + Subtitle2, + tokens, +} from "@fluentui/react-components"; + +type PageHeaderProps = { + title: string; +}; + +const useStyles = makeStyles({ + root: { + display: "flex", + justifyItems: "flex-end", + flexDirection: "column", + }, + point: { + color: tokens.colorBrandBackground, + }, +}); + +export const StorySectionHeader = ({ + title, + children, +}: PropsWithChildren) => { + const styles = useStyles(); + return ( +
+ + {title} + . + + {children} +
+ ); +}; diff --git a/examples/src/components/story/story-section.tsx b/examples/src/components/story/story-section.tsx new file mode 100644 index 00000000..42c1ef27 --- /dev/null +++ b/examples/src/components/story/story-section.tsx @@ -0,0 +1,60 @@ +import { + makeStyles, + mergeClasses, + shorthands, + tokens, +} from "@fluentui/react-components"; +import React, { PropsWithChildren } from "react"; +import { StorySectionHeader } from "./story-section-header"; + +const componentId = "story-section"; +export const storySectionClassNames = { + root: componentId, +}; + +const useStyles = makeStyles({ + root: { + display: "flex", + flexDirection: "column", + ...shorthands.borderRadius(tokens.borderRadiusLarge), + ...shorthands.gap(tokens.spacingHorizontalS), + }, + body: { + display: "flex", + flexDirection: "column", + ...shorthands.border("1px", "solid", tokens.colorNeutralBackground1), + ...shorthands.borderRadius(tokens.borderRadiusLarge), + ...shorthands.padding(tokens.spacingHorizontalM), + ...shorthands.gap(tokens.spacingHorizontalS), + }, +}); + +export function useStorySectionStyles() { + const styles = useStyles(); + const rootStyle = mergeClasses(storySectionClassNames.root, styles.root); + return { styles, rootStyle }; +} + +type TStorySection = { + title?: string; + description?: string; +} & JSX.IntrinsicElements["div"]; + +export function StorySection( + { title, description, children, ...rest }: PropsWithChildren +) { + const { styles, rootStyle } = useStorySectionStyles(); + + return ( +
+ {title && ( + + {description} + + )} +
+ {children} +
+
+ ); +} diff --git a/examples/src/components/story/story.utils.tsx b/examples/src/components/story/story.utils.tsx new file mode 100644 index 00000000..2ccaffb8 --- /dev/null +++ b/examples/src/components/story/story.utils.tsx @@ -0,0 +1,39 @@ +import { StoryCodeBlockAccordion } from "./story-code-block-accordion"; +import { + StoryPageNavigation, + TStoryNavigationLink, +} from "./story-navigation/story-page-navigation"; +import { StorySection } from "./story-section"; +import React, { useMemo } from "react"; + +export type pageData = { + example: JSX.Element; + codeString: string; +} & TStoryNavigationLink; + +export function useExampleWithNavigation(examples: pageData[]) { + const links = examples.map(({ title, anchor }) => ({ + title, + anchor, + })); + + const renderSections = useMemo( + () => + examples.map(({ title, example, codeString, anchor }) => { + return ( + + {example} + + + ); + }), + [examples] + ); + + const renderNavigation = useMemo( + () => , + [links] + ); + + return { renderSections, renderNavigation }; +} diff --git a/examples/src/constants/constants.ts b/examples/src/constants/constants.ts new file mode 100644 index 00000000..4b5b0055 --- /dev/null +++ b/examples/src/constants/constants.ts @@ -0,0 +1,3 @@ +export const GitHubUrls = { + home: "https://github.com/AxisCommunications/fluent-components", +}; diff --git a/examples/src/landingpage/index.tsx b/examples/src/landingpage/index.tsx index 5377dfe5..f8914437 100644 --- a/examples/src/landingpage/index.tsx +++ b/examples/src/landingpage/index.tsx @@ -1,40 +1,137 @@ -import React from "react"; -import { makeStyles, shorthands, tokens } from "@fluentui/react-components"; -import { useLayoutStyles } from "../styles/page"; -import { WelcomeMessage } from "./welcome-message/welcome-message.component"; -import { WelcomeImage } from "./welcome-image/welcome-image.component"; +import { + makeStyles, + shorthands, + Title2, + tokens, +} from "@fluentui/react-components"; +import { + DarkThemeRegular, + DocumentCssRegular, + IconsRegular, + PuzzlePieceRegular, +} from "@fluentui/react-icons"; +import React, { useCallback } from "react"; +import { useNavigate } from "react-router-dom"; import { TestId } from "../../system-test/util/test-id"; +import { getRouteByCategory, RouteCategory } from "../routing/route-map"; +import { routes } from "../routing/routes"; +import { WelcomeCard } from "./welcome-card"; +import { WelcomeImage } from "./welcome-image/welcome-image.component"; const useStyles = makeStyles({ - pageContainer: { - ...shorthands.flex(1), - backgroundColor: tokens.colorNeutralBackground4, + root: { + position: "relative", + display: "flex", + height: "100%", + width: "100%", + ...shorthands.overflow("auto"), + ...shorthands.padding(tokens.spacingVerticalXXL), }, - page: { + image: { + position: "absolute", + bottom: 0, + right: 0, + }, + content: { + zIndex: 1, display: "flex", - height: - `calc(100% - ${tokens.spacingHorizontalL} - ${tokens.spacingVerticalS})`, - justifyContent: "space-between", - paddingRight: tokens.spacingHorizontalXXXL, - backgroundColor: tokens.colorNeutralBackground2, - borderTopLeftRadius: tokens.borderRadiusXLarge, - paddingTop: tokens.spacingHorizontalL, - paddingBottom: tokens.spacingVerticalS, + height: "100%", + width: "100%", + flexDirection: "column", + ...shorthands.gap(tokens.spacingHorizontalL), + }, + cardContainer: { + display: "flex", + flexWrap: "wrap", + ...shorthands.gap(tokens.spacingHorizontalM), }, }); export const WelcomePage = () => { const styles = useStyles(); - const layoutStyles = useLayoutStyles(); + const { + navigateToFirstComponent, + navigateToFirstStyle, + navigateToIcon, + navigateToTheme, + } = useWelcomePage(); return ( -
-
-
- - +
+
+ Welcome to Axis Fluent Components +
+ } + title="Components" + description={"Axis branded component"} + text={"Complement to fluent ui components"} + onClick={navigateToFirstComponent} + /> + } + title="Theme" + description={"Axis branded themes"} + onClick={navigateToTheme} + /> + } + title="Icons" + description={"Axis branded icons"} + onClick={navigateToIcon} + /> + } + title="Styles" + description={"Utilities for existing components"} + onClick={navigateToFirstStyle} + />
+
+ +
); }; + +function useWelcomePage() { + const navigate = useNavigate(); + + const navigateToFirstComponent = useCallback( + () => { + const [firstComponent] = getRouteByCategory(RouteCategory.COMPONENT)[0]; + navigate(firstComponent); + }, + [navigate] + ); + + const navigateToFirstStyle = useCallback( + () => { + const [firstComponent] = getRouteByCategory(RouteCategory.STYLE)[0]; + navigate(firstComponent); + }, + [navigate] + ); + + const navigateToIcon = useCallback( + () => { + navigate(routes.IconCatalog); + }, + [navigate] + ); + + const navigateToTheme = useCallback( + () => { + navigate(routes.Theme); + }, + [navigate] + ); + + return { + navigateToFirstComponent, + navigateToFirstStyle, + navigateToIcon, + navigateToTheme, + }; +} diff --git a/examples/src/landingpage/welcome-card.tsx b/examples/src/landingpage/welcome-card.tsx new file mode 100644 index 00000000..9e0bb9fd --- /dev/null +++ b/examples/src/landingpage/welcome-card.tsx @@ -0,0 +1,57 @@ +import { + Caption1, + Card, + CardHeader, + makeStyles, + shorthands, + Text, + tokens, +} from "@fluentui/react-components"; + +import React from "react"; + +const useStyles = makeStyles({ + root: { + minWidth: "200px", + maxWidth: "100%", + height: "fit-content", + }, + caption: { + color: tokens.colorNeutralForeground3, + }, + text: { + ...shorthands.margin(0), + }, +}); + +type TCardExample = { + title: string; + description?: string; + text?: string; + icon: JSX.Element; + onClick: () => void; +}; + +export const WelcomeCard = ( + { title, description, text, icon, onClick }: TCardExample +) => { + const styles = useStyles(); + + return ( + + {title}} + image={icon} + description={ + {description} + } + /> +

+ {text} +

+
+ ); +}; diff --git a/examples/src/landingpage/welcome-image/welcome-image.component.tsx b/examples/src/landingpage/welcome-image/welcome-image.component.tsx index 11fcfd5a..4dfecb22 100644 --- a/examples/src/landingpage/welcome-image/welcome-image.component.tsx +++ b/examples/src/landingpage/welcome-image/welcome-image.component.tsx @@ -1,25 +1,23 @@ // -------------------------------------------------------------------- // Copyright (c) Axis Communications AB, SWEDEN. All rights reserved. // -------------------------------------------------------------------- +import { axisDarkTheme } from "@axiscommunications/fluent-theme"; +import { Image } from "@fluentui/react-components"; import React from "react"; +import { useAppContext } from "../../context/ApplicationStateProvider"; import welcomeImageDark from "./img/welcome-image-dark.svg"; import welcomeImageLight from "./img/welcome-image-light.svg"; import { useWelcomeImageStyles } from "./welcome-image.styles"; -import { axisDarkTheme } from "@axiscommunications/fluent-theme"; -import { Card, Image } from "@fluentui/react-components"; -import { useAppContext } from "../../context/ApplicationStateProvider"; export const WelcomeImage: React.FC = () => { const theme = useAppContext((context) => context.theme); const styles = useWelcomeImageStyles(); return ( - - - + ); }; diff --git a/examples/src/landingpage/welcome-message/welcome-message.component.tsx b/examples/src/landingpage/welcome-message/welcome-message.component.tsx deleted file mode 100644 index e5e4e308..00000000 --- a/examples/src/landingpage/welcome-message/welcome-message.component.tsx +++ /dev/null @@ -1,36 +0,0 @@ -// -------------------------------------------------------------------- -// Copyright (c) Axis Communications AB, SWEDEN. All rights reserved. -// -------------------------------------------------------------------- -import React from "react"; -import { Body1, Link, Title2 } from "@fluentui/react-components"; -import { useWelcomeMessageStyle } from "./welcome-message.styles"; - -export const WelcomeMessage: React.FC = () => { - const styles = useWelcomeMessageStyle(); - - const bodyText = ( - <> -
- Here you may find{" "} - - fluent - {" "} - customizations regarding: -
-
    -
  • Components
  • -
  • Themes
  • -
  • Icons
  • -
- - ); - return ( -
- Welcome to Axis Fluent Components - {bodyText} -
- ); -}; diff --git a/examples/src/landingpage/welcome-message/welcome-message.styles.ts b/examples/src/landingpage/welcome-message/welcome-message.styles.ts deleted file mode 100644 index dc5b79be..00000000 --- a/examples/src/landingpage/welcome-message/welcome-message.styles.ts +++ /dev/null @@ -1,19 +0,0 @@ -// -------------------------------------------------------------------- -// Copyright (c) Axis Communications AB, SWEDEN. All rights reserved. -// -------------------------------------------------------------------- - -import { makeStyles, shorthands } from "@fluentui/react-components"; - -export const useWelcomeMessageStyle = makeStyles({ - content: { - display: "flex", - flexDirection: "column", - ...shorthands.gap("50px"), - ...shorthands.padding("100px"), - }, - bodyText: { - display: "flex", - flexDirection: "column", - ...shorthands.gap("10px"), - }, -}); diff --git a/examples/src/layout.tsx b/examples/src/layout.tsx index 265ba32a..9c2e1044 100644 --- a/examples/src/layout.tsx +++ b/examples/src/layout.tsx @@ -1,11 +1,6 @@ import React from "react"; -import { - makeStyles, - mergeClasses, - shorthands, - tokens, -} from "@fluentui/react-components"; +import { makeStyles, shorthands, tokens } from "@fluentui/react-components"; type LayoutProps = { readonly header: JSX.Element; @@ -16,28 +11,26 @@ type LayoutProps = { const useStyles = makeStyles({ root: { backgroundColor: tokens.colorNeutralBackground4, - display: "flex", - flexDirection: "column", - height: "100vh", + display: "grid", + gridTemplateColumns: "min-content 1fr", + gridTemplateRows: "min-content 1fr", + gridTemplateAreas: ` + 'header header' + 'navigation outlet'`, width: "100vw", - }, - body: { + height: "100vh", ...shorthands.overflow("hidden"), - display: "flex", - flexGrow: 1, - width: "100vw", }, - content: { - ...shorthands.border( - tokens.strokeWidthThin, - "solid", - tokens.colorNeutralShadowKeyLighter - ), - ...shorthands.borderRadius(tokens.borderRadiusXLarge, 0, 0, 0), + header: { + ...shorthands.gridArea("header"), + }, + navigation: { + ...shorthands.gridArea("navigation"), + }, + outlet: { + backgroundColor: tokens.colorNeutralBackground3, + ...shorthands.gridArea("outlet"), ...shorthands.overflow("hidden"), - backgroundColor: tokens.colorNeutralBackground2, - boxShadow: tokens.shadow4, - width: "100%", }, }); @@ -45,14 +38,12 @@ export const Layout = ({ header, navigation, content }: LayoutProps) => { const styles = useStyles(); return ( -
- {header} -
+
+
{header}
+
{navigation} -
- {content} -
+
{content}
); }; diff --git a/examples/src/main-page.tsx b/examples/src/main-page.tsx index 5f377500..170b7b86 100644 --- a/examples/src/main-page.tsx +++ b/examples/src/main-page.tsx @@ -1,23 +1,11 @@ -/*! ***************************************************************************** -Copyright 2022 Axis Communications AB, SWEDEN. All rights reserved. -***************************************************************************** */ -import { - Divider, - makeStyles, - SelectTabData, - SelectTabEvent, - Tab, - TabList, - tokens, -} from "@fluentui/react-components"; -import React, { useCallback, useMemo } from "react"; -import { Outlet, useLocation, useNavigate } from "react-router-dom"; +import { makeStyles, tokens } from "@fluentui/react-components"; +import React from "react"; +import { Outlet } from "react-router-dom"; import { Navbar } from "./components/top-bar"; import { Layout } from "./layout"; -import { getRouteByGroup, RouteGroup, routeMap } from "./routing/route-map"; -import { routes, TRoute } from "./routing/routes"; import { useStaticStyles } from "./styles/static"; -import { useUtilStyles } from "./styles/utils"; +import { NavigationMenu } from "./components/navigation-menu/navigation-menu"; +import { useScrollToAnchor } from "./routing/use-scroll-to-anchor"; export const useStyles = makeStyles({ navigation: { @@ -27,55 +15,12 @@ export const useStyles = makeStyles({ export const MainPage = () => { useStaticStyles(); - const styles = useStyles(); - const utilStyles = useUtilStyles(); - - const navigate = useNavigate(); - const { pathname } = useLocation(); - - const goTo = useCallback( - (_: SelectTabEvent, { value }: SelectTabData) => { - navigate(value as TRoute); - }, - [navigate] - ); - - const storyRoutes = getRouteByGroup(RouteGroup.STORY); - - const renderStoryTabs = useMemo( - () => - Array.from(storyRoutes.entries()).map((entry) => { - const [key, [route, routeData]] = entry; - return ( - - {routeData.label} - - ); - }), - [storyRoutes] - ); - - const homeRoute = routeMap.get(routes.Home); + useScrollToAnchor(); return ( } - navigation={ - - {homeRoute && ( - - {homeRoute.label} - - )} - - {renderStoryTabs} - - } + navigation={} content={} /> ); diff --git a/examples/src/routing/route-map.tsx b/examples/src/routing/route-map.tsx index b16f28f7..02b4b459 100644 --- a/examples/src/routing/route-map.tsx +++ b/examples/src/routing/route-map.tsx @@ -1,54 +1,34 @@ -import { - bundleIcon, - DarkThemeFilled, - DarkThemeRegular, - DocumentCssFilled, - DocumentCssRegular, - EyeFilled, - EyeRegular, - HomeFilled, - HomeRegular, - IconsFilled, - IconsRegular, - OptionsFilled, - OptionsRegular, - StepsFilled, - StepsRegular, - TableFilled, - TableRegular, -} from "@fluentui/react-icons"; import React from "react"; +import { WelcomePage } from "../landingpage"; import { IconPage } from "../stories/icon-page"; -import { StepperPage } from "../stories/stepper-page"; -import { TableUtilitiesPage } from "../stories/table-utlities-page"; +import { PasswordInputPage } from "../stories/password-input/password-input-page"; +import { SliderPage } from "../stories/slider/slider-page"; +import { StepperPage } from "../stories/stepper/stepper-page"; +import { FluentUiTabStylesPage } from "../stories/tab-list-utilities/tab-list-utilities-page"; import { ThemePage } from "../stories/theme-page"; -import { VerticalStepperPage } from "../stories/vertical-stepper-page"; import { routes, TRoute } from "./routes"; -import { SliderPage } from "../stories/slider-page"; -import { WelcomePage } from "../landingpage"; -import { PasswordInputPage } from "../stories/password-input-page"; -import { FluentUiTabStylesPage } from "../stories/tab-list-utilities/tab-list-utilities-page"; - -const HomeIcon = bundleIcon(HomeFilled, HomeRegular); -const ThemeIcon = bundleIcon(DarkThemeFilled, DarkThemeRegular); -const IconCatalogIcon = bundleIcon(IconsFilled, IconsRegular); -const StepperIcon = bundleIcon(StepsFilled, StepsRegular); -const VStepperIcon = bundleIcon(StepsFilled, StepsRegular); -const TableUtilitiesIcon = bundleIcon(TableFilled, TableRegular); -const SliderIcon = bundleIcon(OptionsFilled, OptionsRegular); -const PasswordIcon = bundleIcon(EyeFilled, EyeRegular); -const TabStylesIcon = bundleIcon(DocumentCssFilled, DocumentCssRegular); +import { TableUtilitiesPage } from "../stories/table-utilities/table-utlities-page"; export enum RouteGroup { MISC, STORY, } +export enum RouteCategory { + MISC, + COMPONENT, + STYLE, +} + type TRouteData = { label: string; element: JSX.Element; - icon?: JSX.Element; group: RouteGroup; + category?: RouteCategory; + ghInfo?: { + url: string; + packageName: string; + }; }; export const routeMap: Map = new Map([ @@ -56,7 +36,6 @@ export const routeMap: Map = new Map([ routes.Home, { label: "Home", - icon: , element: , group: RouteGroup.MISC, }, @@ -64,19 +43,29 @@ export const routeMap: Map = new Map([ [ routes.Theme, { - label: "Theme", + label: "Themes", element: , - icon: , group: RouteGroup.STORY, + category: RouteCategory.MISC, + ghInfo: { + url: + "https://github.com/AxisCommunications/fluent-components/pkgs/npm/fluent-theme", + packageName: "@axiscommunications/fluent-theme", + }, }, ], [ routes.IconCatalog, { - label: "Icon Catalog", + label: "Icons", element: , - icon: , group: RouteGroup.STORY, + category: RouteCategory.MISC, + ghInfo: { + url: + "https://github.com/AxisCommunications/fluent-components/pkgs/npm/fluent-icons", + packageName: "@axiscommunications/fluent-icons", + }, }, ], [ @@ -84,17 +73,13 @@ export const routeMap: Map = new Map([ { label: "Stepper", element: , - icon: , - group: RouteGroup.STORY, - }, - ], - [ - routes.VerticalStepper, - { - label: "Vertical Stepper", - element: , - icon: , group: RouteGroup.STORY, + category: RouteCategory.COMPONENT, + ghInfo: { + url: + "https://github.com/AxisCommunications/fluent-components/pkgs/npm/fluent-stepper", + packageName: "@axiscommunications/fluent-stepper", + }, }, ], [ @@ -102,8 +87,13 @@ export const routeMap: Map = new Map([ { label: "Slider", element: , - icon: , group: RouteGroup.STORY, + category: RouteCategory.COMPONENT, + ghInfo: { + url: + "https://github.com/AxisCommunications/fluent-components/pkgs/npm/fluent-slider", + packageName: "@axiscommunications/fluent-slider", + }, }, ], [ @@ -111,30 +101,61 @@ export const routeMap: Map = new Map([ { label: "Password input", element: , - icon: , group: RouteGroup.STORY, + category: RouteCategory.COMPONENT, + ghInfo: { + url: + "https://github.com/AxisCommunications/fluent-components/pkgs/npm/fluent-password-input", + packageName: "@axiscommunications/fluent-password-input ", + }, }, ], [ routes.TableUtilities, { - label: "Table Utilities", + label: "Table", element: , - icon: , group: RouteGroup.STORY, + category: RouteCategory.STYLE, + ghInfo: { + url: + "https://github.com/AxisCommunications/fluent-components/pkgs/npm/fluent-styles", + packageName: "@axiscommunications/fluent-styles", + }, }, ], [ routes.TabListUtilities, { - label: "Tablist utilities", + label: "Tablist", element: , - icon: , group: RouteGroup.STORY, + category: RouteCategory.STYLE, + ghInfo: { + url: + "https://github.com/AxisCommunications/fluent-components/pkgs/npm/fluent-styles", + packageName: "@axiscommunications/fluent-styles", + }, }, ], ]); +export function getGhInfoByKey( + routeKey: TRoute +): { url: string; packageName: string } { + const routeData = routeMap.get(routeKey); + + if (routeData?.ghInfo) { + return routeData.ghInfo; + } + + throw new Error("getGhInfoByKey should not happen"); +} + export const getRouteByGroup = (group: RouteGroup) => { return [...routeMap.entries()].filter((e) => e[1].group === group); }; + +export const getRouteByCategory = (category: RouteCategory) => { + return [...routeMap.entries()].filter((e) => e[1].category === category); +}; diff --git a/examples/src/routing/routes.ts b/examples/src/routing/routes.ts index a465f7b4..eaa95356 100644 --- a/examples/src/routing/routes.ts +++ b/examples/src/routing/routes.ts @@ -3,7 +3,6 @@ export const routes = { Theme: "/theme", IconCatalog: "/icon-catalog", Stepper: "/stepper", - VerticalStepper: "/vertical-stepper", TableUtilities: "/table-utilities", Slider: "/slider", PasswordInput: "/password-input", diff --git a/examples/src/routing/use-scroll-to-anchor.tsx b/examples/src/routing/use-scroll-to-anchor.tsx new file mode 100644 index 00000000..ccd9a220 --- /dev/null +++ b/examples/src/routing/use-scroll-to-anchor.tsx @@ -0,0 +1,26 @@ +import { useEffect, useRef } from "react"; +import { useLocation } from "react-router-dom"; + +export function useScrollToAnchor() { + const location = useLocation(); + const lastHash = useRef(""); + + // listen to location change using useEffect with location as dependency + // https://jasonwatmore.com/react-router-v6-listen-to-location-route-change-without-history-listen + useEffect(() => { + if (location.hash) { + lastHash.current = location.hash.slice(1); // safe hash for further use after navigation + } + + if (lastHash.current && document.getElementById(lastHash.current)) { + setTimeout(() => { + document + .getElementById(lastHash.current) + ?.scrollIntoView({ behavior: "smooth", block: "start" }); + lastHash.current = ""; + }, 100); + } + }, [location]); + + return null; +} diff --git a/examples/src/stories/icon-page.tsx b/examples/src/stories/icon-page.tsx index 0bbba7e6..645c945e 100644 --- a/examples/src/stories/icon-page.tsx +++ b/examples/src/stories/icon-page.tsx @@ -8,8 +8,11 @@ import { shorthands, } from "@fluentui/react-components"; import React from "react"; -import { PageHeader } from "../components/page-header"; import { SimpleHeader } from "../components/simple-header"; +import { StoryPage } from "../components/story/story-page"; +import { StorySection } from "../components/story/story-section"; +import { getGhInfoByKey } from "../routing/route-map"; +import { routes } from "../routing/routes"; import { useFixedPageStyle, useLayoutStyles, @@ -59,6 +62,8 @@ const axisReactIcons: React.FC[] = Object.keys(AxisReactIcons) .filter((icon) => !!icon && !!icon.displayName); export const IconPage = (): JSX.Element => { + const gh = getGhInfoByKey(routes.IconCatalog); + const [search, setSearch] = React.useState(""); // Fluent default size is 20 const [size, setSize] = React.useState(20); @@ -117,13 +122,13 @@ export const IconPage = (): JSX.Element => { ); return ( -
- -
+ +
@@ -170,7 +175,7 @@ export const IconPage = (): JSX.Element => {
-
-
+ + ); }; diff --git a/examples/src/stories/password-input-page.tsx b/examples/src/stories/password-input-page.tsx deleted file mode 100644 index 9a271373..00000000 --- a/examples/src/stories/password-input-page.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import React from "react"; - -import { PasswordInput } from "@axiscommunications/fluent-password-input"; - -import { mergeClasses } from "@fluentui/react-components"; -import { PageHeader } from "../components/page-header"; -import { useLayoutStyles, useScrollPageStyle } from "../styles/page"; - -export const PasswordInputPage = () => { - const scrollPageStyle = useScrollPageStyle(); - const layoutStyles = useLayoutStyles(); - - return ( -
- -
-
- -
-
-
- ); -}; diff --git a/examples/src/stories/password-input/password-input-example.tsx b/examples/src/stories/password-input/password-input-example.tsx new file mode 100644 index 00000000..9e31d3a3 --- /dev/null +++ b/examples/src/stories/password-input/password-input-example.tsx @@ -0,0 +1,17 @@ +import React from "react"; +import { PasswordInput } from "@axiscommunications/fluent-password-input"; + +export function PasswordInputExample() { + return ; +} + +export const PasswordInputExampleAsString = ` +import React from "react"; +import { PasswordInput } from "@axiscommunications/fluent-password-input"; + +export function PasswordInputExample() { + return ( + + ) +} +`; diff --git a/examples/src/stories/password-input/password-input-page.tsx b/examples/src/stories/password-input/password-input-page.tsx new file mode 100644 index 00000000..d4e2f280 --- /dev/null +++ b/examples/src/stories/password-input/password-input-page.tsx @@ -0,0 +1,56 @@ +import { makeStyles } from "@fluentui/react-components"; +import { pageData } from "examples/src/components/story/story.utils"; +import React from "react"; +import { StoryPage } from "../../components/story/story-page"; +import { useExampleWithNavigation } from "../../components/story/story.utils"; +import { getGhInfoByKey } from "../../routing/route-map"; +import { routes } from "../../routing/routes"; +import { + PasswordInputExample, + PasswordInputExampleAsString, +} from "./password-input-example"; + +const useStyles = makeStyles({ + example: { + maxWidth: "400px", + }, +}); + +const examples: pageData[] = [ + { + title: "Password input", + anchor: "PasswordInputExample", + example: , + codeString: PasswordInputExampleAsString, + }, +]; + +export const PasswordInputPage = () => { + const gh = getGhInfoByKey(routes.PasswordInput); + const styles = useStyles(); + + const { renderSections, renderNavigation } = useExampleWithNavigation( + examples.map(d => { + return { + ...d, + example: ( +
+ {d.example} +
+ ), + }; + }) + ); + + return ( + + {renderSections} + + ); +}; diff --git a/examples/src/stories/slider-page.tsx b/examples/src/stories/slider-page.tsx deleted file mode 100644 index 03db7ac5..00000000 --- a/examples/src/stories/slider-page.tsx +++ /dev/null @@ -1,324 +0,0 @@ -import React, { useCallback, useState } from "react"; - -import { - RangeSlider, - RangeSliderProps, - Slider, - SliderOnChangeData, - SliderProps, -} from "@axiscommunications/fluent-slider"; -import { - Button, - Label, - makeStyles, - mergeClasses, - tokens, - useId, -} from "@fluentui/react-components"; -import { useLayoutStyles, useScrollPageStyle } from "../styles/page"; -import { PageHeader } from "../components/page-header"; - -const useSliderPageStyles = makeStyles({ - container: { - paddingBottom: "120px", - }, - sliderContainer: { - display: "flex", - flexDirection: "column", - marginLeft: tokens.spacingHorizontalM, - maxWidth: "400px", - }, - buttonContainer: { - display: "flex", - flexDirection: "row", - justifyContent: "space-evenly", - marginTop: tokens.spacingVerticalS, - }, -}); - -type DemoSliderProps = SliderProps & { - title: string; -}; - -const DemoSlider = (props: DemoSliderProps) => { - const classes = useSliderPageStyles(); - const { title, ...sliderProps } = props; - const id = useId(); - - return ( -
- - -
- ); -}; - -type DemoRangeSliderProps = RangeSliderProps & { - title: string; -}; - -const DemoRangeSlider = (props: DemoRangeSliderProps) => { - const classes = useSliderPageStyles(); - const { title, ...sliderProps } = props; - const id = useId(); - - return ( -
- - -
- ); -}; - -const RegularSlider = () => { - return ; -}; - -const SliderWithMarks = () => { - return ( - - ); -}; - -const SliderSteppingToMarks = () => { - return ( - - ); -}; - -const TransformedValueSlider = () => { - return ( - (value < 100 ? value * 2 : "∞")} - defaultValue={50} - /> - ); -}; - -const DisabledSlider = () => { - return ( - - ); -}; - -const SliderWithRange = () => { - return ( - - ); -}; - -const SliderWithSteps = () => { - return ( - - ); -}; - -const RangeSliderWithSteps = () => { - return ( - - ); -}; - -const SliderSmall = () => { - return ; -}; - -const SliderWithExternalButtons = () => { - const [value, setValue] = useState(50); - - const onChange = useCallback((data: SliderOnChangeData) => { - setValue(data.value); - }, []); - - const onClick = useCallback( - (value: number) => () => { - setValue(value); - }, - [] - ); - - const pageClasses = useSliderPageStyles(); - const id = useId(); - - return ( -
- - -
- - - -
-
- ); -}; - -const useCustomizedSliderStyles = makeStyles({ - thumb: { - backgroundColor: tokens.colorPaletteRedBackground3, - width: "28px", - height: "28px", - "&:hover": { - backgroundColor: tokens.colorPaletteRedBackground2, - }, - }, - thumbLabel: { - backgroundColor: tokens.colorPaletteRedBackground3, - color: tokens.colorPaletteYellowForeground1, - }, - mark: { - height: "8px", - width: "5px", - }, - markLabel: { - color: tokens.colorPaletteRedForeground1, - }, - track: { - marginLeft: "2px", - marginRight: "2px", - backgroundColor: tokens.colorPaletteYellowBorder1, - "&:hover": { - backgroundColor: tokens.colorPaletteYellowBorderActive, - }, - }, - rail: { - height: "8px", - backgroundColor: tokens.colorPaletteRedBorder1, - "&:hover": { - backgroundColor: tokens.colorPaletteRedBorderActive, - }, - }, -}); - -const CustomizedSlider = () => { - const classes = useCustomizedSliderStyles(); - return ( - 50 }, - { - value: 75, - label: ( -
- 75 -
- ), - }, - { value: 100 }, - ]} - thumb={{ - style: { zIndex: 2 }, - className: classes.thumb, - label: { className: classes.thumbLabel }, - }} - markLabel={{ - style: { fontWeight: "bold" }, - }} - mark={{ - style: { backgroundColor: tokens.colorNeutralBackgroundInverted }, - className: classes.mark, - }} - rail={{ style: { opacity: 0.8 }, className: classes.rail }} - track={{ style: { zIndex: 2 }, className: classes.track }} - /> - ); -}; - -export const SliderPage = () => { - const scrollPageStyle = useScrollPageStyle(); - const layoutStyles = useLayoutStyles(); - const classes = useSliderPageStyles(); - - return ( -
- -
- - - - - - - - - - - -
-
- ); -}; diff --git a/examples/src/stories/slider/examples/custom-example.tsx b/examples/src/stories/slider/examples/custom-example.tsx new file mode 100644 index 00000000..c9dd584a --- /dev/null +++ b/examples/src/stories/slider/examples/custom-example.tsx @@ -0,0 +1,171 @@ +import { Slider } from "@axiscommunications/fluent-slider"; +import { makeStyles, tokens } from "@fluentui/react-components"; +import React from "react"; + +const useCustomizedSliderStyles = makeStyles({ + thumb: { + backgroundColor: tokens.colorPaletteRedBackground3, + width: "28px", + height: "28px", + "&:hover": { + backgroundColor: tokens.colorPaletteRedBackground2, + }, + }, + thumbLabel: { + backgroundColor: tokens.colorPaletteRedBackground3, + color: tokens.colorPaletteYellowForeground1, + }, + mark: { + height: "8px", + width: "5px", + }, + markLabel: { + color: tokens.colorPaletteRedForeground1, + }, + track: { + marginLeft: "2px", + marginRight: "2px", + backgroundColor: tokens.colorPaletteYellowBorder1, + "&:hover": { + backgroundColor: tokens.colorPaletteYellowBorderActive, + }, + }, + rail: { + height: "8px", + backgroundColor: tokens.colorPaletteRedBorder1, + "&:hover": { + backgroundColor: tokens.colorPaletteRedBorderActive, + }, + }, +}); + +export function CustomSliderExample() { + const classes = useCustomizedSliderStyles(); + return ( + 50 }, + { + value: 75, + label: ( +
+ 75 +
+ ), + }, + { value: 100 }, + ]} + thumb={{ + style: { zIndex: 2 }, + className: classes.thumb, + label: { className: classes.thumbLabel }, + }} + markLabel={{ + style: { fontWeight: "bold" }, + }} + mark={{ + style: { backgroundColor: tokens.colorNeutralBackgroundInverted }, + className: classes.mark, + }} + rail={{ style: { opacity: 0.8 }, className: classes.rail }} + track={{ style: { zIndex: 2 }, className: classes.track }} + /> + ); +} + +export const CustomSliderExampleAsString = ` +import { Slider } from "@axiscommunications/fluent-slider"; +import { makeStyles, tokens } from "@fluentui/react-components"; +import React from "react"; + +const useCustomizedSliderStyles = makeStyles({ + thumb: { + backgroundColor: tokens.colorPaletteRedBackground3, + width: "28px", + height: "28px", + "&:hover": { + backgroundColor: tokens.colorPaletteRedBackground2, + }, + }, + thumbLabel: { + backgroundColor: tokens.colorPaletteRedBackground3, + color: tokens.colorPaletteYellowForeground1, + }, + mark: { + height: "8px", + width: "5px", + }, + markLabel: { + color: tokens.colorPaletteRedForeground1, + }, + track: { + marginLeft: "2px", + marginRight: "2px", + backgroundColor: tokens.colorPaletteYellowBorder1, + "&:hover": { + backgroundColor: tokens.colorPaletteYellowBorderActive, + }, + }, + rail: { + height: "8px", + backgroundColor: tokens.colorPaletteRedBorder1, + "&:hover": { + backgroundColor: tokens.colorPaletteRedBorderActive, + }, + }, +}); + +export function CustomSliderExample() { + const classes = useCustomizedSliderStyles(); + return ( + 50 }, + { + value: 75, + label: ( +
+ 75 +
+ ), + }, + { value: 100 }, + ]} + thumb={{ + style: { zIndex: 2 }, + className: classes.thumb, + label: { className: classes.thumbLabel }, + }} + markLabel={{ + style: { fontWeight: "bold" }, + }} + mark={{ + style: { backgroundColor: tokens.colorNeutralBackgroundInverted }, + className: classes.mark, + }} + rail={{ style: { opacity: 0.8 }, className: classes.rail }} + track={{ style: { zIndex: 2 }, className: classes.track }} + /> + ); +} +`; diff --git a/examples/src/stories/slider/examples/disabled-example.tsx b/examples/src/stories/slider/examples/disabled-example.tsx new file mode 100644 index 00000000..dd354d58 --- /dev/null +++ b/examples/src/stories/slider/examples/disabled-example.tsx @@ -0,0 +1,29 @@ +import { Slider } from "@axiscommunications/fluent-slider"; +import React from "react"; + +export function DisabledSliderExample() { + return ( + + ); +} + +export const DisabledSliderExampleAsString = ` +import { Slider } from "@axiscommunications/fluent-slider"; +import React from "react"; + +export function DisabledSliderExample() { + return ( + + ) +} +`; diff --git a/examples/src/stories/slider/examples/range-slider-with-steps-example.tsx b/examples/src/stories/slider/examples/range-slider-with-steps-example.tsx new file mode 100644 index 00000000..f1fd4b17 --- /dev/null +++ b/examples/src/stories/slider/examples/range-slider-with-steps-example.tsx @@ -0,0 +1,31 @@ +import { RangeSlider } from "@axiscommunications/fluent-slider"; +import React from "react"; + +export function WithStepsRangeSliderExample() { + return ( + + ); +} + +export const WithStepsRangeSliderExampleAsString = ` +import { RangeSlider } from "@axiscommunications/fluent-slider"; +import React from "react"; + +export function WithStepsRangeSliderExample() { + return ( + + ) +} +`; diff --git a/examples/src/stories/slider/examples/regular-example.tsx b/examples/src/stories/slider/examples/regular-example.tsx new file mode 100644 index 00000000..c85f8ebe --- /dev/null +++ b/examples/src/stories/slider/examples/regular-example.tsx @@ -0,0 +1,17 @@ +import { Slider } from "@axiscommunications/fluent-slider"; +import React from "react"; + +export function RegularSliderExample() { + return ; +} + +export const RegularSliderExampleAsString = ` +import { Slider } from "@axiscommunications/fluent-slider"; +import React from "react"; + +export function RegularSliderExample() { + return ( + + ) +} +`; diff --git a/examples/src/stories/slider/examples/small-example.tsx b/examples/src/stories/slider/examples/small-example.tsx new file mode 100644 index 00000000..a206a464 --- /dev/null +++ b/examples/src/stories/slider/examples/small-example.tsx @@ -0,0 +1,17 @@ +import { Slider } from "@axiscommunications/fluent-slider"; +import React from "react"; + +export function SmallSliderExample() { + return ; +} + +export const SmallSliderExampleAsString = ` +import { Slider } from "@axiscommunications/fluent-slider"; +import React from "react"; + +export function SmallSliderExample() { + return ( + + ) +} +`; diff --git a/examples/src/stories/slider/examples/stepping-to-marks-example.tsx b/examples/src/stories/slider/examples/stepping-to-marks-example.tsx new file mode 100644 index 00000000..f5a1fbfe --- /dev/null +++ b/examples/src/stories/slider/examples/stepping-to-marks-example.tsx @@ -0,0 +1,39 @@ +import { Slider } from "@axiscommunications/fluent-slider"; +import React from "react"; + +export function SteppingToMarksSliderExample() { + return ( + + ); +} + +export const SteppingToMarksSliderExampleAsString = ` +import { Slider } from "@axiscommunications/fluent-slider"; +import React from "react"; + +export function SteppingToMarksSliderExample() { + return ( + + ) +} +`; diff --git a/examples/src/stories/slider/examples/transform-value-example.tsx b/examples/src/stories/slider/examples/transform-value-example.tsx new file mode 100644 index 00000000..a097e4a5 --- /dev/null +++ b/examples/src/stories/slider/examples/transform-value-example.tsx @@ -0,0 +1,29 @@ +import { Slider } from "@axiscommunications/fluent-slider"; +import React from "react"; + +export function TransformValueSliderExample() { + return ( + (value < 100 ? value * 2 : "∞")} + defaultValue={50} + /> + ); +} + +export const TransformValueSliderExampleAsString = ` +import { Slider } from "@axiscommunications/fluent-slider"; +import React from "react"; + +export function TransformValueSliderExample() { + return ( + (value < 100 ? value * 2 : "∞")} + defaultValue={50} /> + ) +} +`; diff --git a/examples/src/stories/slider/examples/with-external-buttons-example.tsx b/examples/src/stories/slider/examples/with-external-buttons-example.tsx new file mode 100644 index 00000000..2585f8ee --- /dev/null +++ b/examples/src/stories/slider/examples/with-external-buttons-example.tsx @@ -0,0 +1,93 @@ +import { Slider, SliderOnChangeData } from "@axiscommunications/fluent-slider"; +import { Button, makeStyles, tokens } from "@fluentui/react-components"; +import React, { useCallback, useState } from "react"; + +const useSliderPageStyles = makeStyles({ + sliderContainer: { + display: "flex", + flexDirection: "column", + marginLeft: tokens.spacingHorizontalM, + }, + buttonContainer: { + display: "flex", + flexDirection: "row", + justifyContent: "space-evenly", + marginTop: tokens.spacingVerticalS, + }, +}); + +export function ExternalButtonsSliderExample() { + const [value, setValue] = useState(50); + + const onChange = useCallback((data: SliderOnChangeData) => { + setValue(data.value); + }, []); + + const onClick = useCallback( + (value: number) => () => { + setValue(value); + }, + [] + ); + + const pageClasses = useSliderPageStyles(); + + return ( +
+ +
+ + + +
+
+ ); +} + +export const ExternalButtonsSliderExampleAsString = ` +import { Slider, SliderOnChangeData } from "@axiscommunications/fluent-slider"; +import { Button, makeStyles, tokens } from "@fluentui/react-components"; +import React, { useCallback, useState } from "react"; + +const useSliderPageStyles = makeStyles({ + sliderContainer: { + display: "flex", + flexDirection: "column", + marginLeft: tokens.spacingHorizontalM, + }, + buttonContainer: { + display: "flex", + flexDirection: "row", + justifyContent: "space-evenly", + marginTop: tokens.spacingVerticalS, + }, +}); + +export function ExternalButtonsSliderExample() { + const [value, setValue] = useState(50); + + const onChange = useCallback((data: SliderOnChangeData) => { + setValue(data.value); + }, []); + + const onClick = useCallback( + (value: number) => () => { + setValue(value); + }, + [] + ); + + const pageClasses = useSliderPageStyles(); + + return ( +
+ +
+ + + +
+
+ ); +} +`; diff --git a/examples/src/stories/slider/examples/with-marks-example.tsx b/examples/src/stories/slider/examples/with-marks-example.tsx new file mode 100644 index 00000000..b680b09f --- /dev/null +++ b/examples/src/stories/slider/examples/with-marks-example.tsx @@ -0,0 +1,37 @@ +import { Slider } from "@axiscommunications/fluent-slider"; +import React from "react"; + +export function WithMarkSliderExample() { + return ( + + ); +} + +export const WithMarkSliderExampleAsString = ` +import { Slider } from "@axiscommunications/fluent-slider"; +import React from "react"; + +export function WithMarkSliderExample() { + return ( + + ) +} +`; diff --git a/examples/src/stories/slider/examples/with-range-example.tsx b/examples/src/stories/slider/examples/with-range-example.tsx new file mode 100644 index 00000000..46e922fa --- /dev/null +++ b/examples/src/stories/slider/examples/with-range-example.tsx @@ -0,0 +1,41 @@ +import { RangeSlider } from "@axiscommunications/fluent-slider"; +import React from "react"; + +export function RangeSliderExample() { + return ( + + ); +} + +export const RangeSliderExampleAsString = ` +import { RangeSlider } from "@axiscommunications/fluent-slider"; +import React from "react"; + +export function RangeSliderExample() { + return ( + + ) +} +`; diff --git a/examples/src/stories/slider/examples/with-steps-example.tsx b/examples/src/stories/slider/examples/with-steps-example.tsx new file mode 100644 index 00000000..a34a8f93 --- /dev/null +++ b/examples/src/stories/slider/examples/with-steps-example.tsx @@ -0,0 +1,31 @@ +import { Slider } from "@axiscommunications/fluent-slider"; +import React from "react"; + +export function WithStepsSliderExample() { + return ( + + ); +} + +export const WithStepsSliderExampleAsString = ` +import { Slider } from "@axiscommunications/fluent-slider"; +import React from "react"; + +export function WithStepsSliderExample() { + return ( + + ) +} +`; diff --git a/examples/src/stories/slider/slider-page.tsx b/examples/src/stories/slider/slider-page.tsx new file mode 100644 index 00000000..bcb03039 --- /dev/null +++ b/examples/src/stories/slider/slider-page.tsx @@ -0,0 +1,158 @@ +import { makeStyles } from "@fluentui/react-components"; +import React from "react"; +import { StoryPage } from "../../components/story/story-page"; +import { + pageData, + useExampleWithNavigation, +} from "../../components/story/story.utils"; +import { getGhInfoByKey } from "../../routing/route-map"; +import { routes } from "../../routing/routes"; +import { + CustomSliderExample, + CustomSliderExampleAsString, +} from "./examples/custom-example"; +import { + DisabledSliderExample, + DisabledSliderExampleAsString, +} from "./examples/disabled-example"; +import { + WithStepsRangeSliderExample, + WithStepsRangeSliderExampleAsString, +} from "./examples/range-slider-with-steps-example"; +import { + RegularSliderExample, + RegularSliderExampleAsString, +} from "./examples/regular-example"; +import { + SmallSliderExample, + SmallSliderExampleAsString, +} from "./examples/small-example"; +import { + SteppingToMarksSliderExample, + SteppingToMarksSliderExampleAsString, +} from "./examples/stepping-to-marks-example"; +import { + TransformValueSliderExample, + TransformValueSliderExampleAsString, +} from "./examples/transform-value-example"; +import { + ExternalButtonsSliderExample, + ExternalButtonsSliderExampleAsString, +} from "./examples/with-external-buttons-example"; +import { + WithMarkSliderExample, + WithMarkSliderExampleAsString, +} from "./examples/with-marks-example"; +import { + RangeSliderExample, + RangeSliderExampleAsString, +} from "./examples/with-range-example"; +import { + WithStepsSliderExample, + WithStepsSliderExampleAsString, +} from "./examples/with-steps-example"; + +const useStyles = makeStyles({ + example: { + maxWidth: "400px", + }, +}); + +const examples: pageData[] = [ + { + title: "Default", + anchor: "RegularSliderExample", + example: , + codeString: RegularSliderExampleAsString, + }, + { + title: "Disabled", + anchor: "DisabledSliderExample", + example: , + codeString: DisabledSliderExampleAsString, + }, + { + title: "Small", + anchor: "SmallSliderExample", + example: , + codeString: SmallSliderExampleAsString, + }, + { + title: "With marks", + anchor: "WithMarkSliderExample", + example: , + codeString: WithMarkSliderExampleAsString, + }, + { + title: "With steps", + anchor: "WithStepsSliderExample", + example: , + codeString: WithStepsSliderExampleAsString, + }, + { + title: "Stepping to marks", + anchor: "SteppingToMarksSliderExample", + example: , + codeString: SteppingToMarksSliderExampleAsString, + }, + { + title: "Transform value", + anchor: "TransformValueSliderExample", + example: , + codeString: TransformValueSliderExampleAsString, + }, + { + title: "External buttons", + anchor: "ExternalButtonsSliderExample", + example: , + codeString: ExternalButtonsSliderExampleAsString, + }, + { + title: "Custom", + anchor: "CustomSliderExample", + example: , + codeString: CustomSliderExampleAsString, + }, + { + title: "Range slider", + anchor: "RangeSliderExample", + example: , + codeString: RangeSliderExampleAsString, + }, + { + title: "Range slider with steps", + anchor: "WithStepsRangeSliderExample", + example: , + codeString: WithStepsRangeSliderExampleAsString, + }, +]; + +export const SliderPage = () => { + const gh = getGhInfoByKey(routes.Slider); + const styles = useStyles(); + + const { renderSections, renderNavigation } = useExampleWithNavigation( + examples.map(d => { + return { + ...d, + example: ( +
+ {d.example} +
+ ), + }; + }) + ); + + return ( + + {renderSections} + + ); +}; diff --git a/examples/src/stories/stepper-page.tsx b/examples/src/stories/stepper-page.tsx deleted file mode 100644 index 3671db61..00000000 --- a/examples/src/stories/stepper-page.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import React, { useCallback, useState } from "react"; - -import { - DialogStep, - Stepper, - StepperDialog, -} from "@axiscommunications/fluent-stepper"; -import { mergeClasses } from "@fluentui/react-components"; -import { PageHeader } from "../components/page-header"; -import { SectionTitle } from "../components/section-title"; -import { useLayoutStyles, useScrollPageStyle } from "../styles/page"; - -export const steps: DialogStep[] = [ - { - name: "First step", - content: <>{"This is the content of the first step. ".repeat(20)}, - }, - { - name: "Second step", - content: <>{"This is the content of the second step. ".repeat(20)}, - }, - { - name: "Third step", - content: <>{"This is the content of the third step. ".repeat(20)}, - }, -]; - -export const StepperPage = () => { - const [step, setStep] = useState(0); - const scrollPageStyle = useScrollPageStyle(); - const layoutStyles = useLayoutStyles(); - - const onFinish = useCallback(() => alert("Finish!"), []); - const onCancel = useCallback(() => setStep(0), []); - return ( -
- -
- -
- -
- -
-
-
-
- ); -}; diff --git a/examples/src/stories/stepper/examples/stepper-dialog-example.tsx b/examples/src/stories/stepper/examples/stepper-dialog-example.tsx new file mode 100644 index 00000000..273d6359 --- /dev/null +++ b/examples/src/stories/stepper/examples/stepper-dialog-example.tsx @@ -0,0 +1,82 @@ +import React, { useCallback, useState } from "react"; + +import { DialogStep, StepperDialog } from "@axiscommunications/fluent-stepper"; + +const steps: DialogStep[] = [ + { + name: "First step", + content: <>{"This is the content of the first step. ".repeat(20)}, + }, + { + name: "Second step", + content: <>{"This is the content of the second step. ".repeat(20)}, + }, + { + name: "Third step", + content: <>{"This is the content of the third step. ".repeat(20)}, + }, +]; +export function StepperDialogExample() { + const [step, setStep] = useState(0); + const onFinish = useCallback(() => alert("Finish!"), []); + const onCancel = useCallback(() => setStep(0), []); + + return ( + + ); +} + +export const StepperDialogExampleAsString = ` +import React, { useCallback, useState } from "react"; + +import { + DialogStep, + StepperDialog +} from "@axiscommunications/fluent-stepper"; + +const steps: DialogStep[] = [ + { + name: "First step", + content: <>{"This is the content of the first step. ".repeat(20)}, + }, + { + name: "Second step", + content: <>{"This is the content of the second step. ".repeat(20)}, + }, + { + name: "Third step", + content: <>{"This is the content of the third step. ".repeat(20)}, + }, +]; +export function StepperDialogExample() { + const [step, setStep] = useState(0); + const onFinish = useCallback(() => alert("Finish!"), []); + const onCancel = useCallback(() => setStep(0), []); + + return ( + + ) +} +`; diff --git a/examples/src/stories/stepper/examples/vertical-stepper-dialog-example.tsx b/examples/src/stories/stepper/examples/vertical-stepper-dialog-example.tsx new file mode 100644 index 00000000..7e1b7765 --- /dev/null +++ b/examples/src/stories/stepper/examples/vertical-stepper-dialog-example.tsx @@ -0,0 +1,82 @@ +import React, { useCallback, useState } from "react"; + +import { DialogStep, StepperDialog } from "@axiscommunications/fluent-stepper"; + +const steps: DialogStep[] = [ + { + name: "First step", + content: <>{"This is the content of the first step. ".repeat(20)}, + }, + { + name: "Second step", + content: <>{"This is the content of the second step. ".repeat(20)}, + }, + { + name: "Third step", + content: <>{"This is the content of the third step. ".repeat(20)}, + }, +]; +export function VerticalStepperDialogExample() { + const [step, setStep] = useState(0); + const onFinish = useCallback(() => alert("Finish!"), []); + const onCancel = useCallback(() => setStep(0), []); + return ( + + ); +} + +export const VerticalStepperDialogExampleAsString = ` +import React, { useCallback, useState } from "react"; + +import { + DialogStep, + StepperDialog +} from "@axiscommunications/fluent-stepper"; + +const steps: DialogStep[] = [ + { + name: "First step", + content: <>{"This is the content of the first step. ".repeat(20)}, + }, + { + name: "Second step", + content: <>{"This is the content of the second step. ".repeat(20)}, + }, + { + name: "Third step", + content: <>{"This is the content of the third step. ".repeat(20)}, + }, +]; +export function VerticalStepperDialogExample() { + const [step, setStep] = useState(0); + const onFinish = useCallback(() => alert("Finish!"), []); + const onCancel = useCallback(() => setStep(0), []); + return ( + + ) +} +`; diff --git a/examples/src/stories/stepper/stepper-page.tsx b/examples/src/stories/stepper/stepper-page.tsx new file mode 100644 index 00000000..a1cf835a --- /dev/null +++ b/examples/src/stories/stepper/stepper-page.tsx @@ -0,0 +1,50 @@ +import React from "react"; +import { StoryPage } from "../../components/story/story-page"; +import { + pageData, + useExampleWithNavigation, +} from "../../components/story/story.utils"; +import { getGhInfoByKey } from "../../routing/route-map"; +import { routes } from "../../routing/routes"; +import { + StepperDialogExample, + StepperDialogExampleAsString, +} from "./examples/stepper-dialog-example"; +import { + VerticalStepperDialogExample, + VerticalStepperDialogExampleAsString, +} from "./examples/vertical-stepper-dialog-example"; + +const examples: pageData[] = [ + { + title: "Stepper dialog", + anchor: "StepperDialogExample", + example: , + codeString: StepperDialogExampleAsString, + }, + { + title: "Vertical stepper dialog", + anchor: "VerticalStepperDialogExample", + example: , + codeString: VerticalStepperDialogExampleAsString, + }, +]; + +export const StepperPage = () => { + const gh = getGhInfoByKey(routes.Stepper); + const { renderSections, renderNavigation } = useExampleWithNavigation( + examples + ); + + return ( + + {renderSections} + + ); +}; diff --git a/examples/src/stories/tab-list-utilities/tab-list-example.tsx b/examples/src/stories/tab-list-utilities/tab-list-example.tsx new file mode 100644 index 00000000..8c4a2717 --- /dev/null +++ b/examples/src/stories/tab-list-utilities/tab-list-example.tsx @@ -0,0 +1,145 @@ +import { + useTabListStyles, + useTabStyles, +} from "@axiscommunications/fluent-styles"; +import { + Tab, + TabList, + TabListProps, + TabProps, +} from "@fluentui/react-components"; +import { bundleIcon, HomeFilled, HomeRegular } from "@fluentui/react-icons"; +import React, { useState } from "react"; + +const HomeIcon = bundleIcon(HomeFilled, HomeRegular); + +export type TTabListComponent = { + withText?: boolean; +} & TabListProps; + +export function StyledTabListComponent( + { withText = true, ...props }: TTabListComponent +) { + const [selectedTab, setSelectedTab] = useState("tab1"); + const { rootStyle } = useTabListStyles({ vertical: props.vertical }); + + return ( + { + setSelectedTab(value as unknown as string); + }} + {...props} + > + } + value="tab1" + selected={selectedTab === "tab1"} + > + {withText && "First Tab"} + + } + value="tab2" + selected={selectedTab === "tab2"} + > + {withText && "First Tab"} + + } + value="tab3" + selected={selectedTab === "tab3"} + > + {withText && "First Tab"} + + + ); +} + +export type TStyledTabComponent = { + selected?: boolean; +} & TabProps; + +function StyledTabComponent( + { selected, children, ...props }: TStyledTabComponent +) { + const { rootStyle } = useTabStyles({ selected }); + + return {children}; +} + +export const StyledTabListComponentAsJson = ` +import { + useTabListStyles, + useTabStyles, +} from "@axiscommunications/fluent-styles"; +import { + Tab, + TabList, + TabListProps, + TabProps, +} from "@fluentui/react-components"; +import { HomeFilled, HomeRegular, bundleIcon } from "@fluentui/react-icons"; +import React, { useState } from "react"; + +const HomeIcon = bundleIcon(HomeFilled, HomeRegular); + +export type TTabListComponent = { + withText?: boolean; +} & TabListProps; + +export function StyledTabListComponent( + { withText = true, ...props }: TTabListComponent +) { + const [selectedTab, setSelectedTab] = useState("tab1"); + const { rootStyle } = useTabListStyles({ vertical: props.vertical }); + + return ( + { + setSelectedTab(value as unknown as string); + }} + {...props} + > + } + value="tab1" + selected={selectedTab === "tab1"} + > + {withText && "First Tab"} + + } + value="tab2" + selected={selectedTab === "tab2"} + > + {withText && "First Tab"} + + } + value="tab3" + selected={selectedTab === "tab3"} + > + {withText && "First Tab"} + + + ); +} + +export type TStyledTabComponent = { + selected?: boolean; +} & TabProps; + +function StyledTabComponent( + { selected, children, ...props }: TStyledTabComponent +) { + const { rootStyle } = useTabStyles({ selected }); + + return {children}; +} +`; diff --git a/examples/src/stories/tab-list-utilities/tab-list-styled.tsx b/examples/src/stories/tab-list-utilities/tab-list-styled.tsx deleted file mode 100644 index fd2ff3ac..00000000 --- a/examples/src/stories/tab-list-utilities/tab-list-styled.tsx +++ /dev/null @@ -1,94 +0,0 @@ -import React from "react"; -import { - Tab, - TabList, - TabListProps, - TabProps, -} from "@fluentui/react-components"; -import { bundleIcon, HomeFilled, HomeRegular } from "@fluentui/react-icons"; -import { - useTabListStyles, - useTabStyles, -} from "@axiscommunications/fluent-styles"; -import { useTabListContext } from "./tab-list-utilities-page"; - -export const codeBlockStyled = ` -... -import { - useTabListStyles, - useTabStyles, -} from "@axiscommunications/fluent-styles"; -... - -//standard usage -const { rootStyle: tabListStyle } = useTabListStyles({ vertical: true/false }); -const { rootStyle: tabStyle } = useTabStyles({ selected: true/false }); - - - tab1 - - -//not happy with style? all styles can be grabbed from styles prop -const { styles } = useTabListStyles(); -const newStyle = mergeClasses(styles.root, overrideStyles.root ...) -... -`; - -const HomeIcon = bundleIcon(HomeFilled, HomeRegular); - -export type TTabListComponent = { - withText?: boolean; -} & TabListProps; - -export function StyledTabListComponent( - { withText = true, ...props }: TTabListComponent -) { - const { selectedTab, setSelectedTab } = useTabListContext(); - const { rootStyle } = useTabListStyles({ vertical: props.vertical }); - - return ( - { - setSelectedTab(value as unknown as string); - }} - {...props} - > - } - value="tab1" - selected={selectedTab === "tab1"} - > - {withText && "First Tab"} - - } - value="tab2" - selected={selectedTab === "tab2"} - > - {withText && "First Tab"} - - } - value="tab3" - selected={selectedTab === "tab3"} - > - {withText && "First Tab"} - - - ); -} - -export type TStyledTabComponent = { - selected?: boolean; -} & TabProps; - -function StyledTabComponent( - { selected, children, ...props }: TStyledTabComponent -) { - const { rootStyle } = useTabStyles({ selected }); - - return {children}; -} diff --git a/examples/src/stories/tab-list-utilities/tab-list-utilities-page.tsx b/examples/src/stories/tab-list-utilities/tab-list-utilities-page.tsx index 9098033d..8a54d105 100644 --- a/examples/src/stories/tab-list-utilities/tab-list-utilities-page.tsx +++ b/examples/src/stories/tab-list-utilities/tab-list-utilities-page.tsx @@ -1,110 +1,47 @@ -import React, { createContext, useContext, useState } from "react"; +import React from "react"; +import { StoryPage } from "../../components/story/story-page"; import { - makeStyles, - mergeClasses, - shorthands, - Tab, - TabList, - tokens, -} from "@fluentui/react-components"; -import { PageHeader } from "../../components/page-header"; -import { useLayoutStyles, useScrollPageStyle } from "../../styles/page"; -import { SectionTitle } from "../../components/section-title"; -import { bundleIcon, HomeFilled, HomeRegular } from "@fluentui/react-icons"; -import { CodeBlock } from "../../components/code-block"; + pageData, + useExampleWithNavigation, +} from "../../components/story/story.utils"; +import { getGhInfoByKey } from "../../routing/route-map"; +import { routes } from "../../routing/routes"; import { - codeBlockStyled, StyledTabListComponent, - TTabListComponent, -} from "./tab-list-styled"; - -type TTabListContext = { - selectedTab: string; - setSelectedTab: (value: string) => void; -}; - -const TabListContext = createContext(null); -export const useTabListContext = () => { - const context = useContext(TabListContext); - if (context === null) { - throw new Error("cant use context outside its provider"); - } - return context; -}; - -const HomeIcon = bundleIcon(HomeFilled, HomeRegular); - -const useTabListUtilitiesStyles = makeStyles({ - section: { - display: "flex", - flexDirection: "row", - flexWrap: "wrap", - ...shorthands.gap(tokens.spacingHorizontalXXL), - }, - group: { - display: "flex", - alignItems: "flex-start", - flexDirection: "column", - ...shorthands.gap(tokens.spacingHorizontalXS), + StyledTabListComponentAsJson, +} from "./tab-list-example"; + +const examples: pageData[] = [ + { + title: "Default", + anchor: "StepperDialogExample", + example: ( + <> + + + + + + ), + codeString: StyledTabListComponentAsJson, }, -}); +]; export const FluentUiTabStylesPage = () => { - const styles = useTabListUtilitiesStyles(); - const scrollPageStyle = useScrollPageStyle(); - const layoutStyles = useLayoutStyles(); - const [selectedTab, setSelectedTab] = useState("tab1"); - - return ( -
- - -
-
-
- - - - - -
- -
- - - - - - - -
-
-
-
-
+ const gh = getGhInfoByKey(routes.TabListUtilities); + const { renderSections, renderNavigation } = useExampleWithNavigation( + examples ); -}; -function TabListComponent({ withText = true, ...props }: TTabListComponent) { - const { selectedTab, setSelectedTab } = useTabListContext(); return ( - { - setSelectedTab(value as unknown as string); - }} - {...props} + - } value="tab1">{withText && "First Tab"} - } value="tab2">{withText && "First Tab"} - } value="tab3">{withText && "First Tab"} - + {renderSections} + ); -} +}; diff --git a/examples/src/stories/table-utilities/table-example.tsx b/examples/src/stories/table-utilities/table-example.tsx new file mode 100644 index 00000000..d9921adb --- /dev/null +++ b/examples/src/stories/table-utilities/table-example.tsx @@ -0,0 +1,671 @@ +import { usePageController } from "@axiscommunications/fluent-hooks"; +import { + useColumnStyles, + useRowStyles, + useTableStyles, +} from "@axiscommunications/fluent-styles"; +import { + Button, + makeStyles, + Menu, + MenuButton, + MenuItem, + MenuList, + MenuPopover, + MenuTrigger, + mergeClasses, + shorthands, + SkeletonItem, + Table, + TableBody, + TableCell, + TableCellLayout, + TableHeader, + TableHeaderCell, + TableRow, + TableSelectionCell, + tokens, +} from "@fluentui/react-components"; +import { + bundleIcon, + ChevronLeft16Filled, + ChevronLeft16Regular, + ChevronRight16Filled, + ChevronRight16Regular, +} from "@fluentui/react-icons"; +import React, { useMemo, useState } from "react"; +import { useAppContext } from "../../context/ApplicationStateProvider"; + +const users = [ + { user: "Robin", role: "Admin", luckyNumber: 1337 }, + { user: "Batman", role: "Hero", luckyNumber: 7 }, + { user: "Alfred", role: "Butler", luckyNumber: 9 }, + { user: "Joker", role: "Villain", luckyNumber: 4 }, + { user: "Harley Quinn", role: "Villain", luckyNumber: 5 }, + { user: "Bane", role: "Villain", luckyNumber: 6 }, + { user: "Poison Ivy", role: "Villain", luckyNumber: 7 }, + { user: "Vicky Vale", role: "Reporter", luckyNumber: 22 }, + { user: "Jim Gordon", role: "Commissioner", luckyNumber: 13 }, +]; + +const pageSizes = [5, 7, 15]; +const ChevronLeft = bundleIcon(ChevronLeft16Filled, ChevronLeft16Regular); +const ChevronRight = bundleIcon(ChevronRight16Filled, ChevronRight16Regular); + +const useStyles = makeStyles({ + root: { + display: "flex", + flexDirection: "column", + ...shorthands.gap(tokens.spacingVerticalM), + }, +}); + +export function TableExample() { + const [skip, setSkip] = useState(0); + const [take, setTake] = useState(pageSizes[0]); + const [loading, setLoading] = useState(false); + const [total /*, setTotal*/] = useState(users.length); + + const pageController = usePageController({ total, skip, take, setSkip }); + const page = users.slice(skip, skip + take); + + const tableStyles = useTableStyles(); + const rowStyles = useRowStyles(); + const columnStyles = useColumnStyles(); + const styles = useStyles(); + + return ( +
+
+ +
+ + + + + + {loading + ? ( + + ) + : ( + + {page.map((rowContent, index) => ( + + + + {rowContent.user} + + + {rowContent.role} + + + {rowContent.luckyNumber} + + + ))} + + )} +
+ +
+ ); +} + +type TableFooterProps = + & Pick< + ReturnType, + "currentPage" | "totalPages" | "nextPage" | "prevPage" | "goToPage" + > + & { total: number; take: number; setTake: (take: number) => void }; + +const useTableFooterStyles = makeStyles({ + root: { + display: "flex", + justifyContent: "space-between", + marginTop: tokens.spacingVerticalM, + }, + rowsSelectors: { + display: "flex", + alignItems: "center", + ...shorthands.gap(tokens.spacingHorizontalM), + }, + pagesSelectors: { display: "flex" }, +}); + +function TableFooter({ + total, + currentPage, + totalPages, + take, + setTake, + nextPage, + prevPage, + goToPage, +}: TableFooterProps) { + const dir = useAppContext((context) => context.dir); + const styles = useTableFooterStyles(); + const from = currentPage * take + 1; + const to = Math.min(from + take - 1, total); + + return ( +
+
+ Showing rows {from}-{to} of {total} + + + + Rows per page: {take} + + + + + {pageSizes.map((size) => { + return ( + { + setTake(size); + }} + > + {size} + + ); + })} + + + +
+
+ + + + Page: {currentPage + 1} of {totalPages} + + + + + {Array(totalPages) + .fill(0) + .map((_, index) => { + return ( + { + goToPage(index); + }} + > + {index + 1} + + ); + })} + + + + {dir === "ltr" + ? ( + <> +
+
+ ); +} + +/** + * Skeleton TableBody + * Renders a skeleton table matching the specified column widths. + * The number of rows will be used if set and non-zero, otherwise randomized + * for the duration of the component or until data changes. + * + * Switch out the real TableBody for this while query data is `loading` or `stale`, + * and set `rows` from the query's stale data. This makes the number of rows feel + * consistent. + */ + +const useSkeletonTableBodyStyles = makeStyles({ + fillTableCell: { + width: "100%", + }, + nonInteractive: { + pointerEvents: "none", + }, + // Simulates TableSelectionCell which is 44px with a centered 32px Checkbox. + // The inline padding is (44 - skeleton size) / 2 + // (TableLayoutCell has 8px inline padding by default.) + fakeSelectionCell: { + width: "44px", + maxWidth: "44px", + boxSizing: "border-box", + ...shorthands.paddingInline("12px"), + }, +}); + +// `undefined` doesn't set a width and can be used in place of TableSelectionCell +type ColumnWidth = undefined | keyof ReturnType; + +const minRandomRows = 3; +const maxRandomRows = 10; + +interface SkeletonTableBodyProps { + rows?: number; + rowType?: keyof ReturnType; + widths: Array; +} + +// should be wrapped by according to docs, +// however this is only necessary if overriding `animation` or `appearance`. +// seems to work at any level above in the hierachy, +// so it could perhaps go outside the whole Table to avoid interfering with the +// layout, although this is less of a problem when using `noNativeElements`. +export function SkeletonTableBody( + { rows, rowType = "normal", widths }: SkeletonTableBodyProps +) { + const rowCount = useMemo( + () => + rows + || (minRandomRows + + Math.floor((maxRandomRows - minRandomRows + 1) * Math.random())), + [rows] + ); + const rowKeys = Array.from({ length: rowCount }, (_, k) => k); + + const styles = useSkeletonTableBodyStyles(); + const rowStyles = useRowStyles(); + const rowStyle = mergeClasses(rowStyles[rowType], styles.nonInteractive); + + const columnStyles = useColumnStyles(); + return ( + + {rowKeys.map((rowKey) => ( + + {widths.map((w, cellKey) => { + const hasDefinedWidth = !!(w && columnStyles[w]); + if (hasDefinedWidth) { + return ( + + + + ); + } + return ( +
+ +
+ ); + })} +
+ ))} +
+ ); +} + +export const TableExampleAsJson = ` +import { usePageController } from "@axiscommunications/fluent-hooks"; +import { + useColumnStyles, + useRowStyles, + useTableStyles, +} from "@axiscommunications/fluent-styles"; +import { + Button, + Menu, + MenuButton, + MenuItem, + MenuList, + MenuPopover, + MenuTrigger, + SkeletonItem, + Table, + TableBody, + TableCell, + TableCellLayout, + TableHeader, + TableHeaderCell, + TableRow, + TableSelectionCell, + makeStyles, + mergeClasses, + shorthands, + tokens, +} from "@fluentui/react-components"; +import { + ChevronLeft16Filled, + ChevronLeft16Regular, + ChevronRight16Filled, + ChevronRight16Regular, + bundleIcon, +} from "@fluentui/react-icons"; +import React, { useMemo, useState } from "react"; +import { useAppContext } from "../../context/ApplicationStateProvider"; + +const users = [ + { user: "Robin", role: "Admin", luckyNumber: 1337 }, + { user: "Batman", role: "Hero", luckyNumber: 7 }, + { user: "Alfred", role: "Butler", luckyNumber: 9 }, + { user: "Joker", role: "Villain", luckyNumber: 4 }, + { user: "Harley Quinn", role: "Villain", luckyNumber: 5 }, + { user: "Bane", role: "Villain", luckyNumber: 6 }, + { user: "Poison Ivy", role: "Villain", luckyNumber: 7 }, + { user: "Vicky Vale", role: "Reporter", luckyNumber: 22 }, + { user: "Jim Gordon", role: "Commissioner", luckyNumber: 13 }, +]; + +const pageSizes = [5, 7, 15]; +const ChevronLeft = bundleIcon(ChevronLeft16Filled, ChevronLeft16Regular); +const ChevronRight = bundleIcon(ChevronRight16Filled, ChevronRight16Regular); + +const useStyles = makeStyles({ + root: { + display: "flex", + flexDirection: "column", + ...shorthands.gap(tokens.spacingVerticalM) + } +}) + +export function TableExample() { + const [skip, setSkip] = useState(0); + const [take, setTake] = useState(pageSizes[0]); + const [loading, setLoading] = useState(false) + const [total /*, setTotal*/] = useState(users.length); + + const pageController = usePageController({ total, skip, take, setSkip }); + const page = users.slice(skip, skip + take); + + const tableStyles = useTableStyles(); + const rowStyles = useRowStyles(); + const columnStyles = useColumnStyles(); + const styles = useStyles() + + return ( +
+
+ +
+ + + + + + {loading + ? ( + + ) + : ( + + {page.map((rowContent, index) => ( + + + + {rowContent.user} + + + {rowContent.role} + + + {rowContent.luckyNumber} + + + ))} + + )} +
+ +
+ + ); +} + +type TableFooterProps = + & Pick< + ReturnType, + "currentPage" | "totalPages" | "nextPage" | "prevPage" | "goToPage" + > + & { total: number; take: number; setTake: (take: number) => void }; + +const useTableFooterStyles = makeStyles({ + root: { + display: "flex", + justifyContent: "space-between", + marginTop: tokens.spacingVerticalM, + }, + rowsSelectors: { + display: "flex", + alignItems: "center", + ...shorthands.gap(tokens.spacingHorizontalM), + }, + pagesSelectors: { display: "flex" }, +}); + +function TableFooter({ + total, + currentPage, + totalPages, + take, + setTake, + nextPage, + prevPage, + goToPage, +}: TableFooterProps) { + const dir = useAppContext((context) => context.dir); + const styles = useTableFooterStyles(); + const from = currentPage * take + 1; + const to = Math.min(from + take - 1, total); + + return ( +
+
+ Showing rows {from}-{to} of {total} + + + + Rows per page: {take} + + + + + {pageSizes.map((size) => { + return ( + { + setTake(size); + }} + > + {size} + + ); + })} + + + +
+
+ + + + Page: {currentPage + 1} of {totalPages} + + + + + {Array(totalPages) + .fill(0) + .map((_, index) => { + return ( + { + goToPage(index); + }} + > + {index + 1} + + ); + })} + + + + {dir === "ltr" + ? ( + <> +
+
+ ); +} + +const useSkeletonTableBodyStyles = makeStyles({ + fillTableCell: { + width: "100%", + }, + nonInteractive: { + pointerEvents: "none", + }, + // Simulates TableSelectionCell which is 44px with a centered 32px Checkbox. + // The inline padding is (44 - skeleton size) / 2 + // (TableLayoutCell has 8px inline padding by default.) + fakeSelectionCell: { + width: "44px", + maxWidth: "44px", + boxSizing: "border-box", + ...shorthands.paddingInline("12px"), + }, +}); + +type ColumnWidth = undefined | keyof ReturnType; + +const minRandomRows = 3; +const maxRandomRows = 10; + +interface SkeletonTableBodyProps { + rows?: number; + rowType?: keyof ReturnType; + widths: Array; +} + +export function SkeletonTableBody( + { rows, rowType = "normal", widths }: SkeletonTableBodyProps +) { + const rowCount = useMemo( + () => + rows + || (minRandomRows + + Math.floor((maxRandomRows - minRandomRows + 1) * Math.random())), + [rows] + ); + const rowKeys = Array.from({ length: rowCount }, (_, k) => k); + + const styles = useSkeletonTableBodyStyles(); + const rowStyles = useRowStyles(); + const rowStyle = mergeClasses(rowStyles[rowType], styles.nonInteractive); + + const columnStyles = useColumnStyles(); + return ( + + {rowKeys.map((rowKey) => ( + + {widths.map((w, cellKey) => { + const hasDefinedWidth = !!(w && columnStyles[w]); + if (hasDefinedWidth) { + return ( + + + + ); + } + return ( +
+ +
+ ); + })} +
+ ))} +
+ ); +} +`; diff --git a/examples/src/stories/table-utilities/table-utlities-page.tsx b/examples/src/stories/table-utilities/table-utlities-page.tsx new file mode 100644 index 00000000..34d44dc8 --- /dev/null +++ b/examples/src/stories/table-utilities/table-utlities-page.tsx @@ -0,0 +1,37 @@ +import React from "react"; +import { StoryPage } from "../../components/story/story-page"; +import { + pageData, + useExampleWithNavigation, +} from "../../components/story/story.utils"; +import { getGhInfoByKey } from "../../routing/route-map"; +import { routes } from "../../routing/routes"; +import { TableExample, TableExampleAsJson } from "./table-example"; + +const examples: pageData[] = [ + { + title: "Default", + anchor: "TableExample", + example: , + codeString: TableExampleAsJson, + }, +]; + +export const TableUtilitiesPage = () => { + const gh = getGhInfoByKey(routes.TableUtilities); + const { renderSections, renderNavigation } = useExampleWithNavigation( + examples + ); + + return ( + + {renderSections} + + ); +}; diff --git a/examples/src/stories/table-utlities-page.tsx b/examples/src/stories/table-utlities-page.tsx deleted file mode 100644 index 23045faa..00000000 --- a/examples/src/stories/table-utlities-page.tsx +++ /dev/null @@ -1,358 +0,0 @@ -import { usePageController } from "@axiscommunications/fluent-hooks"; -import { - useColumnStyles, - useRowStyles, - useTableStyles, -} from "@axiscommunications/fluent-styles"; -import { - Button, - makeStyles, - Menu, - MenuButton, - MenuItem, - MenuList, - MenuPopover, - MenuTrigger, - mergeClasses, - shorthands, - SkeletonItem, - TableSelectionCell, - tokens, -} from "@fluentui/react-components"; -import { - Table, - TableBody, - TableCell, - TableCellLayout, - TableHeader, - TableHeaderCell, - TableRow, -} from "@fluentui/react-components"; -import { - bundleIcon, - ChevronLeft16Filled, - ChevronLeft16Regular, - ChevronRight16Filled, - ChevronRight16Regular, -} from "@fluentui/react-icons"; -import React, { useEffect, useMemo, useState } from "react"; -import { PageHeader } from "../components/page-header"; -import { useAppContext } from "../context/ApplicationStateProvider"; -import { useLayoutStyles, useScrollPageStyle } from "../styles/page"; - -const users = [ - { user: "Robin", role: "Admin", luckyNumber: 1337 }, - { user: "Batman", role: "Hero", luckyNumber: 7 }, - { user: "Alfred", role: "Butler", luckyNumber: 9 }, - { user: "Joker", role: "Villain", luckyNumber: 4 }, - { user: "Harley Quinn", role: "Villain", luckyNumber: 5 }, - { user: "Bane", role: "Villain", luckyNumber: 6 }, - { user: "Poison Ivy", role: "Villain", luckyNumber: 7 }, - { user: "Vicky Vale", role: "Reporter", luckyNumber: 22 }, - { user: "Jim Gordon", role: "Commissioner", luckyNumber: 13 }, -]; - -const pageSizes = [5, 7, 15]; -const ChevronLeft = bundleIcon(ChevronLeft16Filled, ChevronLeft16Regular); -const ChevronRight = bundleIcon(ChevronRight16Filled, ChevronRight16Regular); - -export const TableUtilitiesPage = () => { - const scrollPageStyle = useScrollPageStyle(); - const layoutStyles = useLayoutStyles(); - const [skip, setSkip] = useState(0); - const [take, setTake] = useState(pageSizes[0]); - const [total /*, setTotal*/] = useState(users.length); - - const pageController = usePageController({ total, skip, take, setSkip }); - - const [loading, setLoading] = useState(false); - useEffect(() => { - if (loading) { - const simulatedLoadingDuration = 700; - const id = setTimeout(() => setLoading(false), simulatedLoadingDuration); - return () => clearTimeout(id); - } - }, [loading]); - - return ( -
- -
- - -
- -
-
-
- ); -}; - -function TableExample( - { loading, skip, take }: { loading: boolean; skip: number; take: number } -) { - const tableStyles = useTableStyles(); - const rowStyles = useRowStyles(); - const columnStyles = useColumnStyles(); - - const page = users.slice(skip, skip + take); - - return ( - - - - - - {loading - ? ( - - ) - : ( - - {page.map((rowContent, index) => ( - - - - {rowContent.user} - - - {rowContent.role} - - - {rowContent.luckyNumber} - - - ))} - - )} -
- ); -} - -type TableFooterProps = - & Pick< - ReturnType, - "currentPage" | "totalPages" | "nextPage" | "prevPage" | "goToPage" - > - & { total: number; take: number; setTake: (take: number) => void }; - -const useTableFooterStyles = makeStyles({ - root: { - display: "flex", - justifyContent: "space-between", - marginTop: tokens.spacingVerticalM, - }, - rowsSelectors: { - display: "flex", - alignItems: "center", - ...shorthands.gap(tokens.spacingHorizontalM), - }, - pagesSelectors: { display: "flex" }, -}); - -function TableFooter({ - total, - currentPage, - totalPages, - take, - setTake, - nextPage, - prevPage, - goToPage, -}: TableFooterProps) { - const dir = useAppContext((context) => context.dir); - const styles = useTableFooterStyles(); - const from = currentPage * take + 1; - const to = Math.min(from + take - 1, total); - - return ( -
-
- Showing rows {from}-{to} of {total} - - - - Rows per page: {take} - - - - - {pageSizes.map((size) => { - return ( - { - setTake(size); - }} - > - {size} - - ); - })} - - - -
-
- - - - Page: {currentPage + 1} of {totalPages} - - - - - {Array(totalPages) - .fill(0) - .map((_, index) => { - return ( - { - goToPage(index); - }} - > - {index + 1} - - ); - })} - - - - {dir === "ltr" - ? ( - <> -
-
- ); -} - -/** - * Skeleton TableBody - * Renders a skeleton table matching the specified column widths. - * The number of rows will be used if set and non-zero, otherwise randomized - * for the duration of the component or until data changes. - * - * Switch out the real TableBody for this while query data is `loading` or `stale`, - * and set `rows` from the query's stale data. This makes the number of rows feel - * consistent. - */ - -const useSkeletonTableBodyStyles = makeStyles({ - fillTableCell: { - width: "100%", - }, - nonInteractive: { - pointerEvents: "none", - }, - // Simulates TableSelectionCell which is 44px with a centered 32px Checkbox. - // The inline padding is (44 - skeleton size) / 2 - // (TableLayoutCell has 8px inline padding by default.) - fakeSelectionCell: { - width: "44px", - maxWidth: "44px", - boxSizing: "border-box", - ...shorthands.paddingInline("12px"), - }, -}); - -// `undefined` doesn't set a width and can be used in place of TableSelectionCell -type ColumnWidth = undefined | keyof ReturnType; - -const minRandomRows = 3; -const maxRandomRows = 10; - -interface SkeletonTableBodyProps { - rows?: number; - rowType?: keyof ReturnType; - widths: Array; -} - -// should be wrapped by according to docs, -// however this is only necessary if overriding `animation` or `appearance`. -// seems to work at any level above in the hierachy, -// so it could perhaps go outside the whole Table to avoid interfering with the -// layout, although this is less of a problem when using `noNativeElements`. -export function SkeletonTableBody( - { rows, rowType = "normal", widths }: SkeletonTableBodyProps -) { - const rowCount = useMemo( - () => - rows - || (minRandomRows - + Math.floor((maxRandomRows - minRandomRows + 1) * Math.random())), - [rows] - ); - const rowKeys = Array.from({ length: rowCount }, (_, k) => k); - - const styles = useSkeletonTableBodyStyles(); - const rowStyles = useRowStyles(); - const rowStyle = mergeClasses(rowStyles[rowType], styles.nonInteractive); - - const columnStyles = useColumnStyles(); - return ( - - {rowKeys.map((rowKey) => ( - - {widths.map((w, cellKey) => { - const hasDefinedWidth = !!(w && columnStyles[w]); - if (hasDefinedWidth) { - return ( - - - - ); - } - return ( -
- -
- ); - })} -
- ))} -
- ); -} diff --git a/examples/src/stories/theme-page.tsx b/examples/src/stories/theme-page.tsx index 212a8f6c..95fb3322 100644 --- a/examples/src/stories/theme-page.tsx +++ b/examples/src/stories/theme-page.tsx @@ -21,16 +21,15 @@ import { TabList, Theme, } from "@fluentui/react-components"; -import { useAppContext } from "../context/ApplicationStateProvider"; import { DarkThemeRegular } from "@fluentui/react-icons"; import React, { memo, useCallback, useState } from "react"; -import { PageHeader } from "../components/page-header"; import { SimpleHeader } from "../components/simple-header"; -import { - useFixedPageStyle, - useLayoutStyles, - useScrollPageStyle, -} from "../styles/page"; +import { StoryPage } from "../components/story/story-page"; +import { StorySection } from "../components/story/story-section"; +import { useAppContext } from "../context/ApplicationStateProvider"; +import { getGhInfoByKey } from "../routing/route-map"; +import { routes } from "../routing/routes"; +import { useLayoutStyles, useScrollPageStyle } from "../styles/page"; const useStyles = makeStyles({ tablist: { @@ -133,16 +132,17 @@ const tokenVariant = { status: "colorStatus", } as const; -type TtokenVariant = typeof tokenVariant[keyof typeof tokenVariant]; +type TTokenVariant = typeof tokenVariant[keyof typeof tokenVariant]; export const ThemePage = () => { + const gh = getGhInfoByKey(routes.Theme); + const styles = useStyles(); - const fixedPageStyle = useFixedPageStyle(); const scrollPageStyle = useScrollPageStyle(); const layoutStyles = useLayoutStyles(); const [selectedTab, setSelectedTab] = useState(axisThemes.main); - const [selectedVariant, setSelectedVariant] = useState( + const [selectedVariant, setSelectedVariant] = useState( tokenVariant.brand ); @@ -158,7 +158,7 @@ export const ThemePage = () => { const onVariantSelect = useCallback( (_: SelectTabEvent, { value }: SelectTabData) => - setSelectedVariant(value as TtokenVariant), + setSelectedVariant(value as TTokenVariant), [] ); @@ -173,13 +173,13 @@ export const ThemePage = () => { }, [selectedTab, setAppTheme]); return ( -
- -
+ +
@@ -234,7 +234,7 @@ export const ThemePage = () => { />
-
-
+ + ); }; diff --git a/examples/src/stories/vertical-stepper-page.tsx b/examples/src/stories/vertical-stepper-page.tsx deleted file mode 100644 index b8897c79..00000000 --- a/examples/src/stories/vertical-stepper-page.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import React, { useCallback, useState } from "react"; - -import { Stepper, StepperDialog } from "@axiscommunications/fluent-stepper"; -import { mergeClasses } from "@fluentui/react-components"; -import { PageHeader } from "../components/page-header"; -import { SectionTitle } from "../components/section-title"; -import { steps } from "./stepper-page"; -import { useLayoutStyles, useScrollPageStyle } from "../styles/page"; - -export const VerticalStepperPage = () => { - const [step, setStep] = useState(0); - const scrollPageStyle = useScrollPageStyle(); - const layoutStyles = useLayoutStyles(); - const onFinish = useCallback(() => alert("Finish!"), []); - const onCancel = useCallback(() => setStep(0), []); - return ( -
- -
- -
- -
- -
-
-
-
- ); -};