diff --git a/README.md b/README.md index c4709a2c..ebbaa0e9 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,16 @@ A [Vite+](https://viteplus.dev) monorepo. +## Docs + +- [`docs/design.md`](./docs/design.md) — the shared design + code contract: how we + name component **properties** and structure component **anatomy**. +- [`docs/integration.md`](./docs/integration.md) — how we adopt propel into Plane: + the explicit-first rollout, composing primitives with Base UI `render`, and the + remaining anatomy work. +- [`packages/propel/README.md`](./packages/propel/README.md) — the `@plane/propel` + package: install, usage, and component conventions. + ## Prerequisites Install the `vp` CLI globally: diff --git a/design.md b/design.md deleted file mode 100644 index 85d15ea3..00000000 --- a/design.md +++ /dev/null @@ -1,68 +0,0 @@ -# Naming component properties - -A short, shared guide for naming the properties on our components in Figma. - -**Why it matters:** when a component's properties use the same names and values -every time, the design maps cleanly onto the code, the library stays predictable -for everyone, and dev/AI tooling can infer how to use a component correctly. The -goal is one small vocabulary, reused everywhere — not a new word per component. - -## 1. Describe styling with a small, fixed set of axes - -Use the **same property name for the same idea on every component**, and only add -the axes a component actually needs. - -| Property | Answers | Example values | -| --------------- | ------------------------------------------------ | ---------------------------------------------------------------------- | -| **`variant`** | What _form / shape_ is it? | `primary` / `secondary` / `tertiary`, or `image` / `initials` / `icon` | -| **`tone`** | What does it _mean_ (its color / intent)? | `neutral` / `info` / `success` / `warning` / `danger` | -| **`magnitude`** | How _big_ is it? | `2xs … 3xl` (or `sm` / `md` / `lg`) | -| **`emphasis`** | How _prominent / filled_ is it? | `soft` / `outline` / `solid` | -| **`surface`** | What background does it _sit on_ (so it adapts)? | `background` / `fill` | -| **`density`** | How _compact_ is it? | `compact` / `default` | - -A button might use `variant · tone · magnitude`; an input might use -`surface · density · magnitude`. Use what fits — skip the rest. - -## 2. Avoid reserved words - -Never name a property **`size`, `type`, `color`, `width`, or `height`** — these are -built-in HTML/code attributes and collide. Use the axis names above instead -(`magnitude`, `variant`, `tone`). Behaviors that _are_ native — a button's submit -behavior, disabled state, a link's destination — are handled automatically in -code and don't need a property. - -## 3. Separate content from style - -Text, images, and icons are **content**, not style variants. Expose them as text -properties or instance-swaps (e.g. `label`, `name`, `icon`, `src`) — never bake -specific people, copy, or images into variants. - -**How many things are inside is also content, not a variant.** Don't make a -property for quantity — member count on an avatar group, the number of list items, -tabs, or steps. The designer adds or removes instances inside the component (like a -real list), and the component just lays out whatever it's given. A `2 members` / -`3 members` variant is the tell to avoid: it explodes the variant matrix and -doesn't match how the component is built in code. Reserve variants for _styling_. - -## 4. Format values consistently - -- **lowercase, no spaces or hyphens** — `initials`, not `Fallback - initials` -- **same spelling everywhere** — `md` means the same size on every component -- **t-shirt scale** for sizes (`2xs / xs / sm / md / lg / xl / 2xl / 3xl`); **plain - words** for tone (`neutral`, `danger`); **descriptive words** for variant - -## 5. Don't model impossible combinations - -If a combination doesn't exist (e.g. a `danger` tone only makes sense on a primary -button), simply don't create that variant. Fewer, meaningful variants are easier -to use and keep the component honest. - -## Quick checklist - -- [ ] Property names reused from the shared axis list (not one-offs) -- [ ] No `size` / `type` / `color` / `width` / `height` -- [ ] Content is content properties, not variants -- [ ] Quantity/repetition is instances you add or remove, not a variant (no `2/3 members`) -- [ ] Values lowercase, consistent spelling, t-shirt sizes -- [ ] No nonsensical variant combinations diff --git a/docs/design.md b/docs/design.md new file mode 100644 index 00000000..6f5d74a8 --- /dev/null +++ b/docs/design.md @@ -0,0 +1,135 @@ +# Designing propel components + +A short, shared guide for the two things every propel component has: the +**properties** it exposes (what knobs you can turn) and its **anatomy** (what parts +it is built from). Both are a contract shared between design and code. + +**Why it matters:** when properties use the same names and values every time, and +parts are named and assembled the same way every time, the design maps cleanly onto +the code, the library stays predictable for everyone, and dev/AI tooling can infer +how to use a component correctly. The goal is one small vocabulary, reused +everywhere — not a new word per component. + +> For how we _adopt_ propel into Plane and evolve these APIs (the explicit-first +> rollout, discovering constraints, and composing primitives with Base UI `render`), +> see [`integration.md`](./integration.md). + +# Naming properties + +## 1. Describe styling with a small, fixed set of axes + +Use the **same property name for the same idea on every component**, and only add +the axes a component actually needs. + +| Property | Answers | Example values | +| --------------- | ------------------------------------------------ | ---------------------------------------------------------------------- | +| **`variant`** | What _form / shape_ is it? | `primary` / `secondary` / `tertiary`, or `image` / `initials` / `icon` | +| **`tone`** | What does it _mean_ (its color / intent)? | `neutral` / `info` / `success` / `warning` / `danger` | +| **`magnitude`** | How _big_ is it? | `2xs … 3xl` (or `sm` / `md` / `lg`) | +| **`emphasis`** | How _prominent / filled_ is it? | `soft` / `outline` / `solid` | +| **`surface`** | What background does it _sit on_ (so it adapts)? | `background` / `fill` | +| **`density`** | How _compact_ is it? | `compact` / `default` | + +A button might use `variant · tone · magnitude`; an input might use +`surface · density · magnitude`. Use what fits — skip the rest. + +## 2. Avoid reserved words + +Never name a property **`size`, `type`, `color`, `width`, or `height`** — these are +built-in HTML/code attributes and collide. Use the axis names above instead +(`magnitude`, `variant`, `tone`). Behaviors that _are_ native — a button's submit +behavior, disabled state, a link's destination — are handled automatically in +code and don't need a property. + +## 3. Separate content from style + +Text, images, and icons are **content**, not style variants. Expose them as text +properties or instance-swaps (e.g. `label`, `name`, `icon`, `src`) — never bake +specific people, copy, or images into variants. + +**How many things are inside is also content, not a variant.** Don't make a +property for quantity — member count on an avatar group, the number of list items, +tabs, or steps. The designer adds or removes instances inside the component (like a +real list), and the component just lays out whatever it's given. A `2 members` / +`3 members` variant is the tell to avoid: it explodes the variant matrix and +doesn't match how the component is built in code. Reserve variants for _styling_. + +## 4. Format values consistently + +- **lowercase, no spaces or hyphens** — `initials`, not `Fallback - initials` +- **same spelling everywhere** — `md` means the same size on every component +- **t-shirt scale** for sizes (`2xs / xs / sm / md / lg / xl / 2xl / 3xl`); **plain + words** for tone (`neutral`, `danger`); **descriptive words** for variant + +## 5. Don't model impossible combinations + +If a combination doesn't exist (e.g. a `danger` tone only makes sense on a primary +button), simply don't create that variant. Fewer, meaningful variants are easier +to use and keep the component honest. + +# Component anatomy + +A component's **anatomy** is the set of named parts it is built from. We name parts +with the same discipline we name properties: one shared vocabulary, reused +everywhere, so a part means the same thing on every component. + +## 6. Start from Base UI's parts; extend only when needed + +Every propel primitive wraps a [Base UI](https://base-ui.com) component, so its +baseline anatomy is Base UI's: `Root`, `Trigger`, `Portal`, `Backdrop`, +`Positioner`, `Popup`, `Viewport`, `Title`, `Description`, `Close`, `Item`, +`Indicator`, and so on. Use those names as-is. + +Only add an **extension** — a part Base UI does not ship — when the layout genuinely +needs one. Name extensions in the same spirit as Base UI's own parts (a noun for the +region: `Header`, `Body`, `Actions`), not after a specific use or style. + +## 7. Group content, and let the parent own the spacing between groups + +Lay content out as a small number of meaningful **groups**, and put the space +_between_ groups on the parent (`gap`) — never as a margin on a child. The canonical +overlay groups: + +- **Intro** — the `Title` + `Description`, with a tight internal gap. +- **Actions** — the row of buttons. +- For larger surfaces, optionally a **Header** (title + a corner close) and a + **Body** (the scrollable content between header and actions). + +A title, a description, and an actions row are not three equal siblings — they are +two groups (intro and actions) the parent separates. **Reaching for a margin to push +two things apart is the tell that a layout boundary — a part — is missing.** + +## 8. Keep chrome and body layout in separate parts + +The **chrome** (the popup surface: border, background, shadow, transitions, +positioning) and the **body layout** (the vertical stack and the gap between groups) +are different concerns. Keep them in separate parts so the same body layout is reused +by both the hand-wired parts and the ready-made compositions. A fixed width is +_sizing_, not layout — it belongs to a sizing axis or the consumer, not the +body-layout part. + +## 9. A raw `
` / `className` / inline style is a missing part + +No component (or story, or consumer) should reach for a raw HTML element, a +`className`, or an inline `style` to build a component's structure. When a story +needs `
` to lay out a component's content, that +`
` is a missing anatomy part: name it and add it to the primitive, then compose +it everywhere. (Defining these parts from the styles currently used in Storybook is +the active work — see the roadmap in [`integration.md`](./integration.md).) + +> Some part names above (`Intro` / `Header` / `Body` / `Actions`) and a few +> ownership questions (where padding lives, whether a corner close is inline or +> corner-absolute) are still being finalized with design. The _principles_ in this +> section are settled; the exact names may shift. + +## Quick checklist + +- [ ] Property names reused from the shared axis list (not one-offs) +- [ ] No `size` / `type` / `color` / `width` / `height` +- [ ] Content is content properties, not variants +- [ ] Quantity/repetition is instances you add or remove, not a variant (no `2/3 members`) +- [ ] Values lowercase, consistent spelling, t-shirt sizes +- [ ] No nonsensical variant combinations +- [ ] Parts use Base UI's names; extensions are named for the region, not the use +- [ ] Space between groups is the parent's `gap`, never a child's margin +- [ ] No raw `
` / `className` / inline `style` standing in for a missing part diff --git a/docs/integration.md b/docs/integration.md new file mode 100644 index 00000000..4d45adbb --- /dev/null +++ b/docs/integration.md @@ -0,0 +1,160 @@ +# Integrating propel into Plane + +How we adopt `@plane/propel` into the existing Plane codebase and evolve its APIs as +we go. This is the dev-facing companion to [`design.md`](./design.md) (the shared +property + anatomy vocabulary). + +## The approach + +We are not trying to specify every component perfectly up front. We let the real +codebase tell us what's missing: + +1. **We don't pre-answer every design question.** Open questions about what should + be fixed vs. adjustable on each component don't block adoption — we move with + what we know. +2. **We use what we have to refactor plane-ee.** propel as it stands today is enough + to start replacing the existing UI. +3. **We find the shortcomings, and update propel.** Every place propel can't yet + express what plane-ee needs is a real, prioritized signal — not a hypothetical. +4. **We discover the design constraints by making the breaking changes.** The + refactor surfaces the constraints that matter; we capture them as we hit them. +5. **The end state: propel represents all of plane-ui.** Once it does, humans _and_ + AI can build Plane UI out of propel, because every pattern Plane needs lives in + the library. + +## The integration plan + +The throughline is **make everything explicit first, add defaults last.** Explicit +APIs turn "what does this component need?" into a question the type-checker answers +for us. + +1. **No default props at the beginning.** Every axis is required; nothing is implied. + (This matches propel's standing rule of no `cva` `defaultVariants` — see the + `propel-no-default-variants` convention.) +2. **Design for what we know now.** Model only the props and parts we can justify + today. Don't speculate. +3. **Expose required changes through the `ui` layer as we learn.** When the refactor + proves a component needs an axis it doesn't have, add it to the `ui` primitive + first, then let the `components` compositions follow. +4. **Use _required_ props to find every call site.** When a component grows a new + axis, make it **required** rather than defaulted, so TypeScript lights up every + place that must choose. Worked example: + + A component ships with no `variant`. The refactor reveals it actually renders two + ways, `"light"` and `"dark"`, and today everything implicitly gets `"light"`. + + ```ts + // before: one implicit look + type Props = { … }; + + // after: make the axis explicit AND required — do NOT default it to "light" + type Props = { variant: "light" | "dark"; … }; + ``` + + Now every existing call site is a type error. Each error is a decision we get to + make deliberately (`light` or `dark`) instead of inheriting a silent default — + so we fix exactly the right places and miss none. + +5. **Let AI set the defaults at the finish line.** Once we give the green light that + a component is "finished," its real-world usage tells us which value is the common + case. At that point we can have AI analyze the call sites and set sensible + defaults — turning required props back into optional ones where it's safe, with + evidence behind each default instead of a guess. + +The payoff of doing it in this order: defaults are derived from how the component is +_actually_ used across Plane, not assumed before we've used it once. + +## Composing primitives with Base UI `render` + +Every propel primitive wraps a [Base UI](https://base-ui.com) component, and Base +UI's `render` prop is how we compose them. A few rules we've learned — they apply +whenever you give a behavior part (a `Trigger`, a `Close`) the look of a styled +primitive (`Button`, `IconButton`). + +### `render` substitutes the rendered element and merges props + +`render` lets a Base UI part render _as_ another element/component, merging its +resolved props (event handlers, `aria-*`, `data-*`, `ref`) onto it. Crucially, +Base UI's `mergeProps` **concatenates `className`** (it does not replace it) and +passes the merged result down to the render target. + +### Put the styled primitive on the outside, the behavior part in `render` + +To make a trigger or close look like a `Button`, write the **styled primitive as the +outer element** and pass the dialog/menu part through its `render`: + +```tsx +// ✅ correct — Button's look wins, AlertDialogClose supplies the close behavior + + +// ✅ corner close — IconButton for the icon-only ✕ +}> + + +``` + +Not the reverse: + +```tsx +// ❌ wrong — renders a bare/ghost element, NOT a styled button +}> + Cancel + +``` + +### Why the order matters + +Many propel parts force their own `className` (e.g. `AlertDialogClose` applies +`alertDialogCloseVariants()`), and every propel primitive is written +`className={variants()} {...props}` — so **`props.className` always wins over the +element's own className.** Combined with Base UI concatenating and passing className +to the render target: + +- **Styled primitive outside:** its `buttonVariants` is the className that gets + spread last onto the inner part → the Button look wins, and the inner leaf (the + Base UI `Close`) still wires the behavior. ✅ +- **Styled primitive inside `render`:** the outer part's forced variant className is + passed _into_ the Button and clobbers `buttonVariants` → you get a bare ghost + element. ❌ (Verified: `Cancel`/`Delete` rendered as plain text this way.) + +So the mnemonic is: **the element whose look you want goes on the outside.** + +### What carries through + +With the styled primitive outside, the inner part contributes everything behavioral +— `onClick` to open/close, `aria-haspopup`/`aria-expanded`, focus management, +`data-*` state attributes — while children, `ref`, and the visible styling come from +the outer primitive. Use `Button` for text buttons and `IconButton` (icon as +`children`, required `aria-label`) for icon-only controls like the corner ✕. + +### Caveat + +This relies on propel's consistent `className={variants()} {...props}` ordering +(props win) in every primitive. Keep that convention — it's also why no propel +component takes a `className` prop (see `propel-no-className-prop-internal`). + +## What's left + +The remaining work is to formalize component anatomy from what the stories already +show, in three steps: + +1. **Define the `ui` component anatomy from the styles used in Storybook.** Where a + story lays out a component with raw `
`, that markup names + a missing part. We've already redrawn these layout boundaries in the overlay + stories (grouping `Title`+`Description` as an intro and the buttons as actions, + with the parent owning the gap) — that's the input for naming the parts. +2. **Name the anatomy extensions that differ from standard Base UI anatomy.** Most + parts are Base UI's (`Popup`, `Title`, `Close`, …); the extensions are the + layout regions Base UI doesn't ship (`Header` / `Body` / `Actions` / an intro + group). Decide these names (and the open ownership questions — where padding + lives, the corner-close treatment) with design. See + [`design.md` → Component anatomy](./design.md#component-anatomy). +3. **Define the new compositions for each `ui` primitive's API.** With the parts + named, give each primitive the parts as real surfaces, and update the + `components`-tier ready-mades to compose them — so consumers stop writing raw + layout and compose the anatomy instead. + +This is sequenced deliberately: we don't harden a part into an API until its +boundary is proven correct in a story first. diff --git a/packages/propel/.storybook/main.ts b/packages/propel/.storybook/main.ts index 5163d9d3..720e7eaa 100644 --- a/packages/propel/.storybook/main.ts +++ b/packages/propel/.storybook/main.ts @@ -3,8 +3,12 @@ import tailwindcss from "@tailwindcss/vite"; const config: StorybookConfig = { framework: "@storybook/react-vite", - // Stories live next to the components/hooks they document. - stories: ["../src/**/*.stories.@(ts|tsx)", "../src/**/*.mdx"], + // Stories live next to the components/hooks they document. (No `*.mdx` glob: + // propel has no standalone MDX docs pages — docs are autodocs-generated from the + // CSF stories below — and an empty glob makes addon-vitest warn "No story files + // found for the specified pattern". Add it back here if a hand-written .mdx page + // is ever introduced.) + stories: ["../src/**/*.stories.@(ts|tsx)"], features: { // RCM (Volar + TS Language Server) — faster, higher-quality component metadata. // Today it only powers addon-mcp (AI docs); Storybook plans to extend it to the diff --git a/packages/propel/.storybook/preview.tsx b/packages/propel/.storybook/preview.tsx index 92d1d769..c01f2593 100644 --- a/packages/propel/.storybook/preview.tsx +++ b/packages/propel/.storybook/preview.tsx @@ -1,45 +1,20 @@ import { DirectionProvider } from "@base-ui/react/direction-provider"; +import { withThemeByDataAttribute } from "@storybook/addon-themes"; import type { Decorator, Preview } from "@storybook/react-vite"; import { useLayoutEffect } from "react"; import "./preview.css"; -import { type Theme, THEMES } from "./themes"; +import { THEMES } from "./themes"; -// Per-test-instance theme, injected by `vite.config.ts` through each Vitest browser -// instance's `env` (`STORYBOOK_TEST_THEME`) so the a11y gate can run every story in -// every theme. An env var is untrusted input, so validate it against the known THEMES; -// anything unset, empty, or unrecognized falls back to `light` (e.g. `storybook dev`, -// where the toolbar global drives the theme instead). Without this, a bogus value would -// set `data-theme` to a theme with no tokens and the gate would run against nothing. -const envTheme = import.meta.env.STORYBOOK_TEST_THEME; -const TEST_THEME: Theme = THEMES.includes(envTheme as Theme) ? (envTheme as Theme) : "light"; - -// Apply the active theme by setting `data-theme` on . We do this with a custom -// decorator rather than `@storybook/addon-themes`' `withThemeByDataAttribute` because -// that addon's decorator does NOT run under `@storybook/addon-vitest` (the headless -// test render leaves `data-theme` unset), which silently left the a11y gate blind to -// every non-light theme. This decorator runs in both environments: it uses the toolbar -// `theme` global when set (manual), else the per-project `TEST_THEME` (CI), else light. -// NOTE: `||` (not `??`) because addon-vitest passes an EMPTY STRING for an unset global, -// not `undefined`. The themed `bg-canvas` on (preview.css) then gives axe the -// real backdrop to compute contrast against. -const withTheme: Decorator = (Story, context) => { - // Validate the toolbar global against the known theme list before trusting it: a - // URL `?globals=theme:...` (or a future global) could hand us an arbitrary string. - // Fall back to the per-project `TEST_THEME` (light in manual) when it isn't valid. - const candidate = context.globals.theme; - const theme: Theme = THEMES.includes(candidate as Theme) ? (candidate as Theme) : TEST_THEME; - useLayoutEffect(() => { - const el = document.documentElement; - const previous = el.dataset.theme; - el.dataset.theme = theme; - return () => { - if (previous == null) delete el.dataset.theme; - else el.dataset.theme = previous; - }; - }, [theme]); - return ; -}; +// Standard `@storybook/addon-themes` theme decorator: sets `data-theme` on (which drives +// propel's tokens via `@variant` in variables.css) from the `theme` global — the toolbar in +// `storybook dev`, or `defaultTheme` when there's no toolbar (the addon-vitest test run). It also +// registers the Theme toolbar dropdown, so no manual `globalTypes.theme` is needed. +const withTheme = withThemeByDataAttribute({ + attributeName: "data-theme", + defaultTheme: "light", + themes: Object.fromEntries(THEMES.map((theme) => [theme, theme])), +}); // Toolbar Direction switcher → drives both the DOM `dir` attribute (which powers // CSS logical properties + Tailwind `rtl:` utilities, and reaches portaled popups) @@ -68,23 +43,11 @@ const preview: Preview = { // opt-in). The Docs page aggregates all of a component's stories + the props // table into one scrollable view — the surface a designer reviews during an audit. tags: ["autodocs"], - // Toolbar theme switcher → sets `data-theme` on , which drives propel's - // multi-theme tokens (light / dark / *-contrast via `@variant` in variables.css). + // Theme (`withThemeByDataAttribute`, sets `data-theme`) + writing direction (LTR/RTL). decorators: [withTheme, withDirection], globalTypes: { - // Toolbar Theme dropdown (light / dark / *-contrast) for manual review. In tests - // the theme comes from the per-project `TEST_THEME` instead (no toolbar there). - theme: { - description: "Theme", - defaultValue: "light", - toolbar: { - title: "Theme", - icon: "paintbrush", - items: THEMES.map((value) => ({ value, title: value })), - dynamicTitle: true, - }, - }, - // Toolbar Direction dropdown (LTR / RTL) for reviewing layout mirroring. + // Toolbar Direction dropdown (LTR / RTL) for reviewing layout mirroring. (The Theme + // dropdown is registered by `withThemeByDataAttribute` above.) direction: { description: "Writing direction (LTR / RTL)", defaultValue: "ltr", @@ -120,10 +83,7 @@ const preview: Preview = { }, a11y: { - // All four themes enforce the axe gate (fail CI on violations): each theme runs in - // its own test project (see vite.config.ts). The #99 label-contrast gaps are fixed in - // variables.css, and primary buttons / the selected calendar day now use `text-inverse` - // for AA contrast on the brand surface. ('todo' = report-only, 'off' = skip.) + // The axe gate fails CI on violations ('todo' = report-only, 'off' = skip). test: "error", }, }, diff --git a/packages/propel/README.md b/packages/propel/README.md index 481f2a5e..e7760c5e 100644 --- a/packages/propel/README.md +++ b/packages/propel/README.md @@ -93,6 +93,17 @@ vp test # run tests folder; static wildcard `exports` (`./components/*`, `./hooks/*`) expose them as `@plane/propel/components/` / `hooks/` automatically — no `exports` edits and no barrel to maintain when you add a folder. +- **Component folders have a public boundary.** `index.tsx` only re-exports the + public API for that component folder. Public components and public child + components live in sibling kebab-case files (`button.tsx`, + `accordion-trigger.tsx`, `table-cell.tsx`). Do not use `Object.assign` or + namespace-style APIs such as `Foo.Bar`; export `FooBar` as its own component + instead. +- **Keep shared implementation private and accurately named.** Shared class + maps, CVA variants, and helpers should live in private sibling files such as + `*-styles.ts`, `*-shared.tsx`, or a real `*-context.tsx` when React context is + involved. Do not create public child files that only re-export from a + monolithic parent file. - `vp pack` needs at least one component or hook to build (a component library with zero entries has nothing to compile). - Compose classes with [`clsx`](https://github.com/lukeed/clsx) only — **do not diff --git a/packages/propel/package.json b/packages/propel/package.json index 39fc172c..5acc6aad 100644 --- a/packages/propel/package.json +++ b/packages/propel/package.json @@ -30,6 +30,14 @@ "**/*.css" ], "exports": { + "./base/*": { + "types": "./dist/base/*/index.d.ts", + "import": "./dist/base/*/index.js" + }, + "./ui/*": { + "types": "./dist/ui/*/index.d.ts", + "import": "./dist/ui/*/index.js" + }, "./components/*": { "types": "./dist/components/*/index.d.ts", "import": "./dist/components/*/index.js" @@ -62,25 +70,25 @@ "react-day-picker": "^10.0.1" }, "devDependencies": { - "@storybook/addon-a11y": "^10.4.5", + "@storybook/addon-a11y": "^10.4.6", "@storybook/addon-designs": "^11.1.3", - "@storybook/addon-docs": "^10.4.5", + "@storybook/addon-docs": "^10.4.6", "@storybook/addon-mcp": "^0.6.0", - "@storybook/addon-themes": "^10.4.5", - "@storybook/addon-vitest": "^10.4.5", - "@storybook/react-vite": "^10.4.5", + "@storybook/addon-themes": "^10.4.6", + "@storybook/addon-vitest": "^10.4.6", + "@storybook/react-vite": "^10.4.6", "@tailwindcss/vite": "^4.3.1", "@types/node": "^25.6.2", "@types/react": "^19.2.17", "@types/react-dom": "^19.2.3", "@typescript/native-preview": "7.0.0-dev.20260509.2", "@vitest/browser-playwright": "4.1.9", - "@vitest/coverage-v8": "4.1.8", + "@vitest/coverage-v8": "4.1.9", "playwright": "^1.61.0", "react": "^19.2.7", "react-dom": "^19.2.7", - "storybook": "^10.4.5", - "storybook-addon-pseudo-states": "^10.4.5", + "storybook": "^10.4.6", + "storybook-addon-pseudo-states": "^10.4.6", "tailwindcss": "^4.3.1", "typescript": "^6.0.3", "vite-plus": "catalog:", diff --git a/packages/propel/src/base/text-area/index.ts b/packages/propel/src/base/text-area/index.ts new file mode 100644 index 00000000..695e7b9b --- /dev/null +++ b/packages/propel/src/base/text-area/index.ts @@ -0,0 +1 @@ +export { BaseTextArea, type BaseTextAreaProps } from "./text-area"; diff --git a/packages/propel/src/base/text-area/text-area.tsx b/packages/propel/src/base/text-area/text-area.tsx new file mode 100644 index 00000000..fcbb0596 --- /dev/null +++ b/packages/propel/src/base/text-area/text-area.tsx @@ -0,0 +1,32 @@ +import { Field as BaseField } from "@base-ui/react/field"; +import * as React from "react"; + +type BaseFieldControlProps = BaseField.Control.Props; + +type NativeTextAreaProps = Omit< + React.ComponentProps<"textarea">, + keyof BaseFieldControlProps | "children" | "className" | "style" +>; + +export type BaseTextAreaProps = BaseFieldControlProps & + NativeTextAreaProps & { + /** The default value of the textarea. Use when uncontrolled. */ + defaultValue?: React.ComponentProps<"textarea">["defaultValue"]; + /** The value of the textarea. Use when controlled. */ + value?: React.ComponentProps<"textarea">["value"]; + }; + +/** + * A native textarea element that automatically works with Field. Renders a `