diff --git a/apps/docs/.eslintrc.json b/apps/docs/.eslintrc.json index ca1d22508..fb85e9f77 100644 --- a/apps/docs/.eslintrc.json +++ b/apps/docs/.eslintrc.json @@ -17,6 +17,7 @@ "Collapsible": true, "Expand": true, "TokenTable": true, + "PropsReferenceTable": true, "MotionPreview": true, "Figure": true, "Footnote": true, diff --git a/apps/docs/app/home.css b/apps/docs/app/home.css index 206f3cecd..1def00fd5 100644 --- a/apps/docs/app/home.css +++ b/apps/docs/app/home.css @@ -171,11 +171,11 @@ a.hd-home-sample__item:hover::after { .hd-home-sample__title-tag { align-self: center; border-radius: var(--hd-border-radius-sm); - background-color: var(--hd-color-accent-surface-strong); - color: var(--hd-white); + color: var(--hd-color-accent-text); + background-color: var(--hd-color-accent-surface); font-size: 0.75rem; font-weight: 400; - padding: var(--hd-space-05); + padding: 0.125rem var(--hd-space-05); } .hd-home-sample__tagline { diff --git a/apps/docs/app/layout.tsx b/apps/docs/app/layout.tsx index 8c01af4da..c3c067f21 100644 --- a/apps/docs/app/layout.tsx +++ b/apps/docs/app/layout.tsx @@ -3,6 +3,8 @@ import { EnvironmentContextProvider } from "@/context/env/EnvironmentProvider"; import { FeatureFlagProvider } from "@/context/feature/FeatureFlagProvider"; import { RACProvider } from "@/context/rac/RACProvider"; import { ThemeProvider } from "@/context/theme/ThemeProvider"; +import "@hopper-ui/tokens/fonts.css"; +import "@hopper-ui/tokens/tokens.css"; import type { ReactNode } from "react"; import "./globals.css"; import "./layout.css"; diff --git a/apps/docs/app/page.tsx b/apps/docs/app/page.tsx index 96378109c..1a0a92dc1 100644 --- a/apps/docs/app/page.tsx +++ b/apps/docs/app/page.tsx @@ -12,7 +12,6 @@ import { SelectArrowIcon, TypescriptIcon } from "@/components/icon"; -import "@hopper-ui/tokens/fonts.css"; import Link from "next/link"; import "./home.css"; import { ComponentsCard } from "./ui/home-page/ComponentsCard"; diff --git a/apps/docs/app/ui/components/propTable/PropTableRender.tsx b/apps/docs/app/ui/components/propTable/PropTableRender.tsx index ed72dff65..c98f0f3bf 100644 --- a/apps/docs/app/ui/components/propTable/PropTableRender.tsx +++ b/apps/docs/app/ui/components/propTable/PropTableRender.tsx @@ -43,10 +43,10 @@ const columns: ColumnDef[] = [ const { name, type, required } = info.getValue() as { name: ReactNode; type: ReactNode; required: boolean }; return ( -
+
{name}{!required && "?"}
{type}
-
+ ); } }, @@ -58,12 +58,12 @@ const columns: ColumnDef[] = [ const { description, defaultValue } = info.getValue() as { description: ReactNode; defaultValue: string }; return ( -
+
{description}
{defaultValue !== "" && (
Defaults to .
)} -
+ ); } } @@ -87,7 +87,7 @@ export const PropTableRender = ({ items }: { items: Item[] }) => { return (
-
+
{table.getRowModel().rows.map(row => (
{row.getVisibleCells().map(cell => { @@ -102,7 +102,7 @@ export const PropTableRender = ({ items }: { items: Item[] }) => { )}
))} -
+
); }; diff --git a/apps/docs/app/ui/components/propsReferenceTable/PropsReferenceTable.tsx b/apps/docs/app/ui/components/propsReferenceTable/PropsReferenceTable.tsx new file mode 100644 index 000000000..a2d482911 --- /dev/null +++ b/apps/docs/app/ui/components/propsReferenceTable/PropsReferenceTable.tsx @@ -0,0 +1,45 @@ +import Table, { type TableProps } from "@/components/table/Table"; +import Link from "next/link"; +import type { ReactNode } from "react"; +import "./propsReferenceTable.css"; + +const ScaleLinks: Record = { + "color-scale": Colors, + "elevation-scale": Elevation, + "dimension-scale": Dimensions, + "spacing-scale": Spacing, + "shape-scale": Shape, + "typography-scale": Typography +}; + +function toScaleLink(scale: string) { + return ScaleLinks[scale] ?? scale; +} + +function toRowValues([propName, cssProperty, scale, supports]: string[]): TableProps["data"][number] { + return { + "Prop": propName, + "CSS property": cssProperty, + "Scale": toScaleLink(scale), + "Supports": supports + }; +} + +export interface PropsReferenceTableProps { + rows: string[][]; +} + +export function PropsReferenceTable({ rows }: PropsReferenceTableProps) { + return ( + toRowValues(x))} + /> + ); +} diff --git a/apps/docs/app/ui/components/propsReferenceTable/propsReferenceTable.css b/apps/docs/app/ui/components/propsReferenceTable/propsReferenceTable.css new file mode 100644 index 000000000..d7bc13722 --- /dev/null +++ b/apps/docs/app/ui/components/propsReferenceTable/propsReferenceTable.css @@ -0,0 +1,3 @@ +.hd-props-reference-table { + table-layout: fixed; +} diff --git a/apps/docs/app/ui/components/simpleTable/SimpleTable.tsx b/apps/docs/app/ui/components/simpleTable/SimpleTable.tsx index d9254c90f..140b4a33b 100644 --- a/apps/docs/app/ui/components/simpleTable/SimpleTable.tsx +++ b/apps/docs/app/ui/components/simpleTable/SimpleTable.tsx @@ -13,7 +13,7 @@ export default async function SimpleTable({ "aria-label": ariaLabel, headers, da {headers.map((header, index) => { - const classNames = clsx("hd-table__column", { "hd-table__colum--right": index === headers.length - 1 && lastColumnAlignment === "right" }); + const classNames = clsx("hd-table__column", { "hd-table__column--right": index === headers.length - 1 && lastColumnAlignment === "right" }); return ( // eslint-disable-next-line react/no-array-index-key diff --git a/apps/docs/app/ui/layout/mobileMenu/mobileMenu.css b/apps/docs/app/ui/layout/mobileMenu/mobileMenu.css index 4a5102d2d..5add320c6 100644 --- a/apps/docs/app/ui/layout/mobileMenu/mobileMenu.css +++ b/apps/docs/app/ui/layout/mobileMenu/mobileMenu.css @@ -96,10 +96,10 @@ .hd-mobile-menu-nav-tag { align-self: center; border-radius: var(--hd-border-radius-sm); - background-color: var(--hd-color-accent-surface-strong); - color: var(--hd-white); + color: var(--hd-color-accent-text); + background-color: var(--hd-color-accent-surface); font-size: 0.75rem; - padding: var(--hd-space-05); + padding: 0.125rem var(--hd-space-05); } .hd-mobile-menu--opening { diff --git a/apps/docs/app/ui/layout/nav/nav.css b/apps/docs/app/ui/layout/nav/nav.css index e53593430..b7271a95f 100644 --- a/apps/docs/app/ui/layout/nav/nav.css +++ b/apps/docs/app/ui/layout/nav/nav.css @@ -58,9 +58,9 @@ /* Tag */ .hd-nav__link-tag { border-radius: var(--hd-border-radius-sm); - background-color: var(--hd-color-accent-surface-strong); - color: var(--hd-white); - font-size: 0.75rem; - padding: var(--hd-space-05); + color: var(--hd-color-accent-text); + background-color: var(--hd-color-accent-surface); + font-size: 0.825rem; + padding: 0.125rem var(--hd-space-05); margin-inline-start: var(--hd-space-05); } diff --git a/apps/docs/app/ui/tokens/preview/Preview.tsx b/apps/docs/app/ui/tokens/preview/Preview.tsx index fca1c0190..ba8a22fe4 100644 --- a/apps/docs/app/ui/tokens/preview/Preview.tsx +++ b/apps/docs/app/ui/tokens/preview/Preview.tsx @@ -1,6 +1,6 @@ import "@hopper-ui/tokens/fonts.css"; -import "./preview.css"; import type { CSSProperties, ReactNode } from "react"; +import "./preview.css"; interface PreviewProps { category?: string; @@ -80,7 +80,7 @@ const Preview = ({ category, name, value }: PreviewProps) => { }; if (matchingPaddingKeyword) { preview = { - style: { padding: value }, + style: { padding: value, justifySelf: "end" }, className: `hd-preview--semantic-size hd-preview--${matchingPaddingKeyword}` }; } @@ -92,7 +92,7 @@ const Preview = ({ category, name, value }: PreviewProps) => { } if (matchingStackKeyword) { preview = { - style: { padding: `0 0 ${value} 0` }, + style: { padding: `0 0 ${value} 0`, justifySelf: "end" }, className: `hd-preview--semantic-size hd-preview--${matchingStackKeyword}` }; } diff --git a/apps/docs/app/ui/tokens/table/IconSpecTable.tsx b/apps/docs/app/ui/tokens/table/IconSpecTable.tsx index 45042d04d..607379a9a 100644 --- a/apps/docs/app/ui/tokens/table/IconSpecTable.tsx +++ b/apps/docs/app/ui/tokens/table/IconSpecTable.tsx @@ -13,11 +13,14 @@ interface IconSpecTableProps { } const IconSpecTable = ({ data }: IconSpecTableProps) => { - return
; + return ( +
+ ); }; export default IconSpecTable; diff --git a/apps/docs/app/ui/tokens/table/TokenTable.tsx b/apps/docs/app/ui/tokens/table/TokenTable.tsx index 8276385e3..e2ebbbb3f 100644 --- a/apps/docs/app/ui/tokens/table/TokenTable.tsx +++ b/apps/docs/app/ui/tokens/table/TokenTable.tsx @@ -3,34 +3,79 @@ import Table from "@/components/table/Table"; import Preview from "@/app/ui/tokens/preview/Preview"; import Code from "@/components/code/Code"; +import type { ReactNode } from "react"; import "./tokenTable.css"; interface TableProps { category: string; noPreview?: boolean; + tokenType?: "core" | "semantic" | null; data: { name: string; value: string; }[]; } -const TokenTable = ({ category, data, noPreview }: TableProps) => { +function formatStyledSystemName(name: string, tokenType: "core" | "semantic" | null) { + let prefix = ""; + if (tokenType === "core") { + prefix = "core_"; + } else if (name?.includes("dataviz")) { + prefix = "dataviz_"; + } + + const formattedName = name + .replace("hop-", "") + .replace("-border", "") + .replace("-surface", "") + .replace("-text", "") + .replace("-icon", "") + .replace("elevation-", "") + .replace("shape-", "") + .replace("space-", "") + .replace("border-", "") + .replace("radius-", "") + .replace("border-", "") + .replace("dataviz-", "") + .replace("shadow-", "") + .replace("font-family-", "") + .replace("font-size-", "") + .replace("font-weight-", "") + .replace("line-height-", "") + ; + + return `${prefix}${formattedName}`; +} + +const TokenTable = ({ category, data, noPreview = false, tokenType }: TableProps) => { const formattedData = data.map(token => { const { name, value } = token; - - return { + const values: Record = { name: {`--${name}`}, + styledSystemValue: tokenType && formatStyledSystemName(name, tokenType), value: value, preview: !noPreview && }; - }); + if (!tokenType) { + delete values.styledSystemValue; + } + + if (noPreview) { + delete values.preview; + } + + return values; + }); + const columns = ["Name", tokenType && "Styled-System Value", "Value", !noPreview && "Preview"].filter(Boolean) as string[]; - return
; + return ( +
+ ); }; export default TokenTable; diff --git a/apps/docs/app/ui/tokens/table/TypographyTable.tsx b/apps/docs/app/ui/tokens/table/TypographyTable.tsx index 5191d06af..d17df78cd 100644 --- a/apps/docs/app/ui/tokens/table/TypographyTable.tsx +++ b/apps/docs/app/ui/tokens/table/TypographyTable.tsx @@ -44,7 +44,7 @@ const TypographyTable = ({ type, data }: TypographyTableProps) => { !hasNoSizes && "Size", "Values", "Preview" - ]} + ].filter(Boolean) as string[]} data={listItems} className={clsx("hd-typo-table", { "hd-typo-table--has-no-sizes": hasNoSizes })} ariaLabel="Typography tokens" diff --git a/apps/docs/app/ui/tokens/table/TypographyVariantTable.tsx b/apps/docs/app/ui/tokens/table/TypographyVariantTable.tsx index 87f55ce1e..b022ae3b0 100644 --- a/apps/docs/app/ui/tokens/table/TypographyVariantTable.tsx +++ b/apps/docs/app/ui/tokens/table/TypographyVariantTable.tsx @@ -33,7 +33,8 @@ const TypographyVariantTable = ({ type, data }: TypographyVariantTableProps) => }); return ( -
{ - const categoryTokens = tokens.filter(token => { - const excludedCategoryTokens = excludedCategories?.some(category => token.name.includes(category)); +const TableSection = ({ tokens, categories, excludedCategories, categoryKey, tokenType }: TableSectionProps) => { + const categoryTokens = useMemo(() => { + return tokens.filter(token => { + const excludedCategoryTokens = excludedCategories?.some(category => token.name.includes(category)); - return categories.some(category => token.name.includes(category)) && !excludedCategoryTokens; - }); + return categories.some(category => token.name.includes(category)) && !excludedCategoryTokens; + }); + }, [tokens, categories, excludedCategories]); - return
- -
; + return ( +
+ +
+ ); }; export default TableSection; diff --git a/apps/docs/components/mdx/components.tsx b/apps/docs/components/mdx/components.tsx index d2beeb5fa..e74acb5e1 100644 --- a/apps/docs/components/mdx/components.tsx +++ b/apps/docs/components/mdx/components.tsx @@ -33,6 +33,7 @@ import { ComponentCodeWrapper } from "@/app/ui/components/componentExample/Compo import type { ComponentExampleProps } from "@/app/ui/components/componentExample/ComponentExample.tsx"; import ComponentPreview from "@/app/ui/components/componentExample/ComponentPreview.tsx"; import type { MigrateGuideProps } from "@/app/ui/components/migrateGuide/MigrateGuide.tsx"; +import { PropsReferenceTable } from "@/app/ui/components/propsReferenceTable/PropsReferenceTable"; import type { PropTableProps } from "@/app/ui/components/propTable/PropTable.tsx"; import SimpleTable from "@/app/ui/components/simpleTable/SimpleTable"; @@ -60,6 +61,7 @@ export const components = { BreakpointTable: BreakpointTable, Footnote: Footnote, TokenTable: TokenTable, + PropsReferenceTable: PropsReferenceTable, TypographyTable: TypographyTable, TypographyVariantTable: TypographyVariantTable, IconTable: IconTable, diff --git a/apps/docs/components/table/Table.tsx b/apps/docs/components/table/Table.tsx index 060814fe1..d07d8ef75 100644 --- a/apps/docs/components/table/Table.tsx +++ b/apps/docs/components/table/Table.tsx @@ -1,69 +1,69 @@ "use client"; import clsx from "clsx"; -import { Cell, Column, Row, Table as TableRA, TableBody, TableHeader } from "react-aria-components"; import { type ReactNode, useMemo } from "react"; - +import { Cell, Column, Table as ReactTable, Row, TableBody, TableHeader } from "react-aria-components"; import "./table.css"; interface dataType { [key: string]: string | number | boolean | undefined | null | ReactNode; } -interface TableProps { - head: (string | boolean)[]; +export interface TableProps { + head: string[]; data: dataType[]; lastColumnAlignment?: "left" | "right"; "ariaLabel"?: string; className?: string; } -function generateUniqueKey() { - return `${Date.now()}-${Math.random()}`; -} - const Table = ({ data, head, lastColumnAlignment = "left", ariaLabel = "standard table", className }: TableProps) => { const textAlignRight = lastColumnAlignment === "right"; - const lastColumn = head.length - 1; - const dataItem = useMemo(() => data, [data]); + const lastColumnIndex = head.length - 1; - const headItems = head.map((item, index) => { - return ( - - {item} - - ); - }); + const headItems = useMemo(() => { + return head.map((item, index) => { + return ( + + {item} + + ); + }); + }, [head, lastColumnIndex, textAlignRight]); - const dataItems = dataItem.map(item => { - return ( - - {Object.keys(item).map((key, index) => { - return ( - - {item[key]} - - ); - })} - - ); - }); + const dataItems = useMemo(() => { + return data.map((item, index) => { + return ( + // eslint-disable-next-line react/no-array-index-key + + {Object.keys(item).map((key, i) => { + return ( + + {item[key]} + + ); + })} + + ); + }); + }, [data, lastColumnIndex, textAlignRight]); return ( - - + {headItems} {dataItems} - + ); }; diff --git a/apps/docs/components/table/table.css b/apps/docs/components/table/table.css index b0c769ddf..33068db8e 100644 --- a/apps/docs/components/table/table.css +++ b/apps/docs/components/table/table.css @@ -38,7 +38,7 @@ padding-left: 0; } -.hd-table__colum--right, +.hd-table__column--right, .hd-table__cell--right { text-align: right; } diff --git a/apps/docs/content/components/concepts/color-schemes.mdx b/apps/docs/content/components/concepts/color-schemes.mdx index 453f4c70d..ba27c2c42 100644 --- a/apps/docs/content/components/concepts/color-schemes.mdx +++ b/apps/docs/content/components/concepts/color-schemes.mdx @@ -1,7 +1,7 @@ --- title: Color Schemes description: Learn how color schemes work in Hopper, including how applications can adapt to operating system's dark mode. -order: 5 +order: 6 --- ## Introduction diff --git a/apps/docs/content/components/concepts/controlled-mode.mdx b/apps/docs/content/components/concepts/controlled-mode.mdx index 65cce3bad..b61d1a5cc 100644 --- a/apps/docs/content/components/concepts/controlled-mode.mdx +++ b/apps/docs/content/components/concepts/controlled-mode.mdx @@ -1,6 +1,6 @@ --- title: Controlled Mode -order: 4 +order: 5 --- When working with Hopper components, you can customize a component's behavior using **controlled** or **uncontrolled** properties, depending on your needs. This flexibility is the foundation for **building custom components** on top of Hopper, enabling you to implement interactive features or modify the default behavior of components while preserving their visual style and structure. diff --git a/apps/docs/content/components/concepts/forms.mdx b/apps/docs/content/components/concepts/forms.mdx index f3e0fdd82..4621600bb 100644 --- a/apps/docs/content/components/concepts/forms.mdx +++ b/apps/docs/content/components/concepts/forms.mdx @@ -1,7 +1,7 @@ --- title: Forms description: Forms allow users to enter and submit data, and provide them with feedback along the way. Hopper includes many components that integrate with HTML forms, with support for custom validation, labels, and help text. -order: 6 +order: 7 --- _Since Hopper components are designed on top of React Aria, this article is heavily inspired by the [Forms](https://react-spectrum.adobe.com/react-spectrum/forms.html) article in React-Aria's documentation._ diff --git a/apps/docs/content/components/concepts/internationalization.mdx b/apps/docs/content/components/concepts/internationalization.mdx index c6f01ae85..904d2b821 100644 --- a/apps/docs/content/components/concepts/internationalization.mdx +++ b/apps/docs/content/components/concepts/internationalization.mdx @@ -1,7 +1,7 @@ --- title: Internationalization description: Adapting components to respect languages and cultures of users around the world is an great way to make your application accessible to the widest number of people. -order: 8 +order: 9 --- ## Introduction diff --git a/apps/docs/content/components/concepts/slots.mdx b/apps/docs/content/components/concepts/slots.mdx index 4581760fa..0c92263dd 100644 --- a/apps/docs/content/components/concepts/slots.mdx +++ b/apps/docs/content/components/concepts/slots.mdx @@ -1,27 +1,135 @@ --- title: Slots description: This page describes how Hopper components include predefined layouts that you can insert elements into via slots. Slots are named areas in a component that receive children and provide style and layout for them. -order: 7 -status: WIP +order: 8 --- -This page will be inspired by https://react-spectrum.adobe.com/react-aria/advanced.html#slots but the focus of the page should be slots, not the context like this page. +_Since Hopper components are designed on top of React Aria, this article is heavily inspired by the [Advanced Customization article](https://react-spectrum.adobe.com/react-aria/advanced.html#slots) in React-Aria's documentation._ ## Introduction -## Contexts +The Hopper component API is designed around composition. Components are reused between patterns to build larger composite components. For example, there is no dedicated `NumberFieldIncrementButton` or `SelectPopover` component. Instead, the standalone [Button](../buttons/Button) and [Popover](../overlays/Popover) components are reused within [NumberField](../forms/NumberField) and [Select](../pickers/Select). This reduces the amount of duplicate styling code you need to write and maintain, and provides powerful composition capabilities you can use in your own components. + +```tsx + + + + +``` + +Slots in Hopper are named areas within a component where developers can insert content. They make it easier to create flexible and reusable components while keeping layouts accessible and consistent. Instead of using only children for content, slots act as specific placeholders that clearly define where each piece of content goes. + +Hopper builds on React Aria's context-based design to make working with slots simple and efficient. This approach gives developers more control over how components are customized and ensures they follow accessibility best practices. This guide explains how slots work in Hopper, how they use contexts, and how to create or extend components with them. + +## Custom patterns + +Each Hopper exports a corresponding context that you can use to build your own compositional APIs similar to the built-in components. You can send any prop or ref via context that you could pass to the corresponding component. The local props and ref on the component are merged with the ones passed via context, with the local props taking precedence (following the rules documented in [mergeProps](https://react-spectrum.adobe.com/react-aria/mergeProps.html)). + +This example shows a `FieldGroup` component that renders a group of text fields. The entire group can be marked as disabled via the isDisabled prop, which is passed to all child text fields via the TextFieldContext provider. + + + +Any `TextField` component you place inside a `FieldGroup` will automatically receive the `isDisabled` prop from the group, including those that are deeply nested inside other components. + +```tsx + + + + +``` + +## Slots + +Some patterns include multiple instances of the same component. These use the `slot` prop to distinguish each instance. Slots are named children within a component that can receive separate behaviors and [styles](./styling.mdx). Separate props can be sent to slots by providing an object with keys for each slot name to the component's context provider. + +This example shows a `Stepper` component with slots for its increment and decrement buttons. + + + + +And it can be used like this: + +```tsx + + + + +``` + +{/* TODO: uncomment this when the anatomy section is done */} +{/* The slots provided by each built-in Hopper component are shown in the Anatomy section of their documentation. */} ### Default slot -### Consuming contexts +The default slot is used to provide props to a component without specifying a slot name. This approach allows you to assign a default slot to a component for its default use case and enables you to specify a slot name for a specific use case. + +This example shows a custom component that passes a specific class name to a standard button child and to a button child with a slot named "end". + + + +And it can be used like this: + +```tsx + + {/* Consumes the props passed to the default slot */} + + + + + {/* Consumes the props passed to the "end" slot */} + + +``` + +## Consuming contexts + +You can also consume from contexts provided by Hopper components in your own custom components. This allows you to replace a component used as part of a larger pattern with a custom implementation. For example, you could consume from `LabelContext` in an existing styled label component to make it compatible with Hopper Components. + +### useContextProps + +The `useContextProps` hook merges the local props and ref with the ones provided via context by a parent component. The local props always take precedence over the context values (following the rules documented in [mergeProps](https://react-spectrum.adobe.com/react-aria/mergeProps.html)). `useContextProps` supports the slot prop to indicate which value to consume from context. + +```tsx +import { type LabelProps, LabelContext, useContextProps } from "@hopper-ui/components"; +import { forwardRef } from "react"; + +export const MyCustomLabel = forwardRef( + (props: LabelProps, ref: React.ForwardedRef) => { + // Merge the local props and ref with the ones provided via context. + [props, ref] = useContextProps(props, ref, LabelContext); + + // ... your existing Label component + return