+
{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 (
-
+