diff --git a/.yarn/install-state.gz b/.yarn/install-state.gz
index 1c54dae0..a2007366 100644
Binary files a/.yarn/install-state.gz and b/.yarn/install-state.gz differ
diff --git a/new-docs/.storybook/preview.tsx b/new-docs/.storybook/preview.tsx
index 8d2ece4c..fe856910 100644
--- a/new-docs/.storybook/preview.tsx
+++ b/new-docs/.storybook/preview.tsx
@@ -2,7 +2,14 @@
// does not use the src of core like the docs, but instead use the dist build
import "@moai/core/dist/bundle.css";
-import { DocsContainer } from "@storybook/blocks";
+import {
+ Controls,
+ Description,
+ DocsContainer,
+ Stories,
+ Subtitle,
+ Title,
+} from "@storybook/blocks";
import type { Preview } from "@storybook/react";
import React, { ReactElement } from "react";
import "../../core/font/remote.css";
@@ -39,6 +46,15 @@ const preview: Preview = {
);
},
+ page: () => (
+ <>
+
+
+
+
+
+ >
+ ),
},
},
tags: ["autodocs"],
diff --git a/new-docs/src/patterns/form.stories.tsx b/new-docs/src/patterns/form.stories.tsx
new file mode 100644
index 00000000..1312c510
--- /dev/null
+++ b/new-docs/src/patterns/form.stories.tsx
@@ -0,0 +1,216 @@
+import { Meta } from "@storybook/react/*";
+import { ErrorMessage, Field, Form, Formik, FormikErrors } from "formik";
+import { CSSProperties, useState } from "react";
+import { Controller, useForm } from "react-hook-form";
+import { Button, FormError, Input, TextArea } from "../../../core/src";
+
+/**
+ * Moai doesn't come with a built-in form solution. Instead, our input components
+ * (like [Input][3] and [TextArea][4]) are designed to work with popular form
+ * builders (such as [Formik][1] and [React Hook Form][2]) out of the box.
+ *
+ * [1]: https://formik.org/
+ * [2]: https://react-hook-form.com/
+ * [3]: /docs/components-input--primary
+ * [4]: /docs/components-textarea--primary
+ */
+const meta: Meta = {
+ title: "Patterns/Form",
+};
+
+export default meta;
+
+interface FormValues {
+ title: string;
+ message: string;
+}
+
+const ERRORS = {
+ titleRequired: "Title is required",
+ messageRequired: "Message is required",
+ messageLength: "Message must be longer than 5 characters",
+};
+
+const formStyles: CSSProperties = {
+ display: "flex",
+ flexDirection: "column",
+ gap: 16,
+};
+
+const SubmitButton = ({ busy }: { busy: boolean }): JSX.Element => (
+
+);
+
+const postToServer = async (values: FormValues): Promise => {
+ return new Promise((resolve) => {
+ setTimeout(() => {
+ alert(JSON.stringify(values, null, 2));
+ resolve();
+ }, 500);
+ });
+};
+
+/**
+ * To use Moai's input components with Formik, pass them to the "as" prop of
+ * Formik's [Field][1] component:
+ *
+ * [1]: https://formik.org/docs/api/field
+ *
+ * ~~~tsx
+ * import { Field } from "formik";
+ * import { Input } from "../../../core/src";
+ *
+ *
+ *
+ * ~~~
+ *
+ * To show errors, pass FormError to the "component" prop of Formik's
+ * [ErrorMessage][2] component:
+ *
+ * ~~~tsx
+ * import { ErrorMessage } from "formik";
+ * import { FormError } from "../../../core/src";
+ *
+ *
+ * ~~~
+ *
+ * [2]: https://formik.org/docs/api/errormessage
+ *
+ * Full example:
+ */
+export const FormikExample = (): JSX.Element => {
+ /* import { Input, Button, FormError } from "../../../core/src" */
+
+ const title = (
+
+
+
+
+
+ );
+
+ const message = (
+
+
+
+
+
+ );
+
+ const validate = (values: FormValues): FormikErrors => {
+ const errors: FormikErrors = {};
+ if (!values.title) errors.title = ERRORS.titleRequired;
+ if (!values.message) errors.message = ERRORS.messageRequired;
+ if (values.message.length < 5) errors.message = ERRORS.messageLength;
+ return errors;
+ };
+
+ return (
+
+ initialValues={{ title: "", message: "" }}
+ validate={validate}
+ onSubmit={async (values, { setSubmitting }) => {
+ await postToServer(values);
+ setSubmitting(false);
+ }}
+ children={({ isSubmitting: busy }) => (
+
+ )}
+ />
+ );
+};
+
+/**
+ * To use Moai's input components with React Hook Form, [render][2] them in the
+ * "render" prop of RHF's [Controller][1] component:
+ *
+ * ~~~tsx
+ * import { Controller } from "react-hook-form";
+ * import { Input } from "../../../core/src";
+ *
+ *
+ * (
+ *
+ * )}
+ * rules={{ required: "Email is required" }}
+ * />
+ * ~~~
+ *
+ * [1]: https://react-hook-form.com/api#Controller
+ * [2]: https://react-hook-form.com/get-started#IntegratingwithUIlibraries
+ *
+ * To show errors, pass RHF's [error messages][3] as children of Moai's FormError
+ * component:
+ *
+ * ~~~tsx
+ * import { FormError } from "../../../core/src";
+ *
+ *
+ * ~~~
+ *
+ * [3]: https://react-hook-form.com/advanced-usage#ErrorMessages
+ *
+ * Full example:
+ */
+export const ReactHookForm = (): JSX.Element => {
+ /* import { Input, Button, FormError } from "../../../core/src" */
+
+ const { control, formState, handleSubmit } = useForm();
+ const { errors } = formState;
+ const [busy, setBusy] = useState(false);
+
+ const title = (
+
+
+ (
+
+ )}
+ rules={{ required: ERRORS.titleRequired }}
+ defaultValue=""
+ />
+
+
+ );
+
+ const message = (
+
+
+ }
+ rules={{
+ required: { value: true, message: ERRORS.messageRequired },
+ minLength: { value: 5, message: ERRORS.messageLength },
+ }}
+ defaultValue=""
+ />
+
+
+ );
+
+ return (
+
+ );
+};
diff --git a/new-docs/src/patterns/icon.stories.tsx b/new-docs/src/patterns/icon.stories.tsx
new file mode 100644
index 00000000..9bf09f8e
--- /dev/null
+++ b/new-docs/src/patterns/icon.stories.tsx
@@ -0,0 +1,102 @@
+import { Meta } from "@storybook/react";
+import { SVGAttributes } from "react";
+import { RiSearchLine } from "react-icons/ri";
+import { Button } from "../../../core/src";
+
+/**
+ * Moai doesn't have a built-in icon set. Instead, Moai's components work with any
+ * SVG icons. This means you can use Moai with popular icon sets, like
+ * [FontAwesome][1] and [Material Icons][2], or even with [your own icons][4].
+ *
+ * This guide covers the usage of icons inside Moai's components (like in buttons
+ * and inputs). To display an icon on its own, with the control of its size and
+ * color, see the [Icon component][3].
+ *
+ * [1]: https://fontawesome.com
+ * [2]: https://fonts.google.com/icons
+ * [3]: /docs/components-icon--primary
+ * [4]: #advanced
+ */
+const meta: Meta = {
+ title: "Patterns/Icon",
+};
+
+export default meta;
+
+/**
+ * Moai components that support icons usually have an \`icon\` prop. The
+ * recommended way to set this prop is using an icon from the [react-icons][1]
+ * package. It provides icons from many popular sets that can be used directly in
+ * Moai:
+ *
+ * ~~~ts
+ *
+ * import { RiSearchLine } from "react-icons/ri";
+ *
+ * ~~~
+ *
+ * [1]: https://react-icons.github.io/react-icons/
+ */
+export const Basic = (): JSX.Element => (
+
+);
+
+/**
+ * In most cases, [screen readers][1] will [skip the icon][2] and only announce
+ * the text content of a component (e.g. the label of a button). When a component
+ * has no content to be announced (e.g. an icon-only button), you'll often be
+ * asked to provide an explicit icon label:
+ *
+ * [1]: https://en.wikipedia.org/wiki/Screen_reader
+ * [2]: https://www.sarasoueidan.com/blog/accessible-icon-buttons/#icon-sitting-next-to-text
+ */
+export const IconLabel = (): JSX.Element => (
+
+);
+
+/**
+ * When using with a component, the color and size of an icon are usually
+ * controlled by the component itself. For example, in a large, highlight button,
+ * the icon is white and enlarged:
+ */
+export const ColorSize = (): JSX.Element => (
+
+);
+
+/**
+ * Technically, these \`icon\` props simply expect a [function component][1] that
+ * returns an SVG element. The type definition looks like this:
+ *
+ * ~~~ts
+ * interface Props {
+ * style?: CSSProperties;
+ * className?: string;
+ * }
+ *
+ * type Icon = (props: Props) => JSX.Element;
+ * ~~~
+ *
+ * This means you can use Moai with your own custom icons (e.g. logos, product
+ * icons), by creating components that return them as SVG elements. For a full
+ * icon set, consider tools like [React SVGR][2] to programmatically generate
+ * these components from SVG files.
+ *
+ * [1]: https://reactjs.org/docs/components-and-props.html#function-and-class-components
+ * [2]: https://react-svgr.com
+ */
+export const Advanced = (): JSX.Element => {
+ // In practice, this should be defined outside of your component, or even
+ // better, automatically generated by a tool like react-svgr.
+ const Icon = (props: SVGAttributes) => (
+
+ );
+ return ;
+};
diff --git a/yarn.lock b/yarn.lock
index 99004ba8..a47eac30 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2379,8 +2379,10 @@ __metadata:
eslint-plugin-react-hooks: "npm:^4.6.2"
eslint-plugin-react-refresh: "npm:^0.4.7"
eslint-plugin-storybook: "npm:^0.8.0"
+ formik: "npm:^2.4.6"
react: "npm:^18.3.1"
react-dom: "npm:^18.3.1"
+ react-hook-form: "npm:^7.52.0"
storybook: "npm:^8.1.11"
typescript: "npm:^5.5.2"
vite: "npm:^5.3.2"
@@ -4248,6 +4250,16 @@ __metadata:
languageName: node
linkType: hard
+"@types/hoist-non-react-statics@npm:^3.3.1":
+ version: 3.3.5
+ resolution: "@types/hoist-non-react-statics@npm:3.3.5"
+ dependencies:
+ "@types/react": "npm:*"
+ hoist-non-react-statics: "npm:^3.3.0"
+ checksum: 10c0/2a3b64bf3d9817d7830afa60ee314493c475fb09570a64e7737084cd482d2177ebdddf888ce837350bac51741278b077683facc9541f052d4bbe8487b4e3e618
+ languageName: node
+ linkType: hard
+
"@types/http-errors@npm:*":
version: 2.0.4
resolution: "@types/http-errors@npm:2.0.4"
@@ -6157,6 +6169,13 @@ __metadata:
languageName: node
linkType: hard
+"deepmerge@npm:^2.1.1":
+ version: 2.2.1
+ resolution: "deepmerge@npm:2.2.1"
+ checksum: 10c0/4379288cabd817587cee92a095ea65d18317b45e48010a2e0d87982b5f432239a144f9c8ebd4ab090cc21f0cb47e51ebfe32921f329b3b3084a2711d5d63e450
+ languageName: node
+ linkType: hard
+
"deepmerge@npm:^4.2.2":
version: 4.3.1
resolution: "deepmerge@npm:4.3.1"
@@ -7503,6 +7522,24 @@ __metadata:
languageName: node
linkType: hard
+"formik@npm:^2.4.6":
+ version: 2.4.6
+ resolution: "formik@npm:2.4.6"
+ dependencies:
+ "@types/hoist-non-react-statics": "npm:^3.3.1"
+ deepmerge: "npm:^2.1.1"
+ hoist-non-react-statics: "npm:^3.3.0"
+ lodash: "npm:^4.17.21"
+ lodash-es: "npm:^4.17.21"
+ react-fast-compare: "npm:^2.0.1"
+ tiny-warning: "npm:^1.0.2"
+ tslib: "npm:^2.0.0"
+ peerDependencies:
+ react: ">=16.8.0"
+ checksum: 10c0/e2853fc7833649386ff183eac9cb6a69a999c4b05aabe5636152e1cec1eb35ac0d9f511425624f0d7a88c6e19d4fd2aa259399a6683f0475c30170a5a3ea4f79
+ languageName: node
+ linkType: hard
+
"forwarded@npm:0.2.0":
version: 0.2.0
resolution: "forwarded@npm:0.2.0"
@@ -8066,6 +8103,15 @@ __metadata:
languageName: node
linkType: hard
+"hoist-non-react-statics@npm:^3.3.0":
+ version: 3.3.2
+ resolution: "hoist-non-react-statics@npm:3.3.2"
+ dependencies:
+ react-is: "npm:^16.7.0"
+ checksum: 10c0/fe0889169e845d738b59b64badf5e55fa3cf20454f9203d1eb088df322d49d4318df774828e789898dcb280e8a5521bb59b3203385662ca5e9218a6ca5820e74
+ languageName: node
+ linkType: hard
+
"hosted-git-info@npm:^2.1.4":
version: 2.8.9
resolution: "hosted-git-info@npm:2.8.9"
@@ -9523,6 +9569,13 @@ __metadata:
languageName: node
linkType: hard
+"lodash-es@npm:^4.17.21":
+ version: 4.17.21
+ resolution: "lodash-es@npm:4.17.21"
+ checksum: 10c0/fb407355f7e6cd523a9383e76e6b455321f0f153a6c9625e21a8827d10c54c2a2341bd2ae8d034358b60e07325e1330c14c224ff582d04612a46a4f0479ff2f2
+ languageName: node
+ linkType: hard
+
"lodash.camelcase@npm:^4.3.0":
version: 4.3.0
resolution: "lodash.camelcase@npm:4.3.0"
@@ -11427,6 +11480,13 @@ __metadata:
languageName: node
linkType: hard
+"react-fast-compare@npm:^2.0.1":
+ version: 2.0.4
+ resolution: "react-fast-compare@npm:2.0.4"
+ checksum: 10c0/f0300c677e95198b5f993cbb8a983dab09586157dc678f9e2b5b29ff941b6677a8776fbbdc425ce102fad86937e36bb45cfcfd797f006270b97ccf287ebfb885
+ languageName: node
+ linkType: hard
+
"react-fast-compare@npm:^3.0.1":
version: 3.2.2
resolution: "react-fast-compare@npm:3.2.2"
@@ -11434,6 +11494,15 @@ __metadata:
languageName: node
linkType: hard
+"react-hook-form@npm:^7.52.0":
+ version: 7.52.0
+ resolution: "react-hook-form@npm:7.52.0"
+ peerDependencies:
+ react: ^16.8.0 || ^17 || ^18 || ^19
+ checksum: 10c0/058bf5596f314c071863bb133979deb56d0a7817d5bf1908a569c003fe03a15736402b040d3e18aeb259723c6e15c243fe75d2d887ea47ff4be87fc472f31ad5
+ languageName: node
+ linkType: hard
+
"react-hot-toast@npm:^2.4.1":
version: 2.4.1
resolution: "react-hot-toast@npm:2.4.1"
@@ -11462,7 +11531,7 @@ __metadata:
languageName: node
linkType: hard
-"react-is@npm:^16.13.1":
+"react-is@npm:^16.13.1, react-is@npm:^16.7.0":
version: 16.13.1
resolution: "react-is@npm:16.13.1"
checksum: 10c0/33977da7a5f1a287936a0c85639fec6ca74f4f15ef1e59a6bc20338fc73dc69555381e211f7a3529b8150a1f71e4225525b41b60b52965bda53ce7d47377ada1
@@ -12813,6 +12882,13 @@ __metadata:
languageName: node
linkType: hard
+"tiny-warning@npm:^1.0.2":
+ version: 1.0.3
+ resolution: "tiny-warning@npm:1.0.3"
+ checksum: 10c0/ef8531f581b30342f29670cb41ca248001c6fd7975ce22122bd59b8d62b4fc84ad4207ee7faa95cde982fa3357cd8f4be650142abc22805538c3b1392d7084fa
+ languageName: node
+ linkType: hard
+
"tinyspy@npm:^2.2.0":
version: 2.2.1
resolution: "tinyspy@npm:2.2.1"