diff --git a/.agents/references/component-design.md b/.agents/references/component-design.md index d878fd90..48a09853 100644 --- a/.agents/references/component-design.md +++ b/.agents/references/component-design.md @@ -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. @@ -441,15 +441,11 @@ Text that a component renders takes its size, line height, weight, and letter sp ```tsx // Good -} -> + {title} - + {updatedAt} +} /> ``` diff --git a/.changeset/skill-text-element-components.md b/.changeset/skill-text-element-components.md new file mode 100644 index 00000000..7aa80429 --- /dev/null +++ b/.changeset/skill-text-element-components.md @@ -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. diff --git a/decisions/typography-roles.md b/decisions/typography-roles.md index 5c09c414..48193676 100644 --- a/decisions/typography-roles.md +++ b/decisions/typography-roles.md @@ -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 `} />`. 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 `} />`. 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. @@ -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` 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. diff --git a/docs-manifest.json b/docs-manifest.json index de1e4925..8767a1dd 100644 --- a/docs-manifest.json +++ b/docs-manifest.json @@ -650,9 +650,9 @@ "symbols": [], "hashes": { "typeSurface": null, - "outline": "a87752a8b4c3e0b7", + "outline": "026a063f3026c87a", "snapshot": null, - "outputMd": "5a6612349cb50ef1", + "outputMd": "930db027a28bbe24", "examples": null } }, @@ -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" } diff --git a/docs-src/pages/document-detail.docs.outline.md b/docs-src/pages/document-detail.docs.outline.md index fc0e36fb..5a45797f 100644 --- a/docs-src/pages/document-detail.docs.outline.md +++ b/docs-src/pages/document-detail.docs.outline.md @@ -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 }) => ( - }> + {children} - + ); /** House link: primary colour at rest, underline only on hover. */ @@ -711,9 +711,9 @@ export default function PurchaseOrderDetailPage({ {order.holds.map((hold) => (
- }> + {hold.reason} - + Placed by {hold.placedBy} on {formatDay(hold.placedAt)} @@ -877,13 +877,9 @@ export default function PurchaseOrderDetailPage({ {line.received} {line.received !== line.qty && ( - } - > + ({line.qty - line.received}) - + )} @@ -965,9 +961,9 @@ export default function PurchaseOrderDetailPage({ {file.name} - }> + {file.sizeLabel} · {formatDay(file.uploadedAt)} - +
))} diff --git a/docs/pages/document-detail.md b/docs/pages/document-detail.md index 8ca76cbf..8ddfe115 100644 --- a/docs/pages/document-detail.md +++ b/docs/pages/document-detail.md @@ -619,9 +619,9 @@ const formatDay = (value: string) => * or the two dates do not match every other value on the card. */ const Value = ({ children }: { children: React.ReactNode }) => ( - }> + {children} - + ); /** House link: primary colour at rest, underline only on hover. */ @@ -685,9 +685,9 @@ export default function PurchaseOrderDetailPage({ {order.holds.map((hold) => (
- }> + {hold.reason} - + Placed by {hold.placedBy} on {formatDay(hold.placedAt)} @@ -851,13 +851,9 @@ export default function PurchaseOrderDetailPage({ {line.received} {line.received !== line.qty && ( - } - > + ({line.qty - line.received}) - + )} @@ -939,9 +935,9 @@ export default function PurchaseOrderDetailPage({ {file.name} - }> + {file.sizeLabel} · {formatDay(file.uploadedAt)} - +
))} diff --git a/packages/core/src/components/activity-card/ActivityCard.tsx b/packages/core/src/components/activity-card/ActivityCard.tsx index a914206e..546ce5ba 100644 --- a/packages/core/src/components/activity-card/ActivityCard.tsx +++ b/packages/core/src/components/activity-card/ActivityCard.tsx @@ -103,14 +103,9 @@ function Root({ > {title != null && (
- } - > + {title} - +
)} {children} @@ -132,13 +127,12 @@ function renderSection( return (
{dayLabel != null && ( - } > {dayLabel} - + )}
{items.map((item, index) => ( diff --git a/packages/core/src/components/activity-card/standalone.tsx b/packages/core/src/components/activity-card/standalone.tsx index 47633078..6f57b1ca 100644 --- a/packages/core/src/components/activity-card/standalone.tsx +++ b/packages/core/src/components/activity-card/standalone.tsx @@ -45,11 +45,11 @@ function ActivityCardStandalone({ ) : undefined } > - }> + {actorName ? {actorName} : null} {actorName ? " " : null} {item.description} - + {formatTimestamp(item.timestamp)} diff --git a/packages/core/src/components/ai-chat/conversation.tsx b/packages/core/src/components/ai-chat/conversation.tsx index a01c3003..0d174b87 100644 --- a/packages/core/src/components/ai-chat/conversation.tsx +++ b/packages/core/src/components/ai-chat/conversation.tsx @@ -178,15 +178,7 @@ function ConversationEmptyState({ {icon} {title == null && description == null ? null : (
- {title ? ( - } - > - {title} - - ) : null} + {title ? {title} : null} {description ? ( {description} diff --git a/packages/core/src/components/ai-chat/header.tsx b/packages/core/src/components/ai-chat/header.tsx index c9eef013..b6a7c66f 100644 --- a/packages/core/src/components/ai-chat/header.tsx +++ b/packages/core/src/components/ai-chat/header.tsx @@ -28,10 +28,9 @@ function Header({ title, icon, actions, className }: AIChatHeaderProps) { className, )} > - } > {icon === undefined ? ( @@ -39,7 +38,7 @@ function Header({ title, icon, actions, className }: AIChatHeaderProps) { icon )} {title == null ? null : {title}} - + {actions == null ? null : (
{actions}
)} diff --git a/packages/core/src/components/ai-chat/tool.tsx b/packages/core/src/components/ai-chat/tool.tsx index f0320544..608b3382 100644 --- a/packages/core/src/components/ai-chat/tool.tsx +++ b/packages/core/src/components/ai-chat/tool.tsx @@ -133,14 +133,12 @@ function ToolInput({ input, className, ...props }: ToolInputProps) { return (
- } > {t("aiChatToolParameters")} - +
); @@ -157,17 +155,15 @@ function ToolOutput({ output, errorText, className, ...props }: ToolOutputProps) if (output == null && !errorText) return null; return (
- } > {errorText ? t("aiChatToolError") : t("aiChatToolResult")} - +
); diff --git a/packages/core/src/components/alert/alert.tsx b/packages/core/src/components/alert/alert.tsx index ba6d6330..78918b1c 100644 --- a/packages/core/src/components/alert/alert.tsx +++ b/packages/core/src/components/alert/alert.tsx @@ -108,10 +108,11 @@ Root.displayName = "Alert.Root"; function Title({ className, ...props }: React.ComponentProps<"div">) { return ( - } + data-slot="alert-title" + {...props} /> ); } diff --git a/packages/core/src/components/appshell/appshell.tsx b/packages/core/src/components/appshell/appshell.tsx index 9c1672fe..612fc2af 100644 --- a/packages/core/src/components/appshell/appshell.tsx +++ b/packages/core/src/components/appshell/appshell.tsx @@ -380,14 +380,9 @@ export const AppShellInternal = (props: AppShellInternalProps) => { return (
- } - > + Configuration Error - + {errorMessage} diff --git a/packages/core/src/components/card/card.tsx b/packages/core/src/components/card/card.tsx index 5e909b2c..bc41a533 100644 --- a/packages/core/src/components/card/card.tsx +++ b/packages/core/src/components/card/card.tsx @@ -33,23 +33,18 @@ function Header({ className, title, description, children, ...props }: HeaderPro {...props} > {title && ( - } + data-slot="card-title" > {title} - + )} {description && ( - } - > + {description} - + )} {children}
diff --git a/packages/core/src/components/csv-importer/CsvImporter.tsx b/packages/core/src/components/csv-importer/CsvImporter.tsx index ac2fc3f0..d4d99c64 100644 --- a/packages/core/src/components/csv-importer/CsvImporter.tsx +++ b/packages/core/src/components/csv-importer/CsvImporter.tsx @@ -145,9 +145,7 @@ function UploadStep({ >
- }> - {t("uploadButton")} - + {t("uploadButton")} CSV (max {maxSizeMB}MB) @@ -161,14 +159,10 @@ function UploadStep({ /> {error && ( - } - > + {error} - + )}
diff --git a/packages/core/src/components/data-table/toolbar.tsx b/packages/core/src/components/data-table/toolbar.tsx index 5fe14c77..4544877d 100644 --- a/packages/core/src/components/data-table/toolbar.tsx +++ b/packages/core/src/components/data-table/toolbar.tsx @@ -621,9 +621,9 @@ function PanelDateTimeInput({ />
- }> + {t("chooseTime")} - + - } - > + {label} - +
{children}
); @@ -207,14 +203,9 @@ export function DescriptionCard>( {/* Header */}
- } - > + {title} - + {headerAction &&
{headerAction}
}
diff --git a/packages/core/src/components/description-card/field-renderers.tsx b/packages/core/src/components/description-card/field-renderers.tsx index 1a00b653..22f413f0 100644 --- a/packages/core/src/components/description-card/field-renderers.tsx +++ b/packages/core/src/components/description-card/field-renderers.tsx @@ -304,13 +304,13 @@ function TextFieldRenderer({ field }: { field: ResolvedField }) { // Always render the same structure, conditionally wrap with tooltip const content = (
- } + ref={textRef} + style={lineClampStyle} > {value} - + {showCopy && }
); @@ -340,9 +340,9 @@ function BadgeFieldRenderer({ field }: { field: ResolvedField }) { if (values.every((v) => isEmpty(v))) { return ( - }> + {EMPTY_DASH} - + ); } @@ -375,9 +375,9 @@ function BadgeFieldRenderer({ field }: { field: ResolvedField }) { function MoneyFieldRenderer({ field }: { field: ResolvedField }) { if (isEmpty(field.value)) { return ( - }> + {EMPTY_DASH} - + ); } @@ -386,9 +386,9 @@ function MoneyFieldRenderer({ field }: { field: ResolvedField }) { const formatted = formatMoney(field.value, currency); return ( - }> + {formatted} - + ); } @@ -400,9 +400,9 @@ function DateFieldRenderer({ field }: { field: ResolvedField }) { if (isEmpty(field.value)) { return ( - }> + {EMPTY_DASH} - + ); } @@ -410,9 +410,9 @@ function DateFieldRenderer({ field }: { field: ResolvedField }) { const formatted = formatDate(field.value, format, t); return ( - }> + {formatted} - + ); } @@ -422,9 +422,9 @@ function DateFieldRenderer({ field }: { field: ResolvedField }) { function LinkFieldRenderer({ field }: { field: ResolvedField }) { if (isEmpty(field.value)) { return ( - }> + {EMPTY_DASH} - + ); } @@ -435,9 +435,9 @@ function LinkFieldRenderer({ field }: { field: ResolvedField }) { if (!href) { return ( - }> + {value} - + ); } @@ -472,9 +472,9 @@ function LinkFieldRenderer({ field }: { field: ResolvedField }) { function AddressFieldRenderer({ field }: { field: ResolvedField }) { if (isEmpty(field.value)) { return ( - }> + {EMPTY_DASH} - + ); } @@ -504,9 +504,9 @@ function AddressFieldRenderer({ field }: { field: ResolvedField }) { function ReferenceFieldRenderer({ field }: { field: ResolvedField }) { if (isEmpty(field.value)) { return ( - }> + {EMPTY_DASH} - + ); } @@ -517,9 +517,9 @@ function ReferenceFieldRenderer({ field }: { field: ResolvedField }) { if (!id || !urlPattern) { return ( - }> + {value} - + ); } @@ -565,9 +565,9 @@ export function renderField(field: ResolvedField): React.ReactNode { return ; default: return ( - }> + {EMPTY_DASH} - + ); } } diff --git a/packages/core/src/components/document-progress-card/DocumentProgressCard.tsx b/packages/core/src/components/document-progress-card/DocumentProgressCard.tsx index 4e76befa..c5f3234b 100644 --- a/packages/core/src/components/document-progress-card/DocumentProgressCard.tsx +++ b/packages/core/src/components/document-progress-card/DocumentProgressCard.tsx @@ -97,10 +97,9 @@ export function DocumentProgressCard({ > {/* Header: optional title + optional completion percentage */} {(title != null || hasPercent) && ( - } > {title} {hasPercent && ( @@ -108,7 +107,7 @@ export function DocumentProgressCard({ {clampedPercent}% )} - + )} {/* Bar + legend */} @@ -174,17 +173,11 @@ function LegendRow({ aria-hidden className={cn("astw:h-3.5 astw:w-0.5 astw:shrink-0 astw:rounded-md", colorFill[color])} /> - } - > - {label} - + {label}
- }> + {value} - +
); } diff --git a/packages/core/src/components/internals/app-info/app-info.tsx b/packages/core/src/components/internals/app-info/app-info.tsx index d5e8ea0d..9f2b1781 100644 --- a/packages/core/src/components/internals/app-info/app-info.tsx +++ b/packages/core/src/components/internals/app-info/app-info.tsx @@ -65,14 +65,9 @@ export const AppInfoPage = () => {
- } - > + {t("appInfoTitle")} - + diff --git a/packages/core/src/components/internals/default-error-boundary/default-error-boundary.tsx b/packages/core/src/components/internals/default-error-boundary/default-error-boundary.tsx index be6f7cb2..3b88cd46 100644 --- a/packages/core/src/components/internals/default-error-boundary/default-error-boundary.tsx +++ b/packages/core/src/components/internals/default-error-boundary/default-error-boundary.tsx @@ -20,13 +20,7 @@ export const DefaultErrorBoundary = () => {
- } - > - {t("error404Title")} - + {t("error404Title")} {t("error404Body")} @@ -52,13 +46,7 @@ export const DefaultErrorBoundary = () => {
- } - > - {t("errorTitle")} - + {t("errorTitle")} {message} diff --git a/packages/core/src/components/layout/Layout.tsx b/packages/core/src/components/layout/Layout.tsx index 56012ac2..3937315c 100644 --- a/packages/core/src/components/layout/Layout.tsx +++ b/packages/core/src/components/layout/Layout.tsx @@ -117,15 +117,7 @@ export function Header({ title, actions, children }: LayoutHeaderProps) {
{hasTitleRow && (
- {title && ( - } - > - {title} - - )} + {title && {title}} {actions != null && actions.length > 0 && (
{actions.map((action, i) => ( @@ -270,15 +262,7 @@ function LayoutRoot({ data-layout-header="" className="astw:w-full astw:flex astw:justify-between astw:items-center astw:pt-4" > - {title && ( - } - > - {title} - - )} + {title && {title}} {actions != null && actions.length > 0 && (
{actions.map((action, i) => ( diff --git a/packages/core/src/components/metric-card/MetricCard.tsx b/packages/core/src/components/metric-card/MetricCard.tsx index f2e32e30..df8ebf03 100644 --- a/packages/core/src/components/metric-card/MetricCard.tsx +++ b/packages/core/src/components/metric-card/MetricCard.tsx @@ -57,9 +57,9 @@ export function MetricCard({ title, value, trend, description, icon, className }
{/* Main value */} - }> + {value} - + {/* Optional meta row: trend + description */} {hasMeta && ( @@ -73,13 +73,9 @@ export function MetricCard({ title, value, trend, description, icon, className } )} {description != null && description !== "" && ( - } - > + {description} - + )}
)} diff --git a/packages/core/src/components/sidebar/default-sidebar.tsx b/packages/core/src/components/sidebar/default-sidebar.tsx index 8ff771ab..6bb22818 100644 --- a/packages/core/src/components/sidebar/default-sidebar.tsx +++ b/packages/core/src/components/sidebar/default-sidebar.tsx @@ -161,14 +161,12 @@ export const DefaultSidebar = (props: DefaultSidebarProps) => { const DefaultHeader = ( {icon} - } > {title} - + ); const DefaultFooter = null; diff --git a/packages/core/src/components/sidebar/global-header.tsx b/packages/core/src/components/sidebar/global-header.tsx index 170e1f38..7aac94b5 100644 --- a/packages/core/src/components/sidebar/global-header.tsx +++ b/packages/core/src/components/sidebar/global-header.tsx @@ -49,9 +49,9 @@ export const GlobalHeader = ({ actions }: GlobalHeaderProps) => {
{icon} - }> + {title} - +