mirror of
https://github.com/onyx-dot-app/onyx.git
synced 2026-03-30 03:52:42 +00:00
Compare commits
1 Commits
cli/v0.1.2
...
opal/linei
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9c3e9a9eea |
84
web/lib/opal/src/components/buttons/LineItemButton/README.md
Normal file
84
web/lib/opal/src/components/buttons/LineItemButton/README.md
Normal file
@@ -0,0 +1,84 @@
|
||||
# LineItemButton
|
||||
|
||||
**Import:** `import { LineItemButton, type LineItemButtonProps } from "@opal/components";`
|
||||
|
||||
A composite component that wraps `Interactive.Base(select) > Interactive.Container > ContentAction` into a single API. Use it for selectable list rows such as model pickers, menu items, or any row that acts like a button.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
Interactive.Base (variant="select") <- prominence, selected, disabled, onClick, href, ref
|
||||
└─ Interactive.Container <- type, width, size, rounding (derived from size)
|
||||
└─ ContentAction <- withInteractive, paddingVariant="fit", widthVariant="full"
|
||||
├─ Content <- icon, title, description, sizePreset, variant, ...
|
||||
└─ rightChildren
|
||||
```
|
||||
|
||||
`paddingVariant` is hardcoded to `"fit"` (Container owns the padding) and `widthVariant` is hardcoded to `"full"`. These are not exposed as props.
|
||||
|
||||
## Props
|
||||
|
||||
### Interactive surface (always `variant="select"`)
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `prominence` | `"light" \| "heavy"` | `"light"` | Interactive select prominence |
|
||||
| `selected` | `boolean` | — | Whether the item appears selected |
|
||||
| `disabled` | `boolean` | — | Disables interaction |
|
||||
| `onClick` | `MouseEventHandler<HTMLElement>` | — | Click handler |
|
||||
| `href` | `string` | — | Renders an anchor instead of a div |
|
||||
| `target` | `string` | — | Anchor target (e.g. `"_blank"`) |
|
||||
| `group` | `string` | — | Interactive group key |
|
||||
| `transient` | `boolean` | — | Transient interactive state |
|
||||
| `ref` | `React.Ref<HTMLElement>` | — | Forwarded ref |
|
||||
|
||||
### Sizing
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `size` | `SizeVariant` | `"lg"` | Container height |
|
||||
| `width` | `WidthVariant` | `"full"` | Container width |
|
||||
| `type` | `"submit" \| "button" \| "reset"` | — | HTML button type |
|
||||
| `tooltip` | `string` | — | Tooltip text shown on hover |
|
||||
| `tooltipSide` | `TooltipSide` | `"top"` | Tooltip side |
|
||||
|
||||
### Content (pass-through to ContentAction)
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `title` | `string` | **(required)** | Row label |
|
||||
| `icon` | `IconFunctionComponent` | — | Left icon |
|
||||
| `description` | `string` | — | Description below the title |
|
||||
| `sizePreset` | `SizePreset` | `"headline"` | Content size preset |
|
||||
| `variant` | `ContentVariant` | `"heading"` | Content layout variant |
|
||||
| `rightChildren` | `ReactNode` | — | Content after the label (e.g. action button) |
|
||||
|
||||
All other `ContentAction` / `Content` props (`editable`, `onTitleChange`, `optional`, `auxIcon`, `tag`, `withInteractive`, etc.) are also passed through.
|
||||
|
||||
## Usage
|
||||
|
||||
```tsx
|
||||
import { LineItemButton } from "@opal/components";
|
||||
|
||||
// Simple selectable row
|
||||
<LineItemButton
|
||||
prominence="heavy"
|
||||
selected={isSelected}
|
||||
size="md"
|
||||
onClick={handleClick}
|
||||
title="gpt-4o"
|
||||
sizePreset="main-ui"
|
||||
variant="section"
|
||||
/>
|
||||
|
||||
// With right-side action
|
||||
<LineItemButton
|
||||
prominence="heavy"
|
||||
selected={isSelected}
|
||||
onClick={handleClick}
|
||||
title="claude-opus-4"
|
||||
sizePreset="main-ui"
|
||||
variant="section"
|
||||
rightChildren={<Tag title="Default" color="blue" />}
|
||||
/>
|
||||
```
|
||||
@@ -0,0 +1,137 @@
|
||||
import "@opal/components/tooltip.css";
|
||||
import { Interactive, type InteractiveBaseProps } from "@opal/core";
|
||||
import type { SizeVariant, WidthVariant } from "@opal/shared";
|
||||
import type { TooltipSide } from "@opal/components";
|
||||
import type { ContentActionProps } from "@opal/layouts/ContentAction/components";
|
||||
import { ContentAction } from "@opal/layouts";
|
||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Types
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
type ContentPassthroughProps = Omit<
|
||||
ContentActionProps,
|
||||
"paddingVariant" | "widthVariant" | "ref"
|
||||
>;
|
||||
|
||||
interface LineItemButtonProps extends ContentPassthroughProps {
|
||||
/** Interactive select prominence. @default "light" */
|
||||
prominence?: "light" | "heavy";
|
||||
|
||||
/** Whether this item is selected. */
|
||||
selected?: boolean;
|
||||
|
||||
/** Whether this item is disabled. */
|
||||
disabled?: boolean;
|
||||
|
||||
/** Click handler. */
|
||||
onClick?: InteractiveBaseProps["onClick"];
|
||||
|
||||
/** When provided, renders an anchor instead of a div. */
|
||||
href?: string;
|
||||
|
||||
/** Anchor target (e.g. "_blank"). */
|
||||
target?: string;
|
||||
|
||||
/** Interactive group key. */
|
||||
group?: string;
|
||||
|
||||
/** Transient interactive state. */
|
||||
transient?: boolean;
|
||||
|
||||
/** Forwarded ref. */
|
||||
ref?: React.Ref<HTMLElement>;
|
||||
|
||||
/** Container height. @default "lg" */
|
||||
size?: SizeVariant;
|
||||
|
||||
/** Container width. @default "full" */
|
||||
width?: WidthVariant;
|
||||
|
||||
/** HTML button type. */
|
||||
type?: "submit" | "button" | "reset";
|
||||
|
||||
/** Tooltip text shown on hover. */
|
||||
tooltip?: string;
|
||||
|
||||
/** Which side the tooltip appears on. @default "top" */
|
||||
tooltipSide?: TooltipSide;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// LineItemButton
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function LineItemButton({
|
||||
// Interactive surface
|
||||
prominence = "light",
|
||||
selected,
|
||||
disabled,
|
||||
onClick,
|
||||
href,
|
||||
target,
|
||||
group,
|
||||
transient,
|
||||
ref,
|
||||
|
||||
// Sizing
|
||||
size = "lg",
|
||||
width = "full",
|
||||
type,
|
||||
tooltip,
|
||||
tooltipSide = "top",
|
||||
|
||||
// ContentAction pass-through
|
||||
...contentActionProps
|
||||
}: LineItemButtonProps) {
|
||||
const item = (
|
||||
<Interactive.Base
|
||||
variant="select"
|
||||
prominence={prominence}
|
||||
selected={selected}
|
||||
disabled={disabled}
|
||||
onClick={onClick}
|
||||
href={href}
|
||||
target={target}
|
||||
group={group}
|
||||
transient={transient}
|
||||
ref={ref}
|
||||
>
|
||||
<Interactive.Container
|
||||
type={type}
|
||||
widthVariant={width}
|
||||
heightVariant={size}
|
||||
roundingVariant={
|
||||
size === "lg" ? "default" : size === "2xs" ? "mini" : "compact"
|
||||
}
|
||||
>
|
||||
<ContentAction
|
||||
{...(contentActionProps as ContentActionProps)}
|
||||
withInteractive
|
||||
paddingVariant="fit"
|
||||
widthVariant="full"
|
||||
/>
|
||||
</Interactive.Container>
|
||||
</Interactive.Base>
|
||||
);
|
||||
|
||||
if (!tooltip) return item;
|
||||
|
||||
return (
|
||||
<TooltipPrimitive.Root>
|
||||
<TooltipPrimitive.Trigger asChild>{item}</TooltipPrimitive.Trigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipPrimitive.Content
|
||||
className="opal-tooltip"
|
||||
side={tooltipSide}
|
||||
sideOffset={4}
|
||||
>
|
||||
{tooltip}
|
||||
</TooltipPrimitive.Content>
|
||||
</TooltipPrimitive.Portal>
|
||||
</TooltipPrimitive.Root>
|
||||
);
|
||||
}
|
||||
|
||||
export { LineItemButton, type LineItemButtonProps };
|
||||
@@ -13,6 +13,12 @@ export {
|
||||
type OpenButtonProps,
|
||||
} from "@opal/components/buttons/OpenButton/components";
|
||||
|
||||
/* LineItemButton */
|
||||
export {
|
||||
LineItemButton,
|
||||
type LineItemButtonProps,
|
||||
} from "@opal/components/buttons/LineItemButton/components";
|
||||
|
||||
/* Tag */
|
||||
export {
|
||||
Tag,
|
||||
|
||||
@@ -2,6 +2,7 @@ import "@opal/core/hoverable/styles.css";
|
||||
import React, { createContext, useContext, useState, useCallback } from "react";
|
||||
import { cn } from "@opal/utils";
|
||||
import type { WithoutStyles } from "@opal/types";
|
||||
import { widthVariants, type WidthVariant } from "@opal/shared";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Context-per-group registry
|
||||
@@ -38,6 +39,10 @@ interface HoverableRootProps
|
||||
extends WithoutStyles<React.HTMLAttributes<HTMLDivElement>> {
|
||||
children: React.ReactNode;
|
||||
group: string;
|
||||
/** Width preset. @default "auto" */
|
||||
widthVariant?: WidthVariant;
|
||||
/** Ref forwarded to the root `<div>`. */
|
||||
ref?: React.Ref<HTMLDivElement>;
|
||||
}
|
||||
|
||||
type HoverableItemVariant = "opacity-on-hover";
|
||||
@@ -47,6 +52,8 @@ interface HoverableItemProps
|
||||
children: React.ReactNode;
|
||||
group?: string;
|
||||
variant?: HoverableItemVariant;
|
||||
/** Ref forwarded to the item `<div>`. */
|
||||
ref?: React.Ref<HTMLDivElement>;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -77,6 +84,8 @@ interface HoverableItemProps
|
||||
function HoverableRoot({
|
||||
group,
|
||||
children,
|
||||
widthVariant = "auto",
|
||||
ref,
|
||||
onMouseEnter: consumerMouseEnter,
|
||||
onMouseLeave: consumerMouseLeave,
|
||||
...props
|
||||
@@ -103,7 +112,13 @@ function HoverableRoot({
|
||||
|
||||
return (
|
||||
<GroupContext.Provider value={hovered}>
|
||||
<div {...props} onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave}>
|
||||
<div
|
||||
{...props}
|
||||
ref={ref}
|
||||
className={cn(widthVariants[widthVariant])}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</GroupContext.Provider>
|
||||
@@ -147,6 +162,7 @@ function HoverableItem({
|
||||
group,
|
||||
variant = "opacity-on-hover",
|
||||
children,
|
||||
ref,
|
||||
...props
|
||||
}: HoverableItemProps) {
|
||||
const contextValue = useContext(
|
||||
@@ -165,6 +181,7 @@ function HoverableItem({
|
||||
return (
|
||||
<div
|
||||
{...props}
|
||||
ref={ref}
|
||||
className={cn("hoverable-item")}
|
||||
data-hoverable-variant={variant}
|
||||
data-hoverable-active={
|
||||
|
||||
@@ -40,6 +40,9 @@ interface BodyLayoutProps {
|
||||
|
||||
/** Title prominence. Default: `"default"`. */
|
||||
prominence?: BodyProminence;
|
||||
|
||||
/** Ref forwarded to the root `<div>`. */
|
||||
ref?: React.Ref<HTMLDivElement>;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -80,6 +83,7 @@ function BodyLayout({
|
||||
sizePreset = "main-ui",
|
||||
orientation = "inline",
|
||||
prominence = "default",
|
||||
ref,
|
||||
}: BodyLayoutProps) {
|
||||
const config = BODY_PRESETS[sizePreset];
|
||||
const titleColorClass =
|
||||
@@ -87,6 +91,7 @@ function BodyLayout({
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className="opal-content-body"
|
||||
data-orientation={orientation}
|
||||
style={{ gap: config.gap }}
|
||||
|
||||
@@ -48,6 +48,12 @@ interface ContentLgProps {
|
||||
|
||||
/** Size preset. Default: `"headline"`. */
|
||||
sizePreset?: ContentLgSizePreset;
|
||||
|
||||
/** When `true`, the title color hooks into `Interactive.Base`'s `--interactive-foreground` variable. */
|
||||
withInteractive?: boolean;
|
||||
|
||||
/** Ref forwarded to the root `<div>`. */
|
||||
ref?: React.Ref<HTMLDivElement>;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -86,6 +92,8 @@ function ContentLg({
|
||||
description,
|
||||
editable,
|
||||
onTitleChange,
|
||||
withInteractive,
|
||||
ref,
|
||||
}: ContentLgProps) {
|
||||
const [editing, setEditing] = useState(false);
|
||||
const [editValue, setEditValue] = useState(title);
|
||||
@@ -104,7 +112,12 @@ function ContentLg({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="opal-content-lg" style={{ gap: config.gap }}>
|
||||
<div
|
||||
ref={ref}
|
||||
className="opal-content-lg"
|
||||
data-interactive={withInteractive || undefined}
|
||||
style={{ gap: config.gap }}
|
||||
>
|
||||
{Icon && (
|
||||
<div
|
||||
className={cn(
|
||||
|
||||
@@ -61,6 +61,12 @@ interface ContentMdProps {
|
||||
|
||||
/** Size preset. Default: `"main-ui"`. */
|
||||
sizePreset?: ContentMdSizePreset;
|
||||
|
||||
/** When `true`, the title color hooks into `Interactive.Base`'s `--interactive-foreground` variable. */
|
||||
withInteractive?: boolean;
|
||||
|
||||
/** Ref forwarded to the root `<div>`. */
|
||||
ref?: React.Ref<HTMLDivElement>;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -130,6 +136,8 @@ function ContentMd({
|
||||
auxIcon,
|
||||
tag,
|
||||
sizePreset = "main-ui",
|
||||
withInteractive,
|
||||
ref,
|
||||
}: ContentMdProps) {
|
||||
const [editing, setEditing] = useState(false);
|
||||
const [editValue, setEditValue] = useState(title);
|
||||
@@ -149,7 +157,12 @@ function ContentMd({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="opal-content-md" style={{ gap: config.gap }}>
|
||||
<div
|
||||
ref={ref}
|
||||
className="opal-content-md"
|
||||
data-interactive={withInteractive || undefined}
|
||||
style={{ gap: config.gap }}
|
||||
>
|
||||
{Icon && (
|
||||
<div
|
||||
className={cn(
|
||||
|
||||
@@ -40,6 +40,12 @@ interface ContentSmProps {
|
||||
|
||||
/** Title prominence. Default: `"default"`. */
|
||||
prominence?: ContentSmProminence;
|
||||
|
||||
/** When `true`, the title color hooks into `Interactive.Base`'s `--interactive-foreground` variable. */
|
||||
withInteractive?: boolean;
|
||||
|
||||
/** Ref forwarded to the root `<div>`. */
|
||||
ref?: React.Ref<HTMLDivElement>;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -80,14 +86,18 @@ function ContentSm({
|
||||
sizePreset = "main-ui",
|
||||
orientation = "inline",
|
||||
prominence = "default",
|
||||
withInteractive,
|
||||
ref,
|
||||
}: ContentSmProps) {
|
||||
const config = CONTENT_SM_PRESETS[sizePreset];
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className="opal-content-sm"
|
||||
data-orientation={orientation}
|
||||
data-prominence={prominence}
|
||||
data-interactive={withInteractive || undefined}
|
||||
style={{ gap: config.gap }}
|
||||
>
|
||||
{Icon && (
|
||||
|
||||
@@ -60,6 +60,12 @@ interface ContentXlProps {
|
||||
|
||||
/** Optional tertiary icon rendered in the icon row. */
|
||||
moreIcon2?: IconFunctionComponent;
|
||||
|
||||
/** When `true`, the title color hooks into `Interactive.Base`'s `--interactive-foreground` variable. */
|
||||
withInteractive?: boolean;
|
||||
|
||||
/** Ref forwarded to the root `<div>`. */
|
||||
ref?: React.Ref<HTMLDivElement>;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -106,6 +112,8 @@ function ContentXl({
|
||||
onTitleChange,
|
||||
moreIcon1: MoreIcon1,
|
||||
moreIcon2: MoreIcon2,
|
||||
withInteractive,
|
||||
ref,
|
||||
}: ContentXlProps) {
|
||||
const [editing, setEditing] = useState(false);
|
||||
const [editValue, setEditValue] = useState(title);
|
||||
@@ -124,7 +132,11 @@ function ContentXl({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="opal-content-xl">
|
||||
<div
|
||||
ref={ref}
|
||||
className="opal-content-xl"
|
||||
data-interactive={withInteractive || undefined}
|
||||
>
|
||||
{(Icon || MoreIcon1 || MoreIcon2) && (
|
||||
<div className="opal-content-xl-icon-row">
|
||||
{Icon && (
|
||||
|
||||
@@ -52,6 +52,9 @@ interface HeadingLayoutProps {
|
||||
|
||||
/** Variant controls icon placement. `"heading"` = top, `"section"` = inline. Default: `"heading"`. */
|
||||
variant?: HeadingVariant;
|
||||
|
||||
/** Ref forwarded to the root `<div>`. */
|
||||
ref?: React.Ref<HTMLDivElement>;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -91,6 +94,7 @@ function HeadingLayout({
|
||||
description,
|
||||
editable,
|
||||
onTitleChange,
|
||||
ref,
|
||||
}: HeadingLayoutProps) {
|
||||
const [editing, setEditing] = useState(false);
|
||||
const [editValue, setEditValue] = useState(title);
|
||||
@@ -112,6 +116,7 @@ function HeadingLayout({
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className="opal-content-heading"
|
||||
data-icon-placement={iconPlacement}
|
||||
style={{ gap: iconPlacement === "left" ? config.gap : undefined }}
|
||||
|
||||
@@ -61,6 +61,9 @@ interface LabelLayoutProps {
|
||||
|
||||
/** Size preset. Default: `"main-ui"`. */
|
||||
sizePreset?: LabelSizePreset;
|
||||
|
||||
/** Ref forwarded to the root `<div>`. */
|
||||
ref?: React.Ref<HTMLDivElement>;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -130,6 +133,7 @@ function LabelLayout({
|
||||
auxIcon,
|
||||
tag,
|
||||
sizePreset = "main-ui",
|
||||
ref,
|
||||
}: LabelLayoutProps) {
|
||||
const [editing, setEditing] = useState(false);
|
||||
const [editValue, setEditValue] = useState(title);
|
||||
@@ -149,7 +153,7 @@ function LabelLayout({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="opal-content-label" style={{ gap: config.gap }}>
|
||||
<div ref={ref} className="opal-content-label" style={{ gap: config.gap }}>
|
||||
{Icon && (
|
||||
<div
|
||||
className={cn(
|
||||
|
||||
@@ -30,6 +30,14 @@ A two-axis layout component for displaying icon + title + description rows. Rout
|
||||
| `main-ui` | 1rem (16px) | `p-0.5` (2px) | `text-03` | 0.25rem (4px) | `font-main-ui-action` | 1.25rem (20px) |
|
||||
| `secondary` | 0.75rem (12px) | `p-0.5` (2px) | `text-04` | 0.125rem (2px) | `font-secondary-action` | 1rem (16px) |
|
||||
|
||||
#### ContentSm presets (variant="body")
|
||||
|
||||
| Preset | Icon | Icon padding | Gap | Title font | Line-height |
|
||||
|---|---|---|---|---|---|
|
||||
| `main-content` | 1rem (16px) | `p-1` (4px) | 0.125rem (2px) | `font-main-content-body` | 1.5rem (24px) |
|
||||
| `main-ui` | 1rem (16px) | `p-0.5` (2px) | 0.25rem (4px) | `font-main-ui-action` | 1.25rem (20px) |
|
||||
| `secondary` | 0.75rem (12px) | `p-0.5` (2px) | 0.125rem (2px) | `font-secondary-action` | 1rem (16px) |
|
||||
|
||||
> Icon container height (icon + 2 x padding) always equals the title line-height.
|
||||
|
||||
### `variant` — controls structure / layout
|
||||
@@ -53,31 +61,42 @@ Invalid combinations (e.g. `sizePreset="headline" + variant="body"`) are exclude
|
||||
|
||||
## Props
|
||||
|
||||
### Common props (all variants)
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|---|---|---|---|
|
||||
| `sizePreset` | `SizePreset` | `"headline"` | Size preset (see tables above) |
|
||||
| `variant` | `ContentVariant` | `"heading"` | Layout variant (see table above) |
|
||||
| `variant` | `ContentVariant` | `"heading"` | Layout variant |
|
||||
| `icon` | `IconFunctionComponent` | — | Optional icon component |
|
||||
| `title` | `string` | **(required)** | Main title text |
|
||||
| `description` | `string` | — | Optional description below the title |
|
||||
| `editable` | `boolean` | `false` | Enable inline editing of the title |
|
||||
| `description` | `string` | — | Optional description (not available for `variant="body"`) |
|
||||
| `editable` | `boolean` | `false` | Enable inline editing (not available for `variant="body"`) |
|
||||
| `onTitleChange` | `(newTitle: string) => void` | — | Called when user commits an edit |
|
||||
| `moreIcon1` | `IconFunctionComponent` | — | Secondary icon in icon row (ContentXl only) |
|
||||
| `moreIcon2` | `IconFunctionComponent` | — | Tertiary icon in icon row (ContentXl only) |
|
||||
| `widthVariant` | `WidthVariant` | `"auto"` | `"auto"` shrink-wraps, `"full"` stretches |
|
||||
| `withInteractive` | `boolean` | — | Opts title into `Interactive.Base`'s `--interactive-foreground` color |
|
||||
| `ref` | `React.Ref<HTMLDivElement>` | — | Ref forwarded to the root `<div>` of the resolved layout |
|
||||
|
||||
## Internal Layouts
|
||||
### ContentXl-only props (`variant="heading"`)
|
||||
|
||||
### ContentXl
|
||||
| Prop | Type | Default | Description |
|
||||
|---|---|---|---|
|
||||
| `moreIcon1` | `IconFunctionComponent` | — | Secondary icon in icon row |
|
||||
| `moreIcon2` | `IconFunctionComponent` | — | Tertiary icon in icon row |
|
||||
|
||||
For `headline` / `section` presets with `variant="heading"`. Icon row on top (flex-col), supports `moreIcon1` and `moreIcon2` in the icon row. Description is always `font-secondary-body text-text-03`.
|
||||
### ContentMd-only props (`sizePreset="main-content" / "main-ui" / "secondary"`, `variant="section"`)
|
||||
|
||||
### ContentLg
|
||||
| Prop | Type | Default | Description |
|
||||
|---|---|---|---|
|
||||
| `optional` | `boolean` | — | Renders "(Optional)" beside the title |
|
||||
| `auxIcon` | `"info-gray" \| "info-blue" \| "warning" \| "error"` | — | Auxiliary status icon beside the title |
|
||||
| `tag` | `TagProps` | — | Tag rendered beside the title |
|
||||
|
||||
For `headline` / `section` presets with `variant="section"`. Always inline (flex-row). Description is always `font-secondary-body text-text-03`.
|
||||
### ContentSm-only props (`variant="body"`)
|
||||
|
||||
### ContentMd
|
||||
|
||||
For `main-content` / `main-ui` / `secondary` presets. Always inline. Both `icon` and `description` are optional. Description is always `font-secondary-body text-text-03`.
|
||||
| Prop | Type | Default | Description |
|
||||
|---|---|---|---|
|
||||
| `orientation` | `"vertical" \| "inline" \| "reverse"` | `"inline"` | Layout orientation |
|
||||
| `prominence` | `"default" \| "muted" \| "muted-2x"` | `"default"` | Title prominence |
|
||||
|
||||
## Usage Examples
|
||||
|
||||
@@ -94,42 +113,34 @@ import SvgSearch from "@opal/icons/search";
|
||||
description="Configure your agent's behavior"
|
||||
/>
|
||||
|
||||
// ContentXl — with more icons
|
||||
<Content
|
||||
icon={SvgSearch}
|
||||
sizePreset="headline"
|
||||
variant="heading"
|
||||
title="Agent Settings"
|
||||
moreIcon1={SvgStar}
|
||||
moreIcon2={SvgLock}
|
||||
/>
|
||||
|
||||
// ContentLg — section, icon inline
|
||||
<Content
|
||||
icon={SvgSearch}
|
||||
sizePreset="section"
|
||||
variant="section"
|
||||
title="Data Sources"
|
||||
description="Connected integrations"
|
||||
/>
|
||||
|
||||
// ContentMd — with icon and description
|
||||
// ContentMd — with tag and optional marker
|
||||
<Content
|
||||
icon={SvgSearch}
|
||||
sizePreset="main-ui"
|
||||
title="Instructions"
|
||||
description="Agent system prompt"
|
||||
tag={{ title: "New", color: "green" }}
|
||||
optional
|
||||
/>
|
||||
|
||||
// ContentMd — title only (no icon, no description)
|
||||
// ContentSm — body text
|
||||
<Content
|
||||
sizePreset="main-content"
|
||||
title="Featured Agent"
|
||||
icon={SvgSearch}
|
||||
sizePreset="main-ui"
|
||||
variant="body"
|
||||
title="Last updated 2 hours ago"
|
||||
prominence="muted"
|
||||
/>
|
||||
|
||||
// Editable title
|
||||
<Content
|
||||
icon={SvgSearch}
|
||||
sizePreset="headline"
|
||||
variant="heading"
|
||||
title="My Agent"
|
||||
|
||||
@@ -59,6 +59,12 @@ interface ContentBaseProps {
|
||||
* @default "auto"
|
||||
*/
|
||||
widthVariant?: WidthVariant;
|
||||
|
||||
/** When `true`, the title color hooks into `Interactive.Base`'s `--interactive-foreground` variable. */
|
||||
withInteractive?: boolean;
|
||||
|
||||
/** Ref forwarded to the root `<div>` of the resolved layout. */
|
||||
ref?: React.Ref<HTMLDivElement>;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
|
||||
.opal-content-xl-icon-row {
|
||||
@apply flex flex-row items-center;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
@@ -385,3 +386,28 @@
|
||||
.opal-content-sm[data-prominence="muted-2x"] .opal-content-sm-title {
|
||||
@apply text-text-02;
|
||||
}
|
||||
|
||||
/* ===========================================================================
|
||||
Interactive-foreground opt-in
|
||||
|
||||
When a Content variant is nested inside an Interactive.Base and
|
||||
`withInteractive` is set, the title delegates its color to the
|
||||
`--interactive-foreground` CSS variable controlled by the ancestor
|
||||
Interactive.Base variant.
|
||||
=========================================================================== */
|
||||
|
||||
.opal-content-xl[data-interactive] .opal-content-xl-title {
|
||||
color: var(--interactive-foreground);
|
||||
}
|
||||
|
||||
.opal-content-lg[data-interactive] .opal-content-lg-title {
|
||||
color: var(--interactive-foreground);
|
||||
}
|
||||
|
||||
.opal-content-md[data-interactive] .opal-content-md-title {
|
||||
color: var(--interactive-foreground);
|
||||
}
|
||||
|
||||
.opal-content-sm[data-interactive] .opal-content-sm-title {
|
||||
color: var(--interactive-foreground);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user