diff --git a/src/docs/patterns/color/background/background.mdx b/src/docs/patterns/color/background/background.mdx new file mode 100644 index 00000000..9c2c9f5f --- /dev/null +++ b/src/docs/patterns/color/background/background.mdx @@ -0,0 +1,37 @@ +import { Canvas, Meta, Story } from "@storybook/blocks"; +import { ColorBackground } from "./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]: /docs/components-pane-docs diff --git a/src/docs/patterns/color/background/background.module.css b/src/docs/patterns/color/background/background.module.css new file mode 100644 index 00000000..d23bccd1 --- /dev/null +++ b/src/docs/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/src/docs/patterns/color/background/background.tsx b/src/docs/patterns/color/background/background.tsx new file mode 100644 index 00000000..a7e6e540 --- /dev/null +++ b/src/docs/patterns/color/background/background.tsx @@ -0,0 +1,48 @@ +import { border, background, Table, text } from "../../../../core"; +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/src/docs/patterns/color/border/border.mdx b/src/docs/patterns/color/border/border.mdx new file mode 100644 index 00000000..02cab140 --- /dev/null +++ b/src/docs/patterns/color/border/border.mdx @@ -0,0 +1,28 @@ +import { Meta, Canvas, Story } from "@storybook/blocks"; +import { ColorBorder } from "./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/src/docs/patterns/color/border/border.module.css b/src/docs/patterns/color/border/border.module.css new file mode 100644 index 00000000..d23bccd1 --- /dev/null +++ b/src/docs/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/src/docs/patterns/color/border/border.tsx b/src/docs/patterns/color/border/border.tsx new file mode 100644 index 00000000..a0512dbe --- /dev/null +++ b/src/docs/patterns/color/border/border.tsx @@ -0,0 +1,48 @@ +import { background, border, Table } from "../../../../core"; +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/src/docs/patterns/color/category/category.mdx b/src/docs/patterns/color/category/category.mdx new file mode 100644 index 00000000..f715c30a --- /dev/null +++ b/src/docs/patterns/color/category/category.mdx @@ -0,0 +1,23 @@ +import { Meta, Canvas, Story } from "@storybook/blocks"; +import { ColorCategoryTable } from "./category"; + + + +# Category Colors + +Category colors have no semantic meaning attached. Instead, they are used to +show relationships between elements (e.g. categorization, labelling in data +visualizations). They still conform to the AA level of [WCAG of contrast +ratios][1]. + +## Usage + +[At the moment][2], Moai does not support using category colors directly. +Instead, category colors are used via several components that support them: + + + +(The list is quite short at the moment. We are adding more to it!) + +[1]: https://developer.mozilla.org/en-US/docs/Web/Accessibility/Understanding_WCAG/Perceivable/Color_contrast +[2]: https://github.com/moaijs/moai/issues/210 diff --git a/src/docs/patterns/color/category/category.module.css b/src/docs/patterns/color/category/category.module.css new file mode 100644 index 00000000..b5002ec9 --- /dev/null +++ b/src/docs/patterns/color/category/category.module.css @@ -0,0 +1,3 @@ +.container { + border-width: 2px; +} diff --git a/src/docs/patterns/color/category/category.tsx b/src/docs/patterns/color/category/category.tsx new file mode 100644 index 00000000..d30c9640 --- /dev/null +++ b/src/docs/patterns/color/category/category.tsx @@ -0,0 +1,37 @@ +import React from "react"; +import { border, Table } from "../../../../core"; +import { GalleryTag } from "../../../../gallery"; +import s from "./category.module.css"; + +interface Row { + name: string; + link: string; + example: React.ReactNode; +} + +const Name = (row: Row): JSX.Element => ( + +); + +const Example = (row: Row): JSX.Element =>
; + +export const ColorCategoryTable = (): JSX.Element => ( +
+ + rows={[ + { + name: "Tag", + link: "/docs/components-tag", + example: , + }, + ]} + rowKey={(row) => row.name} + columns={[ + { title: "Component", render: Name }, + { title: "Example", render: Example }, + ]} + fill + fixed={{ firstColumn: true }} + /> +
+); diff --git a/src/docs/patterns/color/color.mdx b/src/docs/patterns/color/color.mdx new file mode 100644 index 00000000..785ed116 --- /dev/null +++ b/src/docs/patterns/color/color.mdx @@ -0,0 +1,20 @@ +import { Canvas, Meta, Story } from "@storybook/blocks"; + + + +# 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]: /docs/patterns-color-text--docs +[2]: /docs/patterns-color-background--docs +[3]: /docs/patterns-color-border--docs +[4]: /docs/patterns-color-static--docs +[5]: /docs/patterns-color-category--docs diff --git a/src/docs/patterns/color/sample/sample.module.css b/src/docs/patterns/color/sample/sample.module.css new file mode 100644 index 00000000..b50ab3b2 --- /dev/null +++ b/src/docs/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/src/docs/patterns/color/sample/sample.tsx b/src/docs/patterns/color/sample/sample.tsx new file mode 100644 index 00000000..e2036f43 --- /dev/null +++ b/src/docs/patterns/color/sample/sample.tsx @@ -0,0 +1,82 @@ +import Color from "color"; +import { useEffect, useRef, useState } from "react"; +import { HiCheckCircle } from "react-icons/hi"; +import { CategoryColor, Icon, Tag, categoryColors } from "../../../../core"; +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, props]); + + 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/src/docs/patterns/color/static/grid.module.css b/src/docs/patterns/color/static/grid.module.css new file mode 100644 index 00000000..0516c7d0 --- /dev/null +++ b/src/docs/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/src/docs/patterns/color/static/grid.tsx b/src/docs/patterns/color/static/grid.tsx new file mode 100644 index 00000000..9154fc0d --- /dev/null +++ b/src/docs/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/src/docs/patterns/color/static/sample.module.css b/src/docs/patterns/color/static/sample.module.css new file mode 100644 index 00000000..3720a242 --- /dev/null +++ b/src/docs/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/src/docs/patterns/color/static/sample.tsx b/src/docs/patterns/color/static/sample.tsx new file mode 100644 index 00000000..f136ca5d --- /dev/null +++ b/src/docs/patterns/color/static/sample.tsx @@ -0,0 +1,33 @@ +import Color from "color"; +import { useEffect, useRef, useState } from "react"; +import { text } from "../../../../core"; +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/src/docs/patterns/color/static/static.mdx b/src/docs/patterns/color/static/static.mdx new file mode 100644 index 00000000..9d0b991e --- /dev/null +++ b/src/docs/patterns/color/static/static.mdx @@ -0,0 +1,55 @@ +import { Canvas, Meta, Story } from "@storybook/blocks"; +import { ColorStaticGrid } from "./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]: /docs/patterns-color-text--docs +[2]: /docs/patterns-color-background--docs +[3]: /docs/patterns-color-border--docs +[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/src/docs/patterns/color/static/table.module.css b/src/docs/patterns/color/static/table.module.css new file mode 100644 index 00000000..e9491afe --- /dev/null +++ b/src/docs/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/src/docs/patterns/color/static/table.tsx b/src/docs/patterns/color/static/table.tsx new file mode 100644 index 00000000..97bd95ef --- /dev/null +++ b/src/docs/patterns/color/static/table.tsx @@ -0,0 +1,31 @@ +import { border, Table } from "../../../../core"; +import { ColorStaticSample } from "./sample"; +import s from "./table.module.css"; + +interface Props { + name: string; +} + +const Color = + (props: Props) => + (row: number): JSX.Element => ( + + ); + +const Name = + (props: Props) => + (row: number): JSX.Element => ; + +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/src/docs/patterns/color/text/text.mdx b/src/docs/patterns/color/text/text.mdx new file mode 100644 index 00000000..00ee27d9 --- /dev/null +++ b/src/docs/patterns/color/text/text.mdx @@ -0,0 +1,77 @@ +import { Canvas, Meta, Story } from "@storybook/blocks"; +import { ColorText } from "./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/draft-extension--docs +[7]: /docs/patterns-color-static--docs +[8]: /docs/patterns-color-category--docs diff --git a/src/docs/patterns/color/text/text.module.css b/src/docs/patterns/color/text/text.module.css new file mode 100644 index 00000000..50ec057d --- /dev/null +++ b/src/docs/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/src/docs/patterns/color/text/text.tsx b/src/docs/patterns/color/text/text.tsx new file mode 100644 index 00000000..1d039fb5 --- /dev/null +++ b/src/docs/patterns/color/text/text.tsx @@ -0,0 +1,62 @@ +import { background, border, Table, text } from "../../../../core"; +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 }, + ]} + /> +
+);