diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index c206d79..b268636 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -22,7 +22,8 @@ "eamodio.gitlens", "wayou.vscode-todo-highlight", "PKief.material-icon-theme", - "yoavbls.pretty-ts-errors" + "yoavbls.pretty-ts-errors", + "unifiedjs.vscode-mdx" ] } }, diff --git a/.eslintrc.yml b/.eslintrc.yml index 738aa22..716f75c 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -50,8 +50,6 @@ extends: - "plugin:jest/style" # -- STORYBOOK -- # NOTE: Quote from docs: "This plugin will only be applied to files following the *.stories.* (we recommend this) or *.story.* pattern." - # Its recommended to lint also the config files in .storybook, which in our current file context, is painfull to achieve, so this is emmited. - # It would not have made any major difference anyhow, can only help with mistyped addon names, we will survive without it... - "plugin:storybook/recommended" # --------------------------- Formatting ------------------------- diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..bf357fb --- /dev/null +++ b/.prettierrc @@ -0,0 +1,3 @@ +{ + "trailingComma": "all" +} diff --git a/.storybook/main.ts b/.storybook/main.ts index 9346b83..7e085f9 100644 --- a/.storybook/main.ts +++ b/.storybook/main.ts @@ -1,7 +1,7 @@ import { StorybookConfig } from "@storybook/react-vite"; const config: StorybookConfig = { - stories: ["../**/stories.@(ts|tsx)"], + stories: ["../**/stories.@(ts|tsx)", "../**/readme.mdx"], addons: ["@storybook/addon-essentials"], framework: "@storybook/react-vite", }; diff --git a/package-lock.json b/package-lock.json index e9c8abf..9d3d030 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "fluentui-helpers", - "version": "0.0.1", + "version": "0.0.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "fluentui-helpers", - "version": "0.0.1", + "version": "0.0.3", "license": "MIT", "devDependencies": { "@babel/preset-env": "^7.25.9", diff --git a/package.json b/package.json index 06a8a14..75344b6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "fluentui-helpers", - "version": "0.0.3", + "version": "0.1.0", "description": "Helper library for microsofts fluentui react library", "main": "src/index.ts", "author": "bubulus", diff --git a/readme.md b/readme.md index bda3328..e4769d0 100644 --- a/readme.md +++ b/readme.md @@ -1,3 +1,29 @@ -TEST FOR PUBLISHING; UNSTABLE DO NOT USE +**Disclaimer:** _I am not affiliated in any way with Microsoft or Fluent UI. This is **NOT** an official repository from the Fluent UI ecosystem_. -FURTHER REL FOR PIPELINE TEST 0.0.2 +### Who is this for? + +This library is aimed at folks who work with Microsoft's Fluent UI library but think it lacks some features here and there. + +### Is this stable? + +Not quite yet, from `1.*` onward it will be, as long as its in `0.*` consider it unstable. + +### What does it contain? + +It contains helpful hooks, components, and theme utilities that I was maintaining across different projects. I decided to create this central repository to share these utilities. + +These utilities can be common layout utilities (like `Flex`, `Grid`, etc.), generic and library-specific hooks, extended theme tokens, common animations, and also full-fledged components that are not yet available in Fluent UI itself (like `Pagination`, for instance). + +### Where can I find the docs? + +The library documentation resides at a dedicated [Storybook](https://bubulux.github.io/fluentui-helpers). You will also find planned updates and preview components there. + +### How long will I be committed to this? + +I am maintaining two larger projects with Fluent UI, one private and one for my company, so there are long-lasting factors that will bind me to this for a while. + +### How to contribute or report bugs? + +If there are any bugs, please open an issue on GitHub. + +If you want to contribute (other than a bug fix) or suggest a change, open a discussion thread :) diff --git a/src/components/layout/Flex/component.tsx b/src/components/layout/Flex/component.tsx new file mode 100644 index 0000000..dac19ca --- /dev/null +++ b/src/components/layout/Flex/component.tsx @@ -0,0 +1,121 @@ +import React from "react"; +import type { ReactNode } from "react"; + +import { mergeClasses } from "@fluentui/react-components"; +import type { TThemeSpacing, TThemeShorthandSpacing } from "@theme"; + +import { + useGap, + useFlexBox, + useMargin, + usePadding, + useShorthandDimension, + useAriaProps, + usePosition, +} from "@components/layout/Flex/hooks"; + +import type { + TFlexDirection, + TFlexOption, + TFlexShorthandDimensions, + TFlexPosition, +} from "@components/layout/Flex/types"; + +type TProps = { + children: ReactNode; + position?: TFlexPosition; + direction?: TFlexDirection; + justifyContent?: TFlexOption; + alignItems?: TFlexOption; + wrap?: boolean; + className?: string; + gap?: TThemeSpacing; + margin?: TThemeShorthandSpacing; + padding?: TThemeShorthandSpacing; + shWidth?: TFlexShorthandDimensions; + shHeight?: TFlexShorthandDimensions; + testId?: string; +}; + +/** + * @description + * - fluent does not provide a `Flex` component for consistent layout (it was removed in the latest version) + * - having this allows to use fewer makeStyles call and repeting flex configurations in the code + * - its especially usefull when certain layout styles have to be applied conditionally + * - for this the entire conditional logic is abstracted inside this component, providing very much styled-component like ergonomics + * - supports direct data-testid prop as well as all aria props + * + * @props + * - `direction`: flex-direction property + * - `justifyContent`: justify-content property + * - `alignItems`: align-items property + * - `wrap`: flex-wrap property + * - `gap`: gap between children, with fixed predefined values from the design system, not discriminating between horizontal and vertical gap (because there are literally the same values) + * - `margin`: margin property, using the same values like gap, expects the shorthand notation + * - `padding`: same like margin, but for padding, concrete example below + * + * ```jsx + * // the shorthand is not a simple string, but rather defined as an array that can be of size 1 up to 4 + * // each element provides additional restraint from the design system tokens + * // following examples will only use padding, but the same applies to margin + * // the * between tokens.spacing and the further specifier can be interpreted as either horizontal or vertical (both the same values, reasoning is explained in gap comment above) + * + * // like saying padding: tokens.spacing*S; + * // like saying padding: `${tokens.spacing*S} ${tokens.spacing*M}`; + * // like saying padding: `${tokens.spacing*S} ${tokens.spacing*M} ${tokens.spacing*L}`; + * // like saying padding: `${tokens.spacing*S} ${tokens.spacing*M} ${tokens.spacing*L} ${tokens.spacing*XL}`; + * + * ``` + * - `shWidth`: shorthand for width property + * - `shHeight`: shorthand for height property + * - `className`: to add additional classes to the component, will override all specified styles from props + * - `aria-*`: all aria props are supported, they will be spread on the root div + * - `testId`: passed down the data-testid attribute + * + * + * @default + * direction = "row", justifyContent = "start", alignItems = "start", wrap = false, gap = "None", margin = ["None"], padding = ["None"], shHeight = "auto", shWidth = "auto" + */ +export default function Flex({ + direction = "row", + position = "static", + justifyContent = "start", + alignItems = "start", + wrap = false, + gap = "None", + margin = ["None"], + padding = ["None"], + shHeight = "auto", + shWidth = "auto", + className = undefined, + testId = undefined, + children, + ...rest +}: TProps) { + const flexBoxClass = useFlexBox(justifyContent, alignItems, direction, wrap); + const gapClass = useGap(gap); + const marginClass = useMargin(margin); + const paddingClass = usePadding(padding); + const dimensionClass = useShorthandDimension(shWidth, shHeight); + const positionClass = usePosition(position); + const ariaProps = useAriaProps(rest); + return ( +
+ {children} +
+ ); +} diff --git a/src/components/layout/Flex/func.tsx b/src/components/layout/Flex/func.tsx deleted file mode 100644 index c96bcd7..0000000 --- a/src/components/layout/Flex/func.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export default function Flex() { - return
Flex
; -} diff --git a/src/components/layout/Flex/hooks/index.ts b/src/components/layout/Flex/hooks/index.ts new file mode 100644 index 0000000..47af25e --- /dev/null +++ b/src/components/layout/Flex/hooks/index.ts @@ -0,0 +1,17 @@ +import useGap from "@components/layout/Flex/hooks/useGap"; +import useMargin from "@components/layout/Flex/hooks/useMargin"; +import usePadding from "@components/layout/Flex/hooks/usePadding"; +import useFlexBox from "@components/layout/Flex/hooks/useFlexBox"; +import useShorthandDimension from "@components/layout/Flex/hooks/useShorthandDimension"; +import useAriaProps from "@components/layout/Flex/hooks/useAriaProps"; +import usePosition from "@components/layout/Flex/hooks/usePosition"; + +export { + useGap, + useMargin, + usePadding, + useFlexBox, + useShorthandDimension, + useAriaProps, + usePosition, +}; diff --git a/src/components/layout/Flex/hooks/useAriaProps.ts b/src/components/layout/Flex/hooks/useAriaProps.ts new file mode 100644 index 0000000..7e187c8 --- /dev/null +++ b/src/components/layout/Flex/hooks/useAriaProps.ts @@ -0,0 +1,13 @@ +type TAriaAttributes = Record; + +export default function useAriaAprops( + props: Record, +): TAriaAttributes { + return Object.keys(props).reduce((acc, key) => { + if (key.startsWith("aria-")) { + // @ts-expect-error - we know that key is a string, its just difficult to prove to TS by doing key.startsWith + acc[key] = props[key]; + } + return acc; + }, {}); +} diff --git a/src/components/layout/Flex/hooks/useFlexBox.ts b/src/components/layout/Flex/hooks/useFlexBox.ts new file mode 100644 index 0000000..8cebc82 --- /dev/null +++ b/src/components/layout/Flex/hooks/useFlexBox.ts @@ -0,0 +1,36 @@ +import { mergeClasses } from "@fluentui/react-components"; + +import { useFlexBoxClasses } from "@components/layout/Flex/styles"; + +import type { + TFlexDirection, + TFlexOption, +} from "@components/layout/Flex/types"; + +export default function useFlexBox( + justifyContent?: TFlexOption, + alignItems?: TFlexOption, + direction?: TFlexDirection, + wrap?: boolean, +) { + const classes = useFlexBoxClasses(); + const directionClass = direction + ? classes[`${direction}Direction`] + : undefined; + const justifyContentClass = justifyContent + ? classes[`${justifyContent}Content`] + : undefined; + const alignItemsClass = alignItems + ? classes[`${alignItems}Items`] + : undefined; + + const wrapClass = wrap ? classes.wrap : classes.nowrap; + + return mergeClasses( + classes.base, + directionClass, + justifyContentClass, + alignItemsClass, + wrapClass, + ); +} diff --git a/src/components/layout/Flex/hooks/useGap.ts b/src/components/layout/Flex/hooks/useGap.ts new file mode 100644 index 0000000..8809c84 --- /dev/null +++ b/src/components/layout/Flex/hooks/useGap.ts @@ -0,0 +1,9 @@ +import { useGapClasses } from "@components/layout/Flex/styles"; +import type { TThemeSpacing } from "@theme"; + +function useGap(gap?: TThemeSpacing) { + const classes = useGapClasses(); + return gap ? classes[`gap${gap}`] : undefined; +} + +export default useGap; diff --git a/src/components/layout/Flex/hooks/useMargin.ts b/src/components/layout/Flex/hooks/useMargin.ts new file mode 100644 index 0000000..799f764 --- /dev/null +++ b/src/components/layout/Flex/hooks/useMargin.ts @@ -0,0 +1,39 @@ +import { useMarginClasses } from "@components/layout/Flex/styles"; +import { mergeClasses } from "@fluentui/react-components"; + +import type { TThemeShorthandSpacing } from "@theme"; + +function useMargin(margin?: TThemeShorthandSpacing) { + const classes = useMarginClasses(); + + if (margin === undefined) { + return "noMarginValue"; + } + if (margin.length === 1) { + return classes[`margin${margin[0]}`]; + } + if (margin.length === 2) { + return mergeClasses( + classes[`marginTop${margin[0]}`], + classes[`marginRight${margin[1]}`], + classes[`marginBottom${margin[0]}`], + classes[`marginLeft${margin[1]}`], + ); + } + if (margin.length === 3) { + return mergeClasses( + classes[`marginTop${margin[0]}`], + classes[`marginRight${margin[1]}`], + classes[`marginBottom${margin[2]}`], + classes[`marginLeft${margin[1]}`], + ); + } + return mergeClasses( + classes[`marginTop${margin[0]}`], + classes[`marginRight${margin[1]}`], + classes[`marginBottom${margin[2]}`], + classes[`marginLeft${margin[3]}`], + ); +} + +export default useMargin; diff --git a/src/components/layout/Flex/hooks/usePadding.ts b/src/components/layout/Flex/hooks/usePadding.ts new file mode 100644 index 0000000..bbaa4d6 --- /dev/null +++ b/src/components/layout/Flex/hooks/usePadding.ts @@ -0,0 +1,39 @@ +import { usePaddingClasses } from "@components/layout/Flex/styles"; +import { mergeClasses } from "@fluentui/react-components"; + +import type { TThemeShorthandSpacing } from "@theme"; + +function usePadding(padding?: TThemeShorthandSpacing) { + const classes = usePaddingClasses(); + + if (padding === undefined) { + return "noPaddingValue"; + } + if (padding.length === 1) { + return classes[`padding${padding[0]}`]; + } + if (padding.length === 2) { + return mergeClasses( + classes[`paddingTop${padding[0]}`], + classes[`paddingRight${padding[1]}`], + classes[`paddingBottom${padding[0]}`], + classes[`paddingLeft${padding[1]}`], + ); + } + if (padding.length === 3) { + return mergeClasses( + classes[`paddingTop${padding[0]}`], + classes[`paddingRight${padding[1]}`], + classes[`paddingBottom${padding[2]}`], + classes[`paddingLeft${padding[1]}`], + ); + } + return mergeClasses( + classes[`paddingTop${padding[0]}`], + classes[`paddingRight${padding[1]}`], + classes[`paddingBottom${padding[2]}`], + classes[`paddingLeft${padding[3]}`], + ); +} + +export default usePadding; diff --git a/src/components/layout/Flex/hooks/usePosition.ts b/src/components/layout/Flex/hooks/usePosition.ts new file mode 100644 index 0000000..de36b5e --- /dev/null +++ b/src/components/layout/Flex/hooks/usePosition.ts @@ -0,0 +1,8 @@ +import type { TFlexPosition } from "@components/layout/Flex/types"; + +import { usePositionClasses } from "@components/layout/Flex/styles"; + +export default function usePosition(position: TFlexPosition) { + const classes = usePositionClasses(); + return classes[position]; +} diff --git a/src/components/layout/Flex/hooks/useShorthandDimension.ts b/src/components/layout/Flex/hooks/useShorthandDimension.ts new file mode 100644 index 0000000..1ffb935 --- /dev/null +++ b/src/components/layout/Flex/hooks/useShorthandDimension.ts @@ -0,0 +1,17 @@ +import { mergeClasses } from "@fluentui/react-components"; + +import { useDimensionClasses } from "@components/layout/Flex/styles"; + +import type { TFlexShorthandDimensions } from "@components/layout/Flex/types"; + +export default function useShorthandDimension( + shorthandWidth: TFlexShorthandDimensions, + shorthandHeight: TFlexShorthandDimensions, +) { + const classes = useDimensionClasses(); + + const widthClass = classes[`${shorthandWidth}Width`]; + const heightClass = classes[`${shorthandHeight}Height`]; + + return mergeClasses(widthClass, heightClass); +} diff --git a/src/components/layout/Flex/index.ts b/src/components/layout/Flex/index.ts new file mode 100644 index 0000000..e26aa32 --- /dev/null +++ b/src/components/layout/Flex/index.ts @@ -0,0 +1,3 @@ +import Flex from "@components/layout/Flex/component"; + +export default Flex; diff --git a/src/components/layout/Flex/stories.ts b/src/components/layout/Flex/stories.ts deleted file mode 100644 index b31361b..0000000 --- a/src/components/layout/Flex/stories.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type { Meta, StoryObj } from "@storybook/react"; - -import Flex from "@components/layout/Flex/func"; - -const meta: Meta = { - title: "Components/Layout/Flex", - component: Flex, - args: {}, -}; - -export default meta; - -type Story = StoryObj; - -export const Index: Story = {}; diff --git a/src/components/layout/Flex/stories.tsx b/src/components/layout/Flex/stories.tsx new file mode 100644 index 0000000..5ec2523 --- /dev/null +++ b/src/components/layout/Flex/stories.tsx @@ -0,0 +1,79 @@ +import type { Meta, StoryObj } from "@storybook/react"; + +import { Flex } from "@components"; +import { makeStyles } from "@fluentui/react-components"; + +import type { TThemeSpacing } from "@theme"; + +const meta: Meta = { + component: Flex, + title: "Components/Layout/Flex", + args: { + children: ( + <> +
+ Child-1 +
+
+ Child-2 +
+ + ), + }, + argTypes: { + className: { control: false }, + children: { control: false }, + gap: { + control: "select", + options: [ + "XXS", + "XS", + "SNudge", + "S", + "MNudge", + "M", + "L", + "XL", + "XXL", + "XXXL", + ] as TThemeSpacing[], + }, + }, +}; + +export default meta; + +type TStory = StoryObj; + +const useClasses = makeStyles({ + root: { + border: "4px solid black", + height: "100px", + }, +}); + +export const Index: TStory = { + args: {}, + render: (args) => { + // eslint-disable-next-line react-hooks/rules-of-hooks + const classes = useClasses(); + return ( + // eslint-disable-next-line react/jsx-props-no-spreading + + {args.children} + + ); + }, +}; diff --git a/src/components/layout/Flex/styles/dimensions.ts b/src/components/layout/Flex/styles/dimensions.ts new file mode 100644 index 0000000..356a216 --- /dev/null +++ b/src/components/layout/Flex/styles/dimensions.ts @@ -0,0 +1,37 @@ +import { makeStyles } from "@fluentui/react-components"; + +const useDimensionClasses = makeStyles({ + autoWidth: { + width: "auto", + }, + "25%Width": { + width: "25%", + }, + "50%Width": { + width: "50%", + }, + "75%Width": { + width: "75%", + }, + "100%Width": { + width: "100%", + }, + + autoHeight: { + height: "auto", + }, + "25%Height": { + height: "25%", + }, + "50%Height": { + height: "50%", + }, + "75%Height": { + height: "75%", + }, + "100%Height": { + height: "100%", + }, +}); + +export default useDimensionClasses; diff --git a/src/components/layout/Flex/styles/flexBox.ts b/src/components/layout/Flex/styles/flexBox.ts new file mode 100644 index 0000000..272f5a3 --- /dev/null +++ b/src/components/layout/Flex/styles/flexBox.ts @@ -0,0 +1,67 @@ +import { makeStyles } from "@fluentui/react-components"; + +const useFlexBoxClasses = makeStyles({ + base: { + display: "flex", + }, + rowDirection: { + flexDirection: "row", + }, + columnDirection: { + flexDirection: "column", + }, + // justify-content + centerContent: { + justifyContent: "center", + }, + startContent: { + justifyContent: "flex-start", + }, + endContent: { + justifyContent: "flex-end", + }, + spaceBetweenContent: { + justifyContent: "space-between", + }, + spaceAroundContent: { + justifyContent: "space-around", + }, + spaceEvenlyContent: { + justifyContent: "space-evenly", + }, + stretchContent: { + justifyContent: "stretch", + }, + // align-items + centerItems: { + alignItems: "center", + }, + startItems: { + alignItems: "flex-start", + }, + endItems: { + alignItems: "flex-end", + }, + stretchItems: { + alignItems: "stretch", + }, + spaceBetweenItems: { + alignItems: "space-between", + }, + spaceAroundItems: { + alignItems: "space-around", + }, + spaceEvenlyItems: { + alignItems: "space-evenly", + }, + + // wrap + wrap: { + flexWrap: "wrap", + }, + nowrap: { + flexWrap: "nowrap", + }, +}); + +export default useFlexBoxClasses; diff --git a/src/components/layout/Flex/styles/gap.ts b/src/components/layout/Flex/styles/gap.ts new file mode 100644 index 0000000..5ddf608 --- /dev/null +++ b/src/components/layout/Flex/styles/gap.ts @@ -0,0 +1,40 @@ +import { makeStyles } from "@fluentui/react-components"; +import { EThemeSpacing } from "@theme"; + +const useGapClasses = makeStyles({ + gapNone: { + gap: EThemeSpacing.None, + }, + gapXXS: { + gap: EThemeSpacing.XXS, + }, + gapXS: { + gap: EThemeSpacing.XS, + }, + gapSNudge: { + gap: EThemeSpacing.SNudge, + }, + gapS: { + gap: EThemeSpacing.S, + }, + gapMNudge: { + gap: EThemeSpacing.MNudge, + }, + gapM: { + gap: EThemeSpacing.M, + }, + gapL: { + gap: EThemeSpacing.L, + }, + gapXL: { + gap: EThemeSpacing.XL, + }, + gapXXL: { + gap: EThemeSpacing.XXL, + }, + gapXXXL: { + gap: EThemeSpacing.XXXL, + }, +}); + +export default useGapClasses; diff --git a/src/components/layout/Flex/styles/index.ts b/src/components/layout/Flex/styles/index.ts new file mode 100644 index 0000000..71d4db6 --- /dev/null +++ b/src/components/layout/Flex/styles/index.ts @@ -0,0 +1,15 @@ +import useFlexBoxClasses from "@components/layout/Flex/styles/flexBox"; +import useMarginClasses from "@components/layout/Flex/styles/margin"; +import usePaddingClasses from "@components/layout/Flex/styles/padding"; +import useGapClasses from "@components/layout/Flex/styles/gap"; +import useDimensionClasses from "@components/layout/Flex/styles/dimensions"; +import usePositionClasses from "@components/layout/Flex/styles/position"; + +export { + useDimensionClasses, + useFlexBoxClasses, + useMarginClasses, + usePaddingClasses, + useGapClasses, + usePositionClasses, +}; diff --git a/src/components/layout/Flex/styles/margin.ts b/src/components/layout/Flex/styles/margin.ts new file mode 100644 index 0000000..98a8943 --- /dev/null +++ b/src/components/layout/Flex/styles/margin.ts @@ -0,0 +1,172 @@ +import { makeStyles } from "@fluentui/react-components"; +import { EThemeSpacing } from "@theme"; + +const useMarginClasses = makeStyles({ + marginNone: { + margin: EThemeSpacing.None, + }, + marginXXS: { + margin: EThemeSpacing.XXS, + }, + marginXS: { + margin: EThemeSpacing.XS, + }, + marginSNudge: { + margin: EThemeSpacing.SNudge, + }, + marginS: { + margin: EThemeSpacing.S, + }, + marginMNudge: { + margin: EThemeSpacing.MNudge, + }, + marginM: { + margin: EThemeSpacing.M, + }, + marginL: { + margin: EThemeSpacing.L, + }, + marginXL: { + margin: EThemeSpacing.XL, + }, + marginXXL: { + margin: EThemeSpacing.XXL, + }, + marginXXXL: { + margin: EThemeSpacing.XXXL, + }, + marginLeftNone: { + marginLeft: EThemeSpacing.None, + }, + marginLeftXXS: { + marginLeft: EThemeSpacing.XXS, + }, + marginLeftXS: { + marginLeft: EThemeSpacing.XS, + }, + marginLeftSNudge: { + marginLeft: EThemeSpacing.SNudge, + }, + marginLeftS: { + marginLeft: EThemeSpacing.S, + }, + marginLeftMNudge: { + marginLeft: EThemeSpacing.MNudge, + }, + marginLeftM: { + marginLeft: EThemeSpacing.M, + }, + marginLeftL: { + marginLeft: EThemeSpacing.L, + }, + marginLeftXL: { + marginLeft: EThemeSpacing.XL, + }, + marginLeftXXL: { + marginLeft: EThemeSpacing.XXL, + }, + marginLeftXXXL: { + marginLeft: EThemeSpacing.XXXL, + }, + marginRightNone: { + marginRight: EThemeSpacing.None, + }, + marginRightXXS: { + marginRight: EThemeSpacing.XXS, + }, + marginRightXS: { + marginRight: EThemeSpacing.XS, + }, + marginRightSNudge: { + marginRight: EThemeSpacing.SNudge, + }, + marginRightS: { + marginRight: EThemeSpacing.S, + }, + marginRightMNudge: { + marginRight: EThemeSpacing.MNudge, + }, + marginRightM: { + marginRight: EThemeSpacing.M, + }, + marginRightL: { + marginRight: EThemeSpacing.L, + }, + marginRightXL: { + marginRight: EThemeSpacing.XL, + }, + marginRightXXL: { + marginRight: EThemeSpacing.XXL, + }, + marginRightXXXL: { + marginRight: EThemeSpacing.XXXL, + }, + marginTopNone: { + marginTop: EThemeSpacing.None, + }, + marginTopXXS: { + marginTop: EThemeSpacing.XXS, + }, + marginTopXS: { + marginTop: EThemeSpacing.XS, + }, + marginTopSNudge: { + marginTop: EThemeSpacing.SNudge, + }, + marginTopS: { + marginTop: EThemeSpacing.S, + }, + marginTopMNudge: { + marginTop: EThemeSpacing.MNudge, + }, + marginTopM: { + marginTop: EThemeSpacing.M, + }, + marginTopL: { + marginTop: EThemeSpacing.L, + }, + marginTopXL: { + marginTop: EThemeSpacing.XL, + }, + marginTopXXL: { + marginTop: EThemeSpacing.XXL, + }, + marginTopXXXL: { + marginTop: EThemeSpacing.XXXL, + }, + marginBottomNone: { + marginBottom: EThemeSpacing.None, + }, + marginBottomXXS: { + marginBottom: EThemeSpacing.XXS, + }, + marginBottomXS: { + marginBottom: EThemeSpacing.XS, + }, + marginBottomSNudge: { + marginBottom: EThemeSpacing.SNudge, + }, + marginBottomS: { + marginBottom: EThemeSpacing.S, + }, + marginBottomMNudge: { + marginBottom: EThemeSpacing.MNudge, + }, + marginBottomM: { + marginBottom: EThemeSpacing.M, + }, + marginBottomL: { + marginBottom: EThemeSpacing.L, + }, + marginBottomXL: { + marginBottom: EThemeSpacing.XL, + }, + marginBottomXXL: { + marginBottom: EThemeSpacing.XXL, + }, + marginBottomXXXL: { + marginBottom: EThemeSpacing.XXXL, + }, +}); + +export default useMarginClasses; diff --git a/src/components/layout/Flex/styles/padding.ts b/src/components/layout/Flex/styles/padding.ts new file mode 100644 index 0000000..a7f073e --- /dev/null +++ b/src/components/layout/Flex/styles/padding.ts @@ -0,0 +1,172 @@ +import { makeStyles } from "@fluentui/react-components"; +import { EThemeSpacing } from "@theme"; + +const usePaddingClasses = makeStyles({ + paddingNone: { + padding: EThemeSpacing.None, + }, + paddingXXS: { + padding: EThemeSpacing.XXS, + }, + paddingXS: { + padding: EThemeSpacing.XS, + }, + paddingSNudge: { + padding: EThemeSpacing.SNudge, + }, + paddingS: { + padding: EThemeSpacing.S, + }, + paddingMNudge: { + padding: EThemeSpacing.MNudge, + }, + paddingM: { + padding: EThemeSpacing.M, + }, + paddingL: { + padding: EThemeSpacing.L, + }, + paddingXL: { + padding: EThemeSpacing.XL, + }, + paddingXXL: { + padding: EThemeSpacing.XXL, + }, + paddingXXXL: { + padding: EThemeSpacing.XXXL, + }, + paddingLeftNone: { + paddingLeft: EThemeSpacing.None, + }, + paddingLeftXXS: { + paddingLeft: EThemeSpacing.XXS, + }, + paddingLeftXS: { + paddingLeft: EThemeSpacing.XS, + }, + paddingLeftSNudge: { + paddingLeft: EThemeSpacing.SNudge, + }, + paddingLeftS: { + paddingLeft: EThemeSpacing.S, + }, + paddingLeftMNudge: { + paddingLeft: EThemeSpacing.MNudge, + }, + paddingLeftM: { + paddingLeft: EThemeSpacing.M, + }, + paddingLeftL: { + paddingLeft: EThemeSpacing.L, + }, + paddingLeftXL: { + paddingLeft: EThemeSpacing.XL, + }, + paddingLeftXXL: { + paddingLeft: EThemeSpacing.XXL, + }, + paddingLeftXXXL: { + paddingLeft: EThemeSpacing.XXXL, + }, + paddingRightNone: { + paddingRight: EThemeSpacing.None, + }, + paddingRightXXS: { + paddingRight: EThemeSpacing.XXS, + }, + paddingRightXS: { + paddingRight: EThemeSpacing.XS, + }, + paddingRightSNudge: { + paddingRight: EThemeSpacing.SNudge, + }, + paddingRightS: { + paddingRight: EThemeSpacing.S, + }, + paddingRightMNudge: { + paddingRight: EThemeSpacing.MNudge, + }, + paddingRightM: { + paddingRight: EThemeSpacing.M, + }, + paddingRightL: { + paddingRight: EThemeSpacing.L, + }, + paddingRightXL: { + paddingRight: EThemeSpacing.XL, + }, + paddingRightXXL: { + paddingRight: EThemeSpacing.XXL, + }, + paddingRightXXXL: { + paddingRight: EThemeSpacing.XXXL, + }, + paddingTopNone: { + paddingTop: EThemeSpacing.None, + }, + paddingTopXXS: { + paddingTop: EThemeSpacing.XXS, + }, + paddingTopXS: { + paddingTop: EThemeSpacing.XS, + }, + paddingTopSNudge: { + paddingTop: EThemeSpacing.SNudge, + }, + paddingTopS: { + paddingTop: EThemeSpacing.S, + }, + paddingTopMNudge: { + paddingTop: EThemeSpacing.MNudge, + }, + paddingTopM: { + paddingTop: EThemeSpacing.M, + }, + paddingTopL: { + paddingTop: EThemeSpacing.L, + }, + paddingTopXL: { + paddingTop: EThemeSpacing.XL, + }, + paddingTopXXL: { + paddingTop: EThemeSpacing.XXL, + }, + paddingTopXXXL: { + paddingTop: EThemeSpacing.XXXL, + }, + paddingBottomNone: { + paddingBottom: EThemeSpacing.None, + }, + paddingBottomXXS: { + paddingBottom: EThemeSpacing.XXS, + }, + paddingBottomXS: { + paddingBottom: EThemeSpacing.XS, + }, + paddingBottomSNudge: { + paddingBottom: EThemeSpacing.SNudge, + }, + paddingBottomS: { + paddingBottom: EThemeSpacing.S, + }, + paddingBottomMNudge: { + paddingBottom: EThemeSpacing.MNudge, + }, + paddingBottomM: { + paddingBottom: EThemeSpacing.M, + }, + paddingBottomL: { + paddingBottom: EThemeSpacing.L, + }, + paddingBottomXL: { + paddingBottom: EThemeSpacing.XL, + }, + paddingBottomXXL: { + paddingBottom: EThemeSpacing.XXL, + }, + paddingBottomXXXL: { + paddingBottom: EThemeSpacing.XXXL, + }, +}); + +export default usePaddingClasses; diff --git a/src/components/layout/Flex/styles/position.ts b/src/components/layout/Flex/styles/position.ts new file mode 100644 index 0000000..9bc1e7e --- /dev/null +++ b/src/components/layout/Flex/styles/position.ts @@ -0,0 +1,42 @@ +import { makeStyles } from "@fluentui/react-components"; + +const usePositionClasses = makeStyles({ + "-moz-initial": { + position: "-moz-initial", + }, + "-webkit-sticky": { + position: "-webkit-sticky", + }, + absolute: { + position: "absolute", + }, + fixed: { + position: "fixed", + }, + inherit: { + position: "inherit", + }, + initial: { + position: "initial", + }, + relative: { + position: "relative", + }, + revert: { + position: "revert", + }, + "revert-layer": { + position: "revert-layer", + }, + static: { + position: "static", + }, + sticky: { + position: "sticky", + }, + unset: { + position: "unset", + }, +}); + +export default usePositionClasses; diff --git a/src/components/layout/Flex/tests.tsx b/src/components/layout/Flex/tests.tsx index 56ae881..0be08c1 100644 --- a/src/components/layout/Flex/tests.tsx +++ b/src/components/layout/Flex/tests.tsx @@ -1,8 +1,388 @@ -// import { render, screen } from '@test-utils'; +import { render, screen } from "@test-utils"; import "@testing-library/jest-dom"; +import { makeStyles } from "@fluentui/react-components"; +import { Flex } from "@components"; + describe("Flex", () => { - it("should render", () => { - expect(true).toBe(true); + it("should render without required props default config", () => { + render(FlexChild); + const FlexElement = screen.getByText("FlexChild"); + + expect(FlexElement).toBeInTheDocument(); + expect(FlexElement).toHaveTextContent("FlexChild"); + expect(FlexElement).toHaveStyle("display: flex"); + expect(FlexElement).toHaveStyle("flex-direction: row"); + expect(FlexElement).toHaveStyle("justify-content: flex-start"); + expect(FlexElement).toHaveStyle("align-items: flex-start"); + expect(FlexElement).toHaveStyle("flex-wrap: nowrap"); + expect(FlexElement).toHaveStyle("gap: 0rem"); + expect(FlexElement).toHaveStyle("margin: 0rem"); + expect(FlexElement).toHaveStyle("padding: 0rem"); + expect(FlexElement).toHaveStyle("height: auto"); + expect(FlexElement).toHaveStyle("width: auto"); + expect(FlexElement).toHaveStyle("position: static"); + }); + describe("when props are passed", () => { + describe("for direction", () => { + it("should render with direction column", () => { + render(FlexChild); + const FlexElement = screen.getByText("FlexChild"); + expect(FlexElement).toHaveStyle("flex-direction: column"); + }); + it("should render with direction row", () => { + render(FlexChild); + const FlexElement = screen.getByText("FlexChild"); + expect(FlexElement).toHaveStyle("flex-direction: row"); + }); + }); + describe("for justifyContent", () => { + it("should render with justifyContent center", () => { + render(FlexChild); + const FlexElement = screen.getByText("FlexChild"); + expect(FlexElement).toHaveStyle("justify-content: center"); + }); + it("should render with justifyContent end", () => { + render(FlexChild); + const FlexElement = screen.getByText("FlexChild"); + expect(FlexElement).toHaveStyle("justify-content: flex-end"); + }); + it("should render with justifyContent spaceBetween", () => { + render(FlexChild); + const FlexElement = screen.getByText("FlexChild"); + expect(FlexElement).toHaveStyle("justify-content: space-between"); + }); + it("should render with justifyContent spaceAround", () => { + render(FlexChild); + const FlexElement = screen.getByText("FlexChild"); + expect(FlexElement).toHaveStyle("justify-content: space-around"); + }); + it("should render with justifyContent spaceEvenly", () => { + render(FlexChild); + const FlexElement = screen.getByText("FlexChild"); + expect(FlexElement).toHaveStyle("justify-content: space-evenly"); + }); + it("should render with justifyContent stretch", () => { + render(FlexChild); + const FlexElement = screen.getByText("FlexChild"); + expect(FlexElement).toHaveStyle("justify-content: stretch"); + }); + }); + describe("for alignItems", () => { + it("should render with alignItems center", () => { + render(FlexChild); + const FlexElement = screen.getByText("FlexChild"); + expect(FlexElement).toHaveStyle("align-items: center"); + }); + it("should render with alignItems end", () => { + render(FlexChild); + const FlexElement = screen.getByText("FlexChild"); + expect(FlexElement).toHaveStyle("align-items: flex-end"); + }); + it("should render with alignItems spaceBetween", () => { + render(FlexChild); + const FlexElement = screen.getByText("FlexChild"); + expect(FlexElement).toHaveStyle("align-items: space-between"); + }); + it("should render with alignItems spaceAround", () => { + render(FlexChild); + const FlexElement = screen.getByText("FlexChild"); + expect(FlexElement).toHaveStyle("align-items: space-around"); + }); + it("should render with alignItems spaceEvenly", () => { + render(FlexChild); + const FlexElement = screen.getByText("FlexChild"); + expect(FlexElement).toHaveStyle("align-items: space-evenly"); + }); + it("should render with alignItems stretch", () => { + render(FlexChild); + const FlexElement = screen.getByText("FlexChild"); + expect(FlexElement).toHaveStyle("align-items: stretch"); + }); + }); + describe("for wrap", () => { + it("should render with wrap wrap", () => { + render(FlexChild); + const FlexElement = screen.getByText("FlexChild"); + expect(FlexElement).toHaveStyle("flex-wrap: wrap"); + }); + it("should render with wrap nowrap", () => { + render(FlexChild); + const FlexElement = screen.getByText("FlexChild"); + expect(FlexElement).toHaveStyle("flex-wrap: nowrap"); + }); + }); + describe("for gap", () => { + it("should render with gap None", () => { + render(FlexChild); + const FlexElement = screen.getByText("FlexChild"); + expect(FlexElement).toHaveStyle("gap: 0rem"); + }); + it("should render with gap XXS", () => { + render(FlexChild); + const FlexElement = screen.getByText("FlexChild"); + expect(FlexElement).toHaveStyle("gap: 0.125rem"); + }); + it("should render with gap XS", () => { + render(FlexChild); + const FlexElement = screen.getByText("FlexChild"); + expect(FlexElement).toHaveStyle("gap: 0.25rem"); + }); + it("should render with gap SNudge", () => { + render(FlexChild); + const FlexElement = screen.getByText("FlexChild"); + expect(FlexElement).toHaveStyle("gap: 0.375rem"); + }); + it("should render with gap S", () => { + render(FlexChild); + const FlexElement = screen.getByText("FlexChild"); + expect(FlexElement).toHaveStyle("gap: 0.5rem"); + }); + it("should render with gap MNudge", () => { + render(FlexChild); + const FlexElement = screen.getByText("FlexChild"); + expect(FlexElement).toHaveStyle("gap: 0.625rem"); + }); + it("should render with gap M", () => { + render(FlexChild); + const FlexElement = screen.getByText("FlexChild"); + expect(FlexElement).toHaveStyle("gap: 0.75rem"); + }); + it("should render with gap L", () => { + render(FlexChild); + const FlexElement = screen.getByText("FlexChild"); + expect(FlexElement).toHaveStyle("gap: 1rem"); + }); + it("should render with gap XL", () => { + render(FlexChild); + const FlexElement = screen.getByText("FlexChild"); + expect(FlexElement).toHaveStyle("gap: 1.25rem"); + }); + it("should render with gap XXL", () => { + render(FlexChild); + const FlexElement = screen.getByText("FlexChild"); + expect(FlexElement).toHaveStyle("gap: 1.5rem"); + }); + it("should render with gap XXXL", () => { + render(FlexChild); + const FlexElement = screen.getByText("FlexChild"); + expect(FlexElement).toHaveStyle("gap: 2rem"); + }); + }); + describe("for margin", () => { + it("should render with margin None", () => { + render(FlexChild); + const FlexElement = screen.getByText("FlexChild"); + expect(FlexElement).toHaveStyle("margin: 0rem"); + }); + it("should render with margin XS, L", () => { + render(FlexChild); + const FlexElement = screen.getByText("FlexChild"); + expect(FlexElement).toHaveStyle("margin: 0.25rem 1rem 0.25rem 1rem"); + }); + it("should render with margin XS, L, XL", () => { + render(FlexChild); + const FlexElement = screen.getByText("FlexChild"); + expect(FlexElement).toHaveStyle("margin: 0.25rem 1rem 1.25rem 1rem"); + }); + it("should render with margin XS, L, XL, XXL", () => { + render(FlexChild); + const FlexElement = screen.getByText("FlexChild"); + expect(FlexElement).toHaveStyle("margin: 0.25rem 1rem 1.25rem 1.5rem"); + }); + }); + describe("for padding", () => { + it("should render with padding None", () => { + render(FlexChild); + const FlexElement = screen.getByText("FlexChild"); + expect(FlexElement).toHaveStyle("padding: 0rem"); + }); + it("should render with padding XS, L", () => { + render(FlexChild); + const FlexElement = screen.getByText("FlexChild"); + expect(FlexElement).toHaveStyle("padding: 0.25rem 1rem 0.25rem 1rem"); + }); + it("should render with padding XS, L, XL", () => { + render(FlexChild); + const FlexElement = screen.getByText("FlexChild"); + expect(FlexElement).toHaveStyle("padding: 0.25rem 1rem 1.25rem 1rem"); + }); + it("should render with padding XS, L, XL, XXL", () => { + render(FlexChild); + const FlexElement = screen.getByText("FlexChild"); + expect(FlexElement).toHaveStyle("padding: 0.25rem 1rem 1.25rem 1.5rem"); + }); + }); + describe("for shHeight", () => { + it("should render with height 100%", () => { + render(FlexChild); + const FlexElement = screen.getByText("FlexChild"); + expect(FlexElement).toHaveStyle("height: 100%"); + }); + it("should render with height 25%", () => { + render(FlexChild); + const FlexElement = screen.getByText("FlexChild"); + expect(FlexElement).toHaveStyle("height: 25%"); + }); + it("should render with height 50%", () => { + render(FlexChild); + const FlexElement = screen.getByText("FlexChild"); + expect(FlexElement).toHaveStyle("height: 50%"); + }); + it("should render with height 75%", () => { + render(FlexChild); + const FlexElement = screen.getByText("FlexChild"); + expect(FlexElement).toHaveStyle("height: 75%"); + }); + it("should render with height auto", () => { + render(FlexChild); + const FlexElement = screen.getByText("FlexChild"); + expect(FlexElement).toHaveStyle("height: auto"); + }); + }); + describe("for shWidth", () => { + it("should render with width 100%", () => { + render(FlexChild); + const FlexElement = screen.getByText("FlexChild"); + expect(FlexElement).toHaveStyle("width: 100%"); + }); + it("should render with width 25%", () => { + render(FlexChild); + const FlexElement = screen.getByText("FlexChild"); + expect(FlexElement).toHaveStyle("width: 25%"); + }); + it("should render with width 50%", () => { + render(FlexChild); + const FlexElement = screen.getByText("FlexChild"); + expect(FlexElement).toHaveStyle("width: 50%"); + }); + it("should render with width 75%", () => { + render(FlexChild); + const FlexElement = screen.getByText("FlexChild"); + expect(FlexElement).toHaveStyle("width: 75%"); + }); + it("should render with width auto", () => { + render(FlexChild); + const FlexElement = screen.getByText("FlexChild"); + expect(FlexElement).toHaveStyle("width: auto"); + }); + }); + describe("for className", () => { + it("should render with the properties applied from the class and override any other props styles", () => { + const useClasses = makeStyles({ + testClass: { + height: "100px", + border: "1px solid black", + }, + }); + + function Wrapper() { + return FlexChild; + } + + render(); + const FlexElement = screen.getByText("FlexChild"); + expect(FlexElement).toHaveStyle("height: 100px"); + expect(FlexElement).toHaveStyle("border: 1px solid black"); + }); + }); + describe("for testId", () => { + it("should render with data-testid", () => { + render(FlexChild); + const FlexElement = screen.getByTestId("testId"); + expect(FlexElement).toBeInTheDocument(); + }); + }); + describe("for aria-* attributes (only checks some, cannot be exhaustive)", () => { + // not exhaustive, but at least one example of each type + const ariaAttributes = [ + { name: "aria-label", value: "test-label" }, + { name: "aria-labelledby", value: "test-labelledby" }, + { name: "aria-describedby", value: "test-describedby" }, + { name: "aria-hidden", value: true }, + { name: "aria-placeholder", value: "test-placeholder" }, + { name: "aria-expanded", value: "true" }, + { name: "aria-controls", value: "test-controls" }, + { name: "aria-pressed", value: "true" }, + { name: "aria-current", value: "page" }, + { name: "aria-invalid", value: "true" }, + { name: "aria-busy", value: "true" }, + { name: "aria-readonly", value: "true" }, + { name: "aria-required", value: "true" }, + { name: "aria-modal", value: "true" }, + { name: "aria-orientation", value: "horizontal" }, + { name: "aria-valuemin", value: "0" }, + { name: "aria-valuemax", value: "100" }, + { name: "aria-valuenow", value: "50" }, + { name: "aria-valuetext", value: "50%" }, + ]; + + ariaAttributes.forEach(({ name, value }) => { + it(`should render with ${name}`, () => { + const props = { [name]: value }; + // eslint-disable-next-line react/jsx-props-no-spreading + render(FlexChild); + const FlexElement = screen.getByText("FlexChild"); + if (value === true) { + // For boolean attributes like aria-hidden + expect(FlexElement).toHaveAttribute(name); + } else { + expect(FlexElement).toHaveAttribute(name, value.toString()); + } + }); + }); + }); + describe("for position", () => { + it("should render with position -moz-initial", () => { + render(FlexChild); + const FlexElement = screen.getByText("FlexChild"); + expect(FlexElement).toHaveStyle("position: -moz-initial"); + }); + it("should render with position -webkit-sticky", () => { + render(FlexChild); + const FlexElement = screen.getByText("FlexChild"); + expect(FlexElement).toHaveStyle("position: -webkit-sticky"); + }); + it("should render with position absolute", () => { + render(FlexChild); + const FlexElement = screen.getByText("FlexChild"); + expect(FlexElement).toHaveStyle("position: absolute"); + }); + it("should render with position fixed", () => { + render(FlexChild); + const FlexElement = screen.getByText("FlexChild"); + expect(FlexElement).toHaveStyle("position: fixed"); + }); + it("should render with position inherit", () => { + render(FlexChild); + const FlexElement = screen.getByText("FlexChild"); + expect(FlexElement).toHaveStyle("position: inherit"); + }); + it("should render with position initial", () => { + render(FlexChild); + const FlexElement = screen.getByText("FlexChild"); + expect(FlexElement).toHaveStyle("position: initial"); + }); + it("should render with position relative", () => { + render(FlexChild); + const FlexElement = screen.getByText("FlexChild"); + expect(FlexElement).toHaveStyle("position: relative"); + }); + it("should render with position revert", () => { + render(FlexChild); + const FlexElement = screen.getByText("FlexChild"); + expect(FlexElement).toHaveStyle("position: revert"); + }); + it("should render with position revert-layer", () => { + render(FlexChild); + const FlexElement = screen.getByText("FlexChild"); + expect(FlexElement).toHaveStyle("position: revert-layer"); + }); + it("should render with position static", () => { + render(FlexChild); + const FlexElement = screen.getByText("FlexChild"); + expect(FlexElement).toHaveStyle("position: static"); + }); + }); }); }); diff --git a/src/components/layout/Flex/types.ts b/src/components/layout/Flex/types.ts new file mode 100644 index 0000000..14334d5 --- /dev/null +++ b/src/components/layout/Flex/types.ts @@ -0,0 +1,28 @@ +export type TFlexDirection = "row" | "column"; + +export type TFlexOption = + | "start" + | "center" + | "end" + | "spaceBetween" + | "spaceAround" + | "spaceEvenly" + | "stretch"; + +export type TFlexWrap = "wrap" | "nowrap"; + +export type TFlexShorthandDimensions = "25%" | "50%" | "75%" | "100%" | "auto"; + +export type TFlexPosition = + | "-moz-initial" + | "-webkit-sticky" + | "absolute" + | "fixed" + | "inherit" + | "initial" + | "relative" + | "revert" + | "revert-layer" + | "static" + | "sticky" + | "unset"; diff --git a/src/components/layout/index.ts b/src/components/layout/index.ts index 15ebd2e..35f4da9 100644 --- a/src/components/layout/index.ts +++ b/src/components/layout/index.ts @@ -1 +1 @@ -export { default as Flex } from "@components/layout/Flex/func"; +export { default as Flex } from "@components/layout/Flex"; diff --git a/src/hooks/useFuiProviderNode.ts b/src/hooks/useFuiProviderNode/hook.ts similarity index 100% rename from src/hooks/useFuiProviderNode.ts rename to src/hooks/useFuiProviderNode/hook.ts diff --git a/src/hooks/useFuiProviderNode/index.ts b/src/hooks/useFuiProviderNode/index.ts new file mode 100644 index 0000000..52d3594 --- /dev/null +++ b/src/hooks/useFuiProviderNode/index.ts @@ -0,0 +1,3 @@ +import useFuiProviderNode from "@hooks/useFuiProviderNode/hook"; + +export default useFuiProviderNode; diff --git a/src/hooks/useFuiProviderNode/stories.tsx b/src/hooks/useFuiProviderNode/stories.tsx new file mode 100644 index 0000000..ac49f98 --- /dev/null +++ b/src/hooks/useFuiProviderNode/stories.tsx @@ -0,0 +1,68 @@ +import type { Meta, StoryObj } from "@storybook/react"; + +import { Tooltip, Button } from "@fluentui/react-components"; +import useFuiProviderNode from "@hooks/useFuiProviderNode"; + +/** + * @description + * - when you use a component in fluent that requires the parent provider node, for example for relative positioning, you can use this hook to get the parent provider node + * - some components like Tooltip for example allow to set a mountNode prop, which is the parent provider node + * - this will only work if you have exactly one provider node in your app + * + * @example + * + * ```tsx + * import { Tooltip, Button } from "@fluentui/react-components"; + * import useFuiProviderNode from "@hooks/useFuiProviderNode"; + * + * export default function SomeHigherComponent() { + * const { fuiProviderNode } = useFuiProviderNode(); + * return ( + * + * + * + * ); + * } + * ``` + */ +export function Wrapper({ hasProviderNode }: { hasProviderNode: boolean }) { + const { fuiProviderNode } = useFuiProviderNode(); + return hasProviderNode ? ( + + + + ) : ( + + + + ); +} + +const meta: Meta = { + title: "Hooks/Specific/useFuiProviderNode", + component: Wrapper, + args: { + hasProviderNode: true, + }, +}; + +export default meta; + +type Story = StoryObj; + +export const WithNodeFromHook: Story = {}; + +export const DefaultProviderNode: Story = { + args: { hasProviderNode: false }, +}; diff --git a/src/readme.mdx b/src/readme.mdx new file mode 100644 index 0000000..c81dcee --- /dev/null +++ b/src/readme.mdx @@ -0,0 +1,24 @@ +import { Meta } from "@storybook/blocks"; + + + +### Implemented + +For the implemented features just browse the Storybook. +Not only visual components are there, but also hooks and other utilities showcase through wrappers. + +### Planned + +#### Components + +- Atoms: Pagination +- Layout: Grid (not sure, might cause more problems than it solves) +- Animations: FadeIn, FadeOut + +#### Hooks + +_nothing planned_ + +#### Theme + +- More materials / Morphism for web: Window 11 brings lots of glass / acrylic effects from the [fluent UI language](https://fluent2.microsoft.design/material), there should also be a way to access these in web apps. diff --git a/src/theme/index.ts b/src/theme/index.ts index bb3a547..5a826f8 100644 --- a/src/theme/index.ts +++ b/src/theme/index.ts @@ -5,3 +5,5 @@ export { EThemeIconSizes, EThemeDimensions, } from "@theme/tokens"; + +export type { TThemeSpacing, TThemeShorthandSpacing } from "@theme/types"; diff --git a/src/theme/tokens/stories.tsx b/src/theme/tokens/stories.tsx new file mode 100644 index 0000000..0b8a424 --- /dev/null +++ b/src/theme/tokens/stories.tsx @@ -0,0 +1,731 @@ +import type { Meta, StoryObj } from "@storybook/react"; + +import { DeleteFilled } from "@fluentui/react-icons"; + +import { Flex } from "@components"; +import { + EThemeDimensions, + EThemeIconSizes, + EThemeSpacing, +} from "@theme/tokens/index"; + +function Wrapper({ token }: { token: "dimension" | "iconSize" | "spacing" }) { + const showcases = { + iconSize: ( + + + + XXS - 16px + + + + XS - 20px + + + S - 24px + + + M - 28px + + + L - 32px + + + + XL - 48px + + + ), + dimension: ( + + +
+ XS1 - 4px + + +
+ XS2 - 8px + + +
+ XS3 - 12px + + +
+ XS4 - 16px + + +
+ XS5 - 20px + + +
+ XS6 - 24px + + +
+ XS7 - 28px + + +
+ XS8 - 32px + + +
+ XS9 - 36px + + +
+ S1 - 40px + + +
+ S2 - 48px + + +
+ S3 - 56px + + +
+ S4 - 64px + + +
+ S5 - 72px + + +
+ S6 - 80px + + +
+ S7 - 88px + + +
+ S8 - 96px + + +
+ S9 - 104px + + +
+ M1 - 116px + + +
+ M2 - 128px + + +
+ M3 - 140px + + +
+ M4 - 152px + + +
+ M5 - 164px + + +
+ M6 - 176px + + +
+ M7 - 188px + + +
+ M8 - 200px + + +
+ M9 - 212px + + +
+ L1 - 228px + + +
+ L2 - 244px + + +
+ L3 - 260px + + +
+ L4 - 276px + + +
+ L5 - 292px + + +
+ L6 - 308px + + +
+ L7 - 324px + + +
+ L8 - 340px + + +
+ L9 - 356px + + +
+ XL1 - 376px + + +
+ XL2 - 392px + + +
+ XL3 - 408px + + +
+ XL4 - 424px + + +
+ XL5 - 440px + + +
+ XL6 - 456px + + +
+ XL7 - 472px + + +
+ XL8 - 488px + + +
+ XL9 - 504px + + +
+ O1 - 528px + + +
+ O2 - 552px + + +
+ O3 - 576px + + +
+ O4 - 600px + + +
+ O5 - 624px + + +
+ O6 - 648px + + +
+ O7 - 672px + + +
+ O8 - 696px + + +
+ O9 - 720px + + +
+ O10 - 744px + + + ), + spacing: ( + + +
+ XXS - 2px + + +
+ XS - 4px + + +
+ SNudge - 6px + + +
+ S - 8px + + +
+ MNudge - 10px + + +
+ M - 12px + + +
+ L - 16px + + +
+ XL - 20px + + +
+ XXL - 24px + + +
+ XXXL - 32px + + + ), + }; + + return showcases[token]; +} + +const meta: Meta = { + title: "Theme/Tokens", + component: Wrapper, + args: { + token: "iconSize", + }, + argTypes: { + token: { + control: false, + }, + }, +}; + +export default meta; + +type Story = StoryObj; + +export const IconSizes: Story = {}; + +export const Spacing: Story = { + args: { + token: "spacing", + }, +}; + +export const Dimensions: Story = { + args: { + token: "dimension", + }, +}; diff --git a/src/theme/types.ts b/src/theme/types.ts new file mode 100644 index 0000000..095897c --- /dev/null +++ b/src/theme/types.ts @@ -0,0 +1,7 @@ +import type { EThemeSpacing } from "@theme/tokens"; + +export type TThemeSpacing = keyof typeof EThemeSpacing; + +type TThemeShorthand = [T] | [T, T] | [T, T, T] | [T, T, T, T]; + +export type TThemeShorthandSpacing = TThemeShorthand; diff --git a/tsconfig.json b/tsconfig.json index b0bbe77..18e8b91 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -24,10 +24,12 @@ // Aliases "baseUrl": ".", "paths": { - "@animations/*": ["src/animations/*"], "@components/*": ["src/components/*"], + "@components": ["src/components/index.ts"], "@hooks/*": ["src/hooks/*"], + "@hooks": ["src/hooks/index.ts"], "@theme/*": ["src/theme/*"], + "@theme": ["src/theme/index.ts"], "@test-utils": ["tests/react-testing-library.tsx"] } },