diff --git a/.eslintrc.json b/.eslintrc.json index 727a02a9..9db0876a 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -3,7 +3,7 @@ "browser": true, "es2021": true }, - "ignorePatterns": ["dist/**/*", "**/*.css"], + "ignorePatterns": ["dist/**/*", "**/*.css", "**/*.scss"], "extends": [ "eslint:recommended", "plugin:@typescript-eslint/recommended", diff --git a/.storybook/main.ts b/.storybook/main.ts index 4b07c1c8..99b6fb1b 100644 --- a/.storybook/main.ts +++ b/.storybook/main.ts @@ -1,4 +1,5 @@ import type { StorybookConfig } from "@storybook/react-webpack5"; +import * as path from "path"; const config: StorybookConfig = { stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"], @@ -22,5 +23,16 @@ const config: StorybookConfig = { docs: { autodocs: true, }, + webpackFinal: async (config) => { + config.module.rules.push({ + test: /\.scss$/, + use: ["style-loader", "css-loader", "sass-loader"], + include: path.resolve(__dirname, "../"), + }); + + // Add any other webpack config modifications here + + return config; + }, }; export default config; diff --git a/.storybook/preview-body.html b/.storybook/preview-body.html new file mode 100644 index 00000000..6fa7e3ef --- /dev/null +++ b/.storybook/preview-body.html @@ -0,0 +1,8 @@ + diff --git a/bin/create_component.sh b/bin/create_component.sh index cc891768..bbf72860 100755 --- a/bin/create_component.sh +++ b/bin/create_component.sh @@ -27,9 +27,9 @@ function create_index_file() { # Function to create the CSS file function create_css_file() { - echo ".$component_name_lowercase {" > "$2/$component_name_lowercase.css" - echo " /* Rules here. */" >> "$2/$component_name_lowercase.css" - echo "}" >> "$2/$component_name_lowercase.css" + echo ".mykn-$component_name_lowercase {" > "$2/$component_name_lowercase.scss" + echo " /* Rules here. */" >> "$2/$component_name_lowercase.scss" + echo "}" >> "$2/$component_name_lowercase.scss" } # Function to create the stories.tsx file @@ -40,7 +40,7 @@ import type { Meta, StoryObj } from "@storybook/react"; import { ${capitalized_component_name} } from "./$component_name_lowercase"; const meta = { - title: "Components/${capitalized_component_name}", + title: "Uncategorized/${capitalized_component_name}", component: ${capitalized_component_name}, } satisfies Meta; @@ -60,7 +60,7 @@ function create_component_file() { cat > "$2/$component_name_lowercase.tsx" < = ({ children, ...props }) => ( -
+
{children}
); diff --git a/package-lock.json b/package-lock.json index 5740fc23..2818501a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,9 @@ "name": "maykin-ui", "version": "0.0.0", "license": "MIT", + "dependencies": { + "clsx": "^2.1.0" + }, "devDependencies": { "@rollup/plugin-commonjs": "^25.0.7", "@rollup/plugin-node-resolve": "^15.2.3", @@ -40,6 +43,8 @@ "rollup": "^4.9.2", "rollup-plugin-peer-deps-external": "^2.2.4", "rollup-plugin-postcss": "^4.0.2", + "sass": "^1.69.7", + "sass-loader": "^13.3.3", "storybook": "^7.6.7", "tslib": "^2.6.2", "typescript": "^5.3.3" @@ -10164,6 +10169,14 @@ "node": ">=0.10.0" } }, + "node_modules/clsx": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.0.tgz", + "integrity": "sha512-m3iNNWpd9rl3jvvcBnu70ylMdrXt8Vlq4HYadnU5fwcOtvkSQWPmj7amUcDT2qYI7risszBjI5AUIUox9D16pg==", + "engines": { + "node": ">=6" + } + }, "node_modules/co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -14325,6 +14338,12 @@ "node": ">= 4" } }, + "node_modules/immutable": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.4.tgz", + "integrity": "sha512-fsXeu4J4i6WNWSikpI88v/PcVflZz+6kMhUfIwc5SY+poQRPnaf5V7qds6SUyUN3cVxEzuCab7QIoLOQ+DQ1wA==", + "dev": true + }, "node_modules/import-cwd": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/import-cwd/-/import-cwd-3.0.0.tgz", @@ -22282,6 +22301,60 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "dev": true }, + "node_modules/sass": { + "version": "1.69.7", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.69.7.tgz", + "integrity": "sha512-rzj2soDeZ8wtE2egyLXgOOHQvaC2iosZrkF6v3EUG+tBwEvhqUCzm0VP3k9gHF9LXbSrRhT5SksoI56Iw8NPnQ==", + "dev": true, + "dependencies": { + "chokidar": ">=3.0.0 <4.0.0", + "immutable": "^4.0.0", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-loader": { + "version": "13.3.3", + "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-13.3.3.tgz", + "integrity": "sha512-mt5YN2F1MOZr3d/wBRcZxeFgwgkH44wVc2zohO2YF6JiOMkiXe4BYRZpSu2sO1g71mo/j16txzUhsKZlqjVGzA==", + "dev": true, + "dependencies": { + "neo-async": "^2.6.2" + }, + "engines": { + "node": ">= 14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "fibers": ">= 3.1.0", + "node-sass": "^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0", + "sass": "^1.3.0", + "sass-embedded": "*", + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "fibers": { + "optional": true + }, + "node-sass": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + } + } + }, "node_modules/scheduler": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.20.2.tgz", diff --git a/package.json b/package.json index 50d070ca..a4d11861 100644 --- a/package.json +++ b/package.json @@ -67,6 +67,8 @@ "rollup": "^4.9.2", "rollup-plugin-peer-deps-external": "^2.2.4", "rollup-plugin-postcss": "^4.0.2", + "sass": "^1.69.7", + "sass-loader": "^13.3.3", "storybook": "^7.6.7", "tslib": "^2.6.2", "typescript": "^5.3.3" @@ -79,5 +81,8 @@ "*.{css,js,jsx,ts,tsx,md}": "npm run lint:fix" }, "readme": "ERROR: No README data found!", - "_id": "maykin-ui@0.0.0" + "_id": "maykin-ui@0.0.0", + "dependencies": { + "clsx": "^2.1.0" + } } diff --git a/src/components/index.ts b/src/components/index.ts index 6c09c00a..6b1afc76 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -1,2 +1,4 @@ // Auto-generated file. Do not modify manually. +export * from "./layout"; export * from "./logo"; +export * from "./page"; diff --git a/src/components/layout/column/column.scss b/src/components/layout/column/column.scss new file mode 100644 index 00000000..9d068281 --- /dev/null +++ b/src/components/layout/column/column.scss @@ -0,0 +1,34 @@ +@import '../../../settings/style'; + +.mykn-column { + grid-column: auto / 6 span; +} + +.mykn-column--debug { + outline: 1px solid blue; +} + +.mykn-column--debug:before { + color: blue; + content: "Mobile (full span)"; + display: block; + font-family: monospace; + text-align: center; +} + +.mykn-column--debug[data-testid]:before { + content: attr(data-testid)!important; +} + +@media screen and (min-width: $breakpoint-desktop) { + @for $i from 1 through 12 { + .mykn-column--span-#{$i} { + grid-column: auto / span #{$i}; + } + + .mykn-column--debug.mykn-column--span-#{$i}:before { + content: "Span #{$i}"; + } + } +} + diff --git a/src/components/layout/column/column.stories.tsx b/src/components/layout/column/column.stories.tsx new file mode 100644 index 00000000..bb2fc7d2 --- /dev/null +++ b/src/components/layout/column/column.stories.tsx @@ -0,0 +1,42 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import React from "react"; + +import { Container } from "../container"; +import { Grid } from "../grid"; +import { Column } from "./column"; + +const meta = { + title: "Layout/Column", + component: Column, + parameters: { + layout: "fullscreen", + }, + render: (args) => ( + + + + + + + + + + + + + + + + + ), +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const ColumnComponent: Story = { + args: { + debug: true, + span: 1, + }, +}; diff --git a/src/components/layout/column/column.tsx b/src/components/layout/column/column.tsx new file mode 100644 index 00000000..c976b6e2 --- /dev/null +++ b/src/components/layout/column/column.tsx @@ -0,0 +1,40 @@ +import clsx from "clsx"; +import React from "react"; + +import "./column.scss"; + +export type ColumnProps = React.PropsWithChildren<{ + span: number; + + /** If set, show the outline of the column. */ + debug?: boolean; + + /** Gets passed as props. */ + [index: string]: unknown; +}>; + +/** + * Column component, must be placed within a Grid component. + * @param children + * @param debug + * @param span + * @param props + * @constructor + */ +export const Column: React.FC = ({ + children, + debug, + span, + ...props +}) => { + return ( +
+ {children} +
+ ); +}; diff --git a/src/components/layout/column/index.ts b/src/components/layout/column/index.ts new file mode 100644 index 00000000..dc354db7 --- /dev/null +++ b/src/components/layout/column/index.ts @@ -0,0 +1 @@ +export * from "./column"; diff --git a/src/components/layout/container/container.scss b/src/components/layout/container/container.scss new file mode 100644 index 00000000..ed09014f --- /dev/null +++ b/src/components/layout/container/container.scss @@ -0,0 +1,21 @@ +.mykn-container { + margin: 0 auto; + max-width: 1240px; + width: 100%; +} + +.mykn-container--debug { + outline: 1px solid red; +} + +.mykn-container--debug[data-testid]:before { + content: attr(data-testid); + color: red; + display: block; + font-family: monospace; + text-align: center; +} + +.mykn-container--debug[data-testid]:before { + content: attr(data-testid); +} diff --git a/src/components/layout/container/container.stories.tsx b/src/components/layout/container/container.stories.tsx new file mode 100644 index 00000000..a4fbacd5 --- /dev/null +++ b/src/components/layout/container/container.stories.tsx @@ -0,0 +1,21 @@ +import type { Meta, StoryObj } from "@storybook/react"; + +import { Container } from "./container"; + +const meta = { + title: "Layout/Container", + component: Container, + parameters: { + layout: "fullscreen", + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const ContainerComponent: Story = { + args: { + "data-testid": "Container", + debug: true, + }, +}; diff --git a/src/components/layout/container/container.tsx b/src/components/layout/container/container.tsx new file mode 100644 index 00000000..09e9aa8e --- /dev/null +++ b/src/components/layout/container/container.tsx @@ -0,0 +1,32 @@ +import clsx from "clsx"; +import React from "react"; + +import "./container.scss"; + +export type ContainerProps = React.PropsWithChildren<{ + /** If set, show the outline of the container. */ + debug?: boolean; + + /** Gets passed as props. */ + [index: string]: unknown; +}>; + +/** + * Container component. + * @param children + * @param debug + * @param props + * @constructor + */ +export const Container: React.FC = ({ + children, + debug, + ...props +}) => ( +
+ {children} +
+); diff --git a/src/components/layout/container/index.ts b/src/components/layout/container/index.ts new file mode 100644 index 00000000..c5b5e8f3 --- /dev/null +++ b/src/components/layout/container/index.ts @@ -0,0 +1 @@ +export * from "./container"; diff --git a/src/components/layout/grid/grid.scss b/src/components/layout/grid/grid.scss new file mode 100644 index 00000000..8283701e --- /dev/null +++ b/src/components/layout/grid/grid.scss @@ -0,0 +1,32 @@ +@import '../../../settings/style'; + +.mykn-grid { + display: grid; + grid-template-columns: repeat(6, auto); + gap: 12px; + width: 100%; +} + +.mykn-grid--debug { + outline: 1px solid green; +} + +.mykn-grid--debug[data-testid]:before { + content: attr(data-testid); + color: green; + display: block; + font-family: monospace; + grid-column: 1 / 12 span; + text-align: center; +} + +.mykn-grid--debug[data-testid]:before { + content: attr(data-testid); +} + +@media screen and (min-width: $breakpoint-desktop) { + .mykn-grid { + grid-template-columns: repeat(12, auto); + gap: 32px; + } +} diff --git a/src/components/layout/grid/grid.stories.tsx b/src/components/layout/grid/grid.stories.tsx new file mode 100644 index 00000000..c0f7e3d6 --- /dev/null +++ b/src/components/layout/grid/grid.stories.tsx @@ -0,0 +1,28 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import React from "react"; + +import { Container } from "../container"; +import { Grid } from "./grid"; + +const meta = { + title: "Layout/Grid", + component: Grid, + parameters: { + layout: "fullscreen", + }, + render: (args) => ( + + + + ), +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const GridComponent: Story = { + args: { + "data-testid": "Grid", + debug: true, + }, +}; diff --git a/src/components/layout/grid/grid.tsx b/src/components/layout/grid/grid.tsx new file mode 100644 index 00000000..4e7e1681 --- /dev/null +++ b/src/components/layout/grid/grid.tsx @@ -0,0 +1,25 @@ +import clsx from "clsx"; +import React from "react"; + +import "./grid.scss"; + +export type GridProps = React.PropsWithChildren<{ + /** If set, show the outline of the grid. */ + debug?: boolean; + + /** Gets passed as props. */ + [index: string]: unknown; +}>; + +/** + * Grid component, must be placed within a Container component. + * @param children + * @param debug + * @param props + * @constructor + */ +export const Grid: React.FC = ({ children, debug, ...props }) => ( +
+ {children} +
+); diff --git a/src/components/layout/grid/index.ts b/src/components/layout/grid/index.ts new file mode 100644 index 00000000..a138f850 --- /dev/null +++ b/src/components/layout/grid/index.ts @@ -0,0 +1 @@ +export * from "./grid"; diff --git a/src/components/layout/index.tsx b/src/components/layout/index.tsx new file mode 100644 index 00000000..13f1397b --- /dev/null +++ b/src/components/layout/index.tsx @@ -0,0 +1,2 @@ +export * from "./container"; +export * from "./grid"; diff --git a/src/components/layout/layout.stories.tsx b/src/components/layout/layout.stories.tsx new file mode 100644 index 00000000..35cc91d5 --- /dev/null +++ b/src/components/layout/layout.stories.tsx @@ -0,0 +1,92 @@ +import type { Meta } from "@storybook/react"; +import React from "react"; + +import { Column } from "./column"; +import { Container } from "./container"; +import { Grid } from "./grid"; + +const meta = { + title: "Layout/Reference", + parameters: { + layout: "fullscreen", + }, + render: (args) => ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ), +} satisfies Meta; + +export default meta; + +export const ReferenceLayoutDesktop = { + args: { + debug: true, + }, +}; + +export const ReferenceLayoutMobile = { + args: { + debug: true, + }, + parameters: { + viewport: { defaultViewport: "mobile1" }, + }, +}; + +export const SampleLayout = { + args: { + debug: true, + }, + render: (args: { debug: boolean }) => ( + + + + + + + + + + + + ), +}; diff --git a/src/components/logo/logo.css b/src/components/logo/logo.css deleted file mode 100644 index ecb9c56d..00000000 --- a/src/components/logo/logo.css +++ /dev/null @@ -1,29 +0,0 @@ -.logo .logo__image { - width: 100%; - object-fit: contain; - max-width: 155px; - overflow: visible; -} - -.logo .logo__handle { - transition: transform .1s ease-in-out; -} - -.logo[href]:hover .logo__handle--left, -.logo[href]:focus .logo__handle--left { - transform: translateX(-2px); -} - - -.logo[href]:active .logo__handle--left { - transform: translateX(2px); -} - -.logo[href]:hover .logo__handle--right, -.logo[href]:focus .logo__handle--right { - transform: translateX(2px); -} - -.logo[href]:active .logo__handle--right { - transform: translateX(-2px); -} diff --git a/src/components/logo/logo.scss b/src/components/logo/logo.scss new file mode 100644 index 00000000..28e2236e --- /dev/null +++ b/src/components/logo/logo.scss @@ -0,0 +1,30 @@ +@import '../../settings/style'; + +.mykn-logo .mykn-logo__image { + width: 100%; + object-fit: contain; + max-width: 155px; + overflow: visible; +} + +.mykn-logo .mykn-logo__handle { + transition: transform var(--animation-duration) var(--animation-timing-function); +} + +.mykn-logo[href]:hover .mykn-logo__handle--left, +.mykn-logo[href]:focus .mykn-logo__handle--left { + transform: translateX(-2px); +} + +.mykn-logo[href]:active .mykn-logo__handle--left { + transform: translateX(2px); +} + +.mykn-logo[href]:hover .mykn-logo__handle--right, +.mykn-logo[href]:focus .mykn-logo__handle--right { + transform: translateX(2px); +} + +.mykn-logo[href]:active .mykn-logo__handle--right { + transform: translateX(-2px); +} diff --git a/src/components/logo/logo.stories.tsx b/src/components/logo/logo.stories.tsx index a1c3a7cd..1dc55e14 100644 --- a/src/components/logo/logo.stories.tsx +++ b/src/components/logo/logo.stories.tsx @@ -4,7 +4,7 @@ import { userEvent } from "@storybook/test"; import { Logo } from "./logo"; const meta = { - title: "Components/Logo", + title: "Brand/Logo", component: Logo, } satisfies Meta; @@ -13,16 +13,16 @@ type Story = StoryObj; export const LogoComponent: Story = { args: { - href: "/?path=/story/components-logo--logo-component", - hrefLabel: "Navigate to logo component page.", + href: "/?path=/story/brand-logo--logo-component", + hrefLabel: "Navigate to story page", label: "Maykin logo", }, }; export const LogoAnimatesOnHoverAndClick: Story = { args: { - href: "#", - hrefLabel: "Navigate to logo component page.", + href: "/?path=/story/brand-logo--logo-component", + hrefLabel: "Navigate to story page", label: "Maykin logo", }, play: async () => { diff --git a/src/components/logo/logo.tsx b/src/components/logo/logo.tsx index 089a7062..d5ccf1c1 100644 --- a/src/components/logo/logo.tsx +++ b/src/components/logo/logo.tsx @@ -1,6 +1,6 @@ import React from "react"; -import "./logo.css"; +import "./logo.scss"; export type LogoProps = { /** The aria-label to set on the SVG element. */ @@ -16,7 +16,7 @@ export type LogoProps = { }; /** - * The Maykin Media logo + * The Maykin logo. * @param children * @param props * @constructor @@ -30,50 +30,55 @@ export const Logo: React.FC = ({ const Tag = href ? "a" : "span"; return ( - + diff --git a/src/components/page/index.ts b/src/components/page/index.ts new file mode 100644 index 00000000..4962a1f6 --- /dev/null +++ b/src/components/page/index.ts @@ -0,0 +1 @@ +export * from "./page"; diff --git a/src/components/page/page.scss b/src/components/page/page.scss new file mode 100644 index 00000000..a4b06ce9 --- /dev/null +++ b/src/components/page/page.scss @@ -0,0 +1,9 @@ +@import "../../settings/style"; + +.mykn-page { + background-color: var(--theme-color-primary-200); + box-sizing: border-box; + width: 100%; + height: 100%; + padding: 50px 20px; +} diff --git a/src/components/page/page.stories.tsx b/src/components/page/page.stories.tsx new file mode 100644 index 00000000..9cdf0fd0 --- /dev/null +++ b/src/components/page/page.stories.tsx @@ -0,0 +1,42 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import React from "react"; + +import { Container, Grid } from "../layout"; +import { Column } from "../layout/column"; +import { Logo } from "../logo"; +import { Page } from "./page"; + +const meta = { + title: "Building Blocks/Page", + component: Page, + parameters: { + layout: "fullscreen", + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const PageComponent: Story = { + args: { + children: "The quick brown fox jumps over the lazy dog.", + }, +}; + +export const SamplePage: Story = { + args: { + children: ( + + + + + + + + ), + }, +}; diff --git a/src/components/page/page.tsx b/src/components/page/page.tsx new file mode 100644 index 00000000..e52d806b --- /dev/null +++ b/src/components/page/page.tsx @@ -0,0 +1,19 @@ +import React from "react"; + +import "./page.scss"; + +export type PageProps = React.PropsWithChildren<{ + // Props here. +}>; + +/** + * Provides the base theme for a page. + * @param children + * @param props + * @constructor + */ +export const Page: React.FC = ({ children, ...props }) => ( +
+ {children} +
+); diff --git a/src/settings/style.scss b/src/settings/style.scss new file mode 100644 index 00000000..8bb18eb8 --- /dev/null +++ b/src/settings/style.scss @@ -0,0 +1,23 @@ +$breakpoint-desktop: 768px; + +:root { + --animation-timing-function: ease-in-out; + --animation-duration: 0.2s; + + --theme-color-primary-800: #341A90; + --theme-color-primary-700: #5422B9; + --theme-color-primary-600: #8D75E6; + --theme-color-primary-400: #BDB0ED; + --theme-color-primary-200: #ECF1FF; /* FIXME? */ + + --theme-shade-800: #0F172A; + --theme-shade-700: #1E293B; + --theme-shade-600: #334155; + --theme-shade-400: #475569; + + --theme-shade-1000: #000; + --theme-shade-0: #FFF; + + --typography-color-background: var(--theme-shade-0); + --typography-color-text: var(--theme-shade-1000); +}