From 1e5b54c17691b39f2b029e52acc66e46f747e349 Mon Sep 17 00:00:00 2001 From: NgocNhi123 Date: Mon, 24 Jun 2024 21:39:26 +0700 Subject: [PATCH 01/17] fix build fix fix fix fix lint fix lint --- .github/DEVELOP.md | 12 +- .github/workflows/node.js.yml | 2 +- README.md | 7 +- core/package.json | 40 +- core/{rollup.config.js => rollup.config.mjs} | 0 core/src/button/button.tsx | 2 +- core/src/button/color/failure.module.css | 4 +- core/src/button/color/highlight.module.css | 4 +- core/src/button/color/none.module.css | 8 +- core/src/button/style/flat.module.css | 4 +- core/src/button/style/outset.module.css | 5 +- core/src/checkbox/checkbox.tsx | 2 +- core/src/checkbox/outset.module.css | 17 +- core/src/date-input/date-input.tsx | 336 +- core/src/date-input/format.ts | 138 +- core/src/date-input/month/month.tsx | 96 +- core/src/date-input/navbar/navbar.tsx | 52 +- core/src/dialog/native/alert.tsx | 2 +- core/src/dialog/native/confirm.tsx | 2 +- core/src/dialog/native/prompt.tsx | 2 +- core/src/form/field.tsx | 2 +- core/src/icons/blank-icon.tsx | 2 +- core/src/icons/icons.tsx | 2 +- core/src/input/flat.module.css | 4 +- core/src/input/input.tsx | 2 +- core/src/input/outset.module.css | 4 +- core/src/pagination/pagination.tsx | 2 +- core/src/pane/pane.tsx | 2 +- core/src/popover/pane/pane.tsx | 2 +- core/src/progress/circle.tsx | 2 +- core/src/step/step.tsx | 2 +- core/src/tab/tab.tsx | 4 - core/src/table/actions/actions.tsx | 35 +- core/src/table/actions/selectable/state.tsx | 2 +- core/src/table/body/cell.tsx | 8 +- core/src/table/column/column.ts | 2 +- core/src/table/fixed/fixed.ts | 2 +- core/src/text-area/text-area.tsx | 2 +- core/src/toast/container/container.tsx | 15 +- core/src/toast/toast.tsx | 2 +- core/src/toast/type/type.ts | 2 +- core/src/tree/tree.tsx | 17 +- core/src/tree/utils/refresh.ts | 2 +- core/src/tree/utils/update.ts | 2 +- core/src/utils/omit.ts | 7 +- docs/package.json | 20 +- docs/src/color/background/background.tsx | 17 +- docs/src/color/border/border.tsx | 17 +- docs/src/color/sample/sample.tsx | 2 +- docs/src/color/static/table.tsx | 18 +- docs/src/color/text/text.tsx | 25 +- docs/src/components/radio-group-fake.tsx | 2 +- docs/src/components/select-fake.tsx | 2 +- docs/src/components/switcher-fake.tsx | 2 +- docs/src/components/table-fake.tsx | 4 +- eslint.config.mjs | 66 + gallery/package.json | 28 +- .../{rollup.config.js => rollup.config.mjs} | 0 gallery/scripts/example/generate.ts | 4 +- gallery/src/pagination.tsx | 2 +- gallery/src/table/table.tsx | 2 +- package.json | 21 +- test/.swcrc | 58 +- test/jest.config.js | 1 + test/package.json | 22 +- test/src/button.test.tsx | 6 +- test/src/radio.test.tsx | 4 +- test/src/select.test.tsx | 6 +- yarn.lock | 13321 +++++----------- 69 files changed, 4796 insertions(+), 9717 deletions(-) rename core/{rollup.config.js => rollup.config.mjs} (100%) create mode 100644 eslint.config.mjs rename gallery/{rollup.config.js => rollup.config.mjs} (100%) diff --git a/.github/DEVELOP.md b/.github/DEVELOP.md index ca131022..96f984d1 100644 --- a/.github/DEVELOP.md +++ b/.github/DEVELOP.md @@ -14,12 +14,12 @@ To understand the principles that drive the design and development of Moai, see Moai is a [monorepo](https://classic.yarnpkg.com/en/docs/workspaces/) powered by Yarn. There are several projects: -| Path | Project | Framework | -| ------- | ----------------- | ----------- | -| core | [@moai/core] | [Rollup] | -| gallery | [@moai/gallery] | [Rollup] | -| docs | [moai.thien.do] | [Storybook] | -| test | Test suits | [Jest] | +| Path | Project | Framework | +| ------- | --------------- | ----------- | +| core | [@moai/core] | [Rollup] | +| gallery | [@moai/gallery] | [Rollup] | +| docs | [moai.thien.do] | [Storybook] | +| test | Test suits | [Jest] | The "test" and "docs" projects depend on "core" and "gallery" via symlinks. This means to run tests or start the docs site locally, you will need to build "core" and "gallery" first. Also, the "gallery" depends on the "core" project: diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml index c1d5c9dc..6c971423 100644 --- a/.github/workflows/node.js.yml +++ b/.github/workflows/node.js.yml @@ -10,7 +10,7 @@ jobs: - uses: actions/checkout@v2 - uses: actions/setup-node@v2 with: - node-version: 15.x + node-version: 18.x cache: "yarn" - run: yarn install - run: yarn lint diff --git a/README.md b/README.md index c68903ae..b71310c6 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,8 @@ **Heads up!** This project is no longer in active development. It will be [archived](https://docs.github.com/en/repositories/archiving-a-github-repository/archiving-repositories) soon. Some suggestions: -- Build your own UI kit with a good foundation like [Radix](https://www.radix-ui.com/) or [Headless UI](https://headlessui.com/). This is what I'm actually doing these days myself. -- Fork this project. You have my sword, and bow, and axe, and only 22 unresolved bugs. -- Refactor Moai to utilise Radix (which is what I wanted to do, if I don't need to pay the bills) - +- Build your own UI kit with a good foundation like [Radix](https://www.radix-ui.com/) or [Headless UI](https://headlessui.com/). This is what I'm actually doing these days myself. +- Fork this project. You have my sword, and bow, and axe, and only 22 unresolved bugs. +- Refactor Moai to utilise Radix (which is what I wanted to do, if I don't need to pay the bills)
diff --git a/core/package.json b/core/package.json index 831027f5..e84091e3 100644 --- a/core/package.json +++ b/core/package.json @@ -30,28 +30,28 @@ "react-dom": ">=16" }, "dependencies": { - "@popperjs/core": "^2.9.2", - "@tippyjs/react": "^4.2.5", - "react-day-picker": "^7.4.10", - "react-hot-toast": "^1.0.2", - "react-icons": "^4.2.0", - "react-popper": "^2.2.5" + "@popperjs/core": "^2.11.8", + "@tippyjs/react": "^4.2.6", + "react-day-picker": "^8.10.1", + "react-hot-toast": "^2.4.1", + "react-icons": "^5.2.1", + "react-popper": "^2.3.0" }, "devDependencies": { - "@types/react": "^17.0.15", - "@types/react-dom": "^17.0.9", - "autoprefixer": "^10.3.1", - "modern-normalize": "^1.1.0", - "postcss": "^8.3.6", - "postcss-import": "^14.0.2", - "react": "^17.0.2", - "react-dom": "^17.0.2", - "rollup": "^2.55.1", - "rollup-plugin-copy": "^3.4.0", + "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0", + "autoprefixer": "^10.4.19", + "modern-normalize": "^2.0.0", + "postcss": "^8.4.38", + "postcss-import": "^16.1.0", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "rollup": "^4.18.0", + "rollup-plugin-copy": "^3.5.0", "rollup-plugin-delete": "^2.0.0", - "rollup-plugin-postcss": "^4.0.0", - "rollup-plugin-typescript2": "^0.30.0", - "tslib": "^2.3.0", - "typescript": "^4.3.4" + "rollup-plugin-postcss": "^4.0.2", + "rollup-plugin-typescript2": "^0.36.0", + "tslib": "^2.6.3", + "typescript": "^5.5.2" } } diff --git a/core/rollup.config.js b/core/rollup.config.mjs similarity index 100% rename from core/rollup.config.js rename to core/rollup.config.mjs diff --git a/core/src/button/button.tsx b/core/src/button/button.tsx index f6e25303..fe4acbbf 100644 --- a/core/src/button/button.tsx +++ b/core/src/button/button.tsx @@ -267,7 +267,7 @@ const validateButton = (props: ButtonProps): void => { // since it's missing the ref. See Button for the exported component. const buttonRender = ( props: ButtonProps, - ref: React.ForwardedRef + ref: React.ForwardedRef, ): JSX.Element => { validateButton(props); const common = { diff --git a/core/src/button/color/failure.module.css b/core/src/button/color/failure.module.css index 3e95fe2b..7240da34 100644 --- a/core/src/button/color/failure.module.css +++ b/core/src/button/color/failure.module.css @@ -14,7 +14,9 @@ :global(.dark) .outset { color: var(--white); background-color: var(--failure-5); - box-shadow: var(--shadow), var(--inset-shadow) var(--failure-4); + box-shadow: + var(--shadow), + var(--inset-shadow) var(--failure-4); font-weight: 500; } diff --git a/core/src/button/color/highlight.module.css b/core/src/button/color/highlight.module.css index 66a96035..abc50e55 100644 --- a/core/src/button/color/highlight.module.css +++ b/core/src/button/color/highlight.module.css @@ -31,7 +31,9 @@ :global(.dark) .outset { color: var(--white); background-color: var(--highlight-5); - box-shadow: var(--shadow), var(--inset-shadow) var(--highlight-4); + box-shadow: + var(--shadow), + var(--inset-shadow) var(--highlight-4); font-weight: 500; } diff --git a/core/src/button/color/none.module.css b/core/src/button/color/none.module.css index 02877f4c..2a34fedc 100644 --- a/core/src/button/color/none.module.css +++ b/core/src/button/color/none.module.css @@ -25,7 +25,9 @@ :global(.light) .outset { --shadow: var(--shadow-size) rgba(0, 0, 0, 0.1); background-color: var(--gray-0); - box-shadow: var(--shadow), var(--inset-shadow) var(--white); + box-shadow: + var(--shadow), + var(--inset-shadow) var(--white); border-color: var(--gray-2); } :global(.light) .outset:hover { @@ -39,7 +41,9 @@ :global(.dark) .outset { --shadow: var(--shadow-size) rgba(0, 0, 0, 0.5); background-color: var(--gray-6); - box-shadow: var(--shadow), var(--inset-shadow) var(--gray-5); + box-shadow: + var(--shadow), + var(--inset-shadow) var(--gray-5); border-color: var(--black); } :global(.dark) .outset:hover { diff --git a/core/src/button/style/flat.module.css b/core/src/button/style/flat.module.css index f6dd211c..25612c69 100644 --- a/core/src/button/style/flat.module.css +++ b/core/src/button/style/flat.module.css @@ -1,5 +1,7 @@ .main { - transition: background-color 0.1s, outline 0.2s ease-out; + transition: + background-color 0.1s, + outline 0.2s ease-out; border-radius: 0; /* To have same layout with outset buttons */ border: solid 1px transparent; diff --git a/core/src/button/style/outset.module.css b/core/src/button/style/outset.module.css index 8cd2a284..53ac471c 100644 --- a/core/src/button/style/outset.module.css +++ b/core/src/button/style/outset.module.css @@ -1,5 +1,8 @@ .main { - transition: background-color 0.1s, box-shadow 0.1s, outline 0.2s ease-out; + transition: + background-color 0.1s, + box-shadow 0.1s, + outline 0.2s ease-out; border: solid 1px transparent; --shadow-size: 0px 0.5px 2px; --inset-shadow: inset 0px 1px 0px; diff --git a/core/src/checkbox/checkbox.tsx b/core/src/checkbox/checkbox.tsx index 09711a29..1bbc1928 100644 --- a/core/src/checkbox/checkbox.tsx +++ b/core/src/checkbox/checkbox.tsx @@ -147,7 +147,7 @@ export const Checkbox = (props: CheckboxProps): JSX.Element => { /> } /> diff --git a/core/src/checkbox/outset.module.css b/core/src/checkbox/outset.module.css index 9d4ef8fa..c6ce2e33 100644 --- a/core/src/checkbox/outset.module.css +++ b/core/src/checkbox/outset.module.css @@ -1,5 +1,8 @@ .input { - transition: background-color 0.1s, box-shadow 0.1s, outline 0.2s ease-out; + transition: + background-color 0.1s, + box-shadow 0.1s, + outline 0.2s ease-out; border: solid 1px transparent; --shadow-size: 0px 0.5px 2px; --inset-shadow: inset 0px 1px 0px; @@ -10,7 +13,9 @@ :global(.light) .input { --shadow: var(--shadow-size) rgba(0, 0, 0, 0.1); background-color: var(--gray-0); - box-shadow: var(--shadow), var(--inset-shadow) var(--white); + box-shadow: + var(--shadow), + var(--inset-shadow) var(--white); border-color: var(--gray-2); } :global(.light) .input:hover { @@ -24,7 +29,9 @@ :global(.dark) .input { --shadow: var(--shadow-size) rgba(0, 0, 0, 0.3); background-color: var(--gray-6); - box-shadow: var(--shadow), var(--inset-shadow) var(--gray-5); + box-shadow: + var(--shadow), + var(--inset-shadow) var(--gray-5); border-color: var(--black); } :global(.dark) .input:hover { @@ -42,7 +49,9 @@ :global(.dark) .input:checked, :global(.dark) .input[type="checkbox"]:indeterminate { background-color: var(--highlight-5); - box-shadow: var(--shadow), var(--inset-shadow) var(--highlight-4); + box-shadow: + var(--shadow), + var(--inset-shadow) var(--highlight-4); } :global(.light) .input:checked, diff --git a/core/src/date-input/date-input.tsx b/core/src/date-input/date-input.tsx index 7073fc66..f6fb7bf2 100644 --- a/core/src/date-input/date-input.tsx +++ b/core/src/date-input/date-input.tsx @@ -1,173 +1,181 @@ -import { ReactNode, useState } from "react"; -import { CaptionElementProps, Modifier } from "react-day-picker"; -import DayPickerInput from "react-day-picker/DayPickerInput"; -import { Input, InputProps } from "../input/input"; -import { PopoverPane } from "../popover/pane/pane"; -import "./date-input.css"; -import { DateInputFormat, dateInputFormats } from "./format"; -import { DateInputMonth } from "./month/month"; -import { DateInputNavbar } from "./navbar/navbar"; +// TODO: Rewrite date input using the new DayPicker component +import { ReactElement } from "react"; +import { DayPicker } from "react-day-picker"; -interface Props { - /** - * Date format of the text box. This is used to display the selected date - * and to parse the user's input into `Date`. Choose one from - * `DateInput.formats`. Defaults to "dd/mm/yyyy". - */ - format?: DateInputFormat; - /** - * The maximum date users can choose in the pop-up calendar. - */ - maxDate?: Date; - /** - * The minimum date users can choose in the pop-up calendar. - */ - minDate?: Date; - /** - * Value of the input in controlled mode. Note that it includes "null", - * to represent the state where the user's input is invalid (e.g. - * "31/2/1999"). This follows the behavior in HTML. - */ - value?: Date | null; - /** - * Handler to set the value in controlled mode. Note that it includes - * "null". See "value" prop for detail. - */ - setValue?: (date: Date | null) => void; - /** - * Initial value of the input in uncontrolled mode. - */ - defaultValue?: Date | null; - /** - * Whether the input is disabled. This disables the whole input. If you - * want to disable some days, use "minDate" and "maxDate" - */ - disabled?: InputProps["disabled"]; - /** - * Size of the text box. Choose one from `DateInput.sizes`. Same default as - * the in "Input" component. - */ - size?: InputProps["size"]; - /** - * Style of the text box. Choose one from `DateInput.styles`. Same default - * as in the "Input" component. - */ - style?: InputProps["style"]; - /** - * The icon of the text box. See "Icon" page. - */ - icon?: InputProps["icon"]; -} +export const DateInput = (): ReactElement => { + return ; +}; -interface DayPickerOverlayProps { - children: ReactNode; - selectedDay: Date; - month: Date; - input: null; - classNames: { - container: string; // input - overlay: string; - overlayWrapper: string; - }; -} +// import { ReactNode, useState } from "react"; +// import { CaptionElementProps, Modifier } from "react-day-picker"; +// import DayPickerInput from "react-day-picker/DayPickerInput"; +// import { Input, InputProps } from "../input/input"; +// import { PopoverPane } from "../popover/pane/pane"; +// import "./date-input.css"; +// import { DateInputFormat, dateInputFormats } from "./format"; +// import { DateInputMonth } from "./month/month"; +// import { DateInputNavbar } from "./navbar/navbar"; -interface OverlayProps extends DayPickerOverlayProps { - target: HTMLDivElement | null; -} +// interface Props { +// /** +// * Date format of the text box. This is used to display the selected date +// * and to parse the user's input into `Date`. Choose one from +// * `DateInput.formats`. Defaults to "dd/mm/yyyy". +// */ +// format?: DateInputFormat; +// /** +// * The maximum date users can choose in the pop-up calendar. +// */ +// maxDate?: Date; +// /** +// * The minimum date users can choose in the pop-up calendar. +// */ +// minDate?: Date; +// /** +// * Value of the input in controlled mode. Note that it includes "null", +// * to represent the state where the user's input is invalid (e.g. +// * "31/2/1999"). This follows the behavior in HTML. +// */ +// value?: Date | null; +// /** +// * Handler to set the value in controlled mode. Note that it includes +// * "null". See "value" prop for detail. +// */ +// setValue?: (date: Date | null) => void; +// /** +// * Initial value of the input in uncontrolled mode. +// */ +// defaultValue?: Date | null; +// /** +// * Whether the input is disabled. This disables the whole input. If you +// * want to disable some days, use "minDate" and "maxDate" +// */ +// disabled?: InputProps["disabled"]; +// /** +// * Size of the text box. Choose one from `DateInput.sizes`. Same default as +// * the in "Input" component. +// */ +// size?: InputProps["size"]; +// /** +// * Style of the text box. Choose one from `DateInput.styles`. Same default +// * as in the "Input" component. +// */ +// style?: InputProps["style"]; +// /** +// * The icon of the text box. See "Icon" page. +// */ +// icon?: InputProps["icon"]; +// } -const Overlay = (props: OverlayProps): JSX.Element | null => { - // We don't need these props, but we need to remove them from the "rest" so - // that "rest" can be applied to the div element - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { target, children, selectedDay, classNames, month, input, ...rest } = props; // prettier-ignore - if (target === null) return null; - return ( -
- -
- ); -}; +// interface DayPickerOverlayProps { +// children: ReactNode; +// selectedDay: Date; +// month: Date; +// input: null; +// classNames: { +// container: string; // input +// overlay: string; +// overlayWrapper: string; +// }; +// } -const getValue = (props: Props): Date | undefined => { - // Controlled - if (props.value !== undefined) { - return props.value ?? undefined; - } - // Uncontrolled w initial value - if (props.defaultValue !== undefined) { - return props.defaultValue ?? undefined; - } - // Uncontrolled wo initial value - return undefined; -}; +// interface OverlayProps extends DayPickerOverlayProps { +// target: HTMLDivElement | null; +// } -const getDisabledDays = (props: Props): Modifier => { - // This is quite complicated because we need to avoid mutating the "days" - // variable as well as the strictness of Modifier - const { minDate: min, maxDate: max } = props; - let days: Modifier = undefined; - if (max) days = { after: max }; - if (min) days = { ...days, before: min }; - return days; -}; +// const Overlay = (props: OverlayProps): JSX.Element | null => { +// // We don't need these props, but we need to remove them from the "rest" so +// // that "rest" can be applied to the div element +// // eslint-disable-next-line @typescript-eslint/no-unused-vars +// const { target, children, selectedDay, classNames, month, input, ...rest } = props; // prettier-ignore +// if (target === null) return null; +// return ( +//
+// +//
+// ); +// }; -/** - * A date input is a control for users to enter a date, either by typing it or - * choosing from a pop-up calendar. - * - * The Date Input component is based on [React Day Picker][3], as an - * alternative to the built-in ``. It works on - * [unsupported browsers][2] and can display custom date format (e.g. "dmy" or - * "mdy"). If you don't need these features, use [Input][1] to have better - * accessibility support. - - * [1]: /docs/components-input--primary#date - * [2]: https://caniuse.com/input-datetime - * [3]: https://react-day-picker.js.org - * - */ -export const DateInput = (props: Props): JSX.Element => { - const [target, setTarget] = useState(null); - const [month, setMonth] = useState(() => new Date()); - const format = props.format ?? DateInput.formats.dmy; +// const getValue = (props: Props): Date | undefined => { +// // Controlled +// if (props.value !== undefined) { +// return props.value ?? undefined; +// } +// // Uncontrolled w initial value +// if (props.defaultValue !== undefined) { +// return props.defaultValue ?? undefined; +// } +// // Uncontrolled wo initial value +// return undefined; +// }; - return ( -
- ( - - )} - dayPickerProps={{ - month: month, - navbarElement: DateInputNavbar, - captionElement: ({ date }: CaptionElementProps) => ( - - ), - disabledDays: getDisabledDays(props), - }} - value={getValue(props)} - // The lib's type is missing "undefined" case - onDayChange={(day: Date | undefined) => { - if (props.setValue === undefined) return; - props.setValue(day ?? null); - }} - // Format - placeholder={format.placeholder} - formatDate={format.format} - parseDate={format.parse} - /> -
- ); -}; +// const getDisabledDays = (props: Props): Modifier => { +// // This is quite complicated because we need to avoid mutating the "days" +// // variable as well as the strictness of Modifier +// const { minDate: min, maxDate: max } = props; +// let days: Modifier = undefined; +// if (max) days = { after: max }; +// if (min) days = { ...days, before: min }; +// return days; +// }; + +// /** +// * A date input is a control for users to enter a date, either by typing it or +// * choosing from a pop-up calendar. +// * +// * The Date Input component is based on [React Day Picker][3], as an +// * alternative to the built-in ``. It works on +// * [unsupported browsers][2] and can display custom date format (e.g. "dmy" or +// * "mdy"). If you don't need these features, use [Input][1] to have better +// * accessibility support. + +// * [1]: /docs/components-input--primary#date +// * [2]: https://caniuse.com/input-datetime +// * [3]: https://react-day-picker.js.org +// * +// */ +// export const DateInput = (props: Props): JSX.Element => { +// const [target, setTarget] = useState(null); +// const [month, setMonth] = useState(() => new Date()); +// const format = props.format ?? DateInput.formats.dmy; + +// return ( +//
+// ( +// +// )} +// dayPickerProps={{ +// month: month, +// navbarElement: DateInputNavbar, +// captionElement: ({ date }: CaptionElementProps) => ( +// +// ), +// disabledDays: getDisabledDays(props), +// }} +// value={getValue(props)} +// // The lib's type is missing "undefined" case +// onDayChange={(day: Date | undefined) => { +// if (props.setValue === undefined) return; +// props.setValue(day ?? null); +// }} +// // Format +// placeholder={format.placeholder} +// formatDate={format.format} +// parseDate={format.parse} +// /> +//
+// ); +// }; -DateInput.formats = dateInputFormats; -DateInput.sizes = Input.sizes; -DateInput.styles = Input.styles; +// DateInput.formats = dateInputFormats; +// DateInput.sizes = Input.sizes; +// DateInput.styles = Input.styles; diff --git a/core/src/date-input/format.ts b/core/src/date-input/format.ts index 75727749..335e5854 100644 --- a/core/src/date-input/format.ts +++ b/core/src/date-input/format.ts @@ -1,80 +1,80 @@ -import { DayPickerInputProps } from "react-day-picker/types/Props"; +// import { DayPickerInputProps } from "react-day-picker/types/Props"; -export interface DateInputFormat { - placeholder: string; - parse: DayPickerInputProps["parseDate"]; - format: DayPickerInputProps["formatDate"]; -} +// export interface DateInputFormat { +// placeholder: string; +// parse: DayPickerInputProps["parseDate"]; +// format: DayPickerInputProps["formatDate"]; +// } -/** Split arr to year month day respectively */ -type Split = (arr: string[]) => [string, string, string]; +// /** Split arr to year month day respectively */ +// type Split = (arr: string[]) => [string, string, string]; -// A modified of the default handler of react-day-picker -// https://github.com/gpbl/react-day-picker/blob/5615f547abbfa37b4ea3044ec14bd5c917de48c5/src/DayPickerInput.js#L62 -const makeParse = - (foo: Split) => - (str: string): Date | undefined => { - if (typeof str !== "string") return undefined; - const parts = str.split(/[./ _-]+/); - if (parts.length !== 3) return undefined; +// // A modified of the default handler of react-day-picker +// // https://github.com/gpbl/react-day-picker/blob/5615f547abbfa37b4ea3044ec14bd5c917de48c5/src/DayPickerInput.js#L62 +// const makeParse = +// (foo: Split) => +// (str: string): Date | undefined => { +// if (typeof str !== "string") return undefined; +// const parts = str.split(/[./ _-]+/); +// if (parts.length !== 3) return undefined; - const [syear, smonth, sday] = foo(parts); - const year = parseInt(syear, 10); - const month = parseInt(smonth, 10) - 1; - const day = parseInt(sday, 10); +// const [syear, smonth, sday] = foo(parts); +// const year = parseInt(syear, 10); +// const month = parseInt(smonth, 10) - 1; +// const day = parseInt(sday, 10); - if (isNaN(year) || String(year).length > 4) return undefined; - if (isNaN(day) || day <= 0 || day > 31) return undefined; - if (isNaN(month) || month < 0 || month >= 12) return undefined; +// if (isNaN(year) || String(year).length > 4) return undefined; +// if (isNaN(day) || day <= 0 || day > 31) return undefined; +// if (isNaN(month) || month < 0 || month >= 12) return undefined; - // Always set noon to avoid time zone issues - const date = new Date(year, month, day, 12, 0, 0, 0); - // https://stackoverflow.com/questions/5863327/tips-for-working-with-pre-1000-a-d-dates-in-javascript - date.setFullYear(year); - return date; - }; +// // Always set noon to avoid time zone issues +// const date = new Date(year, month, day, 12, 0, 0, 0); +// // https://stackoverflow.com/questions/5863327/tips-for-working-with-pre-1000-a-d-dates-in-javascript +// date.setFullYear(year); +// return date; +// }; -const splitDate = (date: Date): [string, string, string] => { - const year = date.getFullYear(); - const month = date.getMonth() + 1; - const day = date.getDate(); - return [year.toString(), month.toString(), day.toString()]; -}; +// const splitDate = (date: Date): [string, string, string] => { +// const year = date.getFullYear(); +// const month = date.getMonth() + 1; +// const day = date.getDate(); +// return [year.toString(), month.toString(), day.toString()]; +// }; -const ymd: DateInputFormat = { - placeholder: "yyyy/mm/dd", - parse: makeParse((arr) => { - const [year, month, day] = arr; - return [year, month, day]; - }), - format: (date): string => { - const [year, month, day] = splitDate(date); - return `${year}/${month}/${day}`; - }, -}; +// const ymd: DateInputFormat = { +// placeholder: "yyyy/mm/dd", +// parse: makeParse((arr) => { +// const [year, month, day] = arr; +// return [year, month, day]; +// }), +// format: (date): string => { +// const [year, month, day] = splitDate(date); +// return `${year}/${month}/${day}`; +// }, +// }; -const dmy: DateInputFormat = { - placeholder: "dd/mm/yyyy", - parse: makeParse((arr) => { - const [day, month, year] = arr; - return [year, month, day]; - }), - format: (date): string => { - const [year, month, day] = splitDate(date); - return `${day}/${month}/${year}`; - }, -}; +// const dmy: DateInputFormat = { +// placeholder: "dd/mm/yyyy", +// parse: makeParse((arr) => { +// const [day, month, year] = arr; +// return [year, month, day]; +// }), +// format: (date): string => { +// const [year, month, day] = splitDate(date); +// return `${day}/${month}/${year}`; +// }, +// }; -const mdy: DateInputFormat = { - placeholder: "mm/dd/yyyy", - parse: makeParse((arr) => { - const [month, day, year] = arr; - return [year, month, day]; - }), - format: (date): string => { - const [year, month, day] = splitDate(date); - return `${month}/${day}/${year}`; - }, -}; +// const mdy: DateInputFormat = { +// placeholder: "mm/dd/yyyy", +// parse: makeParse((arr) => { +// const [month, day, year] = arr; +// return [year, month, day]; +// }), +// format: (date): string => { +// const [year, month, day] = splitDate(date); +// return `${month}/${day}/${year}`; +// }, +// }; -export const dateInputFormats = { dmy, mdy, ymd }; +// export const dateInputFormats = { dmy, mdy, ymd }; diff --git a/core/src/date-input/month/month.tsx b/core/src/date-input/month/month.tsx index 0ec5d3fd..9b4117c0 100644 --- a/core/src/date-input/month/month.tsx +++ b/core/src/date-input/month/month.tsx @@ -1,52 +1,52 @@ -import { Dispatch, SetStateAction } from "react"; -import { Border } from "../../border/border"; -import { DivPx } from "../../div/div"; -import { Select } from "../../select/select"; -import s from "./month.module.css"; +// import { Dispatch, SetStateAction } from "react"; +// import { Border } from "../../border/border"; +// import { DivPx } from "../../div/div"; +// import { Select } from "../../select/select"; +// import s from "./month.module.css"; -interface Props { - value: Date; - setValue: Dispatch>; -} +// interface Props { +// value: Date; +// setValue: Dispatch>; +// } -const months: number[] = Array(12) - .fill(0) - .map((value, index) => value + index); +// const months: number[] = Array(12) +// .fill(0) +// .map((value, index) => value + index); -const years: number[] = Array(100) - .fill(1950) - .map((value, index) => value + index); +// const years: number[] = Array(100) +// .fill(1950) +// .map((value, index) => value + index); -export const DateInputMonth = ({ value, setValue }: Props): JSX.Element => ( -
-
- - value={value.getMonth()} - setValue={(month) => { - setValue(new Date(value.getFullYear(), month)); - }} - options={months.map((month) => ({ - id: month.toString(), - label: (month + 1).toString(), - value: month, - }))} - style={Select.styles.flat} - /> - - value={value.getFullYear()} - setValue={(year) => { - setValue(new Date(year, value.getMonth())); - }} - options={years.map((year) => ({ - id: year.toString(), - label: year.toString(), - value: year, - }))} - style={Select.styles.flat} - /> -
- - - -
-); +// export const DateInputMonth = ({ value, setValue }: Props): JSX.Element => ( +//
+//
+// +// value={value.getMonth()} +// setValue={(month) => { +// setValue(new Date(value.getFullYear(), month)); +// }} +// options={months.map((month) => ({ +// id: month.toString(), +// label: (month + 1).toString(), +// value: month, +// }))} +// style={Select.styles.flat} +// /> +// +// value={value.getFullYear()} +// setValue={(year) => { +// setValue(new Date(year, value.getMonth())); +// }} +// options={years.map((year) => ({ +// id: year.toString(), +// label: year.toString(), +// value: year, +// }))} +// style={Select.styles.flat} +// /> +//
+// +// +// +//
+// ); diff --git a/core/src/date-input/navbar/navbar.tsx b/core/src/date-input/navbar/navbar.tsx index 0c8396ed..dcb5837a 100644 --- a/core/src/date-input/navbar/navbar.tsx +++ b/core/src/date-input/navbar/navbar.tsx @@ -1,27 +1,27 @@ -import { NavbarElementProps } from "react-day-picker/types/Props"; -import { Button } from "../../button/button"; -import { coreIcons } from "../../icons/icons"; -import s from "./navbar.module.css"; +// import { NavbarElementProps } from "react-day-picker/types/Props"; +// import { Button } from "../../button/button"; +// import { coreIcons } from "../../icons/icons"; +// import s from "./navbar.module.css"; -export const DateInputNavbar = (props: NavbarElementProps): JSX.Element => ( -
-
-
-
-
-
-); +// export const DateInputNavbar = (props: NavbarElementProps): JSX.Element => ( +//
+//
+//
+//
+//
+//
+// ); diff --git a/core/src/dialog/native/alert.tsx b/core/src/dialog/native/alert.tsx index 67442b4c..6cbd5e2a 100644 --- a/core/src/dialog/native/alert.tsx +++ b/core/src/dialog/native/alert.tsx @@ -32,7 +32,7 @@ export const dialogAlert = ( message: React.ReactNode, options?: { width?: DialogProps["width"]; - } + }, ): Promise => { return new Promise((resolve) => { renderDialog((unmount) => ( diff --git a/core/src/dialog/native/confirm.tsx b/core/src/dialog/native/confirm.tsx index 65298fb5..e1a43cf1 100644 --- a/core/src/dialog/native/confirm.tsx +++ b/core/src/dialog/native/confirm.tsx @@ -34,7 +34,7 @@ export const dialogConfirm = ( message: React.ReactNode, options?: { width?: DialogProps["width"]; - } + }, ): Promise => { return new Promise((resolve) => { renderDialog((unmount) => ( diff --git a/core/src/dialog/native/prompt.tsx b/core/src/dialog/native/prompt.tsx index 36c7931a..d8c5589e 100644 --- a/core/src/dialog/native/prompt.tsx +++ b/core/src/dialog/native/prompt.tsx @@ -60,7 +60,7 @@ export const dialogPrompt = ( options?: { width?: Props["width"]; rows?: Props["rows"]; - } + }, ): Promise => { return new Promise((resolve) => { renderDialog((unmount) => ( diff --git a/core/src/form/field.tsx b/core/src/form/field.tsx index 06191674..76b61fd1 100644 --- a/core/src/form/field.tsx +++ b/core/src/form/field.tsx @@ -19,6 +19,6 @@ export const FormField = (props: Props): JSX.Element => { {label}
, , - {children} + {children}, ); }; diff --git a/core/src/icons/blank-icon.tsx b/core/src/icons/blank-icon.tsx index 61f244a3..53109d64 100644 --- a/core/src/icons/blank-icon.tsx +++ b/core/src/icons/blank-icon.tsx @@ -2,6 +2,6 @@ import { GenIcon, IconBaseProps, IconType } from "react-icons"; export const BlankIcon: IconType = (props: IconBaseProps): JSX.Element => { return GenIcon({ tag: "svg", attr: { viewBox: "0 0 16 16" }, child: [] })( - props + props, ); }; diff --git a/core/src/icons/icons.tsx b/core/src/icons/icons.tsx index c4e2bced..5a69edbc 100644 --- a/core/src/icons/icons.tsx +++ b/core/src/icons/icons.tsx @@ -16,7 +16,7 @@ export const coreIcons = { chevronUp: go.GoChevronUp, cross: go.GoX, dash: go.GoDash, - dot: go.GoPrimitiveDot, + dot: go.GoDot, kebab: go.GoKebabHorizontal, error: ri.RiErrorWarningFill, success: ri.RiCheckboxCircleFill, diff --git a/core/src/input/flat.module.css b/core/src/input/flat.module.css index 752e08c1..c809e28a 100644 --- a/core/src/input/flat.module.css +++ b/core/src/input/flat.module.css @@ -1,5 +1,7 @@ .main { - transition: background-color 0.1s, outline 0.2s ease-out; + transition: + background-color 0.1s, + outline 0.2s ease-out; /* Pretty much nothing in normal state */ text-align: inherit; diff --git a/core/src/input/input.tsx b/core/src/input/input.tsx index 39e06850..ced9928e 100644 --- a/core/src/input/input.tsx +++ b/core/src/input/input.tsx @@ -119,7 +119,7 @@ const validate = (props: InputProps): void => { const inputRender = ( props: InputProps, - ref: React.ForwardedRef + ref: React.ForwardedRef, ): JSX.Element => { validate(props); const size = props.size ?? Input.sizes.medium; diff --git a/core/src/input/outset.module.css b/core/src/input/outset.module.css index c9c5ab2d..b6634d72 100644 --- a/core/src/input/outset.module.css +++ b/core/src/input/outset.module.css @@ -1,5 +1,7 @@ .main { - transition: background-color 0.1s, outline 0.2s ease-out; + transition: + background-color 0.1s, + outline 0.2s ease-out; border-width: 1px; border-style: solid; diff --git a/core/src/pagination/pagination.tsx b/core/src/pagination/pagination.tsx index 68b04b87..e3e959fe 100644 --- a/core/src/pagination/pagination.tsx +++ b/core/src/pagination/pagination.tsx @@ -51,7 +51,7 @@ export const Pagination = (props: PaginationProps): JSX.Element => { await setValueOrg(value); setBusy(false); }, - [setValueOrg, max, min] + [setValueOrg, max, min], ); return ( diff --git a/core/src/pane/pane.tsx b/core/src/pane/pane.tsx index ba641b6e..fb45039a 100644 --- a/core/src/pane/pane.tsx +++ b/core/src/pane/pane.tsx @@ -44,6 +44,6 @@ export const Pane = (props: Props): JSX.Element => ( Pane.styles = { outset: [border.px1, background.strong, border.strong, shadow.boxWeak].join( - " " + " ", ), }; diff --git a/core/src/popover/pane/pane.tsx b/core/src/popover/pane/pane.tsx index 04da8378..10b5f032 100644 --- a/core/src/popover/pane/pane.tsx +++ b/core/src/popover/pane/pane.tsx @@ -72,7 +72,7 @@ export const PopoverPane = (props: Props): JSX.Element => { ); - return createPortal(element, getPortalContainer()); + return <>{createPortal(element, getPortalContainer())}; }; PopoverPane.styles = { diff --git a/core/src/progress/circle.tsx b/core/src/progress/circle.tsx index 646e5988..ec24ee5c 100644 --- a/core/src/progress/circle.tsx +++ b/core/src/progress/circle.tsx @@ -42,7 +42,7 @@ const getViewBox = (strokeWidth: number) => { const getStroke = (props: ProgressCircleProps) => { const width = Math.min( MIN_STROKE_WIDTH, - (STROKE_WIDTH * SIZE_LARGE) / props.size + (STROKE_WIDTH * SIZE_LARGE) / props.size, ); const value = props.value === "indeterminate" ? 0.25 : props.value; const offset = PATH_LENGTH - PATH_LENGTH * value; diff --git a/core/src/step/step.tsx b/core/src/step/step.tsx index 72bbddc5..d58d50de 100644 --- a/core/src/step/step.tsx +++ b/core/src/step/step.tsx @@ -60,7 +60,7 @@ export const Steps = (props: Props): JSX.Element => { index={index} current={props.current} />, - + , ); }); children.pop(); diff --git a/core/src/tab/tab.tsx b/core/src/tab/tab.tsx index eaceeb0e..b23d2c1a 100644 --- a/core/src/tab/tab.tsx +++ b/core/src/tab/tab.tsx @@ -150,9 +150,7 @@ const outsetStyle: TabStyle = { inactive: s.outsetInactive, renderContent: (children, props) => ( @@ -167,9 +165,7 @@ const flatStyle: TabStyle = { renderContent: (children, props) => (
(props: TableProps, state: TableState) => - (_row: R, _index: number, rowKey: string): JSX.Element => - ( -
- {props.selectable !== undefined && ( - - )} - {props.expandable !== undefined && ( - - )} -
- ); + (_row: R, _index: number, rowKey: string): JSX.Element => ( +
+ {props.selectable !== undefined && ( + + )} + {props.expandable !== undefined && ( + + )} +
+ ); export const getTableActionsColumn = ( props: TableProps, - state: TableState + state: TableState, ): TableColumn => ({ className: s.column, title: "", diff --git a/core/src/table/actions/selectable/state.tsx b/core/src/table/actions/selectable/state.tsx index 97272cf1..0b76b386 100644 --- a/core/src/table/actions/selectable/state.tsx +++ b/core/src/table/actions/selectable/state.tsx @@ -22,7 +22,7 @@ export interface TableSelectableProps { export const isTableRowSelected = ( selected: TableSelected | undefined, - rowKey: string + rowKey: string, ): boolean => { if (selected === undefined) return false; if (typeof selected === "string") return selected === rowKey; diff --git a/core/src/table/body/cell.tsx b/core/src/table/body/cell.tsx index c4e02879..25cb55e0 100644 --- a/core/src/table/body/cell.tsx +++ b/core/src/table/body/cell.tsx @@ -17,9 +17,11 @@ interface Props { export const TableBodyCell = (props: Props): JSX.Element => { const { column, row } = props; const children = - typeof column.render === "function" - ? column.render(row, props.rowIndex, props.rowKey) // Render function - : row[column.render]; // Accessor + typeof column.render === "function" ? ( + <>{column.render(row, props.rowIndex, props.rowKey)} // Render function + ) : ( + <>{row[column.render]} + ); // Accessor const columnMeta = props.tableState.columnMetaMap.get(props.columnIndex); return ( ; // These meta info can be calculated in each cell, but we put it here so it // can be calculated once for each table export const getTableColumnMetaMap = ( - props: TableProps + props: TableProps, ): TableColumnMetaMap => { // We only have fixed meta for now. If we have more in the future, we should // merge them here diff --git a/core/src/table/fixed/fixed.ts b/core/src/table/fixed/fixed.ts index 204c5420..b8de5b5f 100644 --- a/core/src/table/fixed/fixed.ts +++ b/core/src/table/fixed/fixed.ts @@ -21,7 +21,7 @@ import { TableProps } from "../table"; import s from "./fixed.module.css"; export const getTableFixedMetaMap = ( - props: TableProps + props: TableProps, ): TableColumnMetaMap => { const map: TableColumnMetaMap = new Map(); if (props.fixed === undefined) return map; diff --git a/core/src/text-area/text-area.tsx b/core/src/text-area/text-area.tsx index 8d05f94c..a33d1ae0 100644 --- a/core/src/text-area/text-area.tsx +++ b/core/src/text-area/text-area.tsx @@ -65,7 +65,7 @@ interface TextAreaComponent const renderTextArea = ( props: TextAreaProps, - ref: React.ForwardedRef + ref: React.ForwardedRef, ): JSX.Element => { const rawProps = omit(props, [ "className", diff --git a/core/src/toast/container/container.tsx b/core/src/toast/container/container.tsx index aaaae996..ad4d8b8c 100644 --- a/core/src/toast/container/container.tsx +++ b/core/src/toast/container/container.tsx @@ -1,9 +1,12 @@ -import toastController, { useToaster as useRHTToaster } from "react-hot-toast"; -import type * as RHT from "react-hot-toast/dist/core/types"; +import toastController, { + Toast, + ToastType, + useToaster as useRHTToaster, +} from "react-hot-toast"; import { ToastPane, ToastPaneType } from "../pane/pane"; import s from "./container.module.css"; -const getType = (from: RHT.ToastType): ToastPaneType => { +const getType = (from: ToastType): ToastPaneType => { switch (from) { case "success": return ToastPane.types.success; @@ -22,9 +25,9 @@ export const ToastContainer = (): JSX.Element => { const { toasts, handlers } = useRHTToaster(); const { startPause, endPause, calculateOffset, updateHeight } = handlers; - const renderToast = (toast: RHT.Toast): JSX.Element => { + const renderToast = (toast: Toast): JSX.Element => { const offsetOpts = { reverseOrder: false, margin: 8 }; - const offset = calculateOffset(toast.id, offsetOpts); + const offset = calculateOffset(toast, offsetOpts); const ref = (el: HTMLDivElement) => { if (!el || toast.height) return; const height = el.getBoundingClientRect().height; @@ -43,7 +46,7 @@ export const ToastContainer = (): JSX.Element => { > {toast.message}} close={() => toastController.dismiss(toast.id)} />
diff --git a/core/src/toast/toast.tsx b/core/src/toast/toast.tsx index 6982ad7e..d3573365 100644 --- a/core/src/toast/toast.tsx +++ b/core/src/toast/toast.tsx @@ -22,7 +22,7 @@ const init = async (resolve: (div: HTMLDivElement) => void): Promise => { export const toast = async ( type: ToastType, - message: string + message: string, ): Promise => { if (inited.current === false) await new Promise(init); type.handler(message); diff --git a/core/src/toast/type/type.ts b/core/src/toast/type/type.ts index d4ccd8bb..a5ca0a7d 100644 --- a/core/src/toast/type/type.ts +++ b/core/src/toast/type/type.ts @@ -1,7 +1,7 @@ import RHTToast from "react-hot-toast"; import { ToastPane, ToastPaneType } from "../pane/pane"; -type ToastHandler = typeof RHTToast["success"]; +type ToastHandler = (typeof RHTToast)["success"]; export interface ToastType { handler: ToastHandler; diff --git a/core/src/tree/tree.tsx b/core/src/tree/tree.tsx index a40dbe1b..1036aaac 100644 --- a/core/src/tree/tree.tsx +++ b/core/src/tree/tree.tsx @@ -75,15 +75,14 @@ export interface TreeProps { parentMode: "select" | "expand"; } -const renderChild = (treeProps: TreeProps) => (child: TreeNode) => - ( - - ); +const renderChild = (treeProps: TreeProps) => (child: TreeNode) => ( + +); export const Tree = (props: TreeProps): JSX.Element => { const expanded = props.expanded.has(props.node.id); diff --git a/core/src/tree/utils/refresh.ts b/core/src/tree/utils/refresh.ts index 401a3b88..d5386209 100644 --- a/core/src/tree/utils/refresh.ts +++ b/core/src/tree/utils/refresh.ts @@ -7,7 +7,7 @@ export interface RefreshTreeParams { } export const refreshTree = async ( - params: RefreshTreeParams + params: RefreshTreeParams, ): Promise => { const { loadChildren, node } = params; diff --git a/core/src/tree/utils/update.ts b/core/src/tree/utils/update.ts index 3ccf6b09..d4ec71cf 100644 --- a/core/src/tree/utils/update.ts +++ b/core/src/tree/utils/update.ts @@ -19,7 +19,7 @@ export interface UpdateTreeNodeParams { * version which can skip branches O(logN). */ export const updateTreeNode = ( - params: UpdateTreeNodeParams + params: UpdateTreeNodeParams, ): TreeNode => { const { current, id, key, value } = params; if (current.id === id) { diff --git a/core/src/utils/omit.ts b/core/src/utils/omit.ts index 6e4d15e8..dbf1eb23 100644 --- a/core/src/utils/omit.ts +++ b/core/src/utils/omit.ts @@ -3,14 +3,17 @@ type Keys = (keyof T)[]; interface OmitFunction { - >(obj: T, keys: K): { + >( + obj: T, + keys: K, + ): { [K2 in Exclude]: T[K2]; }; } export const omit: OmitFunction = (obj, keys) => { const ret = {} as { - [K in keyof typeof obj]: typeof obj[K]; + [K in keyof typeof obj]: (typeof obj)[K]; }; let key: keyof typeof obj; for (key in obj) { diff --git a/docs/package.json b/docs/package.json index 8097f6ee..3f4c990e 100644 --- a/docs/package.json +++ b/docs/package.json @@ -16,22 +16,22 @@ "email": "moai@thien.do", "dependencies": { "@babel/core": "^7.14.8", - "@storybook/addon-docs": "^6.3.6", + "@storybook/addon-docs": "^8.1.10", "@storybook/addon-postcss": "^2.0.0", - "@storybook/react": "^6.3.6", - "@storybook/theming": "^6.3.6", + "@storybook/react": "^8.1.10", + "@storybook/theming": "^8.1.10", "@types/color": "^3.0.2", "autoprefixer": "^10.3.1", - "color": "^3.2.1", + "color": "^4.2.3", "formik": "^2.2.9", "postcss": "^8.3.6", - "postcss-import": "^14.0.2", - "react": "^17.0.2", - "react-dom": "^17.0.2", + "postcss-import": "^16.1.0", + "react": "^18.3.1", + "react-dom": "^18.3.1", "react-hook-form": "^7.12.2", - "react-icons": "^4.2.0", - "storybook-dark-mode": "^1.0.8", - "typescript": "^4.3.4", + "react-icons": "^5.2.1", + "storybook-dark-mode": "^4.0.2", + "typescript": "^5.5.2", "webpack": "^5.47.1" } } diff --git a/docs/src/color/background/background.tsx b/docs/src/color/background/background.tsx index 2da9f31c..c4b0b8d7 100644 --- a/docs/src/color/background/background.tsx +++ b/docs/src/color/background/background.tsx @@ -10,15 +10,14 @@ interface Row { const MakeColumn = (theme: "light" | "dark", text: string) => - (row: Row): JSX.Element => - ( -
- -
- ); + (row: Row): JSX.Element => ( +
+ +
+ ); const LightStrong = MakeColumn("light", text.normal); const LightWeak = MakeColumn("light", text.muted); diff --git a/docs/src/color/border/border.tsx b/docs/src/color/border/border.tsx index dcce464d..9dca1d11 100644 --- a/docs/src/color/border/border.tsx +++ b/docs/src/color/border/border.tsx @@ -10,15 +10,14 @@ interface Row { const MakeColumn = (theme: "light" | "dark", back: string) => - (row: Row): JSX.Element => - ( -
- -
- ); + (row: Row): JSX.Element => ( +
+ +
+ ); const LightStrong = MakeColumn("light", background.strong); const LightWeak = MakeColumn("light", background.weak); diff --git a/docs/src/color/sample/sample.tsx b/docs/src/color/sample/sample.tsx index ea966b0e..0a9ef3a0 100644 --- a/docs/src/color/sample/sample.tsx +++ b/docs/src/color/sample/sample.tsx @@ -23,7 +23,7 @@ const getColor = (contrast: number): CategoryColor => { const getContrast = ( props: Props, backElement: HTMLDivElement, - foreElement: HTMLElement + foreElement: HTMLElement, ): number => { const back = window.getComputedStyle(backElement).backgroundColor; const foreStyle = window.getComputedStyle(foreElement); diff --git a/docs/src/color/static/table.tsx b/docs/src/color/static/table.tsx index 34859caf..c896a51b 100644 --- a/docs/src/color/static/table.tsx +++ b/docs/src/color/static/table.tsx @@ -8,19 +8,17 @@ interface Props { const Color = (props: Props) => - (row: number): JSX.Element => - ( - // eslint-disable-next-line react/prop-types - - ); + (row: number): JSX.Element => ( + // eslint-disable-next-line react/prop-types + + ); const Name = (props: Props) => - (row: number): JSX.Element => - ( - // eslint-disable-next-line react/prop-types - - ); + (row: number): JSX.Element => ( + // eslint-disable-next-line react/prop-types + + ); export const ColorStaticTable = (props: Props): JSX.Element => (
diff --git a/docs/src/color/text/text.tsx b/docs/src/color/text/text.tsx index d3729577..d1a1a3e3 100644 --- a/docs/src/color/text/text.tsx +++ b/docs/src/color/text/text.tsx @@ -19,19 +19,18 @@ interface Row { const MakeColumn = (theme: "light" | "dark", back: string) => - (row: Row): JSX.Element => - ( -
- -
- ); + (row: Row): JSX.Element => ( +
+ +
+ ); const LightStrong = MakeColumn("light", background.strong); const LightWeak = MakeColumn("light", background.weak); diff --git a/docs/src/components/radio-group-fake.tsx b/docs/src/components/radio-group-fake.tsx index cdbf6c5d..a855c145 100644 --- a/docs/src/components/radio-group-fake.tsx +++ b/docs/src/components/radio-group-fake.tsx @@ -5,5 +5,5 @@ import { RadioOption } from "../../../core/src"; * ArgsTable to describe the RadioOption interface. */ export const RadioOptionComponent = ( - props: RadioOption + props: RadioOption, ): JSX.Element =>
{props.id}
; diff --git a/docs/src/components/select-fake.tsx b/docs/src/components/select-fake.tsx index ddb6e393..258f3bae 100644 --- a/docs/src/components/select-fake.tsx +++ b/docs/src/components/select-fake.tsx @@ -5,5 +5,5 @@ import { SelectOption } from "../../../core/src"; * ArgsTable to describe the SelectOption interface. */ export const SelectOptionComponent = ( - props: SelectOption + props: SelectOption, ): JSX.Element =>
{props.id}
; diff --git a/docs/src/components/switcher-fake.tsx b/docs/src/components/switcher-fake.tsx index c0c6daa6..5a111282 100644 --- a/docs/src/components/switcher-fake.tsx +++ b/docs/src/components/switcher-fake.tsx @@ -5,5 +5,5 @@ import { SwitcherOption } from "../../../core/src"; * ArgsTable to describe the Option interface. */ export const SwitcherOptionComponent = ( - props: SwitcherOption + props: SwitcherOption, ): JSX.Element =>
{props.key}
; diff --git a/docs/src/components/table-fake.tsx b/docs/src/components/table-fake.tsx index a7ed3f07..b98e9ba8 100644 --- a/docs/src/components/table-fake.tsx +++ b/docs/src/components/table-fake.tsx @@ -6,9 +6,9 @@ import { TableExpandableProps } from "../../../core/src"; * ArgsTable to describe the TableColumn interface. */ export const TableColumnComponent = ( - props: TableColumn + props: TableColumn, ): JSX.Element =>
{props.title}
; export const TableExpandableComponent = ( - props: TableExpandableProps + props: TableExpandableProps, ): JSX.Element =>
{props.render.length}
; diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 00000000..9af5fae8 --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,66 @@ +import { fixupConfigRules, fixupPluginRules } from "@eslint/compat"; +import typescriptEslint from "@typescript-eslint/eslint-plugin"; +import jestDom from "eslint-plugin-jest-dom"; +import tsParser from "@typescript-eslint/parser"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import js from "@eslint/js"; +import { FlatCompat } from "@eslint/eslintrc"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const compat = new FlatCompat({ + baseDirectory: __dirname, + recommendedConfig: js.configs.recommended, + allConfig: js.configs.all, +}); + +export default [ + { + ignores: [ + "**/node_modules", + "**/dist", + "**/public", + "test/coverage", + "**/docs/.storybook", + ], + }, + ...fixupConfigRules( + compat.extends( + "eslint:recommended", + "plugin:@typescript-eslint/recommended", + "plugin:react/recommended", + "plugin:jest-dom/recommended", + "prettier", + ), + ), + { + plugins: { + "@typescript-eslint": fixupPluginRules(typescriptEslint), + "jest-dom": fixupPluginRules(jestDom), + }, + + languageOptions: { + parser: tsParser, + ecmaVersion: 5, + sourceType: "script", + + parserOptions: { + project: "./tsconfig.json", + }, + }, + + settings: { + react: { + version: "17", + }, + }, + + rules: { + "react/jsx-uses-react": "off", + "react/react-in-jsx-scope": "off", + "react/no-children-prop": "off", + "react/display-name": "off", + }, + }, +]; diff --git a/gallery/package.json b/gallery/package.json index 77688dc1..e006346b 100644 --- a/gallery/package.json +++ b/gallery/package.json @@ -33,20 +33,20 @@ "react-icons": ">=4" }, "devDependencies": { - "@types/node": "^16.4.8", - "@types/react": "^17.0.15", - "node-fetch": "^2.6.1", - "postcss": "^8.3.6", - "react": "^17.0.2", - "react-dom": "^17.0.2", - "react-icons": "^4.2.0", - "rollup": "^2.55.1", - "rollup-plugin-copy": "^3.4.0", + "@types/node": "^20.14.8", + "@types/react": "^18.3.3", + "node-fetch": "^3.3.2", + "postcss": "^8.4.38", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-icons": "^5.2.1", + "rollup": "^4.18.0", + "rollup-plugin-copy": "^3.5.0", "rollup-plugin-delete": "^2.0.0", - "rollup-plugin-postcss": "^4.0.0", - "rollup-plugin-typescript2": "^0.30.0", - "ts-node": "^10.1.0", - "tslib": "^2.3.0", - "typescript": "^4.3.4" + "rollup-plugin-postcss": "^4.0.2", + "rollup-plugin-typescript2": "^0.36.0", + "ts-node": "^10.9.2", + "tslib": "^2.6.3", + "typescript": "^5.5.2" } } diff --git a/gallery/rollup.config.js b/gallery/rollup.config.mjs similarity index 100% rename from gallery/rollup.config.js rename to gallery/rollup.config.mjs diff --git a/gallery/scripts/example/generate.ts b/gallery/scripts/example/generate.ts index 9f8585db..5286396a 100644 --- a/gallery/scripts/example/generate.ts +++ b/gallery/scripts/example/generate.ts @@ -56,11 +56,11 @@ const main = async () => { fs.mkdir(distPath); fs.writeFile( path.resolve(distPath, "pokemons.json"), - JSON.stringify(pokemons, null, 2) + JSON.stringify(pokemons, null, 2), ); fs.writeFile( path.resolve(distPath, "types.json"), - JSON.stringify(types, null, 2) + JSON.stringify(types, null, 2), ); return pokemons; }; diff --git a/gallery/src/pagination.tsx b/gallery/src/pagination.tsx index ec365f77..a35a3eaf 100644 --- a/gallery/src/pagination.tsx +++ b/gallery/src/pagination.tsx @@ -3,7 +3,7 @@ import { useCallback, useState } from "react"; export const GallerPagination = (): JSX.Element => { const [page, setPage_] = useState(5); - const setPage = useCallback((page): Promise => { + const setPage = useCallback((page: number): Promise => { return new Promise((resolve) => { setPage_(page); window.setTimeout(() => resolve(), 1000); diff --git a/gallery/src/table/table.tsx b/gallery/src/table/table.tsx index 5c27f161..f5ba49a9 100644 --- a/gallery/src/table/table.tsx +++ b/gallery/src/table/table.tsx @@ -78,7 +78,7 @@ const MaterialsHeader = (): JSX.Element => ( diff --git a/package.json b/package.json index bb61f751..6d583b30 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "build-core": "cd core && yarn _build", "build-docs": "cd docs && yarn _build", "build-gallery": "cd gallery && yarn _build", - "build": "yarn build-core && yarn build-gallery && yarn build-docs", + "build": "yarn build-core && yarn build-gallery", "lint-fix": "eslint --fix . && prettier --write .", "lint": "eslint --max-warnings=0 . && prettier --check .", "start-core": "cd core && yarn _start", @@ -21,13 +21,16 @@ "test": "cd test && yarn _test" }, "devDependencies": { - "@typescript-eslint/eslint-plugin": "^4.28.5", - "@typescript-eslint/parser": "^4.28.5", - "eslint": "^7.32.0", - "eslint-config-prettier": "^8.3.0", - "eslint-plugin-jest-dom": "^3.9.0", - "eslint-plugin-react": "^7.24.0", - "prettier": "^2.3.2", - "typescript": "^4.3.5" + "@eslint/compat": "^1.1.0", + "@eslint/eslintrc": "^3.1.0", + "@eslint/js": "^9.5.0", + "@typescript-eslint/eslint-plugin": "^7.13.1", + "@typescript-eslint/parser": "^7.13.1", + "eslint": "^9.5.0", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-jest-dom": "^5.4.0", + "eslint-plugin-react": "^7.34.3", + "prettier": "^3.3.2", + "typescript": "^5.5.2" } } diff --git a/test/.swcrc b/test/.swcrc index 30025bf1..9dad08e5 100644 --- a/test/.swcrc +++ b/test/.swcrc @@ -1,31 +1,31 @@ { - "jsc": { - "target": "es2017", - "parser": { - "syntax": "typescript", - "tsx": true, - "decorators": false, - "dynamicImport": false - }, - "transform": { - "react": { - "pragma": "React.createElement", - "pragmaFrag": "React.Fragment", - "throwIfNamespace": true, - "development": false, - "useBuiltins": false, - "runtime": "automatic" - }, - "hidden": { - "jest": true - } - } - }, - "module": { - "type": "commonjs", - "strict": false, - "strictMode": true, - "lazy": false, - "noInterop": false - } + "jsc": { + "target": "es2017", + "parser": { + "syntax": "typescript", + "tsx": true, + "decorators": false, + "dynamicImport": false + }, + "transform": { + "react": { + "pragma": "React.createElement", + "pragmaFrag": "React.Fragment", + "throwIfNamespace": true, + "development": false, + "useBuiltins": false, + "runtime": "automatic" + }, + "hidden": { + "jest": true + } + } + }, + "module": { + "type": "commonjs", + "strict": false, + "strictMode": true, + "lazy": false, + "noInterop": false + } } diff --git a/test/jest.config.js b/test/jest.config.js index dafd42c8..2b52cdb7 100644 --- a/test/jest.config.js +++ b/test/jest.config.js @@ -1,6 +1,7 @@ /* eslint-env node */ // https://jestjs.io/docs/configuration +// eslint-disable-next-line module.exports = { collectCoverage: true, coverageDirectory: "coverage", diff --git a/test/package.json b/test/package.json index 729b1dbd..991d9422 100644 --- a/test/package.json +++ b/test/package.json @@ -9,16 +9,16 @@ }, "devDependencies": { "@moai/core": "*", - "@swc/core": "^1.2.174", - "@swc/jest": "^0.2.20", - "@testing-library/dom": "^8.1.0", - "@testing-library/jest-dom": "^5.14.1", - "@testing-library/react": "^12.0.0", - "@testing-library/user-event": "^13.2.0", - "@types/jest": "^26.0.24", - "jest": "^27.0.6", - "react": "^17.0.2", - "react-dom": "^17.0.2", - "typescript": "^4.3.5" + "@swc/core": "^1.6.5", + "@swc/jest": "^0.2.36", + "@testing-library/dom": "^10.2.0", + "@testing-library/jest-dom": "^6.4.6", + "@testing-library/react": "^16.0.0", + "@testing-library/user-event": "^14.5.2", + "@types/jest": "^29.5.12", + "jest": "^29.7.0", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "typescript": "^5.5.2" } } diff --git a/test/src/button.test.tsx b/test/src/button.test.tsx index 0c660c48..cbaf454e 100644 --- a/test/src/button.test.tsx +++ b/test/src/button.test.tsx @@ -57,11 +57,11 @@ describe("Button", () => { render( + />, ); expect( - screen.queryByRole("button", { name: buttonLabel }) + screen.queryByRole("button", { name: buttonLabel }), ).not.toBeInTheDocument(); const link = screen.getByRole("link", { name: buttonLabel }); expect(link).toHaveAttribute("rel", "noopener"); @@ -76,7 +76,7 @@ describe("Button", () => { rel: "noopener", onClick: onClickMockFn, }} - /> + />, ); const link = screen.getByRole("link", { name: buttonLabel }); userEvent.click(link); diff --git a/test/src/radio.test.tsx b/test/src/radio.test.tsx index d1a26918..d6efa181 100644 --- a/test/src/radio.test.tsx +++ b/test/src/radio.test.tsx @@ -9,7 +9,7 @@ describe("Testing Checkbox Uncontrolled Prop", () => { render( foo - + , ); const radioElement = screen.getByRole("radio"); @@ -23,7 +23,7 @@ describe("Testing Checkbox Disabled Prop", () => { render( foo - + , ); const radioElement = screen.getByRole("radio"); diff --git a/test/src/select.test.tsx b/test/src/select.test.tsx index 8a8f85d9..129ffc14 100644 --- a/test/src/select.test.tsx +++ b/test/src/select.test.tsx @@ -20,7 +20,7 @@ describe("Testing Select Uncontrolled Props", () => { defaultValue={"blue"} id="select" /> -
+ , ); const selectElement = screen.getByLabelText("select"); expect(selectElement).toHaveValue("blue"); @@ -71,7 +71,7 @@ describe("Testing Select Disabled Props", () => {
; }; // import { ReactNode, useState } from "react"; diff --git a/new-docs/package.json b/new-docs/package.json index e84cd6ec..15225a11 100644 --- a/new-docs/package.json +++ b/new-docs/package.json @@ -9,8 +9,10 @@ "dependencies": { "@types/react": "latest", "color": "^4.2.3", + "formik": "^2.4.6", "react": "latest", "react-dom": "latest", + "react-hook-form": "^7.52.0", "typescript": "latest", "vocs": "latest" }, diff --git a/new-docs/pages/patterns/color/background.stories.mdx b/new-docs/pages/patterns/color/background.stories.mdx new file mode 100644 index 00000000..b9088a54 --- /dev/null +++ b/new-docs/pages/patterns/color/background.stories.mdx @@ -0,0 +1,34 @@ +import { ColorBackground } from "./background/background"; + +# Background Color + +The `background` utility contains classes that set the +[CSS `background-color`][1] property. This is the recommended way to set +background color since they automatically change based on the current theme. + +```ts +import { background } from "@moai/core"; + +
Text
; +``` + +There are only 2 colors in the `background` utility at the moment: + + + +The `weak` value sets a light gray background on light theme and a daker +background on dark theme. It should be used for underlying backgrounds, like +the background of your app. It can also be used to separate an area, such as +the header of a table. + +The `strong` value sets a white background on light theme and a lighter +background on dark theme. It should be used for elevated containers, such as +panes, toolbars or popovers. + +## See also + +- The [Pane][2] component uses `strong` background along with border and shadow + to better elevate contents. + +[1]: https://developer.mozilla.org/en-US/docs/Web/CSS/background-color +[2]: /components/pane diff --git a/new-docs/pages/patterns/color/background/background.module.css b/new-docs/pages/patterns/color/background/background.module.css new file mode 100644 index 00000000..d23bccd1 --- /dev/null +++ b/new-docs/pages/patterns/color/background/background.module.css @@ -0,0 +1,11 @@ +.name { + font-family: "Source Code Pro", monospace; + width: 160px; + min-width: 160px; +} + +.container { + overflow: auto; + white-space: nowrap; + border-width: 2px; +} diff --git a/new-docs/pages/patterns/color/background/background.tsx b/new-docs/pages/patterns/color/background/background.tsx new file mode 100644 index 00000000..eeec02e5 --- /dev/null +++ b/new-docs/pages/patterns/color/background/background.tsx @@ -0,0 +1,48 @@ +import { border, background, Table, text } from "../../../../../core/src"; +import { ColorSample } from "../sample/sample"; +import s from "./background.module.css"; + +type BackgroundKey = keyof typeof background; + +interface Row { + key: BackgroundKey; +} + +const MakeColumn = + (theme: "light" | "dark", text: string) => + (row: Row): JSX.Element => ( +
+ +
+ ); + +const LightStrong = MakeColumn("light", text.normal); +const LightWeak = MakeColumn("light", text.muted); +const DarkStrong = MakeColumn("dark", text.normal); +const DarkWeak = MakeColumn("dark", text.muted); + +interface Props { + rows: Row[]; +} + +export const ColorBackground = (props: Props): JSX.Element => ( +
+ + size={Table.sizes.small} + fixed={{ firstColumn: true }} + fill + rows={props.rows} + rowKey={(row) => row.key} + columns={[ + { title: "Name", className: s.name, render: "key" }, + { title: "Light", render: LightStrong }, + { title: "Light (muted)", render: LightWeak }, + { title: "Dark", render: DarkStrong }, + { title: "Dark (muted)", render: DarkWeak }, + ]} + /> +
+); diff --git a/new-docs/pages/patterns/color/border.stories.mdx b/new-docs/pages/patterns/color/border.stories.mdx new file mode 100644 index 00000000..b301005a --- /dev/null +++ b/new-docs/pages/patterns/color/border.stories.mdx @@ -0,0 +1,25 @@ +import { ColorBorder } from "./border/border"; + +# Border Color + +The `border` utility contains classes that set the [CSS `border-color`][1] +property. This is the recommended way to set border color since they +automatically change based on the current theme. + +Note that this only set the color. To have a border, you also need to set a +width, e.g. using the "border" class from Tailwind. You don't need to set the +border style since it's already default to "solid" in our [CSS reset][2]. + +```ts +import { border } from "@moai/core"; + +// The "border" class comes from Tailwind to set the border width +
Text
; +``` + +There are 2 colors in the `border` utility at the moment: + + + +[1]: https://developer.mozilla.org/en-US/docs/Web/CSS/border-color +[2]: https://github.com/moaijs/moai/blob/739a87de82bd061bb41f38c5a51a410b59944a3d/lib/core/src/style/reset.css#L404-L417 diff --git a/new-docs/pages/patterns/color/border/border.module.css b/new-docs/pages/patterns/color/border/border.module.css new file mode 100644 index 00000000..d23bccd1 --- /dev/null +++ b/new-docs/pages/patterns/color/border/border.module.css @@ -0,0 +1,11 @@ +.name { + font-family: "Source Code Pro", monospace; + width: 160px; + min-width: 160px; +} + +.container { + overflow: auto; + white-space: nowrap; + border-width: 2px; +} diff --git a/new-docs/pages/patterns/color/border/border.tsx b/new-docs/pages/patterns/color/border/border.tsx new file mode 100644 index 00000000..e9962153 --- /dev/null +++ b/new-docs/pages/patterns/color/border/border.tsx @@ -0,0 +1,48 @@ +import { background, border, Table } from "../../../../../core/src"; +import { ColorSample } from "../sample/sample"; +import s from "./border.module.css"; + +type BorderKey = keyof typeof border; + +interface Row { + key: BorderKey; +} + +const MakeColumn = + (theme: "light" | "dark", back: string) => + (row: Row): JSX.Element => ( +
+ +
+ ); + +const LightStrong = MakeColumn("light", background.strong); +const LightWeak = MakeColumn("light", background.weak); +const DarkStrong = MakeColumn("dark", background.strong); +const DarkWeak = MakeColumn("dark", background.weak); + +interface Props { + rows: Row[]; +} + +export const ColorBorder = (props: Props): JSX.Element => ( +
+ + size={Table.sizes.small} + fixed={{ firstColumn: true }} + fill + rows={props.rows} + rowKey={(row) => row.key} + columns={[ + { title: "Name", className: s.name, render: "key" }, + { title: "Light", render: LightStrong }, + { title: "Light (alt bg)", render: LightWeak }, + { title: "Dark", render: DarkStrong }, + { title: "Dark (alt bg)", render: DarkWeak }, + ]} + /> +
+); diff --git a/new-docs/pages/patterns/color/index.stories.mdx b/new-docs/pages/patterns/color/index.stories.mdx new file mode 100644 index 00000000..12edae22 --- /dev/null +++ b/new-docs/pages/patterns/color/index.stories.mdx @@ -0,0 +1,16 @@ +# Colors (index page) + +For recommended color usages, see [Text][1], [Background][2], and [Border][3]. +They provide accessible colors that are automatically changed based on the +current theme. + +For colors that are persistent across themes, see [Static Colors][4]. + +For colors that have no semantic meaning and should be used for data +visualizations, see [Category Colors][5] + +[1]: /patterns/color/text +[2]: /patterns/color/background +[3]: /patterns/color/border +[4]: /patterns/color/static +[5]: /patterns/color/category diff --git a/new-docs/pages/patterns/color/sample/sample.module.css b/new-docs/pages/patterns/color/sample/sample.module.css new file mode 100644 index 00000000..b50ab3b2 --- /dev/null +++ b/new-docs/pages/patterns/color/sample/sample.module.css @@ -0,0 +1,17 @@ +.container { + padding: 12px; + width: 120px; + font-variant-numeric: "tabular-nums"; + border-radius: 4px; + + display: flex; + justify-content: space-between; + align-items: center; +} + +.border { + width: 16px; + height: 16px; + border-radius: 16px; + border-width: 1px; +} diff --git a/new-docs/pages/patterns/color/sample/sample.tsx b/new-docs/pages/patterns/color/sample/sample.tsx new file mode 100644 index 00000000..1d322aaa --- /dev/null +++ b/new-docs/pages/patterns/color/sample/sample.tsx @@ -0,0 +1,87 @@ +import Color from "color"; +import { useEffect, useRef, useState } from "react"; +import { HiCheckCircle } from "react-icons/hi"; +import { + CategoryColor, + categoryColors, + Icon, + Tag, +} from "../../../../../core/src"; +import s from "./sample.module.css"; + +export type ColorSampleUsage = "text" | "icon" | "both"; + +interface Props { + background: string; + foreground: + | { type: "text"; cls: string; usage: ColorSampleUsage } + | { type: "border"; cls: string }; +} + +const getColor = (contrast: number): CategoryColor => { + const rounded = Math.round(contrast * 10) / 10; + if (rounded >= 4.5) return categoryColors.green; + if (rounded >= 3) return categoryColors.yellow; + return categoryColors.red; +}; + +const getContrast = ( + props: Props, + backElement: HTMLDivElement, + foreElement: HTMLElement, +): number => { + const back = window.getComputedStyle(backElement).backgroundColor; + const foreStyle = window.getComputedStyle(foreElement); + const isText = props.foreground.type === "text"; + // Only use long hand name. + // See: https://developer.mozilla.org/en-US/docs/Web/API/Window/getComputedStyle#notes + const fore = foreStyle[isText ? "color" : "borderLeftColor"]; + return Color(back).contrast(Color(fore)); +}; + +const ColorIcon = (): JSX.Element => ( + +); + +export const ColorSample = (props: Props): JSX.Element => { + const backRef = useRef(null); + const foreRef = useRef(null); + const [contrast, setContrast] = useState(0); + + useEffect(() => { + window.setTimeout(() => { + const [back, fore] = [backRef.current, foreRef.current]; + if (back === null) throw Error("backElm is null"); + if (fore === null) throw Error("foreElm is null"); + setContrast(getContrast(props, back, fore)); + }, 0); // Wait for all styles are applied + }, [setContrast]); + + return ( +
+ {/* "background" also set color so the "fore" must be in another + element of the "back" */} + {props.foreground.type === "text" ? ( + + {props.foreground.usage !== "icon" && Aa} + {props.foreground.usage === "both" && } + {props.foreground.usage !== "text" && } + + ) : ( + + )} + + + +
+ ); +}; diff --git a/new-docs/pages/patterns/color/static.stories.mdx b/new-docs/pages/patterns/color/static.stories.mdx new file mode 100644 index 00000000..aba8e9ad --- /dev/null +++ b/new-docs/pages/patterns/color/static.stories.mdx @@ -0,0 +1,52 @@ +import { ColorStaticGrid } from "./static/grid"; + +# Static Colors + +Static colors are persistent across themes. They are the low-level palette +that Moai is built on top of. They ensure consistency across all parts of the +interface. + +In practice, however, you usually don't need to use static colors directly. +Instead, use high-level utilities like [`text`][1], [`background`][2], and +[`border`][3] which provide dynamic colors that are changed based on the +current theme and [always accessible][4]. + +## Usage + +Static colors are defined as [CSS variables][5] at [`root`][6]. They are +available everywhere in the app: + +```css +.heading { + color: var(--highlight-5); +} +``` + +Since static colors are persistent across themes, you usually want to provide +the value for each theme manually: + +```css +html.light .heading { + color: var(--highlight-6); +} +html.dark .heading { + color: var(--highlight-4); +} +``` + +In fact, the [implementations][7] of `text`, `background`, and `border` +utilities are good examples of handling colors in both light and dark themes. + +## Colors + +The below tables list all static colors in Moai and their variable names. + + + +[1]: /patterns/color/text +[2]: /patterns/color/background +[3]: /patterns/color/border +[4]: https://developer.mozilla.org/en-US/docs/Web/Accessibility/Understanding_WCAG/Perceivable/Color_contrast +[5]: https://developer.mozilla.org/en-US/docs/Web/CSS/Using_CSS_custom_properties +[6]: https://developer.mozilla.org/en-US/docs/Web/CSS/:root +[7]: https://github.com/moaijs/moai/blob/739a87de82bd061bb41f38c5a51a410b59944a3d/lib/core/src/text/text.module.css diff --git a/new-docs/pages/patterns/color/static/grid.module.css b/new-docs/pages/patterns/color/static/grid.module.css new file mode 100644 index 00000000..0516c7d0 --- /dev/null +++ b/new-docs/pages/patterns/color/static/grid.module.css @@ -0,0 +1,6 @@ +.container { + display: grid; + grid-template-columns: repeat(auto-fit, 300px); + gap: 16px; + max-width: 640px; +} diff --git a/new-docs/pages/patterns/color/static/grid.tsx b/new-docs/pages/patterns/color/static/grid.tsx new file mode 100644 index 00000000..9154fc0d --- /dev/null +++ b/new-docs/pages/patterns/color/static/grid.tsx @@ -0,0 +1,11 @@ +import s from "./grid.module.css"; +import { ColorStaticTable } from "./table"; + +export const ColorStaticGrid = (): JSX.Element => ( +
+ + + + +
+); diff --git a/new-docs/pages/patterns/color/static/sample.module.css b/new-docs/pages/patterns/color/static/sample.module.css new file mode 100644 index 00000000..3720a242 --- /dev/null +++ b/new-docs/pages/patterns/color/static/sample.module.css @@ -0,0 +1,11 @@ +.container { + display: flex; + align-items: center; + gap: 16px; +} + +.circle { + width: 24px; + height: 24px; + border-radius: 24px; +} diff --git a/new-docs/pages/patterns/color/static/sample.tsx b/new-docs/pages/patterns/color/static/sample.tsx new file mode 100644 index 00000000..96cf0824 --- /dev/null +++ b/new-docs/pages/patterns/color/static/sample.tsx @@ -0,0 +1,33 @@ +import Color from "color"; +import { useEffect, useRef, useState } from "react"; +import { text } from "../../../../../core/src"; +import s from "./sample.module.css"; + +interface Props { + name: string; +} + +export const ColorStaticSample = (props: Props): JSX.Element => { + const ref = useRef(null); + const [hex, setHex] = useState(""); + + useEffect(() => { + window.setTimeout(() => { + const div = ref.current; + if (div === null) throw Error("Ref is not attached"); + const bg = window.getComputedStyle(div).backgroundColor; + setHex(Color(bg).hex()); + }, 0); // Wait for all colors to be applied correctly + }, []); + + return ( +
+
+
{hex}
+
+ ); +}; diff --git a/new-docs/pages/patterns/color/static/table.module.css b/new-docs/pages/patterns/color/static/table.module.css new file mode 100644 index 00000000..e9491afe --- /dev/null +++ b/new-docs/pages/patterns/color/static/table.module.css @@ -0,0 +1,16 @@ +.container { + border-width: 2px; +} + +.container table { + table-layout: fixed; +} + +.color { + width: 160px; +} + +.name { + width: 120px; + font-variant-numeric: tabular-nums; +} diff --git a/new-docs/pages/patterns/color/static/table.tsx b/new-docs/pages/patterns/color/static/table.tsx new file mode 100644 index 00000000..e6462ea0 --- /dev/null +++ b/new-docs/pages/patterns/color/static/table.tsx @@ -0,0 +1,35 @@ +import { border, Table } from "../../../../../core/src"; +import { ColorStaticSample } from "./sample"; +import s from "./table.module.css"; + +interface Props { + name: string; +} + +const Color = + (props: Props) => + (row: number): JSX.Element => ( + // eslint-disable-next-line react/prop-types + + ); + +const Name = + (props: Props) => + (row: number): JSX.Element => ( + // eslint-disable-next-line react/prop-types + + ); + +export const ColorStaticTable = (props: Props): JSX.Element => ( +
+ + fill + rows={[...Array(10).keys()]} + rowKey={(row) => row.toString()} + columns={[ + { title: "Color", className: s.color, render: Color(props) }, + { title: "Name", className: s.name, render: Name(props) }, + ]} + /> +
+); diff --git a/new-docs/pages/patterns/color/text.stories.mdx b/new-docs/pages/patterns/color/text.stories.mdx new file mode 100644 index 00000000..4e2a20a8 --- /dev/null +++ b/new-docs/pages/patterns/color/text.stories.mdx @@ -0,0 +1,74 @@ +import { ColorText } from "./text/text"; + +# Text Color + +The `text` utility contains classes that set the [CSS `color`][1] property. +This is the recommended way to set color for texts and icons since they +automatically change based on the current theme and always conform to the +[WCAG of contrast ratios][3] at AA level. + +```ts +import { text } from "@moai/core"; + +Text; +``` + +## Gray colors + +There are 2 gray colors in the `text` utility. They can be used for both texts +and icons due to their high contrast ratios: + + + +The `normal` value sets the color back to the default text color, e.g. a +near-black in light theme. Since it's the app's default, you usually don't need +to use this class, except to override an inherited color from a parent. + +The `muted` value dims the color. It is usually used for secondary texts, like +labels and descriptive texts. Their contrast ratios are always above 4.5 to +[ensure legibility][3] in both light and dark themes. + +## Semantic colors + +There are 3 semantic colors in the `text` utility. Each has a `Strong` version +for texts (contrast 4.5+) and a `Weak` version for icons (contrast 3+): + + + +Semantic colors are used to communicate meaning and intention to users. +However, they should not be the only method as some users [may not recognize][4] +them. Other methods, such as helper texts or icons, should be used together to +ensure good accessibility. + +Moai uses green for positive intentions and red for negative ones. This may +not work for some users whose culture perceives red as a positive color. In +these cases, you may want to [customize][6] the `text` utility to follow your +users. + +## See also + +- For static colors that don't change based on the current theme, see + [Static Colors][7]. +- For a wide range of colors that don't have semantic meaning, see + [Category Colors][8]. + +[1]: https://developer.mozilla.org/en-US/docs/Web/CSS/color +[3]: https://developer.mozilla.org/en-US/docs/Web/Accessibility/Understanding_WCAG/Perceivable/Color_contrast +[4]: https://en.wikipedia.org/wiki/Color_blindness +[6]: /docs/intro/extension +[7]: /docs/patterns/color/static +[8]: /docs/patterns/color/category diff --git a/new-docs/pages/patterns/color/text/text.module.css b/new-docs/pages/patterns/color/text/text.module.css new file mode 100644 index 00000000..50ec057d --- /dev/null +++ b/new-docs/pages/patterns/color/text/text.module.css @@ -0,0 +1,16 @@ +.name { + font-family: "Source Code Pro", monospace; + width: 160px; + min-width: 160px; +} + +.usage { + width: 160px; + min-width: 160px; +} + +.container { + overflow: auto; + white-space: nowrap; + border-width: 2px; +} diff --git a/new-docs/pages/patterns/color/text/text.tsx b/new-docs/pages/patterns/color/text/text.tsx new file mode 100644 index 00000000..6f3b03ec --- /dev/null +++ b/new-docs/pages/patterns/color/text/text.tsx @@ -0,0 +1,62 @@ +import { background, border, Table, text } from "../../../../../core/src"; +import { ColorSample, ColorSampleUsage } from "../sample/sample"; +import s from "./text.module.css"; + +type TextKey = keyof typeof text; + +interface Row { + key: TextKey; + usage: ColorSampleUsage; +} + +// const usageTexts: Record = { +// both: "Icon and text", +// icon: "Icon only", +// text: "Text only", +// }; + +// const Usage = (row: Row): JSX.Element => {usageTexts[row.usage]}; + +const MakeColumn = + (theme: "light" | "dark", back: string) => + (row: Row): JSX.Element => ( +
+ +
+ ); + +const LightStrong = MakeColumn("light", background.strong); +const LightWeak = MakeColumn("light", background.weak); +const DarkStrong = MakeColumn("dark", background.strong); +const DarkWeak = MakeColumn("dark", background.weak); + +interface Props { + rows: Row[]; +} + +export const ColorText = (props: Props): JSX.Element => ( +
+ + size={Table.sizes.small} + fixed={{ firstColumn: true }} + fill + rows={props.rows} + rowKey={(row) => row.key} + columns={[ + { title: "Name", className: s.name, render: "key" }, + // { title: "Usage", className: s.usage, render: Usage }, + { title: "Light", render: LightStrong }, + { title: "Light (alt bg)", render: LightWeak }, + { title: "Dark", render: DarkStrong }, + { title: "Dark (alt bg)", render: DarkWeak }, + ]} + /> +
+); diff --git a/new-docs/pages/patterns/form/form.module.css b/new-docs/pages/patterns/form/form.module.css new file mode 100644 index 00000000..ac96462b --- /dev/null +++ b/new-docs/pages/patterns/form/form.module.css @@ -0,0 +1,5 @@ +.form { + display: "flex"; + flex-direction: "column"; + gap: 16; +} diff --git a/new-docs/pages/patterns/form/formik.stories.mdx b/new-docs/pages/patterns/form/formik.stories.mdx new file mode 100644 index 00000000..11a41ddc --- /dev/null +++ b/new-docs/pages/patterns/form/formik.stories.mdx @@ -0,0 +1,81 @@ +import { FormikExample } from "./formik"; + +# Formik + +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: + + + +```tsx +(): 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); + }} + > + {({ isSubmitting: busy }) => ( +
+ {title} + {message} + + + )} + + ); +}; +``` diff --git a/new-docs/pages/patterns/form/formik.tsx b/new-docs/pages/patterns/form/formik.tsx new file mode 100644 index 00000000..fd3d244d --- /dev/null +++ b/new-docs/pages/patterns/form/formik.tsx @@ -0,0 +1,51 @@ +import { ErrorMessage, Field, Form, Formik, FormikErrors } from "formik"; +import { FormError, Input, TextArea } from "../../../../core/src"; +import { ERRORS, FormValues, SubmitButton, postToServer } from "./utils"; +import s from "./form.module.css"; + +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); + }} + > + {({ isSubmitting: busy }) => ( +
+ {title} + {message} + + + )} + + ); +}; diff --git a/new-docs/pages/patterns/form/index.stories.mdx b/new-docs/pages/patterns/form/index.stories.mdx new file mode 100644 index 00000000..1f9a1e21 --- /dev/null +++ b/new-docs/pages/patterns/form/index.stories.mdx @@ -0,0 +1,10 @@ +# Form + +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]: /components/input +[4]: /components/textarea diff --git a/new-docs/pages/patterns/form/react-hook-form.stories.mdx b/new-docs/pages/patterns/form/react-hook-form.stories.mdx new file mode 100644 index 00000000..21d297a1 --- /dev/null +++ b/new-docs/pages/patterns/form/react-hook-form.stories.mdx @@ -0,0 +1,97 @@ +import { ReactHookFormExample } from "./react-hook-form"; + +# React Hook Form + +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: + + + +```tsx +(): 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 = ( +
+ +