diff --git a/.changeset/add-evo-details-leading.md b/.changeset/add-evo-details-leading.md new file mode 100644 index 0000000000..1d03f2384b --- /dev/null +++ b/.changeset/add-evo-details-leading.md @@ -0,0 +1,5 @@ +--- +"@evo-web/marko": patch +--- + +Add leading attribute tag to evo-details diff --git a/.changeset/add-evo-details-react.md b/.changeset/add-evo-details-react.md new file mode 100644 index 0000000000..8ad312b0d7 --- /dev/null +++ b/.changeset/add-evo-details-react.md @@ -0,0 +1,5 @@ +--- +"@evo-web/react": patch +--- + +Add evo-details component diff --git a/.claude/skills/evo-app-migrate-react/SKILL.md b/.claude/skills/evo-app-migrate-react/SKILL.md index 1e8bb3a8e5..66bdbbfd77 100644 --- a/.claude/skills/evo-app-migrate-react/SKILL.md +++ b/.claude/skills/evo-app-migrate-react/SKILL.md @@ -55,6 +55,60 @@ If a component is not listed below, it has **not been migrated yet** — keep us No prop changes. Global renames (Step 2) are sufficient. +### `ebay-details` + +This component has a **new composite API** in evo-react. The flat-prop approach is replaced by named sub-components. + +**Before:** + +```tsx +import { EbayDetails } from "@ebay/ebayui-core-react/ebay-details"; + +} + onToggle={handler} +> + Content here +; +``` + +**After:** + +```tsx +import { + EvoDetails, + EvoDetailsSummary, + EvoDetailsLeading, + EvoDetailsLabel, + EvoDetailsContent, +} from "@evo-web/react"; + + + + + + + Show me the details! + + Content here +; +``` + +**Prop changes:** + +| ebayui-core-react | evo-react | Notes | +| ------------------------ | ----------------------------------- | ---------------------------------------------------------------------------- | +| `text: string` | `` sub-component | Move label text into `` inside `` | +| `leading?: ReactElement` | `` sub-component | Move leading element into `` inside `` | +| `as?: ElementType` | `as` prop on `` | Move to the content sub-component | +| `onToggle` | `onToggle` | Same signature: `(event, { open: boolean }) => void` | +| `children` | `` children | Wrap children in `` | + +**Important:** `` must appear before `` inside `` — order is not enforced by the component. + --- ## Step 4 — Verify diff --git a/.claude/skills/evo-migrate-react/SKILL.md b/.claude/skills/evo-migrate-react/SKILL.md index 3fb58d7722..2c7788dcd0 100644 --- a/.claude/skills/evo-migrate-react/SKILL.md +++ b/.claude/skills/evo-migrate-react/SKILL.md @@ -43,6 +43,7 @@ packages/evo-react/src/evo-{name}/ {name}.tsx ← main component {subcomponent-name}.tsx ← sub-components if present (named after actual sub-component, e.g. button-cell.tsx) types.ts ← all exported types + context.ts ← React context + hook (only if component uses context) README.md ← component name + Documentation section with Storybook link only {name}.stories.tsx ← Storybook stories (co-located, NOT in __tests__/) test/ @@ -66,6 +67,23 @@ packages/evo-react/src/evo-{name}/ ## Component authoring rules +### No `import React from "react"` unless required for typing + +`@evo-web/react` uses the automatic JSX transform — the JSX runtime is injected automatically and `React` does not need to be imported for JSX. Import only what you actually use as named imports: + +```tsx +// ✅ evo-react — import only what is needed +import type { ComponentProps, SyntheticEvent } from "react"; +import classNames from "classnames"; + +// ❌ do NOT import the default React object unless unavoidable +import React from "react"; +``` + +The only time `import React from "react"` is acceptable is when you need the namespace for a specific type like `React.JSX.Element` in overloaded signatures, and even then prefer `import type { JSX } from "react"` with `JSX.Element`. + +--- + ### Named function declarations — no `FC`, no arrow function components ```tsx @@ -129,6 +147,8 @@ import { EvoIconChevronDown16 } from "../evo-icon/icons/evo-icon-chevron-down-16 ``` +This applies to **stories and tests** as well — never use inline `` or custom placeholder icons. Always use an existing `EvoIcon*` component. Available icons are in `packages/evo-react/src/evo-icon/icons/`. + ### Optional callbacks — no required default `() => {}` ```tsx @@ -177,11 +197,13 @@ export function EvoButton({ a11yText, ...rest }: EvoButtonProps) { Do **not** use `Children.map`, `Children.toArray`, `findComponent`, `filterComponent`, or any child-scanning pattern. -If the ebayui-core-react component uses children composition (e.g. finding a sub-component in children), **stop and ask** before proceeding. Propose one or more alternative approaches using explicit props instead of child scanning, for example: +The **preferred approach** is named sub-components with React context (see [ADR 0005](../../../docs/adr/0005-evo-react-child-component-composition.md)). This is the established pattern in `@evo-web/react` and should be the default proposal for consistency. + +If the ebayui-core-react component uses children composition (e.g. finding a sub-component in children), **stop and ask** before proceeding. Lead with the sub-component approach as the recommendation, but present the full picture so the user can confirm: -- Accepting sub-component content as a named prop (`footer`, `header`, `title`) -- Accepting a render prop -- Splitting into separate sibling components +1. What sub-components would be needed, and what state (if any) the parent must share via context. +2. Whether a simpler alternative fits — e.g. a named prop (`footer`, `header`, `title`) if the region is a single, unstructured slot with no BEM class injection needed. +3. Any edge cases specific to this component that could affect the choice (e.g. enforced ordering, complex shared state, accessibility requirements). Do not guess — get alignment before migrating this pattern. @@ -269,7 +291,7 @@ describe("EvoButton SSR", () => { ## Storybook stories — `{name}.stories.tsx` -- One story per component whenever possible. Only add multiple stories when variations require different component structure that cannot be expressed through args/argTypes alone. +- **One story per component** unless the component tree itself must change between variations (e.g. different sub-components, optional children). Visual and prop variations (size, alignment, disabled, open…) must be handled through `args` and `argTypes` controls — not separate stories. - `title` must mirror the ebayui-core-react story title with `ebay` replaced by `evo`. - Description format: one-sentence summary followed by a `## Usage` section with the import snippet. diff --git a/docs/adr/0005-evo-react-child-component-composition.md b/docs/adr/0005-evo-react-child-component-composition.md new file mode 100644 index 0000000000..ae338fff20 --- /dev/null +++ b/docs/adr/0005-evo-react-child-component-composition.md @@ -0,0 +1,48 @@ +# 5. Evo React Child Component Composition + +**Date:** 2026-04-23 + +## Status + +Accepted + +## Context + +`@ebay/ui-core-react` uses `React.Children.map` / `findComponent` to introspect children and wire up composite component structures. This API is fragile, order-sensitive, and deprecated by React. + +### Alternatives considered + +- **`summary={}`** — requires `React.cloneElement` to inject BEM classes into the passed element, which is equally discouraged. +- **`summary={{ children: "", className, ...spanProps }}`** — avoids cloning but forces consumers to pass a plain object instead of JSX, which is poor DX. + +## Decision + +`@evo-web/react` uses **named sub-components** (compound component pattern) instead of `React.Children` scanning. Shared state is passed via React context from the parent. Sub-components consume only what they need. + +```tsx + + + + + + How do I get started? + + Content + +``` + +All sub-components are exported from the package entry point. + +## Consequences + +### Positive + +- More option for developers to customize the layout when possible. +- Better DX with simpler to use component APIs. +- Removes dependency of `React.Children` APIs and `React.cloneElement`. + +### Negative + +- Child rendering order is the consumer's responsibility — the parent does not enforce it. +- The evo-react API will diverge from evo-marko's attribute tag pattern (`<@summary>`), which is a Marko 6-specific construct with no React equivalent. +- More verbose component APIs diff --git a/packages/evo-marko/src/tags/evo-details/details.stories.ts b/packages/evo-marko/src/tags/evo-details/details.stories.ts index 06aa8675ed..5a65b2af53 100644 --- a/packages/evo-marko/src/tags/evo-details/details.stories.ts +++ b/packages/evo-marko/src/tags/evo-details/details.stories.ts @@ -4,6 +4,8 @@ import Readme from "./README.md"; import Details, { type Input } from "./index.marko"; import DefaultTemplate from "./examples/default.marko"; import DefaultTemplateCode from "./examples/default.marko?raw"; +import WithLeadingTemplate from "./examples/with-leading.marko"; +import WithLeadingTemplateCode from "./examples/with-leading.marko?raw"; export default { title: "navigation & disclosure/evo-details", @@ -18,6 +20,15 @@ export default { argTypes: { content: {}, + leading: { + description: "Optional leading element (e.g. an icon) rendered before the summary label", + "@": { + [" attributes" as any]: { + description: + "All attributes and event handlers from [the native HTML `` tag](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/span) will be passed through", + }, + }, + }, summary: { description: "The body which will be wrapped as the details summary", "@": { @@ -63,3 +74,8 @@ export const Default = buildExtensionTemplate( DefaultTemplate, DefaultTemplateCode, ); + +export const WithLeading = buildExtensionTemplate( + WithLeadingTemplate, + WithLeadingTemplateCode, +); diff --git a/packages/evo-marko/src/tags/evo-details/examples/with-leading.marko b/packages/evo-marko/src/tags/evo-details/examples/with-leading.marko new file mode 100644 index 0000000000..cc7bd784d7 --- /dev/null +++ b/packages/evo-marko/src/tags/evo-details/examples/with-leading.marko @@ -0,0 +1,5 @@ + + <@summary>Details + <@leading> + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. + diff --git a/packages/evo-marko/src/tags/evo-details/index.marko b/packages/evo-marko/src/tags/evo-details/index.marko index 0402fb2754..ea8e22a39b 100644 --- a/packages/evo-marko/src/tags/evo-details/index.marko +++ b/packages/evo-marko/src/tags/evo-details/index.marko @@ -1,5 +1,6 @@ export interface Input extends Marko.Input<"details"> { summary?: Marko.AttrTag>; + leading?: Marko.AttrTag>; size?: "regular" | "small"; alignment?: "regular" | "center"; contentAs?: keyof Marko.NativeTags; @@ -12,6 +13,7 @@ export interface Input extends Marko.Input<"details"> { alignment, size, summary, + leading, content, contentAs, ...htmlInput @@ -24,6 +26,9 @@ export interface Input extends Marko.Input<"details"> { size === "small" && "details__summary--small", alignment === "center" && "details__summary--center", ]> + + +