diff --git a/.editorconfig b/.editorconfig index 446768ba..c660c797 100644 --- a/.editorconfig +++ b/.editorconfig @@ -12,7 +12,7 @@ ij_formatter_off_tag = @formatter:off ij_formatter_on_tag = @formatter:on ij_formatter_tags_enabled = true ij_smart_tabs = false -ij_visual_guides = +ij_visual_guides = ij_wrap_on_typing = false [*.css] @@ -143,10 +143,10 @@ ij_xml_space_around_equals_in_attribute = false ij_xml_space_inside_empty_tag = false ij_xml_text_wrap = normal -[{*.ats,*.cts,*.mts,*.ts}] -indent_size = 2 -tab_width = 2 -ij_continuation_indent_size = 2 +[{*.ats,*.cts,*.mts,*.ts,*tsx}] +indent_size = 4 +tab_width = 4 +ij_continuation_indent_size = 4 ij_visual_guides = 80 ij_typescript_align_imports = false ij_typescript_align_multiline_array_initializer_expression = false @@ -235,7 +235,7 @@ ij_typescript_prefer_explicit_types_function_expression_returns = false ij_typescript_prefer_explicit_types_function_returns = false ij_typescript_prefer_explicit_types_vars_fields = false ij_typescript_prefer_parameters_wrap = false -ij_typescript_property_prefix = +ij_typescript_property_prefix = ij_typescript_reformat_c_style_comments = false ij_typescript_space_after_colon = true ij_typescript_space_after_comma = true @@ -417,7 +417,7 @@ ij_javascript_prefer_explicit_types_function_expression_returns = false ij_javascript_prefer_explicit_types_function_returns = false ij_javascript_prefer_explicit_types_vars_fields = false ij_javascript_prefer_parameters_wrap = false -ij_javascript_property_prefix = +ij_javascript_property_prefix = ij_javascript_reformat_c_style_comments = false ij_javascript_space_after_colon = true ij_javascript_space_after_comma = true diff --git a/.stylelintrc b/.stylelintrc index 5e141727..3d614704 100644 --- a/.stylelintrc +++ b/.stylelintrc @@ -16,7 +16,11 @@ "at-rule-no-vendor-prefix": true, "selector-no-vendor-prefix": true, "max-nesting-depth": 3, - "selector-max-compound-selectors": 5 + "selector-max-compound-selectors": 5, + "declaration-block-no-redundant-longhand-properties": null, + "custom-property-pattern": ["^[a-z][a-zA-Z0-9]+$", { + "message": "Expected \"%s\" variable name to be lower camelCase" + }] }, "plugins": [ "stylelint-order" diff --git a/package.json b/package.json index 7a0180bb..38e0724e 100644 --- a/package.json +++ b/package.json @@ -55,10 +55,11 @@ "react-dom": ">=18.2.0" }, "dependencies": { + "classnames": "^2.3.2", + "css-vars-hook": "^0.6.19", "lodash": "^4.17.21" }, "devDependencies": { - "autoprefixer": "10.4.16", "@storybook/addon-essentials": "7.6.2", "@storybook/addon-interactions": "7.6.2", "@storybook/addon-links": "7.6.2", @@ -78,6 +79,7 @@ "@vitejs/plugin-react": "4.2.0", "@yelo/rollup-node-external": "^1.0.1", "alias-hq": "6.2.3", + "autoprefixer": "10.4.16", "eslint": "8.54.0", "eslint-config-prettier": "9.0.0", "eslint-plugin-import": "2.29.0", @@ -95,6 +97,7 @@ "lint-staged": "15.1.0", "npm-run-all": "4.1.5", "postcss": "8.4.31", + "postcss-nesting": "^12.0.1", "prettier": "3.1.0", "react": "18.2.0", "react-dom": "18.2.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 344973d0..e9c29913 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5,6 +5,12 @@ settings: excludeLinksFromLockfile: false dependencies: + classnames: + specifier: ^2.3.2 + version: 2.3.2 + css-vars-hook: + specifier: ^0.6.19 + version: 0.6.19(react-dom@18.2.0)(react@18.2.0) lodash: specifier: ^4.17.21 version: 4.17.21 @@ -121,6 +127,9 @@ devDependencies: postcss: specifier: 8.4.31 version: 8.4.31 + postcss-nesting: + specifier: ^12.0.1 + version: 12.0.1(postcss@8.4.31) prettier: specifier: 3.1.0 version: 3.1.0 @@ -5542,6 +5551,10 @@ packages: static-extend: 0.1.2 dev: true + /classnames@2.3.2: + resolution: {integrity: sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw==} + dev: false + /clean-stack@2.2.0: resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==} engines: {node: '>=6'} @@ -5856,6 +5869,17 @@ packages: source-map-js: 1.0.2 dev: true + /css-vars-hook@0.6.19(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-mILFaXDEbi6rBvom2yIz3B9qZuttameeQL7w2X3gpxrPuX+VZWB360CCJKMA22cPbhRrpXHOvR/oKm9MV+pPtg==} + engines: {node: '>=16'} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + dependencies: + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /css.escape@1.5.1: resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==} dev: true @@ -8714,7 +8738,6 @@ packages: /js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} - dev: true /js-yaml@3.14.1: resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} @@ -9091,7 +9114,6 @@ packages: hasBin: true dependencies: js-tokens: 4.0.0 - dev: true /loupe@2.3.7: resolution: {integrity: sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==} @@ -10021,6 +10043,17 @@ packages: engines: {node: '>=0.10.0'} dev: true + /postcss-nesting@12.0.1(postcss@8.4.31): + resolution: {integrity: sha512-6LCqCWP9pqwXw/njMvNK0hGY44Fxc4B2EsGbn6xDcxbNRzP8GYoxT7yabVVMLrX3quqOJ9hg2jYMsnkedOf8pA==} + engines: {node: ^14 || ^16 || >=18} + peerDependencies: + postcss: ^8.4 + dependencies: + '@csstools/selector-specificity': 3.0.0(postcss-selector-parser@6.0.13) + postcss: 8.4.31 + postcss-selector-parser: 6.0.13 + dev: true + /postcss-resolve-nested-selector@0.1.1: resolution: {integrity: sha512-HvExULSwLqHLgUy1rl3ANIqCsvMS0WHss2UOsXhXnQaZ9VCc2oBvIpXrl00IUFT5ZDITME0o6oiXeiHr2SAIfw==} dev: true @@ -10297,7 +10330,6 @@ packages: loose-envify: 1.4.0 react: 18.2.0 scheduler: 0.23.0 - dev: true /react-element-to-jsx-string@15.0.0(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-UDg4lXB6BzlobN60P8fHWVPX3Kyw8ORrTeBtClmIlGdkOOE+GYQSFvmEU5iLLpwp/6v42DINwNcwOhOLfQ//FQ==} @@ -10390,7 +10422,6 @@ packages: engines: {node: '>=0.10.0'} dependencies: loose-envify: 1.4.0 - dev: true /read-pkg-up@7.0.1: resolution: {integrity: sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==} @@ -10820,7 +10851,6 @@ packages: resolution: {integrity: sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==} dependencies: loose-envify: 1.4.0 - dev: true /semver@5.7.2: resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==} diff --git a/src/lib/CounterDemo/useLogic.ts b/src/lib/CounterDemo/useLogic.ts index a85c49e1..83ddf4ce 100644 --- a/src/lib/CounterDemo/useLogic.ts +++ b/src/lib/CounterDemo/useLogic.ts @@ -1,11 +1,11 @@ import {useState} from 'react'; const useLogic = (initialState: number) => { - const [count, setCount] = useState(initialState); - return { - count, - incrementCount: () => setCount(count + 1), - }; + const [count, setCount] = useState(initialState); + return { + count, + incrementCount: () => setCount(count + 1), + }; }; export default useLogic; diff --git a/src/lib/Layout/Cell.module.css b/src/lib/Layout/Cell.module.css new file mode 100644 index 00000000..7602ff7c --- /dev/null +++ b/src/lib/Layout/Cell.module.css @@ -0,0 +1,12 @@ +.cell { + align-items: center; + background: steelblue; + border-radius: 4px; + color: white; + display: flex; + flex-direction: column; + height: 64px; + justify-content: center; + margin-bottom: 24px; + width: 100%; +} diff --git a/src/lib/Layout/Cell.tsx b/src/lib/Layout/Cell.tsx new file mode 100644 index 00000000..9eef7454 --- /dev/null +++ b/src/lib/Layout/Cell.tsx @@ -0,0 +1,7 @@ +import type {FC, ReactNode} from 'react'; + +import classes from './Cell.module.css'; + +export const Cell: FC<{children?: ReactNode}> = ({children}) => { + return
{children}
; +}; diff --git a/src/lib/Layout/Col.tsx b/src/lib/Layout/Col.tsx new file mode 100644 index 00000000..caa6ff09 --- /dev/null +++ b/src/lib/Layout/Col.tsx @@ -0,0 +1,70 @@ +import type {FC, ReactNode} from 'react'; +import {useEffect} from 'react'; +import {useLocalTheme} from 'css-vars-hook'; +import classNames from 'classnames'; + +import type {OffsetConfig, SizesConfig} from './SizeTypes'; +import classes from './Layout.module.css'; + +export type ColProps = Partial & + Partial & { + /** Select an HTML element to render as a container */ + as?: string; + children?: ReactNode; + className?: string; + }; + +export const Col: FC = ({ + as = 'div', + children, + className, + xs, + sm, + md, + lg, + xl, + offsetXS, + offsetSM, + offsetMD, + offsetLG, + offsetXL, +}) => { + const {LocalRoot, setTheme} = useLocalTheme(); + + useEffect(() => { + setTheme({ + xs: xs ?? '', + sm: sm ?? '', + md: md ?? '', + lg: lg ?? '', + xl: xl ?? '', + offsetXS: offsetXS ?? '', + offsetSM: offsetSM ?? '', + offsetMD: offsetMD ?? '', + offsetLG: offsetLG ?? '', + offsetXL: offsetXL ?? '', + }); + }, [setTheme, xs, sm, md, lg, xl, offsetXS, offsetSM, offsetMD, offsetLG, offsetXL]); + return ( + + {children} + + ); +}; diff --git a/src/lib/Layout/Container.tsx b/src/lib/Layout/Container.tsx new file mode 100644 index 00000000..19617f83 --- /dev/null +++ b/src/lib/Layout/Container.tsx @@ -0,0 +1,48 @@ +import type {FC, ReactNode} from 'react'; +import {useLocalTheme} from 'css-vars-hook'; +import {useEffect} from 'react'; +import classNames from 'classnames'; + +import type {SizeUnit, FluidUnit} from './SizeTypes'; +import classes from './Layout.module.css'; + +export type ContainerProps = { + /** Set Container width in pixels as a number or set to `fluid` to make it 100% */ + containerWidth?: SizeUnit | FluidUnit; + /** Set amount of columns to place in container */ + base?: number; + /** Set a gap between columns in pixels */ + gap?: number; + /** Select HTML element to render as a container */ + as?: string; + /** Specify additional CSS class. This allows you to use styled(Container) or the css prop in styled-components or emotion. */ + className?: string; + children: ReactNode; +}; + +const normalizeWidth = (widthProp: ContainerProps['containerWidth']) => { + if (widthProp === 'fluid') { + return '100%'; + } + return `${widthProp}px`; +}; + +export const Container: FC = ({ + containerWidth = 1280, + className, + as = 'div', + children, + gap = 16, + base = 12, +}) => { + const width = normalizeWidth(containerWidth); + const {LocalRoot, setTheme} = useLocalTheme(); + useEffect(() => { + setTheme({containerWidth: width, base, gap: `${gap}px`}); + }, [setTheme, width, gap, base]); + return ( + + {children} + + ); +}; diff --git a/src/lib/Layout/Layout.mdx b/src/lib/Layout/Layout.mdx new file mode 100644 index 00000000..fc62b4ae --- /dev/null +++ b/src/lib/Layout/Layout.mdx @@ -0,0 +1,36 @@ +import { Meta, ArgsTable, Story } from "@storybook/blocks"; +import * as LayoutStories from './Layout.stories'; +import { Container } from "./Container"; + + + +# Example Component Demo + +## Counter props + + + +## Description + +`Counter` component provides user basic interface to increment counter by one. + +## Demo + + + +## Demo 2 + + + +## Demo 3 + + + +## Demo 4 + + + +## Demo 4 + + + diff --git a/src/lib/Layout/Layout.module.css b/src/lib/Layout/Layout.module.css new file mode 100644 index 00000000..eba09430 --- /dev/null +++ b/src/lib/Layout/Layout.module.css @@ -0,0 +1,108 @@ +.vars { + /* CSS vars to disable IDE error */ + --xs: 0; + --sm: 0; + --md: 0; + --lg: 0; + --xl: 0; + --offsetXS: 0; + --offsetSM: 0; + --offsetMD: 0; + --offsetLG: 0; + --offsetXL: 0; + --gap: 0; + --base: 0; + --containerWidth: 0; +} + +.container { + margin: auto; + max-width: 100%; + width: var(--containerWidth); +} + +.row { + display: flex; + flex-direction: row; + flex-wrap: wrap; + + /* gap variable is taken from Container */ + gap: var(--gap); +} + +.column { + --totalGaps: calc(var(--gap) * (var(--base) - 1)); + --includedGaps: calc(var(--gap) * (var(--span) - 1)); + --singleWidth: calc((var(--containerWidth) - var(--totalGaps)) / var(--base)); + --totalWidth: calc(var(--singleWidth) * var(--span) + var(--includedGaps)); + --offsetWidth: calc(var(--singleWidth) * var(--offset) + var(--gap) * var(--offset)); + + flex: var(--span); + margin-left: var(--offsetWidth); + max-width: var(--totalWidth); + + &.xs { + @media (width >= 360px) { + --span: var(--xs); + --offset: var(--offsetXS); + } + } + + &.sm { + @media (width >= 576px) { + --span: var(--sm); + --offset: var(--offsetSM); + } + } + + &.md { + @media (width >= 768px) { + --span: var(--md); + --offset: var(--offsetMD); + } + } + + &.lg { + @media (width >= 992px) { + --span: var(--lg); + --offset: var(--offsetLG); + } + } + + &.xl { + @media (width >= 1280px) { + --span: var(--xl); + --offset: var(--offsetXL); + } + } + + &.fluid-xs { + @media (width >= 360px) { + flex-grow: 1; + } + } + + &.fluid-sm { + @media (width >= 576px) { + flex-grow: 1; + } + } + + &.fluid-md { + @media (width >= 768px) { + flex-grow: 1; + } + } + + &.fluid-lg { + @media (width >= 992px) { + flex-grow: 1; + } + } + + &.fluid-xl { + @media (width >= 1280px) { + flex-grow: 1; + } + } +} diff --git a/src/lib/Layout/Layout.stories.tsx b/src/lib/Layout/Layout.stories.tsx new file mode 100644 index 00000000..b207943c --- /dev/null +++ b/src/lib/Layout/Layout.stories.tsx @@ -0,0 +1,329 @@ +import type {Meta, StoryObj} from '@storybook/react'; +import {Fragment} from 'react'; + +import {Container} from './Container'; +import {Row} from './Row'; +import {Col} from './Col'; +import {Cell} from './Cell'; + +const meta = { + title: 'Layout/Container', + component: Container, + parameters: { + // More on how to position stories at: https://storybook.js.org/docs/react/configure/story-layout + layout: 'fullscreen', + }, +} as Meta; + +export default meta; +type Story = StoryObj; + +export const ContainerExample: Story = { + args: { + children: ( + + + + span: 1 + + + span: 1 + + + span: 1 + + + span: 1 + + + span: 1 + + + span: 1 + + + span: 1 + + + span: 1 + + + span: 1 + + + span: 1 + + + span: 1 + + + span: 1 + + + + + span: 2 + + + span: 2 + + + span: 2 + + + span: 2 + + + span: 2 + + + span: 2 + + + + + span: 3 + + + span: 3 + + + span: 3 + + + span: 3 + + + + + span: 4 + + + span: 4 + + + span: 4 + + + + + span: 5 + + + span: 5 + + + span: 2 + + + + + span: 6 + + + span: 6 + + + + + span: 7 + + + span: 5 + + + + + span: 8 + + + span: 4 + + + + + span: 9 + + + span: 3 + + + + + span: 10 + + + span: 2 + + + + + span: 11 + + + span: 1 + + + + + span: 12 + + + + ), + }, +}; + +export const ContainerWidth: Story = { + args: { + children: ( + + + + Takes 3 columns. + + + Takes the rest of the width. + + + + + Takes the rest of the width. + + + Takes 6 columns. + + + + ), + }, +}; + +export const ContainerFluid: Story = { + args: { + children: ( +
+ + + + Takes + + + exact + + + width. + + + + + + + Takes + + + all + + + available + + + space. + + + +
+ ), + }, +}; + +export const ContainerResponsive: Story = { + args: { + children: ( + + + + + span: 2 + + + span: 2 + + + span: 2 + + + span: 2 + + + span: 2 + + + span: 2 + + + + + +
xs: 12
+
md: 6
+
+ +
+ + + +
sm: 8
+
xl: 2
+
+ +
+ + + +
md: 4
+
lg: 10
+
+ +
+ + + xs:12 + + +
+
+ ), + }, +}; + +export const ContainerOffset: Story = { + args: { + children: ( + + + + span: 3 + + + span: 3 + + + span: 3 + + + span: 3 + + + + + +
xs: 6
+
offset-xs: 3
+
+ +
+
+ ), + }, +}; diff --git a/src/lib/Layout/Row.tsx b/src/lib/Layout/Row.tsx new file mode 100644 index 00000000..91cbc51e --- /dev/null +++ b/src/lib/Layout/Row.tsx @@ -0,0 +1,21 @@ +import type {ReactNode, FC} from 'react'; +import {useLocalTheme} from 'css-vars-hook'; +import classNames from 'classnames'; + +import classes from './Layout.module.css'; + +type RowProps = { + /** Select an HTML element to render as a container */ + as?: string; + children: ReactNode; + className?: string; +}; + +export const Row: FC = ({className, children, as = 'div'}) => { + const {LocalRoot} = useLocalTheme(); + return ( + + {children} + + ); +}; diff --git a/src/lib/Layout/SizeTypes.tsx b/src/lib/Layout/SizeTypes.tsx new file mode 100644 index 00000000..398c53a4 --- /dev/null +++ b/src/lib/Layout/SizeTypes.tsx @@ -0,0 +1,44 @@ +export enum Sizes { + xs = 'xs', + sm = 'sm', + md = 'md', + lg = 'lg', + xl = 'xl', +} + +export enum Offsets { + xs = 'offsetXS', + sm = 'offsetSM', + md = 'offsetMD', + lg = 'offsetLG', + xl = 'offsetXL', +} + +export type SizeUnit = number; +export type FluidUnit = 'fluid'; + +export type SizesConfig = { + /** The number of columns to span on tiny devices (≥360px) */ + [Sizes.xs]: SizeUnit | FluidUnit; + /** The number of columns to span on small devices (≥576px) */ + [Sizes.sm]: SizeUnit | FluidUnit; + /** The number of columns to span on medium devices (≥768px) */ + [Sizes.md]: SizeUnit | FluidUnit; + /** The number of columns to span on large devices (≥992px) */ + [Sizes.lg]: SizeUnit | FluidUnit; + /** The number of columns to span on extremely large devices (≥1280px) */ + [Sizes.xl]: SizeUnit | FluidUnit; +}; + +export type OffsetConfig = { + /** The number of columns to off set this item from left side on extremely small devices (≥360px) */ + [Offsets.xs]: SizeUnit; + /** The number of columns to off set this item from left side on small devices (≥576px) */ + [Offsets.sm]: SizeUnit; + /** The number of columns to off set this item from left side on medium devices (≥768px) */ + [Offsets.md]: SizeUnit; + /** The number of columns to off set this item from left side on large devices (≥992px) */ + [Offsets.lg]: SizeUnit; + /** The number of columns to off set this item from left side on extremely large devices (≥1280px) */ + [Offsets.xl]: SizeUnit; +}; diff --git a/src/lib/Layout/index.ts b/src/lib/Layout/index.ts new file mode 100644 index 00000000..e69de29b diff --git a/src/lib/Provider/Provider.mdx b/src/lib/Provider/Provider.mdx new file mode 100644 index 00000000..b251ae74 --- /dev/null +++ b/src/lib/Provider/Provider.mdx @@ -0,0 +1,16 @@ +import { Meta, ArgsTable, Story } from "@storybook/blocks"; + +import { Provider } from "./Provider"; + + + +# Example Component Demo + +## Counter props + + + +## Description + +`Provider` component provides user basic interface to increment counter by one. + diff --git a/src/lib/Provider/Provider.tsx b/src/lib/Provider/Provider.tsx new file mode 100644 index 00000000..8591c565 --- /dev/null +++ b/src/lib/Provider/Provider.tsx @@ -0,0 +1,6 @@ +import type {FC, ReactNode} from 'react'; +import {RootThemeProvider} from 'css-vars-hook'; + +export const Provider: FC<{children?: ReactNode}> = ({children}) => { + return {children}; +}; diff --git a/src/lib/Provider/index.ts b/src/lib/Provider/index.ts new file mode 100644 index 00000000..0439b997 --- /dev/null +++ b/src/lib/Provider/index.ts @@ -0,0 +1 @@ +export {Provider} from './Provider.tsx'; diff --git a/src/lib/Theme/index.ts b/src/lib/Theme/index.ts new file mode 100644 index 00000000..e69de29b diff --git a/src/lib/Theme/theme.ts b/src/lib/Theme/theme.ts new file mode 100644 index 00000000..e69de29b diff --git a/src/lib/index.ts b/src/lib/index.ts index 7b9422fa..a3db8d63 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -1 +1,2 @@ export {Counter} from './CounterDemo'; +export {Provider} from './Provider';