diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 570f313b7..0b304190a 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -23,6 +23,7 @@ module.exports = { '@typescript-eslint/restrict-plus-operands': 'off', '@typescript-eslint/no-non-null-asserted-optional-chain': 'off', '@typescript-eslint/no-redundant-type-constituents': 'off', + '@typescript-eslint/no-unused-vars': 'warn', '@typescript-eslint/ban-types': [ 'error', { diff --git a/.npmrc b/.npmrc new file mode 100644 index 000000000..a72003c9c --- /dev/null +++ b/.npmrc @@ -0,0 +1,2 @@ +prefer-workspace-packages=true +legacy-peer-deps=true \ No newline at end of file diff --git a/lib/index.ts b/lib/index.ts index 75e0b61ee..fd1865670 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -174,6 +174,7 @@ export * from '@/utils/functions/copyToClipBoard'; export * from '@/utils/functions/calcCSSValuesWithOffset'; export * from '@/utils/hooks/useActiveBreakpoint'; export * from '@/utils/hooks/useBreakpointComparison'; +export * from '@/utils/hooks/useBodyOverflow'; export * from '@/utils/validations/isColorValid'; // ---------- Design ------- // diff --git a/package.json b/package.json index d05cf1a6a..531b7309f 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "name": "TobiTRy" }, "private": false, - "version": "0.5.7", + "version": "0.5.8", "type": "module", "scripts": { "dev": "vite", @@ -48,11 +48,14 @@ "dependencies": { "@types/react-router-dom": "5.3.3", "@types/react-test-renderer": "^18.3.0", + "react": "^18.3.1", + "react-dom": "^18.3.1", "react-router-dom": "^6.23.0", "rollup-preserve-directives": "^1.1.1", "vite-plugin-dts": "^3.9.0" }, "devDependencies": { + "@react-spring/web": "^9.7.3", "@storybook/addon-essentials": "8.0.5", "@storybook/addon-interactions": "8.0.5", "@storybook/addon-links": "8.0.5", @@ -67,12 +70,15 @@ "@swc/core": "^1.4.17", "@testing-library/jest-dom": "^6.4.5", "@testing-library/react": "^15.0.6", - "@types/react": "^18.3.1", - "@types/react-dom": "^18.3.0", + "@types/color": "^3.0.6", + "@types/jest": "^29.5.14", + "@types/mocha": "^10.0.10", + "@types/uuid": "^9.0.8", "@typescript-eslint/eslint-plugin": "^7.2.0", "@typescript-eslint/parser": "^7.2.0", "@vitejs/plugin-react-swc": "^3.6.0", - "eslint": "^8.57.0", + "color": "^4.2.3", + "eslint": "8.57.0", "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-refresh": "^0.4.6", "eslint-plugin-storybook": "0.8.0", @@ -82,15 +88,16 @@ "json": "11.0.0", "lint-staged": "^15.2.2", "prettier": "^3.2.5", - "react": "^18.3.1", - "react-dom": "^18.3.1", "react-test-renderer": "^18.3.1", "rollup-plugin-visualizer": "^5.12.0", "storybook": "8.0.5", "storybook-dark-mode": "4.0.1", + "styled-components": "^6.1.8", "typescript": "^5.0.2", + "uuid": "^9.0.1", "vite": "^5.2.11", - "vitest": "^1.6.0" + "vitest": "2.1.5", + "zustand": "^4.5.2" }, "peerDependencies": { "@react-spring/web": "9.7.3", @@ -142,5 +149,9 @@ }, "publishConfig": { "registry": "https://registry.npmjs.org/" + }, + "resolutions": { + "@types/react": "18.3.1", + "@types/react-dom": "18.3.1" } } diff --git a/src/App.tsx b/src/App.tsx index db5afcf8b..f6d7de214 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -43,6 +43,7 @@ import StepperRoute from '@/Routes/StepperRoute/StepperRoute'; import SwitchActiveIndicatorRoute from '@/Routes/SwitchActiveIndicatorRoute/SwitchActiveIndicatorRoute'; import TextGradientRoute from '@/Routes/TextGradientRoute/TextGradientRoute'; import { useFancyModalStore } from './Routes/ModalRoute/ModalRoute'; +import MaterialColorGeneratorRoute from '@/Routes/MaterialColorGeneratorRoute/MaterialColorGeneratorRoute'; // const Icon = ( // // @@ -167,6 +168,9 @@ function App() {
  • ColorGenerator
  • +
  • + Meterial Color Generator +
  • FancyContent
  • @@ -238,6 +242,7 @@ function App() { } /> } /> } /> + } /> } /> } /> } /> diff --git a/src/Routes/ColorGeneratorRoute/ColorGeneratorRoute.tsx b/src/Routes/ColorGeneratorRoute/ColorGeneratorRoute.tsx index b4b716050..89a5a940d 100644 --- a/src/Routes/ColorGeneratorRoute/ColorGeneratorRoute.tsx +++ b/src/Routes/ColorGeneratorRoute/ColorGeneratorRoute.tsx @@ -1,95 +1,97 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ -import React from 'react'; - -import styled, { css } from 'styled-components'; - +import { FancyButton } from '@/components/organisms/FancyButton'; +import { TLayer } from '@/types'; import themeStore from '../../design/theme/themeStore/themeStore'; -import { TTheme } from '@/types/TTheme'; export default function ColorGeneratorRoute() { const theme = themeStore((state) => state.theme); + const switchTheme = themeStore((state) => state.switchTheme); //get keys of primary - const primaryKeys = Object?.keys(theme?.color.primary); - const accentKeys = Object?.keys(theme?.color.accent); - const secondaryKeys = Object?.keys(theme?.color.secondary); - const infoKeys = Object?.keys(theme?.color.info); - const warningKeys = Object?.keys(theme?.color.warning); - const errorKeys = Object?.keys(theme?.color.error); - const successKeys = Object?.keys(theme?.color.success); + const primaryKeys = Object?.keys(theme.color.primary).map((key) => parseInt(key)); + const accentKeys = Object?.keys(theme.color.accent).map((key) => parseInt(key)); + const secondaryKeys = Object?.keys(theme.color.secondary).map((key) => parseInt(key)); + const infoKeys = Object?.keys(theme.color.info).map((key) => parseInt(key)); + const warningKeys = Object?.keys(theme.color.warning).map((key) => parseInt(key)); + const errorKeys = Object?.keys(theme.color.error).map((key) => parseInt(key)); + const successKeys = Object?.keys(theme.color.success).map((key) => parseInt(key)); return ( -
    -
    - {primaryKeys?.map((color, index) => { - return ( -
    - ); - })} -
    -
    - {accentKeys?.map((color, index) => { - return ( -
    - ); - })} -
    -
    - {infoKeys?.map((color, index) => { - return ( -
    - ); - })} -
    + <> +
    + switchTheme()} /> -
    - {secondaryKeys?.map((color, index) => { - return ( -
    - ); - })} -
    -
    - {warningKeys?.map((color, index) => { - return ( -
    - ); - })} -
    -
    - {errorKeys?.map((color, index) => { - return ( -
    - ); - })} -
    -
    - {successKeys?.map((color, index) => { - return ( -
    - ); - })} +
    + {primaryKeys?.map((color, index) => { + return ( +
    + ); + })} +
    + +
    + {secondaryKeys?.map((color, index) => { + return ( +
    + ); + })} +
    +
    + {accentKeys?.map((color, index) => { + return ( +
    + ); + })} +
    +
    + {infoKeys?.map((color, index) => { + return ( +
    + ); + })} +
    + +
    + {warningKeys?.map((color, index) => { + return ( +
    + ); + })} +
    +
    + {errorKeys?.map((color, index) => { + return ( +
    + ); + })} +
    +
    + {successKeys?.map((color, index) => { + return ( +
    + ); + })} +
    -
    + ); } diff --git a/src/Routes/MaterialColorGeneratorRoute/MaterialColorGeneratorRoute.tsx b/src/Routes/MaterialColorGeneratorRoute/MaterialColorGeneratorRoute.tsx new file mode 100644 index 000000000..7c00c3905 --- /dev/null +++ b/src/Routes/MaterialColorGeneratorRoute/MaterialColorGeneratorRoute.tsx @@ -0,0 +1,58 @@ +import { FancyBox } from '@/components/atoms/FancyBox'; +import { Typography } from '@/components/atoms/Typography'; +import { FancyFlexBox } from '@/components/templates/FancyFlexBox'; +import { generateMaterialColorPalette } from '@/design/designFunctions/generateMaterialColorPalette/generateMaterialColorPalette'; +import { hexToHSL } from '@/design/designFunctions/hexToHSL/hexToHSL'; +import { useState } from 'react'; + +export default function MaterialColorGeneratorRoute() { + const [baseColor, setBaseColor] = useState('#f32020'); + const [isDark, setIsDark] = useState(false); + + const palette = generateMaterialColorPalette({ baseHex: baseColor, isLightTheme: !isDark }); + + const getContrastText = (bgColor: string) => { + const { l } = hexToHSL(bgColor); + return l > 60 ? '#000000' : '#FFFFFF'; + }; + + return ( + + + Material Color Palette + + + +
    + setBaseColor(e.target.value)} /> + {baseColor} +
    + + + {Object.entries(palette).map(([weight, color]) => ( + + +
    {weight}
    +
    {color.toUpperCase()}
    +
    +
    + ))} +
    + +
    +

    Usage in CSS:

    + {`--color-primary-${isDark ? 'dark' : 'light'}: ${baseColor};`} +
    +
    +
    + ); +} diff --git a/src/Routes/MenuRoute/MenueRoute.tsx b/src/Routes/MenuRoute/MenueRoute.tsx index 6bb853b40..15e0fe7df 100644 --- a/src/Routes/MenuRoute/MenueRoute.tsx +++ b/src/Routes/MenuRoute/MenueRoute.tsx @@ -4,7 +4,6 @@ import React, { useRef, useState } from 'react'; import { FancyButton } from '../../../lib'; import { DesignArea, DesignWrapper } from '../DesignWrapper/Wrapper'; -import Popover from '../../components/shared/FancyPopover/Popover'; import FancyPopover from '../../components/shared/FancyPopover/FancyPopover'; import FancyMenueItem from '../../components/templates/FancyMenuItem/FancyMenuItem'; import { Menu } from '@/components/molecules/Menue'; diff --git a/src/Routes/SkeletonRoute/SkeletonRoute.tsx b/src/Routes/SkeletonRoute/SkeletonRoute.tsx index 5963dc39f..9acada64c 100644 --- a/src/Routes/SkeletonRoute/SkeletonRoute.tsx +++ b/src/Routes/SkeletonRoute/SkeletonRoute.tsx @@ -1,4 +1,5 @@ import { DesignArea, DesignWrapper } from '@/Routes/DesignWrapper/Wrapper'; +import { SkeletonBox } from '@/components/atoms/SkeletonBox'; import FancySkeletonGrid from '@/components/templates/FancySkeletonGrid/FancySkeletonGrid'; export default function SkeletonRoute() { @@ -9,6 +10,10 @@ export default function SkeletonRoute() { gridAreas={['a a b b b b d', 'a a c c c c d', 'e e c c c c f', 'e e g g g g f', 'e e h h h h f']} height="200px" /> */} + {/* eslint-disable-next-line @typescript-eslint/no-unsafe-assignment */} + {[...Array(5)].map((_, index) => ( + + ))} @@ -30,6 +33,7 @@ const items = [ export default function SpeedDailRoute() { const style = css` height: 500px; + width: 500px; align-items: flex-end; `; @@ -38,6 +42,14 @@ export default function SpeedDailRoute() { + { + console.log('Hii'); + }} + icon={SVGSearch} + /> diff --git a/src/Routes/ToastMessageRoute/ToastMessageRoute.tsx b/src/Routes/ToastMessageRoute/ToastMessageRoute.tsx index 80104f5bc..59a4c0c24 100644 --- a/src/Routes/ToastMessageRoute/ToastMessageRoute.tsx +++ b/src/Routes/ToastMessageRoute/ToastMessageRoute.tsx @@ -2,7 +2,9 @@ import React from 'react'; import FancyToastMessage from '../../components/organisms/FancyToastMessage/FancyToastMessage'; import FancyButton from '../../components/organisms/FancyButton/FancyButton'; -import { useFancyToastMessageStore } from '../../components/organisms/FancyToastMessage/FancyToastMessage.state'; +import { createFancyToastMessageStore } from '../../components/organisms/FancyToastMessage/createFancyToastMessageStore.state'; + +const useFancyToastMessageStore = createFancyToastMessageStore(); const messageType = ['success', 'warning', 'error']; @@ -18,9 +20,12 @@ const handler = () => { }; export default function ToastMessageRoute() { + const removeToast = useFancyToastMessageStore((state) => state.removeToast); + const toastMessages = useFancyToastMessageStore((state) => state.toastQueue); + return (
    - +
    ); diff --git a/src/components/atoms/BackDrop/BackDrop.tsx b/src/components/atoms/BackDrop/BackDrop.tsx index 48d8f2c35..011f90b23 100644 --- a/src/components/atoms/BackDrop/BackDrop.tsx +++ b/src/components/atoms/BackDrop/BackDrop.tsx @@ -1,16 +1,16 @@ 'use client'; import { useEffect, useRef } from 'react'; -import styled from 'styled-components'; +import { TStyledPrefixAndPicker } from '@/types'; +import { styled } from 'styled-components'; + +import { TBackDrop } from './TBackdrop.model'; -interface IBackDrop { - isOpen: boolean; - onClick?: () => void; -} // --------------------------------------------------------------------------- // // ------- Only a backdrop for some components with a onclick listener ------- // // --------------------------------------------------------------------------- // -export default function BackDrop({ isOpen, onClick }: IBackDrop) { +export default function BackDrop(props: TBackDrop) { + const { isOpen, onClick, externalStyle } = props; const backdropRef = useRef(null); useEffect(() => { @@ -29,20 +29,21 @@ export default function BackDrop({ isOpen, onClick }: IBackDrop) { } }, [isOpen]); - return ; + return ; } // ------------------------------------------- // // ------- The style for the component ------- // // ------------------------------------------- // -const BackdropContainer = styled.div` +const BackdropContainer = styled.div>` position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.5); - z-index: 100; + z-index: 99; opacity: 0; // Start with opacity 0 transition: opacity 0.3s ease 0s; + ${({ $externalStyle }) => $externalStyle} `; diff --git a/src/components/atoms/BackDrop/TBackdrop.model.ts b/src/components/atoms/BackDrop/TBackdrop.model.ts new file mode 100644 index 000000000..6c3660046 --- /dev/null +++ b/src/components/atoms/BackDrop/TBackdrop.model.ts @@ -0,0 +1,7 @@ +import { CSSProp } from 'styled-components'; + +export type TBackDrop = { + isOpen: boolean; + onClick?: () => void; + externalStyle?: CSSProp; +}; diff --git a/src/components/atoms/BackDrop/index.ts b/src/components/atoms/BackDrop/index.ts index 94f74dfcd..947900d43 100644 --- a/src/components/atoms/BackDrop/index.ts +++ b/src/components/atoms/BackDrop/index.ts @@ -1 +1,3 @@ export { default as BackDrop } from './BackDrop'; + +export type { TBackDrop } from './TBackdrop.model'; diff --git a/src/components/atoms/DateOutput/DateOutput.style.tsx b/src/components/atoms/DateOutput/DateOutput.style.tsx index 3d8fb9444..03e02a1a7 100644 --- a/src/components/atoms/DateOutput/DateOutput.style.tsx +++ b/src/components/atoms/DateOutput/DateOutput.style.tsx @@ -5,6 +5,7 @@ import { TTheme } from '@/types/TTheme'; import { getBackgroundColor, getTextColor } from '@/design/designFunctions/colorCalculatorForComponent'; import { globalElementSizes } from '@/design/theme/globalSizes'; import { sizeSettings } from './sizeSettings'; +import { clampLayer } from '@/utils/functions/clampLayer'; type TDateOutputButton = TStyledPrefixAndPicker< TDateOutput, @@ -15,8 +16,8 @@ export const DateOutputButton = styled.button $isActive - ? getBackgroundColor({ theme, $themeType, $layer: $layer ? $layer + 1 : 3 }) - : getBackgroundColor({ theme, $themeType, $layer: $layer ? $layer : 2 })}; + ? getBackgroundColor({ theme, $themeType, $layer: clampLayer($layer ? $layer + 1 : 3) }) + : getBackgroundColor({ theme, $themeType, $layer: clampLayer($layer ? $layer : 2) })}; color: ${({ theme, $themeType = 'secondary', $layer = 1 }) => getTextColor({ theme, $themeType, $textLayer: $layer, turnColorTheme: true })}; border: none; diff --git a/src/components/atoms/DisableBox/docu/DisabledBox.stories.tsx b/src/components/atoms/DisableBox/docu/DisabledBox.stories.tsx index 98d2aefbd..e938804c0 100644 --- a/src/components/atoms/DisableBox/docu/DisabledBox.stories.tsx +++ b/src/components/atoms/DisableBox/docu/DisabledBox.stories.tsx @@ -18,7 +18,12 @@ const meta = { }, // Define arguments for the story - argTypes: {}, + argTypes: { + disabled: { + description: 'Disables the child elements within the `DisabledBox` component.', + type: 'boolean', + }, + }, } satisfies Meta; // Export the metadata @@ -28,6 +33,12 @@ type Story = StoryObj; // Define the primary story export const Primary: Story = { - render: (args) => , - args: {}, + render: (args) => ( + + + + ), + args: { + disabled: true, + }, }; diff --git a/src/components/atoms/DropDownSelect/DropDownSelect.tsx b/src/components/atoms/DropDownSelect/DropDownSelect.tsx index 90b669865..73e7fb291 100644 --- a/src/components/atoms/DropDownSelect/DropDownSelect.tsx +++ b/src/components/atoms/DropDownSelect/DropDownSelect.tsx @@ -8,21 +8,30 @@ import { forwardRef } from 'react'; // ---------------- the blank drop down select ---------------------- // // ------------------------------------------------------------------ // const DropDownSelect = forwardRef((props, ref) => { - const { values, value, placeholder, children, align = 'left', emptySelect = true, ...htmlInputProps } = props; + const { + values, + value, + placeholder, + children, + align = 'left', + emptySelect = true, + required, + ...htmlInputProps + } = props; return ( {/* Placeholder option */} {/* Empty Select Option */} - {emptySelect && ( - )} - {placeholder && ( - )} diff --git a/src/components/atoms/DropDownSelect/TDropDownSelect.model.ts b/src/components/atoms/DropDownSelect/TDropDownSelect.model.ts index 2d4770ac3..053deb396 100644 --- a/src/components/atoms/DropDownSelect/TDropDownSelect.model.ts +++ b/src/components/atoms/DropDownSelect/TDropDownSelect.model.ts @@ -1,4 +1,4 @@ -import { HTMLAttributes } from 'react'; +import { SelectHTMLAttributes } from 'react'; export type TDropDownSelectItem = { value: string; @@ -17,7 +17,7 @@ export type TDropDownSelect = { }; // the native props of the input element excluding the type attribute -export type TDropDownSelectNativeAttrs = Omit, 'type'>; +export type TDropDownSelectNativeAttrs = Omit, 'type'>; // the props of the input element with the native props export type TDropDownSelectWithNativeAttrs = TDropDownSelect & TDropDownSelectNativeAttrs; diff --git a/src/components/atoms/FancyLine/FancyLine.style.tsx b/src/components/atoms/FancyLine/FancyLine.style.tsx index bac0a4439..cb4cac24b 100644 --- a/src/components/atoms/FancyLine/FancyLine.style.tsx +++ b/src/components/atoms/FancyLine/FancyLine.style.tsx @@ -30,7 +30,7 @@ export const StyledFancyLine = styled.hr` background-color: ${({ $systemMessageType, theme, $isActive, $themeType = 'secondary', $layer }) => getBackgroundColor({ theme, - $themeType: $isActive ? $systemMessageType ?? 'accent' : $themeType, + $themeType: $isActive ? ($systemMessageType ?? 'accent') : $themeType, $layer: $isActive ? 0 : $layer, })}; border: 0; diff --git a/src/components/atoms/FancyProfilePicture/TFancyProfilePicture.model.ts b/src/components/atoms/FancyProfilePicture/TFancyProfilePicture.model.ts index ceb583abe..a46092dc2 100644 --- a/src/components/atoms/FancyProfilePicture/TFancyProfilePicture.model.ts +++ b/src/components/atoms/FancyProfilePicture/TFancyProfilePicture.model.ts @@ -7,7 +7,7 @@ type TtextAvatarSettings = React.ComponentProps; export type TFancyProfilePicture = { borderRadius?: 'sm' | 'md' | 'lg' | 'complete' | 'none'; - sizeC?: TglobalElementSizes | (string & {}); + sizeC?: TglobalElementSizes | 'fit' | (string & {}); externalStyle?: CSSProp; textAvatarSettings?: Omit; nickname?: string; diff --git a/src/components/atoms/FancyProfilePicture/index.ts b/src/components/atoms/FancyProfilePicture/index.ts index 5aa5c1aa4..e4134f82b 100644 --- a/src/components/atoms/FancyProfilePicture/index.ts +++ b/src/components/atoms/FancyProfilePicture/index.ts @@ -1 +1,3 @@ export { default as FancyProfilePicture } from './FancyProfilePicture'; + +export type { TFancyProfilePicture, TFancyProfilePictureWithHTMLAttrs } from './TFancyProfilePicture.model'; diff --git a/src/components/atoms/FancySVGAtom/FancySVGAtom.style.tsx b/src/components/atoms/FancySVGAtom/FancySVGAtom.style.tsx index 6ead3e4a8..2ade9e4c6 100644 --- a/src/components/atoms/FancySVGAtom/FancySVGAtom.style.tsx +++ b/src/components/atoms/FancySVGAtom/FancySVGAtom.style.tsx @@ -7,13 +7,14 @@ import { TStyledPrefixAndOmiter } from '@/types/TStyledPrefixAndOmiter'; import { TFancySVGAtom } from '@/components/atoms/FancySVGAtom/TFancySVGAtom.model'; import { TUiColorsNotTransparent } from '@/types/TUiColorsNotTransparent'; import { TUiColorsSystemMessage } from '@/types/TUiColorsSystemMessage'; +import { TLayer } from '@/types'; interface ICalcIconColor { theme: TTheme; $isActive?: boolean; $systemMessage?: TUiColorsSystemMessage; $themeType: TUiColorsNotTransparent; - $layer?: number; + $layer?: TLayer; } // calculate the color of the icon based on the props and the theme const calcIconColor = ({ theme, $isActive, $systemMessage, $themeType, $layer }: ICalcIconColor): string => { diff --git a/src/components/atoms/FancyXButton/FancyXButton.style.tsx b/src/components/atoms/FancyXButton/FancyXButton.style.tsx index cc54353dc..fba8f3272 100644 --- a/src/components/atoms/FancyXButton/FancyXButton.style.tsx +++ b/src/components/atoms/FancyXButton/FancyXButton.style.tsx @@ -20,6 +20,7 @@ export const StyledFancyXButton = styled.button` font-weight: bolder; cursor: pointer; outline: none; + color: inherit; ${simpleColorTransition} &:hover { diff --git a/src/components/atoms/FancyXButton/FancyXButton.tsx b/src/components/atoms/FancyXButton/FancyXButton.tsx index e98105f7c..148d39b5b 100644 --- a/src/components/atoms/FancyXButton/FancyXButton.tsx +++ b/src/components/atoms/FancyXButton/FancyXButton.tsx @@ -13,7 +13,7 @@ export default function FancyXButton(props: TFancyXButtonWithHTMLAttrs) { //check wich design comes in and add the right color object uiColor or systemMessages to the button return ( - + diff --git a/src/components/atoms/FloatingButton/FloatingButton.tsx b/src/components/atoms/FloatingButton/FloatingButton.tsx new file mode 100644 index 000000000..3167b4502 --- /dev/null +++ b/src/components/atoms/FloatingButton/FloatingButton.tsx @@ -0,0 +1,62 @@ +import { useState } from 'react'; + +import { Button, Ring, Wrapper } from './SpeedDailButton.style'; +import { sizeSettings } from './sizeSettings'; + +import { TFloatingButton } from '@/components/atoms/FloatingButton/TFloatingButton.model'; +import { FancySVGAtom } from '@/components/atoms/FancySVGAtom'; +import { VisuallyHidden } from '@/components/atoms/VisuallyHidden'; + +export default function FloatingButton(props: TFloatingButton) { + const { + isOpen, + onClick, + icon, + size = 'sm', + type = 'switch', + label, + layer = 0, + outline, + themeType = 'accent', + } = props; + const [isOpened, setIsOpened] = useState(true); + + const handleKeyDown = (event: React.KeyboardEvent) => { + if (event.key === 'Enter' || event.key === ' ') { + event.preventDefault(); + handleClick(); + } + }; + + const handleClick = () => { + setIsOpened(!isOpened); + onClick(); + }; + + return ( + + + {type === 'switch' && ( + + ); +} diff --git a/src/components/atoms/FloatingButton/SpeedDailButton.style.tsx b/src/components/atoms/FloatingButton/SpeedDailButton.style.tsx new file mode 100644 index 000000000..99718f9b0 --- /dev/null +++ b/src/components/atoms/FloatingButton/SpeedDailButton.style.tsx @@ -0,0 +1,89 @@ +import { styled } from 'styled-components'; +import { TFloatingButton } from './TFloatingButton.model'; +import { sizeSettings } from './sizeSettings'; + +import { boxShadow } from '@/design/designFunctions/shadows/shadows'; +import { TTheme } from '@/types/TTheme'; +import { globalElementSizes } from '@/design/theme/globalSizes'; +import { TStyledPrefixAndPicker } from '@/types'; +import { generateThemeDesignForComponent } from '@/design/designFunctions/generateThemeDesignForComponent'; + +export const Wrapper = styled.div>` + position: relative; + width: ${({ $size = 'sm' }) => parseInt(globalElementSizes[sizeSettings[$size].height]) + 12 + 'px'}; + height: ${({ $size = 'sm' }) => parseInt(globalElementSizes[sizeSettings[$size].height]) + 12 + 'px'}; + display: flex; + align-items: center; + justify-content: center; +`; + +type TButton = { + $isOpen: boolean; + theme?: TTheme; +} & TStyledPrefixAndPicker; + +export const Button = styled.button` + position: relative; + border: none; + border-radius: 50%; + cursor: pointer; + width: ${({ $size = 'sm' }) => globalElementSizes[sizeSettings[$size].height]}; + height: ${({ $size = 'sm' }) => globalElementSizes[sizeSettings[$size].height]}; + outline: none; + display: flex; + align-items: center; + justify-content: center; + z-index: 50; + ${({ $layer = 0, $themeType = 'accent', $outline, theme }) => + generateThemeDesignForComponent({ + $layer, + $themeType, + $outlined: $outline, + theme, + $backgroundState: 'hover', + })}; + + ${boxShadow.sm} + transition: + background-color 0.125s ease-in-out, + color 0.125s ease-in-out; + + &:focus-visible { + outline: 1px solid ${({ theme }) => theme.color.accent[0]}; + outline-offset: 2px; + } + + svg { + position: absolute; + top: 50%; + left: 50%; + width: 60%; + height: 60%; + font-size: 24px; + transform-origin: 50% 50%; + transform: ${({ $isOpen }) => + $isOpen ? 'translate(-50%, -50%) rotate(45deg)' : 'translate(-50%, -50%) rotate(0)'}; + transition: transform 0.3s ease; + } +`; + +export const Ring = styled.div` + position: absolute; + width: ${({ $size = 'sm' }) => globalElementSizes[sizeSettings[$size].height]}; + height: ${({ $size = 'sm' }) => globalElementSizes[sizeSettings[$size].height]}; + border-bottom: 1.5px solid ${({ theme, $themeType = 'accent', $layer = 0 }) => theme.color[$themeType][$layer]}; + border-top: 1.5px solid ${({ theme, $themeType = 'accent', $layer = 0 }) => theme.color[$themeType][$layer]}; + border-left: solid transparent; + border-right: solid transparent; + border-radius: 50%; + z-index: 49; + top: 50%; + right: 50%; + transform: ${({ $isOpen }) => + $isOpen ? 'translate(50%, -50%) scale(1.12) rotate(125deg)' : 'translate(50%, -50%) scale(0.9) rotate(20deg)'}; + opacity: ${({ $isOpen }) => ($isOpen ? 1 : 0)}; + transition: + opacity 0.3s ease-in-out, + transform 0.3s ease-in-out; + pointer-events: none; +`; diff --git a/src/components/atoms/FloatingButton/TFloatingButton.model.ts b/src/components/atoms/FloatingButton/TFloatingButton.model.ts new file mode 100644 index 000000000..196264a2d --- /dev/null +++ b/src/components/atoms/FloatingButton/TFloatingButton.model.ts @@ -0,0 +1,16 @@ +import { ReactNode } from 'react'; +import { TComponentSizes } from '@/types/TComponentSizes'; +import { TUiColorsNotTransparent } from '@/types/TUiColorsNotTransparent'; +import { TLayer } from '@/types/TLayer'; + +export type TFloatingButton = { + isOpen: boolean; + onClick: () => void; + icon: ReactNode; + type?: 'button' | 'switch'; + size?: TComponentSizes; + label: string; + themeType?: TUiColorsNotTransparent; + outline?: boolean; + layer?: TLayer; +}; diff --git a/src/components/atoms/FloatingButton/sizeSettings.ts b/src/components/atoms/FloatingButton/sizeSettings.ts new file mode 100644 index 000000000..e781f1bf4 --- /dev/null +++ b/src/components/atoms/FloatingButton/sizeSettings.ts @@ -0,0 +1,23 @@ +import { TFancySVGAtom } from '@/components/atoms/FancySVGAtom'; +import { TglobalElementSizes } from '@/types/TGlobalElementSizes'; +import { TSizeSettings } from '@/types/TSizeSettings'; + +type TSizeObj = { + height: TglobalElementSizes; + iconSize: TFancySVGAtom['sizeC']; +}; + +export const sizeSettings: TSizeSettings = { + sm: { + height: 'lg', + iconSize: 'sm', + }, + md: { + height: 'xl', + iconSize: 'md', + }, + lg: { + height: 'xxl', + iconSize: 'lg', + }, +}; diff --git a/src/components/atoms/InputLabel/utils/generateLableVariant.ts b/src/components/atoms/InputLabel/utils/generateLableVariant.ts index f8b844cf6..58b66b256 100644 --- a/src/components/atoms/InputLabel/utils/generateLableVariant.ts +++ b/src/components/atoms/InputLabel/utils/generateLableVariant.ts @@ -1,6 +1,6 @@ -import { TInputLabel } from '@/components/atoms/InputLabel/TInputLabel.model'; -import { TTextAlignLRC } from 'lib'; import { css } from 'styled-components'; +import { TInputLabel } from '@/components/atoms/InputLabel/TInputLabel.model'; +import { TTextAlignLRC } from '@/types/TTextAlignLRC'; type TgenerateLableVariant = { $lableVariant: TInputLabel['lableVariant']; diff --git a/src/components/atoms/RawInput/TRawInput.model.ts b/src/components/atoms/RawInput/TRawInput.model.ts index 4337bf5f4..0461e1e74 100644 --- a/src/components/atoms/RawInput/TRawInput.model.ts +++ b/src/components/atoms/RawInput/TRawInput.model.ts @@ -1,7 +1,7 @@ import { TLayer } from '@/types/TLayer'; +import { TStyledPrefixAndPicker } from '@/types/TStyledPrefixAndPicker'; import { TTextAlignLC } from '@/types/TTextAlignLC'; import { TUiColorsMain } from '@/types/TUiColorsMain'; -import { TStyledPrefixAndPicker } from 'lib'; import { CSSProp } from 'styled-components'; diff --git a/src/components/atoms/ScalingSection/ScalingSection.stories.tsx b/src/components/atoms/ScalingSection/ScalingSection.stories.tsx index 9532398f8..ccbe88712 100644 --- a/src/components/atoms/ScalingSection/ScalingSection.stories.tsx +++ b/src/components/atoms/ScalingSection/ScalingSection.stories.tsx @@ -1,6 +1,7 @@ import type { Meta, StoryObj } from '@storybook/react'; import ScalingSection from './ScalingSection'; +import type { TScalingSection } from './TScalingSection.model'; const meta = { component: ScalingSection, @@ -21,7 +22,7 @@ const meta = { }, }, tags: ['autodocs'], -} satisfies Meta; +} satisfies Meta; export default meta; diff --git a/src/components/atoms/ScalingSection/ScalingSection.tsx b/src/components/atoms/ScalingSection/ScalingSection.tsx index c2ba80bf0..1aedb4257 100644 --- a/src/components/atoms/ScalingSection/ScalingSection.tsx +++ b/src/components/atoms/ScalingSection/ScalingSection.tsx @@ -12,16 +12,12 @@ import { import { styled } from 'styled-components'; import { SwipeUpDash } from '@/components/atoms/SwipeUpDash'; - -interface IScalingSection { - handleScaling: (state: 'move' | 'end', currentPos: number) => void; - onClick?: () => void; -} +import { TScalingSection } from '@/components/atoms/ScalingSection/TScalingSection.model'; // --------------------------------------------------------------------------- // //the ScalingSection is for conroling events on the swipe up dash for better UX // // --------------------------------------------------------------------------- // -const ScalingSection = forwardRef((props, ref) => { +const ScalingSection = forwardRef((props, ref) => { const { handleScaling, onClick } = props; const [isDragging, setIsDragging] = useState(false); @@ -48,7 +44,7 @@ const ScalingSection = forwardRef((props, ref) if (!isDragging) return; const currentY = event.clientY; - const deltaY = currentY + 15; + const deltaY = currentY; handleScaling('move', deltaY); }; @@ -65,7 +61,7 @@ const ScalingSection = forwardRef((props, ref) setIsDragging(false); const touchDuration = Date.now() - touchStartTime.current; - if (touchDuration < 200) { + if (touchDuration < 150) { // Threshold for short tap (adjust as needed) onClick?.(); // Call onClick if it's a short tap } @@ -87,11 +83,12 @@ const ScalingSection = forwardRef((props, ref) document.removeEventListener('touchmove', handleMoveTouch); document.removeEventListener('touchend', handleEnd); }; + // eslint-disable-next-line react-hooks/exhaustive-deps }, [isDragging]); return ( - + ); }); diff --git a/src/components/atoms/ScalingSection/TScalingSection.model.ts b/src/components/atoms/ScalingSection/TScalingSection.model.ts new file mode 100644 index 000000000..c88560e44 --- /dev/null +++ b/src/components/atoms/ScalingSection/TScalingSection.model.ts @@ -0,0 +1,4 @@ +export type TScalingSection = { + handleScaling: (state: 'move' | 'end', currentPos: number) => void; + onClick?: () => void; +}; diff --git a/src/components/atoms/ScalingSection/index.ts b/src/components/atoms/ScalingSection/index.ts index 38e2d3229..8dc9a5850 100644 --- a/src/components/atoms/ScalingSection/index.ts +++ b/src/components/atoms/ScalingSection/index.ts @@ -1 +1,3 @@ export { default as ScalingSection } from './ScalingSection'; + +export type { TScalingSection } from './TScalingSection.model'; diff --git a/src/components/atoms/SimpleDialog/SimpleDialog.model.ts b/src/components/atoms/SimpleDialog/SimpleDialog.model.ts index 6d7a5f1c0..b0633c87d 100644 --- a/src/components/atoms/SimpleDialog/SimpleDialog.model.ts +++ b/src/components/atoms/SimpleDialog/SimpleDialog.model.ts @@ -1,5 +1,6 @@ import { TLayer } from '@/types/TLayer'; import { TUiColorsNotTransparent } from '@/types/TUiColorsNotTransparent'; +import { CSSProp } from 'styled-components'; // Define the props for the SimpleDialog component export type TSimpleDialog = { @@ -7,6 +8,7 @@ export type TSimpleDialog = { children?: React.ReactNode; themeType?: TUiColorsNotTransparent; layer?: TLayer; + externalStyle?: CSSProp; }; export type TSimpleDialogHTMLAttrs = React.HTMLAttributes; diff --git a/src/components/atoms/SimpleDialog/SimpleDialog.style.ts b/src/components/atoms/SimpleDialog/SimpleDialog.style.ts index fcdf2286d..0a83d81a8 100644 --- a/src/components/atoms/SimpleDialog/SimpleDialog.style.ts +++ b/src/components/atoms/SimpleDialog/SimpleDialog.style.ts @@ -1,4 +1,4 @@ -import { styled } from 'styled-components'; +import { CSSProp, styled } from 'styled-components'; import { getColorsForComponent } from '@/design/designFunctions/colorCalculatorForComponent'; import { TLayer } from '@/types/TLayer'; @@ -11,6 +11,7 @@ type TStyledDialog = { theme: TTheme; $themeType: TUiColorsNotTransparent; $layer?: TLayer; + $externalStyle?: CSSProp; }; export const StyledDialog = styled(animated.div)` @@ -19,11 +20,13 @@ export const StyledDialog = styled(animated.div)` left: 50vw; transform: translate(-50dvh, -50vw); padding: ${({ theme }) => theme.spacing.lg}; + box-sizing: border-box; border-radius: ${({ theme }) => theme.borderRadius.lg}; border: none; width: 80%; ${({ theme, $themeType, $layer = 1 }) => getColorsForComponent({ theme, $themeType, $layer })} - z-index: 1000; + z-index: 100; outline: none; max-height: 80dvh; + ${({ $externalStyle }) => $externalStyle}; `; diff --git a/src/components/atoms/SimpleDialog/SimpleDialog.tsx b/src/components/atoms/SimpleDialog/SimpleDialog.tsx index b58e277c8..6d4136959 100644 --- a/src/components/atoms/SimpleDialog/SimpleDialog.tsx +++ b/src/components/atoms/SimpleDialog/SimpleDialog.tsx @@ -10,7 +10,7 @@ import { TSimpleDialogWithHTMLAttrs } from '@/components/atoms/SimpleDialog/Simp // ------- A container that can filld with everythin and acts as a dialog --- // // --------------------------------------------------------------------------- // export default function SimpleDialog(props: TSimpleDialogWithHTMLAttrs) { - const { isOpen = false, children, themeType = 'primary', layer = 1, ...htmlProps } = props; + const { isOpen = false, children, themeType = 'primary', layer = 1, externalStyle, ...htmlProps } = props; const dialogRef = useRef(null); const [shouldRender, setRender] = useState(isOpen); @@ -59,6 +59,7 @@ export default function SimpleDialog(props: TSimpleDialogWithHTMLAttrs) { style={fade} $themeType={themeType} $layer={layer} + $externalStyle={externalStyle} {...htmlProps} > {children} diff --git a/src/components/atoms/SkeletonBox/SkeletonBox.style.tsx b/src/components/atoms/SkeletonBox/SkeletonBox.style.tsx index 4e85b15b3..6855b2060 100644 --- a/src/components/atoms/SkeletonBox/SkeletonBox.style.tsx +++ b/src/components/atoms/SkeletonBox/SkeletonBox.style.tsx @@ -12,12 +12,16 @@ export const StyledSkeletonBox = styled.div $sizeH && ($sizeH === 'fit' ? '100%' : globalElementSizes[$sizeH])}; background-color: ${({ theme, $themeType = 'primary', $layer }) => theme.color[$themeType][$layer || 0]}; animation: ${({ theme, $themeType, $layer }) => createLoadingKeyframes({ theme, $themeType, $layer })} 2s infinite; + animation-delay: ${({ $index = 0 }) => $index * 0.1}s; border-radius: ${({ theme, $borderRadius }) => theme.borderRadius[$borderRadius ?? 'xs'] || '0px'}; + overflow: hidden; + position: relative; ${({ $aspectRatio }) => $aspectRatio && css` aspect-ratio: ${$aspectRatio}; `} + ${({ $externalStyle }) => $externalStyle}; `; diff --git a/src/components/atoms/SkeletonBox/SkeletonBox.tsx b/src/components/atoms/SkeletonBox/SkeletonBox.tsx index 0e0ec529e..65eddd4ec 100644 --- a/src/components/atoms/SkeletonBox/SkeletonBox.tsx +++ b/src/components/atoms/SkeletonBox/SkeletonBox.tsx @@ -13,6 +13,7 @@ export default function SkeletonBox(props: TSkeletonBoxWithHTMLAttrs) { aspectRatio, borderRadius, externalStyle, + index, ...htmlProps } = props; @@ -24,6 +25,7 @@ export default function SkeletonBox(props: TSkeletonBoxWithHTMLAttrs) { $sizeH={sizeH} $sizeW={sizeW} $layer={layer} + $index={index} $aspectRatio={aspectRatio} $borderRadius={borderRadius} $externalStyle={externalStyle} diff --git a/src/components/atoms/SkeletonBox/TSkeleton.model.ts b/src/components/atoms/SkeletonBox/TSkeleton.model.ts index 5cd1bf5d8..bbf3ee4d3 100644 --- a/src/components/atoms/SkeletonBox/TSkeleton.model.ts +++ b/src/components/atoms/SkeletonBox/TSkeleton.model.ts @@ -12,6 +12,7 @@ export type TSkeletonBox = { borderRadius?: TBorderRadiusSizes; aspectRatio?: string; externalStyle?: CSSProp; + index?: number; }; export type TSkeletonBoxWithHTMLAttrs = TSkeletonBox & React.HTMLAttributes; diff --git a/src/components/atoms/SkeletonBox/docu/SkeletonBox.stories.tsx b/src/components/atoms/SkeletonBox/docu/SkeletonBox.stories.tsx index 4d03025c9..20a4700ee 100644 --- a/src/components/atoms/SkeletonBox/docu/SkeletonBox.stories.tsx +++ b/src/components/atoms/SkeletonBox/docu/SkeletonBox.stories.tsx @@ -61,6 +61,12 @@ const meta = { type: 'object', }, }, + index: { + description: 'The index can be used to delay the animation of the skeleton box e.g. in a list', + control: { + type: 'number', + }, + }, ...templateThemeType('allThemeTypes', 'primary', 0), }, } satisfies Meta; @@ -80,5 +86,26 @@ export const Primary: Story = { sizeW: 'md', borderRadius: 'xs', layer: 0, + index: 0, + }, +}; + +export const List: Story = { + render: (args) => ( +
    + + + + + +
    + ), + args: { + aspectRatio: '1:1', + themeType: 'primary', + sizeH: 'md', + sizeW: 'fit', + borderRadius: 'xs', + layer: 0, }, }; diff --git a/src/components/atoms/SystemMessageBox/SystemMessageBox.style.tsx b/src/components/atoms/SystemMessageBox/SystemMessageBox.style.tsx index cc615754f..d7afbb975 100644 --- a/src/components/atoms/SystemMessageBox/SystemMessageBox.style.tsx +++ b/src/components/atoms/SystemMessageBox/SystemMessageBox.style.tsx @@ -1,40 +1,49 @@ import { TSystemMessageBox } from './TSystemMessageBox.model'; import { arrayToCssValues } from '@/design/designFunctions/arrayToCssValues'; import { getBackgroundColor } from '@/design/designFunctions/colorCalculatorForComponent'; +import { colorTransparencyCalculator } from '@/design/designFunctions/colorTransparencyCalculator'; import { themeStore } from '@/design/theme/themeStore'; import { TStyledPrefixAndOmiter } from '@/types/TStyledPrefixAndOmiter'; import { TTheme } from '@/types/TTheme'; +import { adjustSystemMessageColor } from '@/utils/functions/adjustSystemMessageColor'; import { css, styled } from 'styled-components'; type TStyledSystemMessage = TStyledPrefixAndOmiter; export const StyledStystemMessage = styled.aside<{ theme: TTheme } & TStyledSystemMessage>` border-width: 1px; - display: flex; align-items: center; transition: all 0.2s; - - border-radius: ${({ $borderRadius }) => arrayToCssValues($borderRadius, 'borderRadius')}; padding: ${({ $padding }) => arrayToCssValues($padding, 'spacing')}; margin: ${({ $margin }) => arrayToCssValues($margin, 'spacing')}; + position: relative; + border-radius: ${({ $borderRadius }) => arrayToCssValues($borderRadius, 'borderRadius')}; + overflow: hidden; - @supports (color: rgb(from white r g b)) { - ${({ theme, $themeType = 'error', $layer = 0 }) => { - const color = getBackgroundColor({ theme, $themeType, $layer: $layer }); - const isDarkTheme = themeStore((state) => state.isDarkTheme); + ${({ theme, $themeType = 'error', $layer = 0 }) => { + const color = getBackgroundColor({ theme, $themeType, $layer: $layer }); + const isDarkTheme = themeStore((state) => state.isDarkTheme); - return css` - border-color: oklch(from ${color} l c h / 25%); - border-width: 1px; - border-style: solid; - background: oklch(from ${color} calc(l * 1) c h / 20%); - color: oklch(from ${color} calc(l * (${isDarkTheme ? 1.3 : 0.7})) c h); + return css` + border-width: 1px; + border-style: solid; + background: hsla(from ${color} h s l / ${isDarkTheme ? '15%' : '3%'}); + color: ${adjustSystemMessageColor(color)}; + border-left: 4px solid; + border-color: ${adjustSystemMessageColor(color)}; + `; + }} - &::selection { - background: oklch(from ${color} calc(l * 1.1) c h); - color: oklch(from ${color} 1 c h); - } - `; - }} + &::after { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: ${({ theme }) => colorTransparencyCalculator(theme.color.primary['0'], 0.8)}; + z-index: -1; } + + ${({ $externalStyle }) => $externalStyle} `; diff --git a/src/components/atoms/SystemMessageBox/SystemMessageBox.tsx b/src/components/atoms/SystemMessageBox/SystemMessageBox.tsx index 034dcfa0f..6abe48342 100644 --- a/src/components/atoms/SystemMessageBox/SystemMessageBox.tsx +++ b/src/components/atoms/SystemMessageBox/SystemMessageBox.tsx @@ -1,10 +1,12 @@ 'use client'; +import { forwardRef } from 'react'; + import { StyledStystemMessage } from './SystemMessageBox.style'; import { TSystemMessageBoxWithHTMLAttrs } from './TSystemMessageBox.model'; import { sizes } from './sizeSettings'; -export default function SystemMessageBox(props: TSystemMessageBoxWithHTMLAttrs) { +const SystemMessageBox = forwardRef((props, ref) => { const { children, layer, @@ -22,6 +24,7 @@ export default function SystemMessageBox(props: TSystemMessageBoxWithHTMLAttrs) return ( ); -} +}); + +SystemMessageBox.displayName = 'SystemMessageBox'; + +export default SystemMessageBox; diff --git a/src/components/atoms/Typography/Typography.style.tsx b/src/components/atoms/Typography/Typography.style.tsx index c27fe4364..e4fb8e514 100644 --- a/src/components/atoms/Typography/Typography.style.tsx +++ b/src/components/atoms/Typography/Typography.style.tsx @@ -1,4 +1,3 @@ -/* eslint-disable react-refresh/only-export-components */ import { ReactNode } from 'react'; import { styled, CSSProp } from 'styled-components'; diff --git a/src/components/atoms/VisuallyHidden/VisuallyHidden.mdx b/src/components/atoms/VisuallyHidden/VisuallyHidden.mdx new file mode 100644 index 000000000..b92a73a56 --- /dev/null +++ b/src/components/atoms/VisuallyHidden/VisuallyHidden.mdx @@ -0,0 +1,50 @@ +# VisuallyHidden + +A utility component that visually hides content while keeping it accessible to screen readers. This is particularly useful for accessibility labels that shouldn't be visible on screen but need to be announced by screen readers. + +## Installation + +```bash +npm install fui-fancyui +``` + +## Usage + +```jsx +import { VisuallyHidden } from 'fui-fancyui'; + +function Example() { + return ( + + ); +} +``` + +## Best Practices + +- Use for content that should be announced by screen readers but hidden visually +- Prefer this over `display: none` or `visibility: hidden` which hide content from screen readers +- Ideal for close buttons, icon-only buttons, or form labels that need screen reader context + +## Common Use Cases + +1. Icon buttons with screen reader labels +2. Skip navigation links +3. Form input descriptions +4. Additional context for interactive elements + +## Accessibility + +The component uses established CSS techniques to hide content visually while ensuring: + +- Content remains accessible to screen readers +- The element doesn't create a tab stop +- The content doesn't affect document flow +- The content isn't affected by viewport resizing + +## Props + +Accepts all standard HTML `span` element props. diff --git a/src/components/atoms/VisuallyHidden/VisuallyHidden.tsx b/src/components/atoms/VisuallyHidden/VisuallyHidden.tsx new file mode 100644 index 000000000..f8e487d02 --- /dev/null +++ b/src/components/atoms/VisuallyHidden/VisuallyHidden.tsx @@ -0,0 +1,13 @@ +import { styled } from 'styled-components'; + +export const VisuallyHidden = styled.span` + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +`; diff --git a/src/components/atoms/VisuallyHidden/index.ts b/src/components/atoms/VisuallyHidden/index.ts new file mode 100644 index 000000000..893063f2b --- /dev/null +++ b/src/components/atoms/VisuallyHidden/index.ts @@ -0,0 +1 @@ +export { VisuallyHidden } from './VisuallyHidden'; diff --git a/src/components/molecules/FancyDropDownUL/FancyDropDownUL.tsx b/src/components/molecules/FancyDropDownUL/FancyDropDownUL.tsx index f0a38737e..0922fd7dd 100644 --- a/src/components/molecules/FancyDropDownUL/FancyDropDownUL.tsx +++ b/src/components/molecules/FancyDropDownUL/FancyDropDownUL.tsx @@ -24,8 +24,8 @@ export default function FancyDropDownUL({ // This useEffect hook animates the height of the UL element useEffect(() => { if (!listRef.current) return; - // eslint-disable-next-line @typescript-eslint/no-floating-promises - animate.start({ + + void animate.start({ height: (isOpen ? listRef?.current.offsetHeight : 0) + 'px', }); }, [animate, listRef, isOpen]); diff --git a/src/components/molecules/FancyModalHeadLine/FancyModalHeadLine.model.ts b/src/components/molecules/FancyModalHeadLine/FancyModalHeadLine.model.ts index f10bc742c..ccda8ab82 100644 --- a/src/components/molecules/FancyModalHeadLine/FancyModalHeadLine.model.ts +++ b/src/components/molecules/FancyModalHeadLine/FancyModalHeadLine.model.ts @@ -7,7 +7,7 @@ export type TFancyModalHeadLine = { title?: string | React.ReactNode; subTitle?: string | React.ReactNode; gapBetweenText?: TSpacings; - hr?: TFancyLine; + hr?: TFancyLine | boolean; onXButtonClick?: (e: React.MouseEvent) => void; }; diff --git a/src/components/molecules/FancyModalHeadLine/FancyModalHeadLine.style.tsx b/src/components/molecules/FancyModalHeadLine/FancyModalHeadLine.style.tsx index b4b9db787..78de9b8a3 100644 --- a/src/components/molecules/FancyModalHeadLine/FancyModalHeadLine.style.tsx +++ b/src/components/molecules/FancyModalHeadLine/FancyModalHeadLine.style.tsx @@ -22,6 +22,7 @@ export const StyledHeadLineTitles = styled.span ($alignCenter ? 'center' : 'flex-start')}; + text-align: ${({ $alignCenter }) => ($alignCenter ? 'center' : 'start')}; gap: ${({ theme, $gapBetweenText }) => $gapBetweenText && theme.spacing[$gapBetweenText || 'xxs']}; padding: ${({ $alignCenter, theme }) => ($alignCenter ? `0 ${theme.spacing.xl}` : `0 ${theme.spacing.xl} 0 0 `)}; width: 100%; diff --git a/src/components/molecules/FancyModalHeadLine/FancyModalHeadLine.tsx b/src/components/molecules/FancyModalHeadLine/FancyModalHeadLine.tsx index e6ec9b5eb..4738b0590 100644 --- a/src/components/molecules/FancyModalHeadLine/FancyModalHeadLine.tsx +++ b/src/components/molecules/FancyModalHeadLine/FancyModalHeadLine.tsx @@ -12,7 +12,7 @@ import { FancyXButton } from '@/components/atoms/FancyXButton'; // -------- The main HeadLineComponent for the heading for the modals -------- // // --------------------------------------------------------------------------- // export default function FancyModalHeadLine(props: TFancyModalHeadLineWithHTMLProps) { - const { title, subTitle, alignCenter = false, hr = {}, gapBetweenText = 'xxs', onXButtonClick, ...htmlProps } = props; + const { title, subTitle, alignCenter = false, hr, gapBetweenText = 'xxs', onXButtonClick, ...htmlProps } = props; const TitleReactNode = createTitleNode(title); const SubTitleReactNode = createSubtitleNode(subTitle); @@ -24,9 +24,11 @@ export default function FancyModalHeadLine(props: TFancyModalHeadLineWithHTMLPro {title && TitleReactNode} {subTitle && SubTitleReactNode} - - - + {onXButtonClick && ( + + + + )} {hr && (title || subTitle) && } diff --git a/src/components/molecules/FancySelectWrapper/FancySelectWrapper.style.tsx b/src/components/molecules/FancySelectWrapper/FancySelectWrapper.style.tsx index 7611f9099..9f0d8e03d 100644 --- a/src/components/molecules/FancySelectWrapper/FancySelectWrapper.style.tsx +++ b/src/components/molecules/FancySelectWrapper/FancySelectWrapper.style.tsx @@ -13,9 +13,7 @@ type TLabelWrapper = { }; export const LabelWrapper = styled.label` display: flex; - - gap: ${({ theme }) => theme.spacing.sm}; - + gap: ${({ theme }) => theme.spacing.xs}; justify-content: ${({ $align }) => { switch ($align) { case 'left': diff --git a/src/components/molecules/Modal/Modal.tsx b/src/components/molecules/Modal/Modal.tsx index 6f50e65b4..21a68994c 100644 --- a/src/components/molecules/Modal/Modal.tsx +++ b/src/components/molecules/Modal/Modal.tsx @@ -5,42 +5,56 @@ import { useEffect, useState } from 'react'; import { BackDrop } from '@/components/atoms/BackDrop'; import { SimpleDialog } from '@/components/atoms/SimpleDialog'; import { TModalWithHTMLAttributes } from './TModal.model'; +import { useBodyOverflow } from '@/utils/hooks/useBodyOverflow'; +import { CSSObject } from 'styled-components'; // ----------------------------- ---------------------------------------------- // // ------ The main Modal Component to comstomize the Head/Bottomline ------- // // --------------------------------------------------------------------------- // export default function Modal(props: TModalWithHTMLAttributes) { - const { children, isOpen, onClose, isCloseable, themeType, layer, backDrop = true, ...htmlProps } = props; + const { + children, + isOpen, + onClose, + isCloseable = true, + themeType, + layer, + backDrop = true, + externalStyle, + zIndex = 100, + ...htmlProps + } = props; const [modalVisible, setModalVisible] = useState(false); + useBodyOverflow('hidden'); // close the modal when the user clicks on the backdrop const closeModalHanlder = () => { - if (isCloseable === false) return; + if (!isCloseable) return; // enable the scroll on the body when the modal is closed - document.body.style.overflow = 'auto'; //if a components needs controle from outside if (onClose) onClose(); setModalVisible(false); }; useEffect(() => { - if (isOpen) { - // disable the scroll on the body when the modal is open - document.body.style.overflowY = 'hidden'; - setModalVisible(true); - } else { - // enable the scroll on the body when the modal is closed - document.body.style.overflowY = 'auto'; - setModalVisible(false); - } + setModalVisible(!!isOpen); }, [isOpen]); return ( <> - + {children} - {backDrop && } + {backDrop && } ); } diff --git a/src/components/molecules/Modal/TModal.model.ts b/src/components/molecules/Modal/TModal.model.ts index 7fe5c50f0..1a931a213 100644 --- a/src/components/molecules/Modal/TModal.model.ts +++ b/src/components/molecules/Modal/TModal.model.ts @@ -4,6 +4,9 @@ export type TModal = { onClose?: () => void; isCloseable?: boolean; backDrop?: boolean; + zIndex?: number; } & TSimpleDialog; +export type TModalHTMLAttributes = React.HTMLAttributes; + export type TModalWithHTMLAttributes = TModal & React.HTMLAttributes; diff --git a/src/components/molecules/Modal/index.ts b/src/components/molecules/Modal/index.ts index 1a8133665..e94feccd6 100644 --- a/src/components/molecules/Modal/index.ts +++ b/src/components/molecules/Modal/index.ts @@ -1,3 +1,3 @@ export { default as Modal } from './Modal'; -export type { TModal, TModalWithHTMLAttributes } from './TModal.model'; +export type { TModal, TModalHTMLAttributes, TModalWithHTMLAttributes } from './TModal.model'; diff --git a/src/components/molecules/SingleToastMessage/SingleToastMessage.style.tsx b/src/components/molecules/SingleToastMessage/SingleToastMessage.style.tsx index 122f46cb0..6b6685614 100644 --- a/src/components/molecules/SingleToastMessage/SingleToastMessage.style.tsx +++ b/src/components/molecules/SingleToastMessage/SingleToastMessage.style.tsx @@ -1,19 +1,13 @@ -import { styled, keyframes } from 'styled-components'; +import { keyframes, styled } from 'styled-components'; -import colorTransparencyCalculator from '../../../design/designFunctions/colorTransparencyCalculator/colorTransparencyCalculator'; -import { boxShadow } from '../../../design/designFunctions/shadows/shadows'; +import { colorTransparencyCalculator } from '@/design/designFunctions/colorTransparencyCalculator'; import { TLayer } from '@/types/TLayer'; -import { getBackgroundColor } from '../../../design/designFunctions/colorCalculatorForComponent/colorCalculatorForComponent'; import { TTheme } from '@/types/TTheme'; +import { adjustSystemMessageColor } from '@/utils/functions/adjustSystemMessageColor'; +import { getBackgroundColor } from '@/design/designFunctions/colorCalculatorForComponent'; type ToastMessageProps = 'success' | 'warning' | 'error' | 'info'; -interface TToastMessage { - $messageType: ToastMessageProps; - $layer?: TLayer; - theme: TTheme; -} - interface TimerLineProps { $messageType: ToastMessageProps; $layer?: TLayer; @@ -21,23 +15,14 @@ interface TimerLineProps { $time: number; } -// styles for single toast message -export const Container = styled.div` - z-index: 99; - overflow: hidden; - display: flex; - position: relative; - flex-direction: column; - align-items: left; - color: ${({ $messageType, theme, $layer = 5 }) => - getBackgroundColor({ $themeType: $messageType, theme, $layer })}; //theme[$messageType]['5'] - border-radius: ${({ theme }) => theme.borderRadius.xs}; - padding: ${({ theme }) => theme.spacing.md}; +export const Background = styled.div<{ theme: TTheme }>` + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; background-color: ${({ theme }) => colorTransparencyCalculator(theme.color.primary['0'], 0.95)}; - border-left: 4px solid - ${({ $messageType, theme, $layer = 3 }) => getBackgroundColor({ $themeType: $messageType, theme, $layer })}; - box-sizing: border-box; - ${boxShadow.md} + z-index: -1; `; export const Headline = styled.div` @@ -67,7 +52,10 @@ export const TimerLine = styled.div` left: 0; height: 2px; width: 100%; - background-color: ${({ $messageType, theme, $layer = 3 }) => - getBackgroundColor({ $themeType: $messageType, theme, $layer })}; + background-color: ${({ $messageType, theme, $layer = 2 }) => { + const color = getBackgroundColor({ $themeType: $messageType, theme, $layer }); + + return adjustSystemMessageColor(color); + }}; animation: ${() => timerAnimation} ${({ $time }) => $time - 300}ms linear forwards; `; diff --git a/src/components/molecules/SingleToastMessage/SingleToastMessage.tsx b/src/components/molecules/SingleToastMessage/SingleToastMessage.tsx index 314c1424d..976d337d6 100644 --- a/src/components/molecules/SingleToastMessage/SingleToastMessage.tsx +++ b/src/components/molecules/SingleToastMessage/SingleToastMessage.tsx @@ -4,8 +4,10 @@ import { forwardRef, useEffect } from 'react'; import { FancyXButton } from '@/components/atoms/FancyXButton'; import { Typography } from '@/components/atoms/Typography'; -import { Container, Headline, TimerLine } from './SingleToastMessage.style'; +import { SystemMessageBox } from '@/components/atoms/SystemMessageBox'; + import { TSingleToastMessageWithHTMLAttrs } from './TToastMessage.model'; +import { Background, Headline, TimerLine } from './SingleToastMessage.style'; // A Single Toast Message Component wich const SingleToastMessage = forwardRef((props, ref) => { @@ -22,16 +24,17 @@ const SingleToastMessage = forwardRef + {title} - remove?.(id)} themeType={themeType} layer={layer || 5} /> + remove?.(id)} themeType={themeType} /> {message} - + + ); }); diff --git a/src/components/molecules/SwipeUpModal/SwipeUpModal.tsx b/src/components/molecules/SwipeUpModal/SwipeUpModal.tsx index a5a245966..f241e4f0e 100644 --- a/src/components/molecules/SwipeUpModal/SwipeUpModal.tsx +++ b/src/components/molecules/SwipeUpModal/SwipeUpModal.tsx @@ -1,6 +1,5 @@ 'use client'; - -import { useEffect, useId, useRef, useState } from 'react'; +import { useCallback, useEffect, useId, useRef, useState } from 'react'; import { BackDrop } from '@/components/atoms/BackDrop'; import { ScalingSection } from '@/components/atoms/ScalingSection'; @@ -10,10 +9,8 @@ import { useWindowDimensions } from '@/utils/hooks/useWindowDimensions'; import { TModalStatus } from '@/types/TModalStatus'; import { Content, ContentBox, WrapperContent, WrapperModal } from './SwipeUpModal.style'; import { TSwipeUpModalWithHTMLAttrs } from './TSwipeUpModal.model'; +import { useBodyOverflow } from '@/utils/hooks/useBodyOverflow'; -// --------------------------------------------------------------------------- // -// ----------- The Modal Molecule the displays the complete modal - ---------- // -// --------------------------------------------------------------------------- // export default function SwipeUpModal(props: TSwipeUpModalWithHTMLAttrs) { const { children, @@ -29,126 +26,128 @@ export default function SwipeUpModal(props: TSwipeUpModalWithHTMLAttrs) { } = props; const modalId = useId(); + const initialHeightRef = useRef(0); + const contentRef = useRef(null); const dialogRef = useRef(null); const lastFocusedElement = useRef(null); + const wrapperModal = useRef(null); + const scalingSection = useRef(null); + const { height: windowHeight } = useWindowDimensions(); + const [contentHeight, setContentHeight] = useState(0); const [statusModal, setStatusModal] = useState('closed'); const [modalPosition, setModalPosition] = useState(120); - const initialHeightRef = useRef(0); - const { height } = useWindowDimensions(); - const contentRef = useRef(null); - const scalingSection = useRef(null); - const [contentHeight, setContentHeight] = useState(0); const scrollY = useRef(0); + useBodyOverflow('hidden'); - //Opens the modal and set the overfolw to hidden const openModal = () => { - // if the content is higher than the window height, set the height to the window - // fixes safari bug where the modal jumps back wehn it reaches the top - const contentHeight = contentRef?.current?.offsetHeight ?? 0; // Get content height, defaulting to 0 - const scalingSectionHeight = scalingSection.current?.offsetHeight ?? 0; // Get the height of the scaling section - const maxHeight = height; // reduce the height of the scaling section + // Store initial window height when opening + const contentHeight = contentRef?.current?.offsetHeight ?? 0; + const scalingSectionHeight = scalingSection.current?.offsetHeight ?? 0; - // Set the height of the modal to the content height + the scaling section height - const minHeight = Math.min(contentHeight + scalingSectionHeight, maxHeight); - const position = calcPositionInPercent(minHeight, height); + const minHeight = Math.min(contentHeight + scalingSectionHeight, windowHeight); + const position = calcPositionInPercent(minHeight, windowHeight); initialHeightRef.current = minHeight; setModalPosition(position); setStatusModal('opening'); }; - //the closedBy is needed is needed to prevent the modal from closing --> - //when the user is interacting with the %modal and the modal is not closeable - const closeModal = (cloesedBy: 'status' | 'intercation') => { - if (cloesedBy === 'intercation' && !isCloseAble) return; - // Reset the overflow of the body to its initial state - resetOverflow(scrollY.current); + const closeModal = useCallback( + (closedBy: 'status' | 'interaction') => { + if (closedBy === 'interaction' && !isCloseAble) return; - setStatusModal('closing'); - }; + // Reset any viewport-specific styles + if (contentRef.current) { + contentRef.current.style.height = ''; + contentRef.current.style.maxHeight = ''; + } + + // Remove fixed positioning + document.body.style.position = ''; + document.body.style.top = ''; + document.body.style.width = ''; + + window.scrollTo(0, scrollY.current); + setStatusModal('closing'); + }, + [isCloseAble] + ); - // if the modal is open, open the modal else close it useEffect(() => { if (isOpen) { - // Store the currently focused element (before modal opens) lastFocusedElement.current = document.activeElement as HTMLElement; openModal(); } else { closeModal('status'); } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isOpen]); - - // sets the overflow to hidden when the modal is opening - const setOverflowHidden = () => { - if (window.scrollY > 0) { - scrollY.current = window.scrollY; - document.body.style.top = `-${scrollY.current}px`; // Store the scroll position - } + }, [isOpen, openModal, closeModal]); - // Apply styles to prevent scrolling - document.body.style.position = 'fixed'; // Prevent scrolling on the body in iOS Safari - document.body.style.overflow = 'hidden'; // you need this (NOT Y) to stop safari from entering refresh section - }; + const handleOpeningAndClosing = useCallback( + (e: React.TransitionEvent) => { + const targetElement = e.target as HTMLDivElement; + if (targetElement.id !== modalId) return; + const contentHeight = contentRef?.current?.offsetHeight ?? 0; + const scalingSectionHeight = scalingSection.current?.offsetHeight ?? 0; - const handleOpeningAndClosing = (e: React.TransitionEvent) => { - const targetElement = e.target as HTMLDivElement; - if (targetElement.id !== modalId) return; + if (statusModal === 'opening') { + if (dialogRef.current) dialogRef.current.focus(); - // Focus the dialog when it is rendered - if (statusModal === 'opening') { - if (dialogRef.current) dialogRef.current.focus(); + // Store scroll position before fixing position + scrollY.current = window.scrollY; - // Set the overflow of the body to hidden - setOverflowHidden(); + // Apply fixed positioning in a way that works better with iOS + document.body.style.position = 'fixed'; + document.body.style.top = `-${scrollY.current}px`; + document.body.style.width = '100%'; // Prevent horizontal shift - setContentHeight(height - (contentRef?.current?.offsetHeight ?? 0)); - setStatusModal('open'); - } else { - if (statusModal === 'closing') { + setContentHeight(windowHeight - contentHeight + scalingSectionHeight / 2); + setStatusModal('open'); + } else if (statusModal === 'closing') { setStatusModal('closed'); - //close the gobal modal state + if (onClose) onClose(); + lastFocusedElement?.current?.focus(); } - // Return focus to the last focused element when modal closes - if (lastFocusedElement.current) { - lastFocusedElement.current.focus(); - } - } - }; - - const handleScaling = (state: 'move' | 'end', currentPos: number) => { - const scalingSectionHeight = scalingSection.current?.offsetHeight ?? 0; - - const flipedPosition = height - currentPos + scalingSectionHeight; - - // calculate the position in percent - const position = calcPositionInPercent(flipedPosition, height); - - // if the user is moving the modal - if (state === 'move') { - setContentHeight(height - flipedPosition + scalingSectionHeight); + }, + [modalId, onClose, statusModal, windowHeight] + ); - setModalPosition(position); - // if the user is done moving the modal - } else if (state === 'end') { - const inititialHeight = calcPositionInPercent(initialHeightRef.current, height) + 100; - // this calulation is for good user experience. Its closes the modal when the user is moving the modal down a specific amount - if (initialHeightRef.current !== 0 && position > inititialHeight * 0.4) { - closeModal('intercation'); + const handleScaling = useCallback( + (state: 'move' | 'end', currentPos: number) => { + const scalingSectionHeight = scalingSection.current?.offsetHeight ?? 0; + + const flippedPosition = windowHeight - currentPos + scalingSectionHeight / 2; + const position = calcPositionInPercent(flippedPosition, windowHeight); + + if (state === 'move') { + setContentHeight(windowHeight - (flippedPosition + scalingSectionHeight)); + setModalPosition(position); + } else if (state === 'end') { + const initialHeight = calcPositionInPercent(initialHeightRef.current, windowHeight) + 100; + if (initialHeightRef.current !== 0 && position > initialHeight * 0.4) { + closeModal('interaction'); + } } - } - }; + }, + [windowHeight, closeModal] + ); + + useEffect(() => { + // recalculate content height when window height changes + setContentHeight( + windowHeight - ((contentRef?.current?.offsetHeight ?? 0) + (scalingSection.current?.offsetHeight ?? 0)) + ); + }, [windowHeight]); return ( - + - {/*// ---------- The top of the modal is used for the scaling ---------- //*/} {isScalable && isCloseAble && ( closeModal('intercation')} + onClick={() => closeModal('interaction')} /> )} - {/*// ---------- Content Area ---------- //*/} - {/*// ---------- Header ---------- //*/} {children} @@ -182,7 +180,7 @@ export default function SwipeUpModal(props: TSwipeUpModalWithHTMLAttrs) { {backdrop && ( closeModal('intercation')} + onClick={() => closeModal('interaction')} /> )} @@ -190,17 +188,5 @@ export default function SwipeUpModal(props: TSwipeUpModalWithHTMLAttrs) { } const calcPositionInPercent = (currentPos: number, height: number) => { - const windowHeight = height; - const position = windowHeight - currentPos; - return (position / windowHeight) * 100; -}; - -// Reset the overflow of the body to its initial state -const resetOverflow = (scrollY: number) => { - document.body.style.top = ''; - document.body.style.position = ''; - - window.scrollTo(0, scrollY); // Restore scroll position - - document.body.style.overflow = ''; // Restore vertical scroll + return ((height - currentPos) / height) * 100; }; diff --git a/src/components/organisms/FancyModal/FancyModal.tsx b/src/components/organisms/FancyModal/FancyModal.tsx index 9538f2135..a3d597bbf 100644 --- a/src/components/organisms/FancyModal/FancyModal.tsx +++ b/src/components/organisms/FancyModal/FancyModal.tsx @@ -8,7 +8,9 @@ import { TFancyModal } from './TFancyModal.modal'; // --------------------------------------------------------------------------- // // ----------------- The modalModule to build up a Moadal ------------------- // // --------------------------------------------------------------------------- // -export default function FancyModal({ appendToDomID, modals, closeModal }: TFancyModal) { +export default function FancyModal(props: TFancyModal) { + const { appendToDomID, modals, closeModal, externalStyle, zIndex, ...htmlProps } = props; + /* ----- The FancModal Ports the Modal out of the root div in the spearte "modal" div ----- */ return ( @@ -18,6 +20,9 @@ export default function FancyModal({ appendToDomID, modals, closeModal }: TFancy id={modal.id} isOpen={modal.status === 'open'} onClose={() => closeModal(modal.id)} + zIndex={zIndex} + externalStyle={externalStyle} + {...htmlProps} {...modal.config} > {modal.children} diff --git a/src/components/organisms/FancyModal/TFancyModal.modal.ts b/src/components/organisms/FancyModal/TFancyModal.modal.ts index c438436b0..74818c4ce 100644 --- a/src/components/organisms/FancyModal/TFancyModal.modal.ts +++ b/src/components/organisms/FancyModal/TFancyModal.modal.ts @@ -1,7 +1,12 @@ +import { CSSObject } from 'styled-components'; + +import { TModalHTMLAttributes } from '@/components/molecules/Modal'; import { TFancyModals } from '@/components/organisms/FancyModal/createFancyModalStore'; export type TFancyModal = { appendToDomID: string; modals: TFancyModals[]; closeModal: (id: string) => void; -}; + zIndex?: number; + externalStyle?: CSSObject; +} & TModalHTMLAttributes; diff --git a/src/components/organisms/FancyModal/createFancyModalStore.tsx b/src/components/organisms/FancyModal/createFancyModalStore.tsx index e59238039..aff5ba1de 100644 --- a/src/components/organisms/FancyModal/createFancyModalStore.tsx +++ b/src/components/organisms/FancyModal/createFancyModalStore.tsx @@ -1,9 +1,9 @@ import { create } from 'zustand'; import { TModalStatus } from '@/types/TModalStatus'; -import { TModal } from '@/components/molecules/Modal'; +import { TModalWithHTMLAttributes } from '@/components/molecules/Modal'; -type TModalConfig = Omit; +export type TModalConfig = Omit; export type TFancyModals = { id: string; diff --git a/src/components/organisms/FancyModal/docu/FancyModal.stories.tsx b/src/components/organisms/FancyModal/docu/FancyModal.stories.tsx index fc49b482f..b9093fb47 100644 --- a/src/components/organisms/FancyModal/docu/FancyModal.stories.tsx +++ b/src/components/organisms/FancyModal/docu/FancyModal.stories.tsx @@ -38,6 +38,12 @@ const meta = { type: 'object', }, }, + zIndex: { + description: 'The z-index value for the modal and backdrop.', + control: { + type: 'number', + }, + }, closeModal: { description: 'A function to close the modal with the specified ID.', control: { @@ -96,6 +102,7 @@ export const Primary: Story = { args: { modals: [], appendToDomID: '', + zIndex: 100, children: <>, closeModal: () => {}, }, diff --git a/src/components/organisms/FancyModal/index.ts b/src/components/organisms/FancyModal/index.ts index 9d3e26c9a..61a136984 100644 --- a/src/components/organisms/FancyModal/index.ts +++ b/src/components/organisms/FancyModal/index.ts @@ -1,3 +1,4 @@ export { default as FancyModal } from './FancyModal'; export { createFancyModalStore } from './createFancyModalStore'; +export type { TFancyModals, TModalConfig } from './createFancyModalStore'; export type { TFancyModal } from './TFancyModal.modal'; diff --git a/src/components/organisms/FancySideBar/FancySideBar.style.tsx b/src/components/organisms/FancySideBar/FancySideBar.style.tsx index c7e1ee3fe..eddc65958 100644 --- a/src/components/organisms/FancySideBar/FancySideBar.style.tsx +++ b/src/components/organisms/FancySideBar/FancySideBar.style.tsx @@ -19,7 +19,7 @@ export const SlotTop = styled.div<{ theme: TTheme; $sizeC: TFancySideBar['sizeC' flex-direction: column; margin: ${({ theme, $sizeC = 'sm' }) => `0 ${theme.spacing[originalSizeSettings[$sizeC].margin]} 0 ${theme.spacing[originalSizeSettings[$sizeC].margin]}`}; - gap: ${({ $sizeC = 'sm' }) => `${sideBarSizeSettings[$sizeC].gapBetweenItems}`}; + gap: ${({ $sizeC = 'sm', theme }) => `${theme.spacing[sideBarSizeSettings[$sizeC].gapBetweenItems]}`}; `; export const SlotBottom = styled.div<{ theme: TTheme; $sizeC: TFancySideBar['sizeC'] }>` @@ -28,6 +28,7 @@ export const SlotBottom = styled.div<{ theme: TTheme; $sizeC: TFancySideBar['siz margin-top: auto; margin: ${({ theme, $sizeC = 'sm' }) => `auto ${theme.spacing[originalSizeSettings[$sizeC].margin]} 0 ${theme.spacing[originalSizeSettings[$sizeC].margin]}`}; + gap: ${({ $sizeC = 'sm', theme }) => `${theme.spacing[sideBarSizeSettings[$sizeC].gapBetweenItems]}`}; `; export const MainItemsWrapper = styled.div<{ theme: TTheme; $gap: TSpacings }>` diff --git a/src/components/organisms/FancyToastMessage/FancyToastMessage.state.tsx b/src/components/organisms/FancyToastMessage/FancyToastMessage.state.tsx deleted file mode 100644 index e19e5d758..000000000 --- a/src/components/organisms/FancyToastMessage/FancyToastMessage.state.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { create } from 'zustand'; - -import { TFancyToastMessageStore } from './TFancyToastMessage.model'; - -//the toastQueue with the add and remove functions -export const useFancyToastMessageStore = create((set) => ({ - toastQueue: [], - addToast: (toast) => - set((state) => ({ - toastQueue: [...state.toastQueue, { ...toast, id: Date.now(), time: toast.time || 5000 }], - })), - removeToast: (id) => - set((state) => ({ - toastQueue: state.toastQueue.filter((toast) => toast.id !== id), - })), -})); diff --git a/src/components/organisms/FancyToastMessage/FancyToastMessage.style.tsx b/src/components/organisms/FancyToastMessage/FancyToastMessage.style.tsx index c5c78226e..a35a8c93c 100644 --- a/src/components/organisms/FancyToastMessage/FancyToastMessage.style.tsx +++ b/src/components/organisms/FancyToastMessage/FancyToastMessage.style.tsx @@ -1,8 +1,8 @@ -import { styled } from 'styled-components'; +import { CSSProp, styled } from 'styled-components'; import { TTheme } from '@/types/TTheme'; -export const ToastsWrapper = styled.div<{ theme: TTheme }>` +export const ToastsWrapper = styled.div<{ theme: TTheme; $externalStyle: CSSProp }>` position: fixed; z-index: 1000; top: ${({ theme }) => theme.spacing.md}; @@ -11,4 +11,5 @@ export const ToastsWrapper = styled.div<{ theme: TTheme }>` max-width: 350px; display: flex; flex-direction: column; + ${({ $externalStyle }) => $externalStyle}; `; diff --git a/src/components/organisms/FancyToastMessage/FancyToastMessage.tsx b/src/components/organisms/FancyToastMessage/FancyToastMessage.tsx index bd4b1c202..37050a1d2 100644 --- a/src/components/organisms/FancyToastMessage/FancyToastMessage.tsx +++ b/src/components/organisms/FancyToastMessage/FancyToastMessage.tsx @@ -3,11 +3,11 @@ import { useMemo } from 'react'; import { useTransition, animated } from '@react-spring/web'; -import { useFancyToastMessageStore } from './FancyToastMessage.state'; import { SingleToastMessage } from '@/components/molecules/SingleToastMessage'; import { TToastMessage } from '@/components/molecules/SingleToastMessage'; import { ToastsWrapper } from './FancyToastMessage.style'; +import { TFancyToastMessages } from '@/components/organisms/FancyToastMessage/TFancyToastMessage.model'; //this comonent should be used as overlay in the application to make sure the toast messages are always displaey on top // to use this component in the application you have to add this component to the dom (Main Component) of the application @@ -22,15 +22,14 @@ import { ToastsWrapper } from './FancyToastMessage.style'; // --------------------------------------------------------------------------- // // ------- The Main Toast Message Module to displayed multible messages ------ // // --------------------------------------------------------------------------- // -export default function FancyToastMessage() { - const toastQueue = useFancyToastMessageStore((state) => state.toastQueue); - const removeToast = useFancyToastMessageStore((state) => state.removeToast); +export default function FancyToastMessage(props: TFancyToastMessages) { + const { toastMessages, closeToast, externalStyle, ...htmlProps } = props; // create a refMap to store the height of each toast message const refMap = useMemo(() => new WeakMap(), []); // create the transitions for the toast messages - const transitions = useTransition(toastQueue, { + const transitions = useTransition(toastMessages, { from: { opacity: 0, height: '0px', transform: 'translateX(200%)', marginBottom: '0px' }, keys: (item: TToastMessage) => item.id, enter: (item: TToastMessage) => async (next) => { @@ -51,17 +50,17 @@ export default function FancyToastMessage() { }, { height: '0px', marginBottom: '0px', config: { duration: 265, tension: 250, friction: 125, precision: 0.8 } }, ], - onDestroyed: (item: TToastMessage) => removeToast(item.id), + onDestroyed: (item: TToastMessage) => closeToast(item.id), }); return ( - + {transitions(({ ...style }, item: TToastMessage) => ( ref && refMap.set(item, ref)} toast={item} - remove={removeToast} + remove={closeToast} /> ))} diff --git a/src/components/organisms/FancyToastMessage/TFancyToastMessage.model.ts b/src/components/organisms/FancyToastMessage/TFancyToastMessage.model.ts index 32b0c8add..79263f9d3 100644 --- a/src/components/organisms/FancyToastMessage/TFancyToastMessage.model.ts +++ b/src/components/organisms/FancyToastMessage/TFancyToastMessage.model.ts @@ -1,4 +1,12 @@ import { TToastMessage } from '@/components/molecules/SingleToastMessage'; +import { HTMLAttributes } from 'react'; +import { CSSProp } from 'styled-components'; + +export type TFancyToastMessages = { + externalStyle?: CSSProp; + toastMessages: TToastMessage[]; + closeToast: (id: number) => void; +} & HTMLAttributes; //omit id from TToastMessage because the store will add the id export type TFancyToastMessage = Omit; diff --git a/src/components/organisms/FancyToastMessage/createFancyToastMessageStore.state.tsx b/src/components/organisms/FancyToastMessage/createFancyToastMessageStore.state.tsx new file mode 100644 index 000000000..20594f968 --- /dev/null +++ b/src/components/organisms/FancyToastMessage/createFancyToastMessageStore.state.tsx @@ -0,0 +1,20 @@ +import { create } from 'zustand'; + +import { TFancyToastMessageStore } from './TFancyToastMessage.model'; + +//the toastQueue with the add and remove functions +export function createFancyToastMessageStore() { + return create((set) => ({ + toastQueue: [], + // add a new toast to the queue + addToast: (toast) => + set((state) => ({ + toastQueue: [...state.toastQueue, { ...toast, id: Date.now(), time: toast.time || 5000 }], + })), + // remove a toast from the queue + removeToast: (id) => + set((state) => ({ + toastQueue: state.toastQueue.filter((toast) => toast.id !== id), + })), + })); +} diff --git a/src/components/organisms/FancyToastMessage/docu/FancyToastMessage.mdx b/src/components/organisms/FancyToastMessage/docu/FancyToastMessage.mdx index 7a35730fa..1275dcf05 100644 --- a/src/components/organisms/FancyToastMessage/docu/FancyToastMessage.mdx +++ b/src/components/organisms/FancyToastMessage/docu/FancyToastMessage.mdx @@ -19,21 +19,24 @@ import { FancyToastMessage } from 'fui-fancyui'; 2. If using TypeScript, you can import the type definitions for prop validation: ```jsx -import { TFancyToastMessageStore, TFancyToastMessage } from 'fui-fancyui'; +import { createFancyToastMessageStore, TFancyToastMessage } from 'fui-fancyui'; ``` ### Integration 1. Place the `FancyToastMessage` component in your application's main component to allow toast messages to overlay other content. -2. Utilize the `useFancyToastMessageStore` to add or remove toast messages. Import `useFancyToastMessageStore` and call `addToast` with the desired toast message properties. +2. Utilize the `createFancyToastMessageStore` to add or remove toast messages. Import `createFancyToastMessageStore` and call `addToast` with the desired toast message properties. ## Example Usage ```jsx import React from 'react'; -import { useFancyToastMessageStore } from 'fui-fancyui'; +import { createFancyToastMessageStore } from 'fui-fancyui'; import { FancyToastMessage } from 'fui-fancyui'; +// you can create the store in a separate file and import it where needed +export const useFancyToastMessageStore = createFancyToastMessageStore(); + function App() { const addToast = useFancyToastMessageStore((state) => state.addToast); diff --git a/src/components/organisms/FancyToastMessage/docu/FancyToastMessage.stories.tsx b/src/components/organisms/FancyToastMessage/docu/FancyToastMessage.stories.tsx index 7a24ecad5..c6b9d9cff 100644 --- a/src/components/organisms/FancyToastMessage/docu/FancyToastMessage.stories.tsx +++ b/src/components/organisms/FancyToastMessage/docu/FancyToastMessage.stories.tsx @@ -3,10 +3,10 @@ import { Meta, StoryObj } from '@storybook/react'; // Import the component to be tested import FancyToastMessage from '../FancyToastMessage'; -import { useFancyToastMessageStore } from '../FancyToastMessage.state'; +import { createFancyToastMessageStore } from '../createFancyToastMessageStore.state'; import { FancyButton } from '../../FancyButton'; import { TFancyToastMessage } from '@/components/organisms/FancyToastMessage'; -import templateThemeType from '@/stories/templateSettingsForStorys/templatesForThemeType'; +import { TFancyToastMessages } from '@/components/organisms/FancyToastMessage/TFancyToastMessage.model'; // Define metadata for the story const meta = { @@ -25,33 +25,36 @@ const meta = { }, // Define arguments for the story argTypes: { - ...templateThemeType('systemMessage', 'success', 0), - title: { - description: 'The title of the toast.', - control: { type: 'text' }, - }, - message: { - description: 'The message to be displayed in the toast.', - control: { type: 'text' }, + toastMessages: { + description: 'Queue of toast messages to be displayed', + control: { type: 'object' }, + table: { + type: { summary: 'TFancyToastMessage[]' }, + defaultValue: { summary: '[]' }, + }, }, - time: { - description: 'The time in milliseconds the toast will be displayed.', - control: { type: 'number' }, + closeToast: { + description: 'Function to close a specific toast message', + control: { type: 'function' }, table: { - defaultValue: { summary: '5000' }, + type: { summary: '(id: string) => void' }, + defaultValue: { summary: 'undefined' }, }, }, }, -} satisfies Meta; +} satisfies Meta; // Export the metadata export default meta; // Define the story object type Story = StoryObj; +const useFancyToastMessageStore = createFancyToastMessageStore(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any const HelperComponent = (props: any) => { const addToast = useFancyToastMessageStore((state) => state.addToast); + const toastMessages = useFancyToastMessageStore((state) => state.toastQueue); const { title, message, time, themeType } = props as TFancyToastMessage; const hanldeAddToast = () => { @@ -65,7 +68,7 @@ const HelperComponent = (props: any) => { return ( <> - + ); @@ -75,9 +78,7 @@ const HelperComponent = (props: any) => { export const Primary: Story = { render: (args) => , args: { - title: 'My Title of the titel ', - message: 'This is my toast message hjsadhjgdshjag.', - time: 50500, - themeType: 'error', + closeToast: (id) => useFancyToastMessageStore.getState().removeToast(id), + toastMessages: useFancyToastMessageStore.getState().toastQueue, }, }; diff --git a/src/components/organisms/FancyToastMessage/index.ts b/src/components/organisms/FancyToastMessage/index.ts index 036167f52..d94fb1d6c 100644 --- a/src/components/organisms/FancyToastMessage/index.ts +++ b/src/components/organisms/FancyToastMessage/index.ts @@ -1,4 +1,4 @@ export { default as FancyToastMessage } from './FancyToastMessage'; -export { useFancyToastMessageStore } from './FancyToastMessage.state'; +export { createFancyToastMessageStore } from './createFancyToastMessageStore.state'; export type { TFancyToastMessageStore, TFancyToastMessage } from './TFancyToastMessage.model'; diff --git a/src/components/shared/FancyPopover/FancyPopover.model.ts b/src/components/shared/FancyPopover/FancyPopover.model.ts new file mode 100644 index 000000000..4e3c387e2 --- /dev/null +++ b/src/components/shared/FancyPopover/FancyPopover.model.ts @@ -0,0 +1,10 @@ +import React from 'react'; + +export type TFancyPopover = { + refComponent: React.ReactElement; + contentComponent: React.ReactElement; + offsetX?: number; // horizontal offset + offsetY?: number; // vertical offset +}; + +export type TFancyPopoverWithHTMLAttrs = TFancyPopover & React.HTMLAttributes; diff --git a/src/components/shared/FancyPopover/Popover.style.tsx b/src/components/shared/FancyPopover/FancyPopover.style.tsx similarity index 94% rename from src/components/shared/FancyPopover/Popover.style.tsx rename to src/components/shared/FancyPopover/FancyPopover.style.tsx index 52ccef39d..43680a9a1 100644 --- a/src/components/shared/FancyPopover/Popover.style.tsx +++ b/src/components/shared/FancyPopover/FancyPopover.style.tsx @@ -2,7 +2,6 @@ import { styled } from 'styled-components'; export const RefContainer = styled.div` position: relative; - display: inline-flex; `; export const ContentContainer = styled.div` diff --git a/src/components/shared/FancyPopover/FancyPopover.tsx b/src/components/shared/FancyPopover/FancyPopover.tsx index 0ec0cca35..cee5b2608 100644 --- a/src/components/shared/FancyPopover/FancyPopover.tsx +++ b/src/components/shared/FancyPopover/FancyPopover.tsx @@ -1,17 +1,12 @@ 'use client'; -import React, { useState, useRef, useEffect } from 'react'; +import { useEffect, useRef, useState } from 'react'; -import { ContentContainer, RefContainer } from './Popover.style'; +import { TFancyPopoverWithHTMLAttrs } from './FancyPopover.model'; +import { ContentContainer, RefContainer } from './FancyPopover.style'; -interface FancyPopoverProps { - refComponent: React.ReactElement; - contentComponent: React.ReactElement; - offsetX?: number; // horizontal offset - offsetY?: number; // vertical offset -} -export default function FancyPopover(props: FancyPopoverProps) { - const { refComponent, contentComponent, offsetX = 0, offsetY = 0 } = props; +export default function FancyPopover(props: TFancyPopoverWithHTMLAttrs) { + const { refComponent, contentComponent, offsetX = 0, offsetY = 0, ...htmlProps } = props; const [isContentVisible, setContentVisible] = useState(false); const ref = useRef(null); const contentRef = useRef(null); @@ -70,7 +65,7 @@ export default function FancyPopover(props: FancyPopoverProps) { }; return ( - + {refComponent} {isContentVisible && {contentComponent}} diff --git a/src/components/shared/FancyPopover/Popover.tsx b/src/components/shared/FancyPopover/Popover.tsx deleted file mode 100644 index 7dfd15467..000000000 --- a/src/components/shared/FancyPopover/Popover.tsx +++ /dev/null @@ -1,89 +0,0 @@ -'use client'; - -import React, { useState, useLayoutEffect, useRef } from 'react'; - -import { PopoverContainer } from './Popover.style'; - -type PopoverProps = { - buttonRef: React.RefObject; - content: React.ReactNode; - isVisible: boolean; - onClose: () => void; - xOffset?: number; - yOffset?: number; -}; - -const Popover: React.FC = ({ buttonRef, content, isVisible, onClose, xOffset = 0, yOffset = 0 }) => { - const [position, setPosition] = useState({ top: 0, left: 0 }); - const popoverRef = useRef(null); - - const handleClickOutside = (event: MouseEvent) => { - if ( - popoverRef.current && - !popoverRef.current.contains(event.target as Node) && - !buttonRef.current?.contains(event.target as Node) - ) { - onClose(); - } - }; - - useLayoutEffect(() => { - function updatePosition() { - if (buttonRef.current && popoverRef.current) { - const buttonRect = buttonRef.current.getBoundingClientRect(); - const popoverRect = popoverRef.current.getBoundingClientRect(); - - const isButtonOnLeftSide = buttonRect.left < window.innerWidth / 2; - - let newLeft = isButtonOnLeftSide - ? buttonRect.left + xOffset - : buttonRect.right - popoverRef.current.offsetWidth - xOffset; - - let newTop = buttonRect.bottom + yOffset; - - // Check for window boundaries - if (newLeft < 0) { - newLeft = xOffset; // Add space to avoid touching the window edge - } - if (newLeft + popoverRect.width > window.innerWidth) { - newLeft = window.innerWidth - popoverRect.width - xOffset; // Subtract space to avoid touching the window edge - } - if (newTop + popoverRect.height > window.innerHeight) { - newTop = buttonRect.top - popoverRect.height - yOffset; - } - - const newPosition = { - top: newTop, - left: newLeft, - }; - - setPosition(newPosition); - } - } - - if (isVisible) { - updatePosition(); - } - - // Attach event listeners - if (isVisible) { - document.addEventListener('click', handleClickOutside, true); - window.addEventListener('resize', updatePosition); - } - - // Cleanup event listeners on cleanup - return () => { - document.removeEventListener('click', handleClickOutside, true); - window.removeEventListener('resize', updatePosition); - }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isVisible, buttonRef, xOffset, yOffset, content]); - - return ( - - {content} - - ); -}; - -export default Popover; diff --git a/src/components/shared/FancyPopover/index.ts b/src/components/shared/FancyPopover/index.ts index 1068bda89..f6c4aad13 100644 --- a/src/components/shared/FancyPopover/index.ts +++ b/src/components/shared/FancyPopover/index.ts @@ -1 +1,3 @@ export { default as FancyPopover } from './FancyPopover'; + +export type { TFancyPopover, TFancyPopoverWithHTMLAttrs } from './FancyPopover.model'; diff --git a/src/components/templates/FancyGrid/FancyGrid.model.ts b/src/components/templates/FancyGrid/FancyGrid.model.ts index bd4f15330..18ec8bd61 100644 --- a/src/components/templates/FancyGrid/FancyGrid.model.ts +++ b/src/components/templates/FancyGrid/FancyGrid.model.ts @@ -1,7 +1,8 @@ -import { TThemeArrayOrValueCSS } from '@/design/designFunctions/arrayToCssValues'; -import { TThemeValueOrCSS } from 'lib'; import { CSSProp } from 'styled-components'; +import { TThemeArrayOrValueCSS } from '@/design/designFunctions/arrayToCssValues'; +import { TThemeValueOrCSS } from '@/design/designFunctions/getThemeOrValueAsCss'; + export interface ICustomBreakpoint { breakpoint: string; // Breakpoint-Größe, z.B. '768px' gridSize: number; // Anzahl der Spalten für diesen Breakpoint diff --git a/src/components/templates/FancyGridItem/FancyGridItem.model.ts b/src/components/templates/FancyGridItem/FancyGridItem.model.ts index efaf98426..ee6b8cf5a 100644 --- a/src/components/templates/FancyGridItem/FancyGridItem.model.ts +++ b/src/components/templates/FancyGridItem/FancyGridItem.model.ts @@ -1,6 +1,6 @@ import { CSSProp } from 'styled-components'; import { ICustomBreakpoint } from '@/components/templates/FancyGrid/FancyGrid.model'; -import { TDynamicElement } from 'lib'; +import { TDynamicElement } from '@/types/TDynamicElement'; export type TFancyGridItemProps = { gridSpace?: number; diff --git a/src/components/templates/FancySystemMessageBox/utils/SystemMessageIcon.ts b/src/components/templates/FancySystemMessageBox/utils/SystemMessageIcon.ts index 02dd448e1..a9045e7f8 100644 --- a/src/components/templates/FancySystemMessageBox/utils/SystemMessageIcon.ts +++ b/src/components/templates/FancySystemMessageBox/utils/SystemMessageIcon.ts @@ -5,9 +5,7 @@ import { SVGWarningSign } from '@/components/icons'; import { SVGCircleCheck } from '@/components/icons'; import { TUiColorsNotTransparent } from '@/types/TUiColorsNotTransparent'; -type TSystemMessageIcon = Record JSX.Element>; - -export const SystemMessageIcon: TSystemMessageIcon = { +export const SystemMessageIcon = { success: SVGCircleCheck, error: SVGStopSign, info: SVGInfoSign, diff --git a/src/design/designFunctions/colorCalculatorForComponent/colorCalculatorForComponent.ts b/src/design/designFunctions/colorCalculatorForComponent/colorCalculatorForComponent.ts index 72c521d58..b810be12e 100644 --- a/src/design/designFunctions/colorCalculatorForComponent/colorCalculatorForComponent.ts +++ b/src/design/designFunctions/colorCalculatorForComponent/colorCalculatorForComponent.ts @@ -6,9 +6,11 @@ import { TthemeColorGroup } from '../../theme/generateThemeColor/generateThemeCo import { TTheme } from '@/types/TTheme'; import { TUiColorTypes } from '@/types/TUiColorTypes'; import { themeStore } from '@/design/theme/themeStore'; +import { TLayer } from '@/types'; // Define the types for the arguments that will be passed to the getBackgroundColor function -type IGetBackgroundColor = Pick; +type IGetBackgroundColor = Pick & + Partial>; // Define the getBackgroundColor function export function getBackgroundColor({ theme, $themeType, $customColor, $layer }: IGetBackgroundColor) { @@ -28,7 +30,7 @@ export function getBackgroundColor({ theme, $themeType, $customColor, $layer }: } // Return the background color as a styled-component CSS string - return proviedColor || theme.color.primary[0]; + return proviedColor || ''; } // --------------------------------------------------------------------------- // @@ -88,8 +90,8 @@ type IGetColorForComponent = { $themeType: TUiColorTypes; $customColor?: string | TthemeColorGroup; $customTextColor?: string | TthemeColorGroup; - $layer?: number; - $textLayer?: number; + $layer?: TLayer; + $textLayer?: TLayer; }; // Define the getColorsForComponent function diff --git a/src/design/designFunctions/generateItemTheme/utils/generateNormalStyle.ts b/src/design/designFunctions/generateItemTheme/utils/generateNormalStyle.ts index 33de349e2..29cf05f1c 100644 --- a/src/design/designFunctions/generateItemTheme/utils/generateNormalStyle.ts +++ b/src/design/designFunctions/generateItemTheme/utils/generateNormalStyle.ts @@ -7,6 +7,7 @@ import { boxShadow } from '@/design/designFunctions/shadows'; import { calcTextColor } from './calcTextColor'; import { generateBackgroundColor } from './generateBackgroundColor'; import { IGenerateThemeItem } from '../IGenerateThemeItemProps.model'; +import { clampLayer } from '@/utils/functions/clampLayer'; //-----this funktion generates a button that looks like a normal button-----// type IGenerateNormalitem = Pick< @@ -23,8 +24,12 @@ export const generateNormal = (props: IGenerateNormalitem) => { // generates the hover style for the button const hoverBackgroundColorStyle = () => { if ($themeType === 'transparent') return 'transparent'; - if ($hoverColor) return theme.color[$hoverColor][1]; - return getBackgroundColor({ theme, $themeType: $themeType ?? 'accent', $layer: ($layer && $layer + 1) ?? 1 }); + if ($hoverColor) return theme.color[$hoverColor]['1']; + return getBackgroundColor({ + theme, + $themeType: $themeType ?? 'accent', + $layer: clampLayer(($layer && $layer + 1) ?? 1), + }); }; const generatedBackgroundColor = generateBackgroundColor({ $themeType, $layer }); diff --git a/src/design/designFunctions/generateMaterialColorPalette/generateMaterialColorPalette.ts b/src/design/designFunctions/generateMaterialColorPalette/generateMaterialColorPalette.ts new file mode 100644 index 000000000..3d4ea9b31 --- /dev/null +++ b/src/design/designFunctions/generateMaterialColorPalette/generateMaterialColorPalette.ts @@ -0,0 +1,43 @@ +import { hexToHSL } from '@/design/designFunctions/hexToHSL/hexToHSL'; +import { hslToHex } from '@/design/designFunctions/hslToHex/hslToHex'; +import { TShades } from '@/types/TShades'; + +type TgenerateMaterialColorPalette = { + baseHex: string; + isLightTheme?: boolean; +}; + +export function generateMaterialColorPalette(props: TgenerateMaterialColorPalette): Record { + const { baseHex, isLightTheme } = props; + const { h, s } = hexToHSL(baseHex); + + if (isLightTheme) { + return { + 50: hslToHex(h, s, 98), + 100: hslToHex(h, s, 95), + 200: hslToHex(h, s, 90), + 300: hslToHex(h, s, 80), + 400: hslToHex(h, s, 70), + 500: hslToHex(h, s, 60), // Consistent with progression + 600: hslToHex(h, s, 49), + 700: hslToHex(h, s, 37), + 800: hslToHex(h, s, 26), + 900: hslToHex(h, s, 17), + 950: hslToHex(h, s, 10), + }; + } else { + return { + 50: hslToHex(h, s, 20), + 100: hslToHex(h, s, 30), + 200: hslToHex(h, s, 40), + 300: hslToHex(h, s, 50), + 400: hslToHex(h, s, 55), + 500: hslToHex(h, s, 60), // Consistent with progression + 600: hslToHex(h, s, 70), + 700: hslToHex(h, s, 80), + 800: hslToHex(h, s, 90), + 900: hslToHex(h, s, 95), + 950: hslToHex(h, s, 98), + }; + } +} diff --git a/src/design/designFunctions/generateThemeColorSteps/generateThemeColorSteps.ts b/src/design/designFunctions/generateThemeColorSteps/generateThemeColorSteps.ts index 36b102f3d..55f4c07e7 100644 --- a/src/design/designFunctions/generateThemeColorSteps/generateThemeColorSteps.ts +++ b/src/design/designFunctions/generateThemeColorSteps/generateThemeColorSteps.ts @@ -1,51 +1,53 @@ import Color from 'color'; -import { TUiColorTypes } from '@/types/TUiColorTypes'; +import { hexToHSL } from '@/design/designFunctions/hexToHSL/hexToHSL'; +import { hslToHex } from '@/design/designFunctions/hslToHex/hslToHex'; import { TLayer } from '@/types/TLayer'; +import { TUiColorTypes } from '@/types/TUiColorTypes'; // Function to adjust lightness -function adjustLightness(color: Color, delta: number): Color { - const currentLightness = color.lightness(); - const newLightness = Math.max(0, Math.min(100, currentLightness + delta)); - return color.lightness(newLightness); -} +// function adjustLightness(color: Color, delta: number): Color { +// const currentLightness = color.lightness(); +// const newLightness = Math.max(0, Math.min(100, currentLightness + delta)); +// return color.lightness(newLightness); +// } // Generate colors at different steps for a single base color -function generateColorVariations(baseColor: string, steps: number[]): string[] { - const color = Color(baseColor); - return steps.map((step) => { - return adjustLightness(color, step).hexa(); - }); -} +// function generateColorVariations(baseColor: string, steps: number[]): string[] { +// const color = Color(baseColor); +// return steps.map((step) => { +// return adjustLightness(color, step).hexa(); +// }); +// } // Define the steps for the different color types -const degreeSteps = [0, 3, 7, 10, 18, 25, 34, 40, 45, 60]; -const degreeStepsAccent = [0, 5, 10, 15, 20, 25, 30, 35, 40, 45]; -const systemMessagesSteps = [0, 3, 7, 10, 13, 18, 23, 30, 35, 40]; +//const degreeSteps = [0, 3, 7, 10, 18, 25, 34, 40, 45, 60]; +//const degreeStepsAccent = [0, 5, 10, 15, 20, 25, 30, 35, 40, 45]; +//const systemMessagesSteps = [0, 3, 7, 10, 13, 18, 23, 30, 35, 40]; + +const lightnessSteps = [50, 43, 40, 35, 30, 24, 18, 15, 12, 8].reverse(); // Generate colors at different steps for a single base color function lightenColors({ pimaryColor, themeType, color }: IGenerateColorSteps) { //checkColor is dark or light const isPrimaryColorDark = Color(pimaryColor).isDark(); + const { h, s } = hexToHSL(color); // if the color is dark, mirror the degreeSteps - const modifiedForBirigthnesDegreeSteps = !isPrimaryColorDark - ? systemMessagesSteps - : systemMessagesSteps.map((step) => -step); + const modifiedForBirigthnesDegreeSteps = !isPrimaryColorDark ? lightnessSteps : lightnessSteps.map((step) => -step); switch (themeType) { case 'primary': - return generateColorVariations(color, degreeSteps); + return lightnessSteps.map((step) => hslToHex(h, s, step)); case 'secondary': { - const mappedDegreeSteps = degreeSteps.map((step) => -step); - return generateColorVariations(color, mappedDegreeSteps); + return lightnessSteps.map((step) => hslToHex(h, s, 100 - step)); } case 'accent': { - const mappedDegreeSteps = degreeStepsAccent.map((step) => -step); - return generateColorVariations(color, mappedDegreeSteps); + const mappedDegreeSteps = [...lightnessSteps].reverse().map((step) => hslToHex(h, s, step)); + return mappedDegreeSteps; } default: - return generateColorVariations(color, modifiedForBirigthnesDegreeSteps); + return [...modifiedForBirigthnesDegreeSteps].reverse().map((step) => hslToHex(h, s, 100 - step)); } } @@ -57,7 +59,7 @@ type ColorSteps = { interface IGenerateColorSteps { themeType: TUiColorTypes; color: string; - pimaryColor: string; + pimaryColor: string; // primary color of the theme to calculate if the color is dark or light } export default function generateThemeColorSteps({ themeType, color, pimaryColor }: IGenerateColorSteps): ColorSteps { const lightColors = lightenColors({ color, themeType, pimaryColor }); //generate the colors diff --git a/src/design/designFunctions/generateThemeForCard/generateThemeForCard.ts b/src/design/designFunctions/generateThemeForCard/generateThemeForCard.ts index 57aa79314..0fb334d27 100644 --- a/src/design/designFunctions/generateThemeForCard/generateThemeForCard.ts +++ b/src/design/designFunctions/generateThemeForCard/generateThemeForCard.ts @@ -5,6 +5,7 @@ import { getBackgroundColor } from '../colorCalculatorForComponent/colorCalculat import colorTransparencyCalculator from '../colorTransparencyCalculator/colorTransparencyCalculator'; import { TGenerateThemeForCardProps } from './TGenerateThemeForCardProps'; import { themeStore } from '@/design/theme/themeStore'; +import { clampLayer } from '@/utils/functions/clampLayer'; const generateOutlineStyle = (props: TGenerateColorDesign) => { const { $themeType, theme, $layer = 3, $outlinedBackgroundStrength = 0.5, $outlinedRemoveBorder } = props; @@ -14,7 +15,7 @@ const generateOutlineStyle = (props: TGenerateColorDesign) => { // generate the background color with a transparency of the background color const generateSlightBackgroundColor = colorTransparencyCalculator( - getBackgroundColor({ theme, $themeType: $themeType || 'primary', $layer: Math.max(1, $layer - 3) }), + getBackgroundColor({ theme, $themeType: $themeType || 'primary', $layer: clampLayer($layer - 3) }), $outlinedBackgroundStrength ); diff --git a/src/design/designFunctions/getSimpleColorThemeType/getSimpleColorThemeType.mdx b/src/design/designFunctions/getSimpleColorThemeType/getSimpleColorThemeType.mdx index fb9468751..938b4598f 100644 --- a/src/design/designFunctions/getSimpleColorThemeType/getSimpleColorThemeType.mdx +++ b/src/design/designFunctions/getSimpleColorThemeType/getSimpleColorThemeType.mdx @@ -6,7 +6,7 @@ The `getSimpleColorThemeType` function is a utility for determining a simplified ### Input -- **themeType** (`"transparent" | "primary" | "accent" | "accentDarken" | "secondary" | "info" | "success" | "warning" | "error"`, optional): Specifies the desired theme type. +- **themeType** (`"transparent" | "primary" | "accent" | "secondary" | "info" | "success" | "warning" | "error"`, optional): Specifies the desired theme type. ### Output diff --git a/src/design/designFunctions/hexToHSL/hexToHSL.ts b/src/design/designFunctions/hexToHSL/hexToHSL.ts new file mode 100644 index 000000000..ea25a4ebc --- /dev/null +++ b/src/design/designFunctions/hexToHSL/hexToHSL.ts @@ -0,0 +1,35 @@ +export const hexToHSL = (hex: string): { h: number; s: number; l: number } => { + const r = parseInt(hex.slice(1, 3), 16) / 255; + const g = parseInt(hex.slice(3, 5), 16) / 255; + const b = parseInt(hex.slice(5, 7), 16) / 255; + + const max = Math.max(r, g, b); + const min = Math.min(r, g, b); + let h = 0; + let s = 0; + const l = (max + min) / 2; + + if (max !== min) { + const d = max - min; + s = l > 0.5 ? d / (2 - max - min) : d / (max + min); + + switch (max) { + case r: + h = (g - b) / d + (g < b ? 6 : 0); + break; + case g: + h = (b - r) / d + 2; + break; + case b: + h = (r - g) / d + 4; + break; + } + h /= 6; + } + + return { + h: Math.round(h * 360), + s: Math.round(s * 100), + l: Math.round(l * 100), + }; +}; diff --git a/src/design/designFunctions/hslToHex/hslToHex.ts b/src/design/designFunctions/hslToHex/hslToHex.ts new file mode 100644 index 000000000..8f896124d --- /dev/null +++ b/src/design/designFunctions/hslToHex/hslToHex.ts @@ -0,0 +1,12 @@ +export const hslToHex = (h: number, s: number, l: number): string => { + l /= 100; + const a = (s * Math.min(l, 1 - l)) / 100; + const f = (n: number): string => { + const k = (n + h / 30) % 12; + const color = l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1); + return Math.round(255 * color) + .toString(16) + .padStart(2, '0'); + }; + return `#${f(0)}${f(8)}${f(4)}`; +}; diff --git a/src/design/theme/dummyTheme.mdx b/src/design/theme/dummyTheme.mdx index 0cd5513bf..ddfefa4bf 100644 --- a/src/design/theme/dummyTheme.mdx +++ b/src/design/theme/dummyTheme.mdx @@ -30,18 +30,6 @@ export const dummyTheme = { '8': '#341A03FF', '9': '#1C0E02FF', }, - accentDarken: { - '0': '#F17C12FF', - '1': '#F28420FF', - '2': '#F38F34FF', - '3': '#F49742FF', - '4': '#F59F51FF', - '5': '#F6AC69FF', - '6': '#F8B981FF', - '7': '#FACCA2FF', - '8': '#FBD9BBFF', - '9': '#FCE6D3FF', - }, secondary: { '0': '#F0F0EFFF', '1': '#E9E9E7FF', diff --git a/src/design/theme/generateThemeColor/generateThemeColor.ts b/src/design/theme/generateThemeColor/generateThemeColor.ts index 729571999..2efe2bc81 100644 --- a/src/design/theme/generateThemeColor/generateThemeColor.ts +++ b/src/design/theme/generateThemeColor/generateThemeColor.ts @@ -1,14 +1,14 @@ -import { TUiColorsTypeObject } from '@/types/TUiColorsTypeObject'; -import generateThemeColorSteps from '../../designFunctions/generateThemeColorSteps/generateThemeColorSteps'; -import { isColorValid } from '@/utils//validations/isColorValid'; -import { TUiColorTypes } from '@/types/TUiColorTypes'; import validateThemeColorSteps from '@/design/designFunctions/generateThemeColorSteps/validateThemeColorSteps'; +import { TLayer } from '@/types/TLayer'; import { TUiColorsNotTransparent } from '@/types/TUiColorsNotTransparent'; +import { TUiColorsTypeObject } from '@/types/TUiColorsTypeObject'; +import { TUiColorTypes } from '@/types/TUiColorTypes'; +import { isColorValid } from '@/utils//validations/isColorValid'; +import generateThemeColorSteps from '../../designFunctions/generateThemeColorSteps/generateThemeColorSteps'; const themeColors = { primary: '#131825', accent: '#F17C12', - accentDarken: '', secondary: '#f0f0ef', info: '#287fd7', success: '#009688', @@ -16,11 +16,10 @@ const themeColors = { error: '#D21414', }; // success: '#22C390', -themeColors.accentDarken = themeColors.accent; export { themeColors }; -export type TthemeColorGroup = { [key: string]: string }; +export type TthemeColorGroup = Record; export let uiColors: TUiColorsTypeObject = {} as TUiColorsTypeObject; @@ -82,12 +81,11 @@ export const updateThemeColors = (colorObject: IUiColorPops) => { break; } - // accentDarken should not provided by the user% - if (colorType !== 'accentDarken' && colorObject[colorTypedkey] !== undefined) { + if (colorObject[colorTypedkey] !== undefined) { // update the theme color with the new color const generatedColorSteps = generateThemeColorSteps({ themeType: colorTypedkey, - color: colorObject[colorTypedkey]! as string, + color: colorObject[colorTypedkey], pimaryColor: themeColors.secondary, }); // update the themeColors with the new color @@ -97,7 +95,7 @@ export const updateThemeColors = (colorObject: IUiColorPops) => { // if the user provides a object with the color steps // than we need to update the themeColors with the new color steps // and generate the new uiColors - const colorSteps = colorObject[colorTypedkey]! as object; + const colorSteps = colorObject[colorTypedkey]; //Validate ColorSteps object (check if the object has the correct keys) // and then update the themeColor with the new color steps diff --git a/src/design/theme/globalStyle/DefaultStyle/DefaultStyle.ts b/src/design/theme/globalStyle/DefaultStyle/DefaultStyle.ts index cbe982ae1..f51bda236 100644 --- a/src/design/theme/globalStyle/DefaultStyle/DefaultStyle.ts +++ b/src/design/theme/globalStyle/DefaultStyle/DefaultStyle.ts @@ -13,6 +13,11 @@ const DefaultStyle = createGlobalStyle<{ theme?: TTheme }>` position: relative; overflow-y: auto; width: 100%; + + &::selection { + background: ${({ theme }) => theme.color.accent[0]}; + color: ${({ theme }) => theme.color.primary[0]}; + } } ${scrollbar}; diff --git a/src/design/theme/theme.ts b/src/design/theme/theme.ts index 09dc3522f..351ac9f8d 100644 --- a/src/design/theme/theme.ts +++ b/src/design/theme/theme.ts @@ -24,18 +24,6 @@ export const theme = { '8': '#341A03FF', '9': '#1C0E02FF', }, - accentDarken: { - '0': '#F17C12FF', - '1': '#F28420FF', - '2': '#F38F34FF', - '3': '#F49742FF', - '4': '#F59F51FF', - '5': '#F6AC69FF', - '6': '#F8B981FF', - '7': '#FACCA2FF', - '8': '#FBD9BBFF', - '9': '#FCE6D3FF', - }, secondary: { '0': '#F0F0EFFF', '1': '#E9E9E7FF', diff --git a/src/test/mockTheme.ts b/src/test/mockTheme.ts index a76dd263e..8aaf85adc 100644 --- a/src/test/mockTheme.ts +++ b/src/test/mockTheme.ts @@ -1,11 +1,3 @@ -import { breakpoints } from '@/design/theme/brakePoints'; -import { uiColors } from '@/design/theme/generateThemeColor/generateThemeColor'; -import { borderRadius, typography, spacingPx } from '@/design/theme/designSizes'; +import { theme } from '@/design/theme/theme'; -export const mockTheme = { - colors: uiColors, - spacing: spacingPx, - borderRadius: borderRadius, - fontSizes: typography, - breakpoints: breakpoints, -}; +export const mockTheme = theme; diff --git a/src/types/TShades.ts b/src/types/TShades.ts new file mode 100644 index 000000000..d1d9802af --- /dev/null +++ b/src/types/TShades.ts @@ -0,0 +1 @@ +export type TShades = 50 | 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900 | 950; diff --git a/src/utils/functions/adjustSystemMessageColor.ts b/src/utils/functions/adjustSystemMessageColor.ts new file mode 100644 index 000000000..d8711a1a2 --- /dev/null +++ b/src/utils/functions/adjustSystemMessageColor.ts @@ -0,0 +1,6 @@ +import { themeStore } from '@/design/theme/themeStore'; + +export const adjustSystemMessageColor = (color: string) => { + const isDarkTheme = themeStore.getState().isDarkTheme; + return isDarkTheme ? `hsla(from ${color} h s calc(l * 1.4) / 100%)` : `hsla(from ${color} h s calc(l * 0.55) / 100%)`; +}; diff --git a/src/utils/hooks/useActiveBreakpoint/useActiveBreakpoint.ts b/src/utils/hooks/useActiveBreakpoint/useActiveBreakpoint.ts index af2a87f81..b0eb4b39d 100644 --- a/src/utils/hooks/useActiveBreakpoint/useActiveBreakpoint.ts +++ b/src/utils/hooks/useActiveBreakpoint/useActiveBreakpoint.ts @@ -2,18 +2,24 @@ import { useEffect, useState } from 'react'; import { TBreakPoints, TBreakPointsSizes } from '@/types/TBreakPoints'; +import { themeStore } from '@/design/theme/themeStore'; export const useActiveBreakpoint = (breakpoints?: TBreakPoints) => { const [activeBreakpoint, setActiveBreakpoint] = useState(null); + const theme = themeStore((state) => state.theme); useEffect(() => { const handleResize = () => { - setActiveBreakpoint(getActiveBreakpoint(breakpoints)); + setActiveBreakpoint(getActiveBreakpoint(breakpoints || theme.breakpoints)); }; window.addEventListener('resize', handleResize); + + // Call once to set initial breakpoint + handleResize(); + return () => window.removeEventListener('resize', handleResize); - }, [breakpoints]); // Re-evaluate on breakpoints change + }, [breakpoints, theme]); // Re-evaluate on breakpoints change return activeBreakpoint; }; @@ -22,11 +28,12 @@ export const useActiveBreakpoint = (breakpoints?: TBreakPoints) => { const getActiveBreakpoint = (breakpoints?: TBreakPoints) => { if (!breakpoints) return null; - Object.entries(breakpoints).map(([id, query]) => { - if (window.matchMedia(query).matches) { - return id; - } + // Using find instead of map + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const match = Object.entries(breakpoints).find(([_, query]) => { + return window.matchMedia(query).matches; }); - return null; // No match found + // Return the id if found, null otherwise + return match ? (match[0] as TBreakPointsSizes) : null; }; diff --git a/src/utils/hooks/useBodyOverflow/index.ts b/src/utils/hooks/useBodyOverflow/index.ts new file mode 100644 index 000000000..f1e320936 --- /dev/null +++ b/src/utils/hooks/useBodyOverflow/index.ts @@ -0,0 +1 @@ +export { useBodyOverflow } from './useBodyOverflow'; diff --git a/src/utils/hooks/useBodyOverflow/useBodyOverflow.mdx b/src/utils/hooks/useBodyOverflow/useBodyOverflow.mdx new file mode 100644 index 000000000..04c3749e8 --- /dev/null +++ b/src/utils/hooks/useBodyOverflow/useBodyOverflow.mdx @@ -0,0 +1,46 @@ +# useBodyOverflow Hook + +The `useBodyOverflow` hook is a custom React hook that manages the document body's overflow property. It allows you to control the scrolling behavior of the page by setting and resetting the overflow style of the body element. + +## Parameters + +- `overflow`: `string` (optional) + - The desired overflow value to be applied to the document body + - Default value: `'hidden'` + +## Return Value + +This hook doesn't return any value. It performs side effects on the document body's overflow style. + +## Usage + +```jsx +import React from 'react'; +import { useBodyOverflow } from 'fui-fancyui'; + +function Modal() { + // Prevent body scrolling when modal is open + useBodyOverflow('hidden'); + + return
    Modal content
    ; +} +``` + +In this example, the hook is used in a modal component to prevent the body from scrolling when the modal is open. When the modal is unmounted, the original overflow value is restored. + +## Implementation Details + +- The hook takes an optional `overflow` parameter that defaults to `'hidden'` +- On component mount, it stores the original overflow value of the document body +- It then sets the new overflow value specified by the parameter +- When the component unmounts, it restores the original overflow value using the cleanup function +- The effect is re-run whenever the `overflow` parameter changes + +## Notes + +- This hook is particularly useful for modal windows, overlays, or other UI components where you want to temporarily disable page scrolling +- The hook maintains the previous overflow state and restores it on cleanup, ensuring proper behavior when components unmount +- The cleanup function prevents memory leaks and ensures the body overflow style is properly restored +- The hook's dependency array includes the `overflow` parameter, so it will update if the desired overflow value changes + +This hook provides a simple and effective way to manage document body scrolling behavior in React applications, particularly useful for creating modal interfaces and overlay components.ƒ diff --git a/src/utils/hooks/useBodyOverflow/useBodyOverflow.test.tsx b/src/utils/hooks/useBodyOverflow/useBodyOverflow.test.tsx new file mode 100644 index 000000000..952a0589e --- /dev/null +++ b/src/utils/hooks/useBodyOverflow/useBodyOverflow.test.tsx @@ -0,0 +1,61 @@ +import { renderHook } from '@testing-library/react'; +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { useBodyOverflow } from './useBodyOverflow'; + +describe('useBodyOverflow', () => { + // Store original overflow value before each test + let originalOverflow: string; + + beforeEach(() => { + // Save the original overflow value + originalOverflow = document.body.style.overflow; + }); + + afterEach(() => { + // Reset overflow after each test + document.body.style.overflow = originalOverflow; + }); + + it('should set overflow to hidden by default', () => { + renderHook(() => useBodyOverflow()); + expect(document.body.style.overflow).toBe('hidden'); + }); + + it('should set custom overflow value when provided', () => { + renderHook(() => useBodyOverflow('scroll')); + expect(document.body.style.overflow).toBe('scroll'); + }); + + it('should restore original overflow value on unmount', () => { + // Set initial overflow value + document.body.style.overflow = 'auto'; + + const { unmount } = renderHook(() => useBodyOverflow('hidden')); + expect(document.body.style.overflow).toBe('hidden'); + + // Unmount the hook + unmount(); + expect(document.body.style.overflow).toBe('auto'); + }); + + it('should update overflow when prop changes', () => { + const { rerender } = renderHook(({ overflow }) => useBodyOverflow(overflow), { + initialProps: { overflow: 'hidden' }, + }); + + expect(document.body.style.overflow).toBe('hidden'); + + // Rerender with new overflow value + rerender({ overflow: 'scroll' }); + expect(document.body.style.overflow).toBe('scroll'); + }); + + it('should handle empty string as original overflow value', () => { + document.body.style.overflow = ''; + const { unmount } = renderHook(() => useBodyOverflow('hidden')); + + expect(document.body.style.overflow).toBe('hidden'); + unmount(); + expect(document.body.style.overflow).toBe(''); + }); +}); diff --git a/src/utils/hooks/useBodyOverflow/useBodyOverflow.tsx b/src/utils/hooks/useBodyOverflow/useBodyOverflow.tsx new file mode 100644 index 000000000..97316d2a2 --- /dev/null +++ b/src/utils/hooks/useBodyOverflow/useBodyOverflow.tsx @@ -0,0 +1,18 @@ +'use client'; + +import { useEffect } from 'react'; + +export const useBodyOverflow = (overflow = 'hidden') => { + useEffect(() => { + // Store the original overflow value + const originalOverflow = document.body.style.overflow; + + // Set new overflow when component mounts + document.body.style.overflow = overflow; + + // Reset overflow when component unmounts + return () => { + document.body.style.overflow = originalOverflow; + }; + }, [overflow]); +}; diff --git a/src/utils/hooks/useSlider/useSilder.tsx b/src/utils/hooks/useSlider/useSilder.tsx index ea22d54d2..48bd927ec 100644 --- a/src/utils/hooks/useSlider/useSilder.tsx +++ b/src/utils/hooks/useSlider/useSilder.tsx @@ -22,7 +22,7 @@ const useSlider = ({ }: IUseSlider): IUseSliderReturn => { const [markerPosition, setMarkerPosition] = useState({ x: 0, y: 0 }); const [isInteracting, setIsInteracting] = useState(false); - const sliderRef = useRef(); + const sliderRef = useRef(null); //updates the marker position when the color changes const updateMarkerPosition = useCallback( diff --git a/src/utils/hooks/useWindowDimensions/useWindowDimensions.tsx b/src/utils/hooks/useWindowDimensions/useWindowDimensions.tsx index a465051a5..7865f06e6 100644 --- a/src/utils/hooks/useWindowDimensions/useWindowDimensions.tsx +++ b/src/utils/hooks/useWindowDimensions/useWindowDimensions.tsx @@ -1,10 +1,11 @@ 'use client'; - import { useState, useEffect } from 'react'; // This function gets the current window dimensions -function getWindowDimensions() { - const { innerWidth: width, innerHeight: height } = window; +function getWindowDimensions(vp?: VisualViewport | null) { + const width = vp ? vp.width : window.innerWidth; + const height = vp ? vp.height : window.innerHeight; + return { width, height, @@ -17,12 +18,19 @@ export default function useWindowDimensions() { // This useEffect hook adds a window resize event listener and removes it on cleanup useEffect(() => { + const viewport = window.visualViewport; + function handleResize() { - setWindowDimensions(getWindowDimensions()); + setWindowDimensions(getWindowDimensions(viewport)); } - window.addEventListener('resize', handleResize); - return () => window.removeEventListener('resize', handleResize); + if (!viewport) return; + viewport.addEventListener('scroll', handleResize); + viewport.addEventListener('resize', handleResize); + return () => { + viewport.removeEventListener('resize', handleResize); + viewport.removeEventListener('scroll', handleResize); + }; }, []); return windowDimensions; diff --git a/tsconfig.json b/tsconfig.json index 863e5e7e0..05f2a892b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -5,9 +5,12 @@ "lib": ["ES2020", "DOM", "DOM.Iterable"], "module": "ESNext", "skipLibCheck": true, - "baseUrl": ".", + "rootDir": ".", + "baseUrl": "./src", + + "typeRoots": ["./node_modules/@types", "../../node_modules/@types"], "paths": { - "@/*": ["./src/*"] + "@/*": ["./*"] }, /* Bundler mode */ @@ -18,13 +21,14 @@ "noEmit": true, "outDir": "dist", "jsx": "react-jsx", - "types": ["vitest/globals"], + /* Linting */ "strict": true, "noUnusedLocals": false, "noUnusedParameters": false, "noFallthroughCasesInSwitch": true }, + "types": ["vitest/globals"], "include": [ "src/**/*", "dist/index.d.ts",