Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 4 additions & 8 deletions .agents/references/component-design.md
Original file line number Diff line number Diff line change
Expand Up @@ -431,7 +431,7 @@ Review whether component styling remains self-contained and consistent:

Text that a component renders takes its size, line height, weight, and letter spacing from a typography role, not from a raw size + weight combination. The roles are listed in the Typography section of `docs/concepts/styling-theming.md`.

- Render content text with `Text`: titles, descriptions, captions, hints, messages, error text, and key and value display. Pass a Base UI part, or an element that differs from the role's default tag, with `render`, and keep the part's `data-slot` on the render element.
- Render content text with `Text`: titles, descriptions, captions, hints, messages, error text, and key and value display. For a plain `h1` to `h6`, `p`, `span`, or `div`, use the element component (`Text.h3`, `Text.span`, ...), and put the element's `data-slot` on it. Write `variant` only when the role differs from the element's default (`heading-lg` for `h1`, `heading-md` for `h2`, `heading-sm` for `h3` to `h6`, `body-md` for `p` and `div`, `label-md` for `span`). Use `render` only for a Base UI part or another component, or for other elements such as `dt` or `code`, and keep the part's `data-slot` on the render element.
- Use the role utility (`astw:text-heading-md`, `astw:text-label-md`, ...) for text that is part of a control or sits inside one (buttons, inputs, menu and list items, badges, toolbars, table cells), in cva strings, and on containers that set the size for their children.
- Do not write new raw combinations such as `astw:text-lg astw:font-semibold` or `astw:text-sm astw:font-medium`. `tests/typography-usage.test.ts` fails on a raw size.
- Combine a role only with the four modifiers: `astw:leading-none` (single-line text), `astw:tabular-nums`, `astw:font-semibold` (emphasis on inline text), and `astw:uppercase`. Do not add other `leading-*`, `tracking-*`, or `text-[Npx]` values. 12px is the minimum size.
Expand All @@ -441,15 +441,11 @@ Text that a component renders takes its size, line height, weight, and letter sp

```tsx
// Good
<Text
variant="heading-md"
className="astw:leading-none"
// eslint-disable-next-line jsx-a11y/heading-has-content
render={<h3 data-slot="card-title" />}
>
<Text.h3 variant="heading-md" className="astw:leading-none" data-slot="card-title">
{title}
</Text>
</Text.h3>
<Text variant="body-sm" className="astw:text-muted-foreground">{updatedAt}</Text>
<Text variant="heading-md" render={<BaseDialog.Title data-slot="dialog-title" {...props} />} />
<button className={cn("astw:rounded-md astw:px-3 astw:text-label-md", className)}>{label}</button>
```

Expand Down
5 changes: 5 additions & 0 deletions .changeset/skill-text-element-components.md

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I felt changes in this PR are all internal changes, and visual changes are already shipped earlier, right?

If so we don't have to have changeset. Consumers won't do anything reading this. What do you think?

Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@tailor-platform/app-shell": patch
---

AppShell components and the document detail page example in the `app-shell-patterns` skill use the `Text` element components, such as `Text.h3` and `Text.span`, where they used `render` only to choose a plain HTML element. The rendered elements and classes do not change. `render` stays for Base UI parts, other components, and other elements.
6 changes: 3 additions & 3 deletions decisions/typography-roles.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,8 +104,8 @@ Limit: this does not let a role replace a size that core sets on its own element
> Status: **Decided.**

- **Text or the role utility.** Content text renders with `Text`: titles, descriptions, captions, hints, messages, error text, and key and value display. Text that is part of a control, or sits inside one, keeps the role utility: buttons, inputs, menu, listbox, palette and tab items and their group labels, badges, counters, toolbars, pagination, and table cells. cva strings, shared class strings, and containers that set the size for their children also keep the utility. Radix Themes, Primer, and the Atlassian Design System split their components the same way. Polaris uses its `Text` almost everywhere; Mantine and Chakra do not use theirs inside components.
- **`render` and `data-slot`.** A Base UI part, or an element whose tag differs from the role's default, is passed with `render`, for example `<Text variant="heading-md" render={<BaseDialog.Title data-slot="dialog-title" {...props} />} />`. The `data-slot` stays on the render element, because its props win over the `data-slot="text"` that `Text` sets.
- **Lint.** Where `render` gets an empty `h1`, `h3`, `h4`, `a`, or `label`, the matching `jsx-a11y` rule is disabled on that line, as the `Text` documentation shows for apps.
- **`render` and `data-slot`.** A Base UI part, or an element whose tag differs from the role's default, is passed with `render` (for a plain `h1` to `h6`, `p`, `span`, or `div`, see the fourth change), for example `<Text variant="heading-md" render={<BaseDialog.Title data-slot="dialog-title" {...props} />} />`. The `data-slot` stays on the render element, because its props win over the `data-slot="text"` that `Text` sets.
- **Lint.** Where `render` gets an empty `h1`, `h3`, `h4`, `a`, or `label`, the matching `jsx-a11y` rule is disabled on that line, as the `Text` documentation shows for apps. _Updated by the fourth change: headings now use `Text.h1` to `Text.h6`, so only the `a` and `label` disable comments remain._
- **`Text` passes children only when given.** A render part that supplies its own content keeps it. Base UI's `Field.Error` shows the validation message this way.
- **Value changes.** Text below 12px moves to 12px. Line heights outside the allowed modifiers move to the role's line height, or to a relaxed role for chat messages. The uppercase section labels, the ActivityCard day label, and the menu group label use `label-sm` at weight 500. `code-sm` adds 2px of line height. In key and value pairs, the key is `label-md` with a muted color and the value is `body-md`, the same as numbers in DataTable. An `h1` to `h6` element takes a heading role (chat empty state title, default sidebar app title). `Alert.Title` drops `tracking-tight`. A selected item (sidebar, chat history, DataTable filter panel) uses `font-semibold` with its background, as Primer and Polaris do.
- **Element types do not change.** Under the cream and bloom palettes, headings render as before. The open question about their `h1` to `h6` rule stays open.
Expand All @@ -123,4 +123,4 @@ Limit: this does not let a role replace a size that core sets on its own element
- **No exported props type.** `React.ComponentProps<typeof Text.h2>` gives the props type. A named export would be another public name that cannot be removed later without a breaking change, while adding one later is not breaking.
- **Implementation.** The element components render the element directly, without Base UI's `useRender`, because they take no `render` prop. They set `data-slot="text"` and the same classes as `Text`.
- **Documentation.** The heading examples in the quickstart, the styling guide, and the `app-shell-patterns` skill use the element components.
- **Follow-up change** (not in this PR): AppShell's own components keep `render` for now; changing their headings to element components is a separate change.
- **AppShell's own components.** A follow-up change replaced the 43 places where a component passed a plain `h1` to `h6`, `p`, `span`, or `div` to `render` with the element components, and removed the 13 `heading-has-content` disable comments. Each place keeps its role: it writes `variant` only where the role differs from the element's default (36 places), so the rendered element and classes do not change. A `data-slot` or other attribute on the render element moves to the element component, where it still wins over `data-slot="text"`. `render` stays for Base UI parts and other components (`Dialog.Title`, `Field.Error`, `Link`) and for other elements (`dt`, `dd`, `code`, `pre`, `caption`, `address`, `a`). The document detail page example changed in the same way.
6 changes: 3 additions & 3 deletions docs-manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -650,9 +650,9 @@
"symbols": [],
"hashes": {
"typeSurface": null,
"outline": "a87752a8b4c3e0b7",
"outline": "026a063f3026c87a",
"snapshot": null,
"outputMd": "5a6612349cb50ef1",
"outputMd": "930db027a28bbe24",
"examples": null
}
},
Expand Down Expand Up @@ -1854,7 +1854,7 @@
"packages/core/skills/app-shell-patterns/references/patterns/interaction-multi-select.md": "aa60091d0e430762",
"packages/core/skills/app-shell-patterns/references/patterns/interaction-toast.md": "f68bdab855730f48",
"packages/core/skills/app-shell-patterns/references/patterns/list-dense-scan.md": "59c93617dc135033",
"packages/core/skills/app-shell-patterns/references/pages/document-detail.md": "5a6612349cb50ef1",
"packages/core/skills/app-shell-patterns/references/pages/document-detail.md": "930db027a28bbe24",
"packages/core/skills/app-shell-patterns/references/migrations.md": "e0286979718e2769",
"packages/core/skills/app-shell-patterns/SKILL.md": "1aa14970eee9bce0"
}
Expand Down
20 changes: 8 additions & 12 deletions docs-src/pages/document-detail.docs.outline.md
Original file line number Diff line number Diff line change
Expand Up @@ -645,9 +645,9 @@ const formatDay = (value: string) =>
* or the two dates do not match every other value on the card.
*/
const Value = ({ children }: { children: React.ReactNode }) => (
<Text variant="body-md" className="text-foreground" render={<span />}>
<Text.span variant="body-md" className="text-foreground">
{children}
</Text>
</Text.span>
);

/** House link: primary colour at rest, underline only on hover. */
Expand Down Expand Up @@ -711,9 +711,9 @@ export default function PurchaseOrderDetailPage({
{order.holds.map((hold) => (
<div key={hold.id} className="flex items-start justify-between gap-4">
<div>
<Text variant="label-md" className="text-foreground" render={<p />}>
<Text.p variant="label-md" className="text-foreground">
{hold.reason}
</Text>
</Text.p>
<Text variant="body-sm" className="text-muted-foreground">
Placed by {hold.placedBy} on {formatDay(hold.placedAt)}
</Text>
Expand Down Expand Up @@ -877,13 +877,9 @@ export default function PurchaseOrderDetailPage({
<Table.Cell align="right" className="tabular-nums">
{line.received}
{line.received !== line.qty && (
<Text
variant="body-sm"
className="ml-1 text-muted-foreground"
render={<span />}
>
<Text.span variant="body-sm" className="ml-1 text-muted-foreground">
({line.qty - line.received})
</Text>
</Text.span>
)}
</Table.Cell>
<Table.Cell align="right" className="tabular-nums">
Expand Down Expand Up @@ -965,9 +961,9 @@ export default function PurchaseOrderDetailPage({
{file.name}
<ExternalLink className="size-3 shrink-0" />
</a>
<Text variant="body-sm" className="shrink-0 text-muted-foreground" render={<span />}>
<Text.span variant="body-sm" className="shrink-0 text-muted-foreground">
{file.sizeLabel} · {formatDay(file.uploadedAt)}
</Text>
</Text.span>
</div>
))}
</Card.Content>
Expand Down
20 changes: 8 additions & 12 deletions docs/pages/document-detail.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 4 additions & 10 deletions packages/core/src/components/activity-card/ActivityCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -103,14 +103,9 @@ function Root<T extends ActivityCardBaseItem>({
>
{title != null && (
<div className="astw:flex astw:items-center astw:px-6 astw:pt-5 astw:pb-2">
<Text
variant="heading-md"
className="astw:text-card-foreground"
// eslint-disable-next-line jsx-a11y/heading-has-content
render={<h3 />}
>
<Text.h3 variant="heading-md" className="astw:text-card-foreground">
{title}
</Text>
</Text.h3>
</div>
)}
{children}
Expand All @@ -132,13 +127,12 @@ function renderSection<T extends ActivityCardBaseItem>(
return (
<div key={dayLabel ?? "single"} className="astw:flex astw:flex-col astw:gap-5">
{dayLabel != null && (
<Text
<Text.p
variant="label-sm"
className="astw:leading-none astw:opacity-60 astw:text-foreground"
render={<p />}
>
{dayLabel}
</Text>
</Text.p>
)}
<div className="astw:relative astw:flex astw:flex-col astw:gap-5">
{items.map((item, index) => (
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/components/activity-card/standalone.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,11 @@ function ActivityCardStandalone({
) : undefined
}
>
<Text variant="label-md" className="astw:text-foreground" render={<p />}>
<Text.p variant="label-md" className="astw:text-foreground">
{actorName ? <span>{actorName}</span> : null}
{actorName ? " " : null}
<span className="astw:text-muted-foreground">{item.description}</span>
</Text>
</Text.p>
<Text variant="body-sm" className="astw:leading-none astw:text-muted-foreground">
{formatTimestamp(item.timestamp)}
</Text>
Expand Down
10 changes: 1 addition & 9 deletions packages/core/src/components/ai-chat/conversation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -178,15 +178,7 @@ function ConversationEmptyState({
{icon}
{title == null && description == null ? null : (
<div className="astw:space-y-1">
{title ? (
<Text
variant="heading-sm"
// eslint-disable-next-line jsx-a11y/heading-has-content
render={<h3 />}
>
{title}
</Text>
) : null}
{title ? <Text.h3>{title}</Text.h3> : null}
{description ? (
<Text variant="body-md" className="astw:text-muted-foreground">
{description}
Expand Down
5 changes: 2 additions & 3 deletions packages/core/src/components/ai-chat/header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,18 +28,17 @@ function Header({ title, icon, actions, className }: AIChatHeaderProps) {
className,
)}
>
<Text
<Text.div
variant="heading-sm"
className="astw:flex astw:min-w-0 astw:items-center astw:gap-1.5 astw:text-foreground"
render={<div />}
>
{icon === undefined ? (
<Sparkles className="astw:size-4 astw:shrink-0 astw:text-primary" aria-hidden />
) : (
icon
)}
{title == null ? null : <span className="astw:truncate">{title}</span>}
</Text>
</Text.div>
{actions == null ? null : (
<div className="astw:flex astw:shrink-0 astw:items-center astw:gap-0.5">{actions}</div>
)}
Expand Down
12 changes: 4 additions & 8 deletions packages/core/src/components/ai-chat/tool.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -133,14 +133,12 @@ function ToolInput({ input, className, ...props }: ToolInputProps) {

return (
<div className={cn("astw:space-y-1", className)} {...props}>
<Text
<Text.h4
variant="label-sm"
className="astw:uppercase astw:tracking-wide astw:text-muted-foreground"
// eslint-disable-next-line jsx-a11y/heading-has-content
render={<h4 />}
>
{t("aiChatToolParameters")}
</Text>
</Text.h4>
<JsonBlock value={input} />
</div>
);
Expand All @@ -157,17 +155,15 @@ function ToolOutput({ output, errorText, className, ...props }: ToolOutputProps)
if (output == null && !errorText) return null;
return (
<div className={cn("astw:space-y-1", className)} {...props}>
<Text
<Text.h4
variant="label-sm"
className={cn(
"astw:uppercase astw:tracking-wide",
errorText ? "astw:text-danger-text" : "astw:text-muted-foreground",
)}
// eslint-disable-next-line jsx-a11y/heading-has-content
render={<h4 />}
>
{errorText ? t("aiChatToolError") : t("aiChatToolResult")}
</Text>
</Text.h4>
<JsonBlock value={errorText ?? output} />
</div>
);
Expand Down
5 changes: 3 additions & 2 deletions packages/core/src/components/alert/alert.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -108,10 +108,11 @@ Root.displayName = "Alert.Root";

function Title({ className, ...props }: React.ComponentProps<"div">) {
return (
<Text
<Text.div
variant="label-md"
className={cn("astw:col-start-2 astw:line-clamp-1 astw:min-h-4", className)}
render={<div data-slot="alert-title" {...props} />}
data-slot="alert-title"
{...props}
/>
);
}
Expand Down
9 changes: 2 additions & 7 deletions packages/core/src/components/appshell/appshell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -380,14 +380,9 @@ export const AppShellInternal = (props: AppShellInternalProps) => {
return (
<div className="astw:flex astw:min-h-screen astw:items-center astw:justify-center astw:bg-background astw:p-4">
<div className="astw:max-w-md astw:rounded-lg astw:border astw:border-danger-border astw:bg-danger-surface astw:p-6 astw:text-center">
<Text
variant="heading-md"
className="astw:mb-2 astw:text-danger-text"
// eslint-disable-next-line jsx-a11y/heading-has-content
render={<h1 />}
>
<Text.h1 variant="heading-md" className="astw:mb-2 astw:text-danger-text">
Configuration Error
</Text>
</Text.h1>
<Text variant="body-md" className="astw:text-muted-foreground">
{errorMessage}
</Text>
Expand Down
15 changes: 5 additions & 10 deletions packages/core/src/components/card/card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,23 +33,18 @@ function Header({ className, title, description, children, ...props }: HeaderPro
{...props}
>
{title && (
<Text
<Text.h3
variant="heading-md"
className="astw:leading-none astw:text-card-foreground"
// eslint-disable-next-line jsx-a11y/heading-has-content
render={<h3 data-slot="card-title" />}
data-slot="card-title"
>
{title}
</Text>
</Text.h3>
)}
{description && (
<Text
variant="body-md"
className="astw:text-muted-foreground"
render={<div data-slot="card-description" />}
>
<Text.div className="astw:text-muted-foreground" data-slot="card-description">
{description}
</Text>
</Text.div>
)}
{children}
</div>
Expand Down
Loading
Loading