mirror of
https://github.com/onyx-dot-app/onyx.git
synced 2026-03-09 01:32:40 +00:00
Compare commits
9 Commits
cli/v0.1.0
...
refactor/c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c20e5205ca | ||
|
|
19151c2c44 | ||
|
|
72b1771bc2 | ||
|
|
8778266521 | ||
|
|
a8cbba86b4 | ||
|
|
fb21c6cae5 | ||
|
|
922154f3d6 | ||
|
|
5e9ea9edef | ||
|
|
dfc31e5d37 |
@@ -15,6 +15,7 @@ const iconPaddingInRemVariants = {
|
||||
md: "p-0.5",
|
||||
sm: "p-0",
|
||||
xs: "p-0.5",
|
||||
"2xs": "p-0",
|
||||
fit: "p-0.5",
|
||||
} as const;
|
||||
const iconSizeInRemVariants = {
|
||||
@@ -22,6 +23,7 @@ const iconSizeInRemVariants = {
|
||||
md: 1,
|
||||
sm: 1,
|
||||
xs: 0.75,
|
||||
"2xs": 0.75,
|
||||
fit: 1,
|
||||
} as const;
|
||||
|
||||
@@ -128,7 +130,9 @@ function Button({
|
||||
type={type}
|
||||
border={interactiveBaseProps.prominence === "secondary"}
|
||||
heightVariant={size}
|
||||
roundingVariant={isLarge ? "default" : "compact"}
|
||||
roundingVariant={
|
||||
isLarge ? "default" : size === "2xs" ? "mini" : "compact"
|
||||
}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
|
||||
@@ -12,3 +12,18 @@ export {
|
||||
OpenButton,
|
||||
type OpenButtonProps,
|
||||
} from "@opal/components/buttons/OpenButton/components";
|
||||
|
||||
/* Content */
|
||||
export {
|
||||
Content,
|
||||
type ContentProps,
|
||||
type SizePreset,
|
||||
type ContentVariant,
|
||||
} from "@opal/components/layouts/Content/components";
|
||||
|
||||
/* AuxiliaryTag */
|
||||
export {
|
||||
AuxiliaryTag,
|
||||
type AuxiliaryTagProps,
|
||||
type AuxiliaryTagColor,
|
||||
} from "@opal/components/tags/AuxiliaryTag/components";
|
||||
|
||||
121
web/lib/opal/src/components/layouts/Content/BodyLayout.tsx
Normal file
121
web/lib/opal/src/components/layouts/Content/BodyLayout.tsx
Normal file
@@ -0,0 +1,121 @@
|
||||
"use client";
|
||||
|
||||
import type { IconFunctionComponent } from "@opal/types";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Types
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
type BodySizePreset = "main-content" | "main-ui" | "secondary";
|
||||
type BodyOrientation = "vertical" | "inline" | "reverse";
|
||||
type BodyProminence = "default" | "muted";
|
||||
|
||||
interface BodyPresetConfig {
|
||||
/** Icon width/height (CSS value). */
|
||||
iconSize: string;
|
||||
/** Tailwind padding class for the icon container. */
|
||||
iconContainerPadding: string;
|
||||
/** Tailwind font class for the title. */
|
||||
titleFont: string;
|
||||
/** Title line-height — also used as icon container min-height (CSS value). */
|
||||
lineHeight: string;
|
||||
/** Gap between icon container and title (CSS value). */
|
||||
gap: string;
|
||||
}
|
||||
|
||||
/** Props for {@link BodyLayout}. Does not support editing or descriptions. */
|
||||
interface BodyLayoutProps {
|
||||
/** Optional icon component. */
|
||||
icon?: IconFunctionComponent;
|
||||
|
||||
/** Main title text (read-only — editing is not supported). */
|
||||
title: string;
|
||||
|
||||
/** Size preset. Default: `"main-ui"`. */
|
||||
sizePreset?: BodySizePreset;
|
||||
|
||||
/** Layout orientation. Default: `"inline"`. */
|
||||
orientation?: BodyOrientation;
|
||||
|
||||
/** Title prominence. Default: `"default"`. */
|
||||
prominence?: BodyProminence;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Presets
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const BODY_PRESETS: Record<BodySizePreset, BodyPresetConfig> = {
|
||||
"main-content": {
|
||||
iconSize: "1rem",
|
||||
iconContainerPadding: "p-1",
|
||||
titleFont: "font-main-content-body",
|
||||
lineHeight: "1.5rem",
|
||||
gap: "0.125rem",
|
||||
},
|
||||
"main-ui": {
|
||||
iconSize: "1rem",
|
||||
iconContainerPadding: "p-0.5",
|
||||
titleFont: "font-main-ui-action",
|
||||
lineHeight: "1.25rem",
|
||||
gap: "0.25rem",
|
||||
},
|
||||
secondary: {
|
||||
iconSize: "0.75rem",
|
||||
iconContainerPadding: "p-0.5",
|
||||
titleFont: "font-secondary-action",
|
||||
lineHeight: "1rem",
|
||||
gap: "0.125rem",
|
||||
},
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// BodyLayout
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function BodyLayout({
|
||||
icon: Icon,
|
||||
title,
|
||||
sizePreset = "main-ui",
|
||||
orientation = "inline",
|
||||
prominence = "default",
|
||||
}: BodyLayoutProps) {
|
||||
const config = BODY_PRESETS[sizePreset];
|
||||
const titleColorClass =
|
||||
prominence === "muted" ? "text-text-03" : "text-text-04";
|
||||
|
||||
return (
|
||||
<div
|
||||
className="opal-content-body"
|
||||
data-orientation={orientation}
|
||||
style={{ gap: config.gap }}
|
||||
>
|
||||
{Icon && (
|
||||
<div
|
||||
className={`opal-content-body-icon-container shrink-0 ${config.iconContainerPadding}`}
|
||||
style={{ minHeight: config.lineHeight }}
|
||||
>
|
||||
<Icon
|
||||
className="opal-content-body-icon text-text-03"
|
||||
style={{ width: config.iconSize, height: config.iconSize }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<span
|
||||
className={`opal-content-body-title ${config.titleFont} ${titleColorClass}`}
|
||||
style={{ height: config.lineHeight }}
|
||||
>
|
||||
{title}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
BodyLayout,
|
||||
type BodyLayoutProps,
|
||||
type BodySizePreset,
|
||||
type BodyOrientation,
|
||||
type BodyProminence,
|
||||
};
|
||||
179
web/lib/opal/src/components/layouts/Content/HeadingLayout.tsx
Normal file
179
web/lib/opal/src/components/layouts/Content/HeadingLayout.tsx
Normal file
@@ -0,0 +1,179 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@opal/components/buttons/Button/components";
|
||||
import type { InteractiveContainerHeightVariant } from "@opal/core";
|
||||
import SvgEdit from "@opal/icons/edit";
|
||||
import type { IconFunctionComponent } from "@opal/types";
|
||||
import { useRef, useState } from "react";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Types
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
type HeadingSizePreset = "headline" | "section";
|
||||
type HeadingVariant = "heading" | "section";
|
||||
|
||||
interface HeadingPresetConfig {
|
||||
/** Icon width/height (CSS value). */
|
||||
iconSize: string;
|
||||
/** Tailwind padding class for the icon container. */
|
||||
iconContainerPadding: string;
|
||||
/** Gap between icon container and content (CSS value). */
|
||||
gap: string;
|
||||
/** Tailwind font class for the title. */
|
||||
titleFont: string;
|
||||
/** Title line-height — also used as icon container min-height (CSS value). */
|
||||
lineHeight: string;
|
||||
/** Button `size` prop for the edit button. */
|
||||
editButtonSize: InteractiveContainerHeightVariant;
|
||||
/** Tailwind padding class for the edit button container. */
|
||||
editButtonPadding: string;
|
||||
}
|
||||
|
||||
interface HeadingLayoutProps {
|
||||
/** Optional icon component. */
|
||||
icon?: IconFunctionComponent;
|
||||
|
||||
/** Main title text. */
|
||||
title: string;
|
||||
|
||||
/** Optional description below the title. */
|
||||
description?: string;
|
||||
|
||||
/** Enable inline editing of the title. */
|
||||
editable?: boolean;
|
||||
|
||||
/** Called when the user commits an edit. */
|
||||
onTitleChange?: (newTitle: string) => void;
|
||||
|
||||
/** Size preset. Default: `"headline"`. */
|
||||
sizePreset?: HeadingSizePreset;
|
||||
|
||||
/** Variant controls icon placement. `"heading"` = top, `"section"` = inline. Default: `"heading"`. */
|
||||
variant?: HeadingVariant;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Presets
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const HEADING_PRESETS: Record<HeadingSizePreset, HeadingPresetConfig> = {
|
||||
headline: {
|
||||
iconSize: "2rem",
|
||||
iconContainerPadding: "p-0.5",
|
||||
gap: "0.25rem",
|
||||
titleFont: "font-heading-h2",
|
||||
lineHeight: "2.25rem",
|
||||
editButtonSize: "md",
|
||||
editButtonPadding: "p-1",
|
||||
},
|
||||
section: {
|
||||
iconSize: "1.25rem",
|
||||
iconContainerPadding: "p-1",
|
||||
gap: "0rem",
|
||||
titleFont: "font-heading-h3",
|
||||
lineHeight: "1.75rem",
|
||||
editButtonSize: "sm",
|
||||
editButtonPadding: "p-0.5",
|
||||
},
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// HeadingLayout
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function HeadingLayout({
|
||||
sizePreset = "headline",
|
||||
variant = "heading",
|
||||
icon: Icon,
|
||||
title,
|
||||
description,
|
||||
editable,
|
||||
onTitleChange,
|
||||
}: HeadingLayoutProps) {
|
||||
const [editing, setEditing] = useState(false);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const config = HEADING_PRESETS[sizePreset];
|
||||
const iconPlacement = variant === "heading" ? "top" : "left";
|
||||
|
||||
function commit() {
|
||||
if (!inputRef.current) return;
|
||||
const value = inputRef.current.value.trim();
|
||||
if (value && value !== title) onTitleChange?.(value);
|
||||
setEditing(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="opal-content-heading"
|
||||
data-icon-placement={iconPlacement}
|
||||
style={{ gap: iconPlacement === "left" ? config.gap : undefined }}
|
||||
>
|
||||
{Icon && (
|
||||
<div
|
||||
className={`opal-content-heading-icon-container shrink-0 ${config.iconContainerPadding}`}
|
||||
style={{ minHeight: config.lineHeight }}
|
||||
>
|
||||
<Icon
|
||||
className="opal-content-heading-icon"
|
||||
style={{ width: config.iconSize, height: config.iconSize }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="opal-content-heading-body">
|
||||
<div className="opal-content-heading-title-row">
|
||||
{editing ? (
|
||||
<input
|
||||
ref={inputRef}
|
||||
className={`opal-content-heading-input ${config.titleFont} text-text-04`}
|
||||
defaultValue={title}
|
||||
autoFocus
|
||||
onFocus={(e) => e.currentTarget.select()}
|
||||
onBlur={commit}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") commit();
|
||||
if (e.key === "Escape") setEditing(false);
|
||||
}}
|
||||
style={{ height: config.lineHeight }}
|
||||
/>
|
||||
) : (
|
||||
<span
|
||||
className={`opal-content-heading-title ${
|
||||
config.titleFont
|
||||
} text-text-04${editable ? " cursor-pointer" : ""}`}
|
||||
onClick={editable ? () => setEditing(true) : undefined}
|
||||
style={{ height: config.lineHeight }}
|
||||
>
|
||||
{title}
|
||||
</span>
|
||||
)}
|
||||
|
||||
{editable && !editing && (
|
||||
<div
|
||||
className={`opal-content-heading-edit-button ${config.editButtonPadding}`}
|
||||
>
|
||||
<Button
|
||||
icon={SvgEdit}
|
||||
prominence="internal"
|
||||
size={config.editButtonSize}
|
||||
tooltip="Edit"
|
||||
tooltipSide="right"
|
||||
onClick={() => setEditing(true)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{description && (
|
||||
<div className="opal-content-heading-description font-secondary-body text-text-03">
|
||||
{description}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { HeadingLayout, type HeadingLayoutProps, type HeadingSizePreset };
|
||||
254
web/lib/opal/src/components/layouts/Content/LabelLayout.tsx
Normal file
254
web/lib/opal/src/components/layouts/Content/LabelLayout.tsx
Normal file
@@ -0,0 +1,254 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@opal/components/buttons/Button/components";
|
||||
import type { InteractiveContainerHeightVariant } from "@opal/core";
|
||||
import SvgAlertCircle from "@opal/icons/alert-circle";
|
||||
import SvgAlertTriangle from "@opal/icons/alert-triangle";
|
||||
import SvgEdit from "@opal/icons/edit";
|
||||
import SvgXOctagon from "@opal/icons/x-octagon";
|
||||
import type { IconFunctionComponent } from "@opal/types";
|
||||
import { useRef, useState } from "react";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Types
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
type LabelSizePreset = "main-content" | "main-ui" | "secondary";
|
||||
|
||||
type LabelAuxIcon = "info-gray" | "info-blue" | "warning" | "error";
|
||||
|
||||
interface LabelPresetConfig {
|
||||
iconSize: string;
|
||||
iconContainerPadding: string;
|
||||
iconColorClass: string;
|
||||
titleFont: string;
|
||||
lineHeight: string;
|
||||
gap: string;
|
||||
editButtonSize: InteractiveContainerHeightVariant;
|
||||
editButtonPadding: string;
|
||||
optionalFont: string;
|
||||
/** Aux icon size = lineHeight − 2 × p-0.5. */
|
||||
auxIconSize: string;
|
||||
}
|
||||
|
||||
interface LabelLayoutProps {
|
||||
/** Optional icon component. */
|
||||
icon?: IconFunctionComponent;
|
||||
|
||||
/** Main title text. */
|
||||
title: string;
|
||||
|
||||
/** Optional description text below the title. */
|
||||
description?: string;
|
||||
|
||||
/** Enable inline editing of the title. */
|
||||
editable?: boolean;
|
||||
|
||||
/** Called when the user commits an edit. */
|
||||
onTitleChange?: (newTitle: string) => void;
|
||||
|
||||
/** When `true`, renders "(Optional)" beside the title. */
|
||||
optional?: boolean;
|
||||
|
||||
/** Auxiliary status icon rendered beside the title. */
|
||||
auxIcon?: LabelAuxIcon;
|
||||
|
||||
/** Size preset. Default: `"main-ui"`. */
|
||||
sizePreset?: LabelSizePreset;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Presets
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const LABEL_PRESETS: Record<LabelSizePreset, LabelPresetConfig> = {
|
||||
"main-content": {
|
||||
iconSize: "1rem",
|
||||
iconContainerPadding: "p-1",
|
||||
iconColorClass: "text-text-04",
|
||||
titleFont: "font-main-content-emphasis",
|
||||
lineHeight: "1.5rem",
|
||||
gap: "0.125rem",
|
||||
editButtonSize: "sm",
|
||||
editButtonPadding: "p-0",
|
||||
optionalFont: "font-main-content-muted",
|
||||
auxIconSize: "1.25rem",
|
||||
},
|
||||
"main-ui": {
|
||||
iconSize: "1rem",
|
||||
iconContainerPadding: "p-0.5",
|
||||
iconColorClass: "text-text-03",
|
||||
titleFont: "font-main-ui-action",
|
||||
lineHeight: "1.25rem",
|
||||
gap: "0.25rem",
|
||||
editButtonSize: "xs",
|
||||
editButtonPadding: "p-0",
|
||||
optionalFont: "font-main-ui-muted",
|
||||
auxIconSize: "1rem",
|
||||
},
|
||||
secondary: {
|
||||
iconSize: "0.75rem",
|
||||
iconContainerPadding: "p-0.5",
|
||||
iconColorClass: "text-text-04",
|
||||
titleFont: "font-secondary-action",
|
||||
lineHeight: "1rem",
|
||||
gap: "0.125rem",
|
||||
editButtonSize: "2xs",
|
||||
editButtonPadding: "p-0",
|
||||
optionalFont: "font-secondary-action",
|
||||
auxIconSize: "0.75rem",
|
||||
},
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// LabelLayout
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const AUX_ICON_CONFIG: Record<
|
||||
LabelAuxIcon,
|
||||
{ icon: IconFunctionComponent; colorClass: string }
|
||||
> = {
|
||||
"info-gray": { icon: SvgAlertCircle, colorClass: "text-text-02" },
|
||||
"info-blue": { icon: SvgAlertCircle, colorClass: "text-status-info-05" },
|
||||
warning: { icon: SvgAlertTriangle, colorClass: "text-status-warning-05" },
|
||||
error: { icon: SvgXOctagon, colorClass: "text-status-error-05" },
|
||||
};
|
||||
|
||||
function LabelLayout({
|
||||
icon: Icon,
|
||||
title,
|
||||
description,
|
||||
editable,
|
||||
onTitleChange,
|
||||
optional,
|
||||
auxIcon,
|
||||
sizePreset = "main-ui",
|
||||
}: LabelLayoutProps) {
|
||||
const [editing, setEditing] = useState(false);
|
||||
const [editValue, setEditValue] = useState(title);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const config = LABEL_PRESETS[sizePreset];
|
||||
|
||||
function startEditing() {
|
||||
setEditValue(title);
|
||||
setEditing(true);
|
||||
}
|
||||
|
||||
function commit() {
|
||||
const value = editValue.trim();
|
||||
if (value && value !== title) onTitleChange?.(value);
|
||||
setEditing(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="opal-content-label" style={{ gap: config.gap }}>
|
||||
{Icon && (
|
||||
<div
|
||||
className={`opal-content-label-icon-container shrink-0 ${config.iconContainerPadding}`}
|
||||
style={{ minHeight: config.lineHeight }}
|
||||
>
|
||||
<Icon
|
||||
className={`opal-content-label-icon ${config.iconColorClass}`}
|
||||
style={{ width: config.iconSize, height: config.iconSize }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="opal-content-label-body">
|
||||
<div className="opal-content-label-title-row">
|
||||
{editing ? (
|
||||
<div className="opal-content-label-input-sizer">
|
||||
<span
|
||||
className={`opal-content-label-input-mirror ${config.titleFont}`}
|
||||
>
|
||||
{editValue || "\u00A0"}
|
||||
</span>
|
||||
<input
|
||||
ref={inputRef}
|
||||
className={`opal-content-label-input ${config.titleFont} text-text-04`}
|
||||
value={editValue}
|
||||
onChange={(e) => setEditValue(e.target.value)}
|
||||
size={1}
|
||||
autoFocus
|
||||
onFocus={(e) => e.currentTarget.select()}
|
||||
onBlur={commit}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") commit();
|
||||
if (e.key === "Escape") setEditing(false);
|
||||
}}
|
||||
style={{ height: config.lineHeight }}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<span
|
||||
className={`opal-content-label-title ${
|
||||
config.titleFont
|
||||
} text-text-04${editable ? " cursor-pointer" : ""}`}
|
||||
onClick={editable ? startEditing : undefined}
|
||||
style={{ height: config.lineHeight }}
|
||||
>
|
||||
{title}
|
||||
</span>
|
||||
)}
|
||||
|
||||
{optional && (
|
||||
<span
|
||||
className={`${config.optionalFont} text-text-03 shrink-0`}
|
||||
style={{ height: config.lineHeight }}
|
||||
>
|
||||
(Optional)
|
||||
</span>
|
||||
)}
|
||||
|
||||
{auxIcon &&
|
||||
(() => {
|
||||
const { icon: AuxIcon, colorClass } = AUX_ICON_CONFIG[auxIcon];
|
||||
return (
|
||||
<div
|
||||
className="opal-content-label-aux-icon shrink-0 p-0.5"
|
||||
style={{ height: config.lineHeight }}
|
||||
>
|
||||
<AuxIcon
|
||||
className={colorClass}
|
||||
style={{
|
||||
width: config.auxIconSize,
|
||||
height: config.auxIconSize,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
|
||||
{editable && !editing && (
|
||||
<div
|
||||
className={`opal-content-label-edit-button ${config.editButtonPadding}`}
|
||||
>
|
||||
<Button
|
||||
icon={SvgEdit}
|
||||
prominence="internal"
|
||||
size={config.editButtonSize}
|
||||
tooltip="Edit"
|
||||
tooltipSide="right"
|
||||
onClick={startEditing}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{description && (
|
||||
<div className="opal-content-label-description font-secondary-body text-text-03">
|
||||
{description}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
LabelLayout,
|
||||
type LabelLayoutProps,
|
||||
type LabelSizePreset,
|
||||
type LabelAuxIcon,
|
||||
};
|
||||
116
web/lib/opal/src/components/layouts/Content/README.md
Normal file
116
web/lib/opal/src/components/layouts/Content/README.md
Normal file
@@ -0,0 +1,116 @@
|
||||
# Content
|
||||
|
||||
**Import:** `import { Content, type ContentProps } from "@opal/components";`
|
||||
|
||||
A two-axis layout component for displaying icon + title + description rows. Routes to an internal layout based on the `sizePreset` and `variant` combination.
|
||||
|
||||
## Two-Axis Architecture
|
||||
|
||||
### `sizePreset` — controls sizing (icon, padding, gap, font)
|
||||
|
||||
#### HeadingLayout presets
|
||||
|
||||
| Preset | Icon | Icon padding | Gap | Title font | Line-height |
|
||||
|---|---|---|---|---|---|
|
||||
| `headline` | 2rem (32px) | `p-0.5` (2px) | 0.25rem (4px) | `font-heading-h2` | 2.25rem (36px) |
|
||||
| `section` | 1.25rem (20px) | `p-1` (4px) | 0rem | `font-heading-h3` | 1.75rem (28px) |
|
||||
|
||||
#### LabelLayout presets
|
||||
|
||||
| Preset | Icon | Icon padding | Icon color | Gap | Title font | Line-height |
|
||||
|---|---|---|---|---|---|---|
|
||||
| `main-content` | 1rem (16px) | `p-1` (4px) | `text-04` | 0.125rem (2px) | `font-main-content-emphasis` | 1.5rem (24px) |
|
||||
| `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) |
|
||||
|
||||
> Icon container height (icon + 2 x padding) always equals the title line-height.
|
||||
|
||||
### `variant` — controls structure / layout
|
||||
|
||||
| variant | Description |
|
||||
|---|---|
|
||||
| `heading` | Icon on **top** (flex-col) — HeadingLayout |
|
||||
| `section` | Icon **inline** (flex-row) — HeadingLayout or LabelLayout |
|
||||
| `body` | Body text layout — BodyLayout (future) |
|
||||
|
||||
### Valid Combinations -> Internal Routing
|
||||
|
||||
| sizePreset | variant | Routes to |
|
||||
|---|---|---|
|
||||
| `headline` / `section` | `heading` | **HeadingLayout** (icon on top) |
|
||||
| `headline` / `section` | `section` | **HeadingLayout** (icon inline) |
|
||||
| `main-content` / `main-ui` / `secondary` | `section` | **LabelLayout** |
|
||||
| `main-content` / `main-ui` / `secondary` | `body` | BodyLayout (future) |
|
||||
|
||||
Invalid combinations (e.g. `sizePreset="headline" + variant="body"`) are excluded at the type level.
|
||||
|
||||
## Props
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|---|---|---|---|
|
||||
| `sizePreset` | `SizePreset` | `"headline"` | Size preset (see tables above) |
|
||||
| `variant` | `ContentVariant` | `"heading"` | Layout variant (see table above) |
|
||||
| `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 |
|
||||
| `onTitleChange` | `(newTitle: string) => void` | — | Called when user commits an edit |
|
||||
|
||||
## Internal Layouts
|
||||
|
||||
### HeadingLayout
|
||||
|
||||
For `headline` / `section` presets. Supports `variant="heading"` (icon on top) and `variant="section"` (icon inline). Description is always `font-secondary-body text-text-03`.
|
||||
|
||||
### LabelLayout
|
||||
|
||||
For `main-content` / `main-ui` / `secondary` presets. Always inline. Both `icon` and `description` are optional. Description is always `font-secondary-body text-text-03`.
|
||||
|
||||
## Usage Examples
|
||||
|
||||
```tsx
|
||||
import { Content } from "@opal/components";
|
||||
import SvgSearch from "@opal/icons/search";
|
||||
|
||||
// HeadingLayout — headline, icon on top
|
||||
<Content
|
||||
icon={SvgSearch}
|
||||
sizePreset="headline"
|
||||
variant="heading"
|
||||
title="Agent Settings"
|
||||
description="Configure your agent's behavior"
|
||||
/>
|
||||
|
||||
// HeadingLayout — section, icon inline
|
||||
<Content
|
||||
icon={SvgSearch}
|
||||
sizePreset="section"
|
||||
variant="section"
|
||||
title="Data Sources"
|
||||
description="Connected integrations"
|
||||
/>
|
||||
|
||||
// LabelLayout — with icon and description
|
||||
<Content
|
||||
icon={SvgSearch}
|
||||
sizePreset="main-ui"
|
||||
title="Instructions"
|
||||
description="Agent system prompt"
|
||||
/>
|
||||
|
||||
// LabelLayout — title only (no icon, no description)
|
||||
<Content
|
||||
sizePreset="main-content"
|
||||
title="Featured Agent"
|
||||
/>
|
||||
|
||||
// Editable title
|
||||
<Content
|
||||
icon={SvgSearch}
|
||||
sizePreset="headline"
|
||||
variant="heading"
|
||||
title="My Agent"
|
||||
editable
|
||||
onTitleChange={(newTitle) => save(newTitle)}
|
||||
/>
|
||||
```
|
||||
139
web/lib/opal/src/components/layouts/Content/components.tsx
Normal file
139
web/lib/opal/src/components/layouts/Content/components.tsx
Normal file
@@ -0,0 +1,139 @@
|
||||
import "@opal/components/layouts/Content/styles.css";
|
||||
|
||||
import {
|
||||
BodyLayout,
|
||||
type BodyOrientation,
|
||||
type BodyProminence,
|
||||
} from "@opal/components/layouts/Content/BodyLayout";
|
||||
import {
|
||||
HeadingLayout,
|
||||
type HeadingLayoutProps,
|
||||
} from "@opal/components/layouts/Content/HeadingLayout";
|
||||
import {
|
||||
LabelLayout,
|
||||
type LabelLayoutProps,
|
||||
} from "@opal/components/layouts/Content/LabelLayout";
|
||||
import type { IconFunctionComponent } from "@opal/types";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Shared types
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
type SizePreset =
|
||||
| "headline"
|
||||
| "section"
|
||||
| "main-content"
|
||||
| "main-ui"
|
||||
| "secondary";
|
||||
|
||||
type ContentVariant = "heading" | "section" | "body";
|
||||
|
||||
interface ContentBaseProps {
|
||||
/** Optional icon component. */
|
||||
icon?: IconFunctionComponent;
|
||||
|
||||
/** Main title text. */
|
||||
title: string;
|
||||
|
||||
/** Optional description below the title. */
|
||||
description?: string;
|
||||
|
||||
/** Enable inline editing of the title. */
|
||||
editable?: boolean;
|
||||
|
||||
/** Called when the user commits an edit. */
|
||||
onTitleChange?: (newTitle: string) => void;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Discriminated union: valid sizePreset × variant combinations
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
type HeadingContentProps = ContentBaseProps & {
|
||||
/** Size preset. Default: `"headline"`. */
|
||||
sizePreset?: "headline" | "section";
|
||||
/** Variant. Default: `"heading"` for heading-eligible presets. */
|
||||
variant?: "heading" | "section";
|
||||
};
|
||||
|
||||
type LabelContentProps = ContentBaseProps & {
|
||||
sizePreset: "main-content" | "main-ui" | "secondary";
|
||||
variant?: "section";
|
||||
/** When `true`, renders "(Optional)" beside the title in the muted font variant. */
|
||||
optional?: boolean;
|
||||
/** Auxiliary status icon rendered beside the title. */
|
||||
auxIcon?: "info-gray" | "info-blue" | "warning" | "error";
|
||||
};
|
||||
|
||||
/** BodyLayout does not support descriptions or inline editing. */
|
||||
type BodyContentProps = Omit<
|
||||
ContentBaseProps,
|
||||
"description" | "editable" | "onTitleChange"
|
||||
> & {
|
||||
sizePreset: "main-content" | "main-ui" | "secondary";
|
||||
variant: "body";
|
||||
/** Layout orientation. Default: `"inline"`. */
|
||||
orientation?: BodyOrientation;
|
||||
/** Title prominence. Default: `"default"`. */
|
||||
prominence?: BodyProminence;
|
||||
};
|
||||
|
||||
type ContentProps = HeadingContentProps | LabelContentProps | BodyContentProps;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Content — routes to the appropriate internal layout
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function Content(props: ContentProps) {
|
||||
const { sizePreset = "headline", variant = "heading", ...rest } = props;
|
||||
|
||||
// Heading layout: headline/section presets with heading/section variant
|
||||
if (sizePreset === "headline" || sizePreset === "section") {
|
||||
return (
|
||||
<HeadingLayout
|
||||
sizePreset={sizePreset}
|
||||
variant={variant as HeadingLayoutProps["variant"]}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// Label layout: main-content/main-ui/secondary with section variant
|
||||
if (variant === "section" || variant === "heading") {
|
||||
return (
|
||||
<LabelLayout
|
||||
sizePreset={sizePreset}
|
||||
{...(rest as Omit<LabelLayoutProps, "sizePreset">)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// Body layout: main-content/main-ui/secondary with body variant
|
||||
if (variant === "body") {
|
||||
return (
|
||||
<BodyLayout
|
||||
sizePreset={sizePreset}
|
||||
{...(rest as Omit<
|
||||
React.ComponentProps<typeof BodyLayout>,
|
||||
"sizePreset"
|
||||
>)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Exports
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
Content,
|
||||
type ContentProps,
|
||||
type SizePreset,
|
||||
type ContentVariant,
|
||||
type HeadingContentProps,
|
||||
type LabelContentProps,
|
||||
type BodyContentProps,
|
||||
};
|
||||
252
web/lib/opal/src/components/layouts/Content/styles.css
Normal file
252
web/lib/opal/src/components/layouts/Content/styles.css
Normal file
@@ -0,0 +1,252 @@
|
||||
/* ---------------------------------------------------------------------------
|
||||
Content — HeadingLayout
|
||||
|
||||
Two icon placement modes (driven by variant):
|
||||
left (variant="section") : flex-row — icon beside content
|
||||
top (variant="heading") : flex-col — icon above content
|
||||
|
||||
Sizing (icon size, gap, padding, font, line-height) is driven by the
|
||||
sizePreset prop via inline styles + Tailwind classes in the component.
|
||||
--------------------------------------------------------------------------- */
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
Layout — icon placement
|
||||
--------------------------------------------------------------------------- */
|
||||
|
||||
.opal-content-heading {
|
||||
@apply flex items-start;
|
||||
}
|
||||
|
||||
.opal-content-heading[data-icon-placement="left"] {
|
||||
@apply flex-row;
|
||||
}
|
||||
|
||||
.opal-content-heading[data-icon-placement="top"] {
|
||||
@apply flex-col;
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
Icon
|
||||
--------------------------------------------------------------------------- */
|
||||
|
||||
.opal-content-heading-icon-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.opal-content-heading-icon {
|
||||
color: var(--text-04);
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
Body column
|
||||
--------------------------------------------------------------------------- */
|
||||
|
||||
.opal-content-heading-body {
|
||||
@apply flex flex-1 flex-col items-start;
|
||||
min-width: 0.0625rem;
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
Title row — title (or input) + edit button
|
||||
--------------------------------------------------------------------------- */
|
||||
|
||||
.opal-content-heading-title-row {
|
||||
@apply flex items-center w-full;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.opal-content-heading-title {
|
||||
@apply text-left overflow-hidden;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 1;
|
||||
padding: 0 0.125rem;
|
||||
min-width: 0.0625rem;
|
||||
}
|
||||
|
||||
.opal-content-heading-input {
|
||||
@apply flex-1 bg-transparent outline-none border-none;
|
||||
padding: 0 0.125rem;
|
||||
min-width: 0.0625rem;
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
Edit button — visible only on hover of the outer container
|
||||
--------------------------------------------------------------------------- */
|
||||
|
||||
.opal-content-heading-edit-button {
|
||||
@apply opacity-0 transition-opacity shrink-0;
|
||||
}
|
||||
|
||||
.opal-content-heading:hover .opal-content-heading-edit-button {
|
||||
@apply opacity-100;
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
Description
|
||||
--------------------------------------------------------------------------- */
|
||||
|
||||
.opal-content-heading-description {
|
||||
@apply text-left w-full;
|
||||
padding: 0 0.125rem;
|
||||
}
|
||||
|
||||
/* ===========================================================================
|
||||
Content — LabelLayout
|
||||
|
||||
Always inline (flex-row). Icon color varies per sizePreset and is applied
|
||||
via Tailwind class from the component.
|
||||
=========================================================================== */
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
Layout
|
||||
--------------------------------------------------------------------------- */
|
||||
|
||||
.opal-content-label {
|
||||
@apply flex flex-row items-start;
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
Icon
|
||||
--------------------------------------------------------------------------- */
|
||||
|
||||
.opal-content-label-icon-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
Body column
|
||||
--------------------------------------------------------------------------- */
|
||||
|
||||
.opal-content-label-body {
|
||||
@apply flex flex-1 flex-col items-start;
|
||||
min-width: 0.0625rem;
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
Title row — title (or input) + edit button
|
||||
--------------------------------------------------------------------------- */
|
||||
|
||||
.opal-content-label-title-row {
|
||||
@apply flex items-center w-full;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.opal-content-label-title {
|
||||
@apply text-left overflow-hidden;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 1;
|
||||
padding: 0 0.125rem;
|
||||
min-width: 0.0625rem;
|
||||
}
|
||||
|
||||
.opal-content-label-input-sizer {
|
||||
display: inline-grid;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.opal-content-label-input-sizer > * {
|
||||
grid-area: 1 / 1;
|
||||
padding: 0 0.125rem;
|
||||
min-width: 0.0625rem;
|
||||
}
|
||||
|
||||
.opal-content-label-input-mirror {
|
||||
visibility: hidden;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.opal-content-label-input {
|
||||
@apply bg-transparent outline-none border-none;
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
Aux icon
|
||||
--------------------------------------------------------------------------- */
|
||||
|
||||
.opal-content-label-aux-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
Edit button — visible only on hover of the outer container
|
||||
--------------------------------------------------------------------------- */
|
||||
|
||||
.opal-content-label-edit-button {
|
||||
@apply opacity-0 transition-opacity shrink-0;
|
||||
}
|
||||
|
||||
.opal-content-label:hover .opal-content-label-edit-button {
|
||||
@apply opacity-100;
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
Description
|
||||
--------------------------------------------------------------------------- */
|
||||
|
||||
.opal-content-label-description {
|
||||
@apply text-left w-full;
|
||||
padding: 0 0.125rem;
|
||||
}
|
||||
|
||||
/* ===========================================================================
|
||||
Content — BodyLayout
|
||||
|
||||
Three orientation modes (driven by orientation prop):
|
||||
inline : flex-row — icon left, title right
|
||||
vertical: flex-col — icon top, title below
|
||||
reverse : flex-row-reverse — title left, icon right
|
||||
|
||||
Icon color is always text-03. Title color varies by prominence
|
||||
(text-04 default, text-03 muted) and is applied via Tailwind class.
|
||||
=========================================================================== */
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
Layout — orientation
|
||||
--------------------------------------------------------------------------- */
|
||||
|
||||
.opal-content-body {
|
||||
@apply flex items-start;
|
||||
}
|
||||
|
||||
.opal-content-body[data-orientation="inline"] {
|
||||
@apply flex-row;
|
||||
}
|
||||
|
||||
.opal-content-body[data-orientation="vertical"] {
|
||||
@apply flex-col;
|
||||
}
|
||||
|
||||
.opal-content-body[data-orientation="reverse"] {
|
||||
@apply flex-row-reverse;
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
Icon
|
||||
--------------------------------------------------------------------------- */
|
||||
|
||||
.opal-content-body-icon-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
Title
|
||||
--------------------------------------------------------------------------- */
|
||||
|
||||
.opal-content-body-title {
|
||||
@apply text-left overflow-hidden;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 1;
|
||||
padding: 0 0.125rem;
|
||||
min-width: 0.0625rem;
|
||||
}
|
||||
61
web/lib/opal/src/components/tags/AuxiliaryTag/components.tsx
Normal file
61
web/lib/opal/src/components/tags/AuxiliaryTag/components.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
import "@opal/components/tags/AuxiliaryTag/styles.css";
|
||||
|
||||
import type { IconFunctionComponent } from "@opal/types";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Types
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
type AuxiliaryTagColor = "green" | "purple" | "blue" | "gray" | "amber";
|
||||
|
||||
interface AuxiliaryTagProps {
|
||||
/** Optional icon component. */
|
||||
icon?: IconFunctionComponent;
|
||||
|
||||
/** Tag label text. */
|
||||
title: string;
|
||||
|
||||
/** Color variant. Default: `"gray"`. */
|
||||
color?: AuxiliaryTagColor;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Color config
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const COLOR_CONFIG: Record<AuxiliaryTagColor, { bg: string; text: string }> = {
|
||||
green: { bg: "bg-theme-green-01", text: "text-theme-green-05" },
|
||||
blue: { bg: "bg-theme-blue-05", text: "text-theme-blue-01" },
|
||||
purple: { bg: "bg-theme-purple-05", text: "text-theme-purple-01" },
|
||||
amber: { bg: "bg-theme-amber-05", text: "text-theme-amber-01" },
|
||||
gray: { bg: "bg-background-tint-02", text: "text-text-03" },
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// AuxiliaryTag
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function AuxiliaryTag({
|
||||
icon: Icon,
|
||||
title,
|
||||
color = "gray",
|
||||
}: AuxiliaryTagProps) {
|
||||
const config = COLOR_CONFIG[color];
|
||||
|
||||
return (
|
||||
<div className={`opal-auxiliary-tag ${config.bg}`}>
|
||||
{Icon && (
|
||||
<div className="opal-auxiliary-tag-icon-container">
|
||||
<Icon className={`opal-auxiliary-tag-icon ${config.text}`} />
|
||||
</div>
|
||||
)}
|
||||
<span
|
||||
className={`opal-auxiliary-tag-title font-figure-small-value ${config.text}`}
|
||||
>
|
||||
{title}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { AuxiliaryTag, type AuxiliaryTagProps, type AuxiliaryTagColor };
|
||||
30
web/lib/opal/src/components/tags/AuxiliaryTag/styles.css
Normal file
30
web/lib/opal/src/components/tags/AuxiliaryTag/styles.css
Normal file
@@ -0,0 +1,30 @@
|
||||
/* ---------------------------------------------------------------------------
|
||||
AuxiliaryTag
|
||||
|
||||
Fixed height of 1rem (16px). Icon is 0.75rem (12px) with p-0.5 (2px)
|
||||
padding to match the font-figure-small-value line-height (12px).
|
||||
--------------------------------------------------------------------------- */
|
||||
|
||||
.opal-auxiliary-tag {
|
||||
@apply flex flex-row items-center shrink-0;
|
||||
height: 1rem;
|
||||
border-radius: 0.25rem;
|
||||
padding: 0 0.25rem;
|
||||
gap: 0.125rem;
|
||||
}
|
||||
|
||||
.opal-auxiliary-tag-icon-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0.125rem;
|
||||
}
|
||||
|
||||
.opal-auxiliary-tag-icon {
|
||||
width: 0.75rem;
|
||||
height: 0.75rem;
|
||||
}
|
||||
|
||||
.opal-auxiliary-tag-title {
|
||||
white-space: nowrap;
|
||||
}
|
||||
@@ -45,6 +45,7 @@ type InteractiveBaseVariantProps =
|
||||
* - `"md"` — 1.75rem (28px), standard compact size
|
||||
* - `"sm"` — 1.5rem (24px), for denser UIs
|
||||
* - `"xs"` — 1.25rem (20px), for inline elements
|
||||
* - `"2xs"` — 1rem (16px), for micro elements
|
||||
* - `"fit"` — Shrink-wraps to content height (`h-fit`), for variable-height layouts
|
||||
*/
|
||||
type InteractiveContainerHeightVariant =
|
||||
@@ -54,6 +55,7 @@ const interactiveContainerHeightVariants = {
|
||||
md: "h-[1.75rem]",
|
||||
sm: "h-[1.5rem]",
|
||||
xs: "h-[1.25rem]",
|
||||
"2xs": "h-[1rem]",
|
||||
fit: "h-fit",
|
||||
} as const;
|
||||
const interactiveContainerMinWidthVariants = {
|
||||
@@ -61,6 +63,7 @@ const interactiveContainerMinWidthVariants = {
|
||||
md: "min-w-[1.75rem]",
|
||||
sm: "min-w-[1.5rem]",
|
||||
xs: "min-w-[1.25rem]",
|
||||
"2xs": "min-w-[1rem]",
|
||||
fit: "",
|
||||
} as const;
|
||||
const interactiveContainerPaddingVariants = {
|
||||
@@ -68,7 +71,8 @@ const interactiveContainerPaddingVariants = {
|
||||
md: "p-1",
|
||||
sm: "p-1",
|
||||
xs: "p-0.5",
|
||||
fit: "",
|
||||
"2xs": "p-0.5",
|
||||
fit: "p-0",
|
||||
} as const;
|
||||
|
||||
/**
|
||||
@@ -82,6 +86,7 @@ type InteractiveContainerRoundingVariant =
|
||||
const interactiveContainerRoundingVariants = {
|
||||
default: "rounded-12",
|
||||
compact: "rounded-08",
|
||||
mini: "rounded-04",
|
||||
} as const;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -354,6 +359,7 @@ interface InteractiveContainerProps
|
||||
* - `"md"` — 1.75rem (28px), standard compact size
|
||||
* - `"sm"` — 1.5rem (24px), for denser UIs
|
||||
* - `"xs"` — 1.25rem (20px), for inline elements
|
||||
* - `"2xs"` — 1rem (16px), for micro elements
|
||||
* - `"fit"` — Shrink-wraps to content height (`h-fit`)
|
||||
*
|
||||
* @default "lg"
|
||||
@@ -377,7 +383,7 @@ interface InteractiveContainerProps
|
||||
* // Standard card-like container
|
||||
* <Interactive.Base>
|
||||
* <Interactive.Container border>
|
||||
* <LineItemLayout icon={SvgIcon} title="Option" />
|
||||
* <Content icon={SvgIcon} title="Option" />
|
||||
* </Interactive.Container>
|
||||
* </Interactive.Base>
|
||||
*
|
||||
|
||||
@@ -136,25 +136,64 @@
|
||||
--purple-05: #f1ebfa;
|
||||
--purple-01: #f9f7fd;
|
||||
|
||||
/* Neon Scale */
|
||||
/* Neon Scale
|
||||
Base vars (--neon-X) are the /40 level.
|
||||
Alpha variants use -aXX suffix (e.g. -a60 = 40 at 60% opacity).
|
||||
Numeric suffixes are Figma scale levels (e.g. -50 = Neon/X/50). */
|
||||
--neon-yellow-90: #5a581d;
|
||||
--neon-yellow-80: #979430;
|
||||
--neon-yellow-50: #ece600;
|
||||
--neon-yellow: #fef800;
|
||||
--neon-yellow-60: #fef80099;
|
||||
--neon-yellow-30: #fef8004d;
|
||||
--neon-yellow-a60: #fef80099;
|
||||
--neon-yellow-a30: #fef8004d;
|
||||
--neon-yellow-20: #fcfa8f;
|
||||
--neon-yellow-05: #f9faeb;
|
||||
|
||||
--neon-amber-90: #625025;
|
||||
--neon-amber-80: #a68018;
|
||||
--neon-amber-60: #d9a500;
|
||||
--neon-amber-50: #ecb400;
|
||||
--neon-amber: #ffc733;
|
||||
--neon-amber-60: #ffc73399;
|
||||
--neon-amber-30: #ffc7334d;
|
||||
--neon-amber-a60: #ffc73399;
|
||||
--neon-amber-a30: #ffc7334d;
|
||||
--neon-amber-20: #ffd985;
|
||||
--neon-amber-05: #fef8ea;
|
||||
|
||||
--neon-sky-90: #204f67;
|
||||
--neon-sky-80: #3989b3;
|
||||
--neon-sky-50: #1ebcff;
|
||||
--neon-sky: #4dc3ff;
|
||||
--neon-sky-60: #4dc3ff99;
|
||||
--neon-sky-30: #4dc3ff4d;
|
||||
--neon-sky-a60: #4dc3ff99;
|
||||
--neon-sky-a30: #4dc3ff4d;
|
||||
--neon-sky-20: #93d8ff;
|
||||
--neon-sky-05: #f2faff;
|
||||
|
||||
--neon-cyan-90: #1a5e5d;
|
||||
--neon-cyan-80: #009a99;
|
||||
--neon-cyan-50: #00ebea;
|
||||
--neon-cyan: #00f9f9;
|
||||
--neon-cyan-60: #00f9f999;
|
||||
--neon-cyan-30: #00f9f94d;
|
||||
--neon-cyan-a60: #00f9f999;
|
||||
--neon-cyan-a30: #00f9f94d;
|
||||
--neon-cyan-20: #62fefd;
|
||||
--neon-cyan-05: #eafdfc;
|
||||
|
||||
--neon-lime-90: #3f5b39;
|
||||
--neon-lime-80: #639e56;
|
||||
--neon-lime-60: #53cd32;
|
||||
--neon-lime: #6dff46;
|
||||
--neon-lime-60: #6dff4699;
|
||||
--neon-lime-30: #6dff464d;
|
||||
--neon-lime-a60: #6dff4699;
|
||||
--neon-lime-a30: #6dff464d;
|
||||
--neon-lime-20: #a8ff94;
|
||||
--neon-lime-05: #f2fcf0;
|
||||
|
||||
--neon-magenta-90: #654666;
|
||||
--neon-magenta-80: #ab6bac;
|
||||
--neon-magenta-50: #f198f2;
|
||||
--neon-magenta: #fea1ff;
|
||||
--neon-magenta-60: #fea1ff99;
|
||||
--neon-magenta-30: #fea1ff4d;
|
||||
--neon-magenta-a60: #fea1ff99;
|
||||
--neon-magenta-a30: #fea1ff4d;
|
||||
--neon-magenta-20: #fec4fe;
|
||||
--neon-magenta-05: #fff5ff;
|
||||
|
||||
/* Stone Scale */
|
||||
--stone-98: #0b0b0f;
|
||||
@@ -254,8 +293,8 @@
|
||||
--background-neutral-02: var(--grey-06);
|
||||
--background-neutral-03: var(--grey-10);
|
||||
--background-neutral-04: var(--grey-20);
|
||||
--background-neutral-inverted-04: var(--grey-75);
|
||||
--background-neutral-inverted-03: var(--grey-80);
|
||||
--background-neutral-inverted-04: var(--grey-60);
|
||||
--background-neutral-inverted-03: var(--grey-75);
|
||||
--background-neutral-inverted-02: var(--grey-85);
|
||||
--background-neutral-inverted-01: var(--grey-90);
|
||||
--background-neutral-inverted-00: var(--grey-100);
|
||||
@@ -269,7 +308,7 @@
|
||||
--background-tint-02: var(--tint-05);
|
||||
--background-tint-03: var(--tint-10);
|
||||
--background-tint-04: var(--tint-20);
|
||||
--background-tint-inverted-04: var(--tint-80);
|
||||
--background-tint-inverted-04: var(--tint-60);
|
||||
--background-tint-inverted-03: var(--tint-85);
|
||||
--background-tint-inverted-02: var(--tint-90);
|
||||
--background-tint-inverted-01: var(--tint-95);
|
||||
@@ -296,7 +335,7 @@
|
||||
|
||||
/* Theme / Gradient */
|
||||
--theme-gradient-05: var(--tint-50);
|
||||
--theme-gradient-00: var(--grey-00);
|
||||
--theme-gradient-00: var(--grey-100);
|
||||
|
||||
/* Theme / Red */
|
||||
--theme-red-05: var(--red-50);
|
||||
@@ -311,15 +350,15 @@
|
||||
--theme-orange-01: var(--orange-05);
|
||||
|
||||
/* Theme / Amber */
|
||||
--theme-amber-05: var(--neon-amber);
|
||||
--theme-amber-05: var(--neon-amber-50);
|
||||
--theme-amber-04: var(--neon-amber);
|
||||
--theme-amber-02: var(--neon-amber-30);
|
||||
--theme-amber-01: var(--neon-amber-30);
|
||||
--theme-amber-02: var(--neon-amber-20);
|
||||
--theme-amber-01: var(--neon-amber-05);
|
||||
|
||||
/* Theme / Yellow */
|
||||
--theme-yellow-05: var(--neon-yellow);
|
||||
--theme-yellow-02: var(--neon-yellow-30);
|
||||
--theme-yellow-01: var(--neon-yellow-30);
|
||||
--theme-yellow-05: var(--neon-yellow-50);
|
||||
--theme-yellow-02: var(--neon-yellow-20);
|
||||
--theme-yellow-01: var(--neon-yellow-05);
|
||||
|
||||
/* Theme / Green */
|
||||
--theme-green-05: var(--green-60);
|
||||
@@ -328,18 +367,18 @@
|
||||
|
||||
/* Theme / Lime */
|
||||
--theme-lime-05: var(--neon-lime-60);
|
||||
--theme-lime-02: var(--neon-lime-30);
|
||||
--theme-lime-01: var(--neon-lime-30);
|
||||
--theme-lime-02: var(--neon-lime-20);
|
||||
--theme-lime-01: var(--neon-lime-05);
|
||||
|
||||
/* Theme / Cyan */
|
||||
--theme-cyan-05: var(--neon-cyan);
|
||||
--theme-cyan-02: var(--neon-cyan-30);
|
||||
--theme-cyan-01: var(--neon-cyan-30);
|
||||
--theme-cyan-05: var(--neon-cyan-50);
|
||||
--theme-cyan-02: var(--neon-cyan-20);
|
||||
--theme-cyan-01: var(--neon-cyan-05);
|
||||
|
||||
/* Theme / Sky */
|
||||
--theme-sky-05: var(--neon-sky);
|
||||
--theme-sky-02: var(--neon-sky-30);
|
||||
--theme-sky-01: var(--neon-sky-30);
|
||||
--theme-sky-05: var(--neon-sky-50);
|
||||
--theme-sky-02: var(--neon-sky-20);
|
||||
--theme-sky-01: var(--neon-sky-05);
|
||||
|
||||
/* Theme / Blue */
|
||||
--theme-blue-05: var(--blue-50);
|
||||
@@ -352,9 +391,9 @@
|
||||
--theme-purple-01: var(--purple-05);
|
||||
|
||||
/* Theme / Magenta */
|
||||
--theme-magenta-05: var(--neon-magenta);
|
||||
--theme-magenta-02: var(--neon-magenta-30);
|
||||
--theme-magenta-01: var(--neon-magenta-30);
|
||||
--theme-magenta-05: var(--neon-magenta-50);
|
||||
--theme-magenta-02: var(--neon-magenta-20);
|
||||
--theme-magenta-01: var(--neon-magenta-05);
|
||||
|
||||
/* Status */
|
||||
--status-success-05: var(--green-50);
|
||||
@@ -408,10 +447,10 @@
|
||||
--code-definition: var(--orange-55);
|
||||
|
||||
/* Highlight */
|
||||
--highlight-match: var(--neon-yellow-30);
|
||||
--highlight-selection: var(--neon-sky-30);
|
||||
--highlight-active: var(--neon-amber-60);
|
||||
--highlight-accent: var(--neon-magenta-60);
|
||||
--highlight-match: var(--neon-yellow-a30);
|
||||
--highlight-selection: var(--neon-sky-a30);
|
||||
--highlight-active: var(--neon-amber-a60);
|
||||
--highlight-accent: var(--neon-magenta-a60);
|
||||
|
||||
/* Shadow */
|
||||
--shadow-01: var(--alpha-grey-100-05);
|
||||
@@ -419,7 +458,7 @@
|
||||
--shadow-03: var(--alpha-grey-100-20);
|
||||
|
||||
/* Mask */
|
||||
--mask-01: var(--alpha-grey-100-10);
|
||||
--mask-01: var(--alpha-grey-00-10);
|
||||
--mask-02: var(--alpha-grey-100-20);
|
||||
--mask-03: var(--alpha-grey-100-40);
|
||||
|
||||
@@ -514,13 +553,13 @@
|
||||
/* Theme / Amber */
|
||||
--theme-amber-05: var(--neon-amber);
|
||||
--theme-amber-04: var(--neon-amber-60);
|
||||
--theme-amber-02: var(--neon-amber-60);
|
||||
--theme-amber-01: var(--neon-amber-60);
|
||||
--theme-amber-02: var(--neon-amber-80);
|
||||
--theme-amber-01: var(--neon-amber-90);
|
||||
|
||||
/* Theme / Yellow */
|
||||
--theme-yellow-05: var(--neon-yellow);
|
||||
--theme-yellow-02: var(--neon-yellow-60);
|
||||
--theme-yellow-01: var(--neon-yellow-60);
|
||||
--theme-yellow-02: var(--neon-yellow-80);
|
||||
--theme-yellow-01: var(--neon-yellow-90);
|
||||
|
||||
/* Theme / Green */
|
||||
--theme-green-05: var(--green-50);
|
||||
@@ -529,18 +568,18 @@
|
||||
|
||||
/* Theme / Lime */
|
||||
--theme-lime-05: var(--neon-lime);
|
||||
--theme-lime-02: var(--neon-lime-60);
|
||||
--theme-lime-01: var(--neon-lime-60);
|
||||
--theme-lime-02: var(--neon-lime-80);
|
||||
--theme-lime-01: var(--neon-lime-90);
|
||||
|
||||
/* Theme / Cyan */
|
||||
--theme-cyan-05: var(--neon-cyan);
|
||||
--theme-cyan-02: var(--neon-cyan-60);
|
||||
--theme-cyan-01: var(--neon-cyan-60);
|
||||
--theme-cyan-02: var(--neon-cyan-80);
|
||||
--theme-cyan-01: var(--neon-cyan-90);
|
||||
|
||||
/* Theme / Sky */
|
||||
--theme-sky-05: var(--neon-sky);
|
||||
--theme-sky-02: var(--neon-sky-60);
|
||||
--theme-sky-01: var(--neon-sky-60);
|
||||
--theme-sky-02: var(--neon-sky-80);
|
||||
--theme-sky-01: var(--neon-sky-90);
|
||||
|
||||
/* Theme / Blue */
|
||||
--theme-blue-05: var(--blue-45);
|
||||
@@ -554,8 +593,8 @@
|
||||
|
||||
/* Theme / Magenta */
|
||||
--theme-magenta-05: var(--neon-magenta);
|
||||
--theme-magenta-02: var(--neon-magenta-60);
|
||||
--theme-magenta-01: var(--neon-magenta-60);
|
||||
--theme-magenta-02: var(--neon-magenta-80);
|
||||
--theme-magenta-01: var(--neon-magenta-90);
|
||||
|
||||
/* Status */
|
||||
--status-success-05: var(--green-50);
|
||||
@@ -609,10 +648,10 @@
|
||||
--code-definition: var(--orange-50);
|
||||
|
||||
/* Highlight */
|
||||
--highlight-match: var(--neon-yellow-30);
|
||||
--highlight-selection: var(--neon-sky-30);
|
||||
--highlight-active: var(--neon-amber-60);
|
||||
--highlight-accent: var(--neon-magenta-60);
|
||||
--highlight-match: var(--neon-yellow-a30);
|
||||
--highlight-selection: var(--neon-sky-a30);
|
||||
--highlight-active: var(--neon-amber-a60);
|
||||
--highlight-accent: var(--neon-magenta-a60);
|
||||
|
||||
/* Shadow */
|
||||
--shadow-01: var(--alpha-grey-00-05);
|
||||
@@ -620,7 +659,7 @@
|
||||
--shadow-03: var(--alpha-grey-00-20);
|
||||
|
||||
/* Mask */
|
||||
--mask-01: var(--alpha-grey-100-10);
|
||||
--mask-01: var(--alpha-grey-00-10);
|
||||
--mask-02: var(--alpha-grey-100-20);
|
||||
--mask-03: var(--alpha-grey-100-40);
|
||||
|
||||
|
||||
557
web/src/app/storybook/page.tsx
Normal file
557
web/src/app/storybook/page.tsx
Normal file
@@ -0,0 +1,557 @@
|
||||
"use client";
|
||||
|
||||
import { AuxiliaryTag, Content } from "@opal/components";
|
||||
import SvgSearch from "@opal/icons/search";
|
||||
import SvgStar from "@opal/icons/star";
|
||||
|
||||
export default function StorybookPage() {
|
||||
return (
|
||||
<div style={{ padding: "2rem", maxWidth: 960, margin: "0 auto" }}>
|
||||
<h1 style={{ fontSize: 28, fontWeight: 700, marginBottom: "2rem" }}>
|
||||
Content — Storybook
|
||||
</h1>
|
||||
|
||||
{/* ================================================================= */}
|
||||
{/* AuxiliaryTag */}
|
||||
{/* ================================================================= */}
|
||||
|
||||
<LayoutGroup label="AuxiliaryTag">
|
||||
<Section label="colors">
|
||||
<div style={{ display: "flex", gap: "0.5rem", flexWrap: "wrap" }}>
|
||||
<AuxiliaryTag title="Green" color="green" />
|
||||
<AuxiliaryTag title="Blue" color="blue" />
|
||||
<AuxiliaryTag title="Purple" color="purple" />
|
||||
<AuxiliaryTag title="Amber" color="amber" />
|
||||
<AuxiliaryTag title="Gray" color="gray" />
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section label="with icon">
|
||||
<div style={{ display: "flex", gap: "0.5rem", flexWrap: "wrap" }}>
|
||||
<AuxiliaryTag icon={SvgStar} title="Green" color="green" />
|
||||
<AuxiliaryTag icon={SvgStar} title="Blue" color="blue" />
|
||||
<AuxiliaryTag icon={SvgStar} title="Purple" color="purple" />
|
||||
<AuxiliaryTag icon={SvgStar} title="Amber" color="amber" />
|
||||
<AuxiliaryTag icon={SvgStar} title="Gray" color="gray" />
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section label="icon only">
|
||||
<div style={{ display: "flex", gap: "0.5rem", flexWrap: "wrap" }}>
|
||||
<AuxiliaryTag icon={SvgStar} title="" color="green" />
|
||||
<AuxiliaryTag icon={SvgStar} title="" color="blue" />
|
||||
</div>
|
||||
</Section>
|
||||
</LayoutGroup>
|
||||
|
||||
{/* ================================================================= */}
|
||||
{/* HeadingLayout */}
|
||||
{/* ================================================================= */}
|
||||
|
||||
<LayoutGroup label="HeadingLayout">
|
||||
{/* ── headline + heading ── */}
|
||||
<Section label="headline / heading (icon top)">
|
||||
<Content
|
||||
icon={SvgSearch}
|
||||
sizePreset="headline"
|
||||
variant="heading"
|
||||
title="Headline Heading"
|
||||
description="Icon placed above the content"
|
||||
/>
|
||||
<Content
|
||||
icon={SvgSearch}
|
||||
sizePreset="headline"
|
||||
variant="heading"
|
||||
title="Click edit to rename"
|
||||
description="Editable headline with icon on top"
|
||||
editable
|
||||
onTitleChange={(v) => console.log("title changed:", v)}
|
||||
/>
|
||||
</Section>
|
||||
|
||||
{/* ── headline + section ── */}
|
||||
<Section label="headline / section (icon inline)">
|
||||
<Content
|
||||
icon={SvgSearch}
|
||||
sizePreset="headline"
|
||||
variant="section"
|
||||
title="Headline Section"
|
||||
description="Icon placed inline with the content"
|
||||
/>
|
||||
<Content
|
||||
icon={SvgSearch}
|
||||
sizePreset="headline"
|
||||
variant="section"
|
||||
title="Click edit to rename"
|
||||
description="Editable headline with icon inline"
|
||||
editable
|
||||
onTitleChange={(v) => console.log("title changed:", v)}
|
||||
/>
|
||||
</Section>
|
||||
|
||||
{/* ── section + heading ── */}
|
||||
<Section label="section / heading (icon top)">
|
||||
<Content
|
||||
icon={SvgSearch}
|
||||
sizePreset="section"
|
||||
variant="heading"
|
||||
title="Section Heading"
|
||||
description="Smaller preset, icon placed above the content"
|
||||
/>
|
||||
<Content
|
||||
icon={SvgSearch}
|
||||
sizePreset="section"
|
||||
variant="heading"
|
||||
title="Click edit to rename"
|
||||
description="Editable section with icon on top"
|
||||
editable
|
||||
onTitleChange={(v) => console.log("title changed:", v)}
|
||||
/>
|
||||
</Section>
|
||||
|
||||
{/* ── section + section ── */}
|
||||
<Section label="section / section (icon inline)">
|
||||
<Content
|
||||
icon={SvgSearch}
|
||||
sizePreset="section"
|
||||
variant="section"
|
||||
title="Section Section"
|
||||
description="Smaller preset, icon placed inline"
|
||||
/>
|
||||
<Content
|
||||
icon={SvgSearch}
|
||||
sizePreset="section"
|
||||
variant="section"
|
||||
title="Click edit to rename"
|
||||
description="Editable section with icon inline"
|
||||
editable
|
||||
onTitleChange={(v) => console.log("title changed:", v)}
|
||||
/>
|
||||
</Section>
|
||||
|
||||
{/* ── optional props ── */}
|
||||
<Section label="optional props">
|
||||
<Content
|
||||
sizePreset="headline"
|
||||
variant="heading"
|
||||
title="No icon"
|
||||
description="Only title and description"
|
||||
/>
|
||||
<Content
|
||||
icon={SvgSearch}
|
||||
sizePreset="headline"
|
||||
variant="heading"
|
||||
title="No description"
|
||||
/>
|
||||
<Content sizePreset="headline" variant="heading" title="Title only" />
|
||||
</Section>
|
||||
</LayoutGroup>
|
||||
|
||||
{/* ================================================================= */}
|
||||
{/* LabelLayout */}
|
||||
{/* ================================================================= */}
|
||||
|
||||
<LayoutGroup label="LabelLayout">
|
||||
{/* ── main-content ── */}
|
||||
<Section label="main-content">
|
||||
<Content
|
||||
icon={SvgSearch}
|
||||
sizePreset="main-content"
|
||||
title="Main Content Label"
|
||||
description="font-main-content-emphasis title, stroke-text-04 icon"
|
||||
/>
|
||||
<Content
|
||||
icon={SvgSearch}
|
||||
sizePreset="main-content"
|
||||
title="Click edit to rename"
|
||||
description="Editable main-content label"
|
||||
editable
|
||||
onTitleChange={(v) => console.log("title changed:", v)}
|
||||
/>
|
||||
</Section>
|
||||
|
||||
{/* ── main-ui ── */}
|
||||
<Section label="main-ui">
|
||||
<Content
|
||||
icon={SvgSearch}
|
||||
sizePreset="main-ui"
|
||||
title="Main UI Label"
|
||||
description="font-main-ui-action title, stroke-text-03 icon"
|
||||
/>
|
||||
<Content
|
||||
icon={SvgSearch}
|
||||
sizePreset="main-ui"
|
||||
title="Click edit to rename"
|
||||
description="Editable main-ui label"
|
||||
editable
|
||||
onTitleChange={(v) => console.log("title changed:", v)}
|
||||
/>
|
||||
</Section>
|
||||
|
||||
{/* ── secondary ── */}
|
||||
<Section label="secondary">
|
||||
<Content
|
||||
icon={SvgSearch}
|
||||
sizePreset="secondary"
|
||||
title="Secondary Label"
|
||||
description="font-secondary-action title, stroke-text-04 icon"
|
||||
/>
|
||||
<Content
|
||||
icon={SvgSearch}
|
||||
sizePreset="secondary"
|
||||
title="Click edit to rename"
|
||||
description="Editable secondary label"
|
||||
editable
|
||||
onTitleChange={(v) => console.log("title changed:", v)}
|
||||
/>
|
||||
</Section>
|
||||
|
||||
{/* ── optional indicator ── */}
|
||||
<Section label="optional indicator">
|
||||
<Content
|
||||
icon={SvgSearch}
|
||||
sizePreset="main-content"
|
||||
title="Main Content"
|
||||
description="With (Optional) indicator"
|
||||
optional
|
||||
/>
|
||||
<Content
|
||||
icon={SvgSearch}
|
||||
sizePreset="main-ui"
|
||||
title="Main UI"
|
||||
description="With (Optional) indicator"
|
||||
optional
|
||||
/>
|
||||
<Content
|
||||
icon={SvgSearch}
|
||||
sizePreset="secondary"
|
||||
title="Secondary"
|
||||
description="With (Optional) indicator"
|
||||
optional
|
||||
/>
|
||||
</Section>
|
||||
|
||||
{/* ── aux icon ── */}
|
||||
<Section label="aux icon">
|
||||
<Content
|
||||
icon={SvgSearch}
|
||||
sizePreset="main-ui"
|
||||
title="Info gray"
|
||||
description="Neutral informational icon"
|
||||
auxIcon="info-gray"
|
||||
/>
|
||||
<Content
|
||||
icon={SvgSearch}
|
||||
sizePreset="main-ui"
|
||||
title="Info blue"
|
||||
description="Highlighted informational icon"
|
||||
auxIcon="info-blue"
|
||||
/>
|
||||
<Content
|
||||
icon={SvgSearch}
|
||||
sizePreset="main-ui"
|
||||
title="Warning"
|
||||
description="Warning status icon"
|
||||
auxIcon="warning"
|
||||
/>
|
||||
<Content
|
||||
icon={SvgSearch}
|
||||
sizePreset="main-ui"
|
||||
title="Error"
|
||||
description="Error status icon"
|
||||
auxIcon="error"
|
||||
/>
|
||||
</Section>
|
||||
|
||||
<Section label="aux icon + optional + editable">
|
||||
<Content
|
||||
icon={SvgSearch}
|
||||
sizePreset="main-content"
|
||||
title="All accessories"
|
||||
description="Optional + aux icon + editable"
|
||||
optional
|
||||
auxIcon="warning"
|
||||
editable
|
||||
onTitleChange={(v) => console.log("title changed:", v)}
|
||||
/>
|
||||
<Content
|
||||
icon={SvgSearch}
|
||||
sizePreset="secondary"
|
||||
title="All accessories"
|
||||
description="Optional + aux icon + editable at secondary"
|
||||
optional
|
||||
auxIcon="error"
|
||||
editable
|
||||
onTitleChange={(v) => console.log("title changed:", v)}
|
||||
/>
|
||||
</Section>
|
||||
|
||||
{/* ── optional props ── */}
|
||||
<Section label="optional props">
|
||||
<Content
|
||||
sizePreset="main-ui"
|
||||
title="No icon"
|
||||
description="Only title and description"
|
||||
/>
|
||||
<Content
|
||||
icon={SvgSearch}
|
||||
sizePreset="main-ui"
|
||||
title="No description"
|
||||
/>
|
||||
<Content sizePreset="main-ui" title="Title only" />
|
||||
</Section>
|
||||
</LayoutGroup>
|
||||
|
||||
{/* ================================================================= */}
|
||||
{/* BodyLayout */}
|
||||
{/* ================================================================= */}
|
||||
|
||||
<LayoutGroup label="BodyLayout">
|
||||
{/* ── main-content ── */}
|
||||
<Section label="main-content / inline">
|
||||
<Content
|
||||
icon={SvgSearch}
|
||||
sizePreset="main-content"
|
||||
variant="body"
|
||||
orientation="inline"
|
||||
title="Default inline"
|
||||
/>
|
||||
<Content
|
||||
icon={SvgSearch}
|
||||
sizePreset="main-content"
|
||||
variant="body"
|
||||
orientation="inline"
|
||||
prominence="muted"
|
||||
title="Muted inline"
|
||||
/>
|
||||
</Section>
|
||||
|
||||
<Section label="main-content / vertical">
|
||||
<Content
|
||||
icon={SvgSearch}
|
||||
sizePreset="main-content"
|
||||
variant="body"
|
||||
orientation="vertical"
|
||||
title="Default vertical"
|
||||
/>
|
||||
<Content
|
||||
icon={SvgSearch}
|
||||
sizePreset="main-content"
|
||||
variant="body"
|
||||
orientation="vertical"
|
||||
prominence="muted"
|
||||
title="Muted vertical"
|
||||
/>
|
||||
</Section>
|
||||
|
||||
<Section label="main-content / reverse">
|
||||
<Content
|
||||
icon={SvgSearch}
|
||||
sizePreset="main-content"
|
||||
variant="body"
|
||||
orientation="reverse"
|
||||
title="Default reverse"
|
||||
/>
|
||||
<Content
|
||||
icon={SvgSearch}
|
||||
sizePreset="main-content"
|
||||
variant="body"
|
||||
orientation="reverse"
|
||||
prominence="muted"
|
||||
title="Muted reverse"
|
||||
/>
|
||||
</Section>
|
||||
|
||||
{/* ── main-ui ── */}
|
||||
<Section label="main-ui / inline">
|
||||
<Content
|
||||
icon={SvgSearch}
|
||||
sizePreset="main-ui"
|
||||
variant="body"
|
||||
orientation="inline"
|
||||
title="Default inline"
|
||||
/>
|
||||
<Content
|
||||
icon={SvgSearch}
|
||||
sizePreset="main-ui"
|
||||
variant="body"
|
||||
orientation="inline"
|
||||
prominence="muted"
|
||||
title="Muted inline"
|
||||
/>
|
||||
</Section>
|
||||
|
||||
<Section label="main-ui / vertical">
|
||||
<Content
|
||||
icon={SvgSearch}
|
||||
sizePreset="main-ui"
|
||||
variant="body"
|
||||
orientation="vertical"
|
||||
title="Default vertical"
|
||||
/>
|
||||
<Content
|
||||
icon={SvgSearch}
|
||||
sizePreset="main-ui"
|
||||
variant="body"
|
||||
orientation="vertical"
|
||||
prominence="muted"
|
||||
title="Muted vertical"
|
||||
/>
|
||||
</Section>
|
||||
|
||||
<Section label="main-ui / reverse">
|
||||
<Content
|
||||
icon={SvgSearch}
|
||||
sizePreset="main-ui"
|
||||
variant="body"
|
||||
orientation="reverse"
|
||||
title="Default reverse"
|
||||
/>
|
||||
<Content
|
||||
icon={SvgSearch}
|
||||
sizePreset="main-ui"
|
||||
variant="body"
|
||||
orientation="reverse"
|
||||
prominence="muted"
|
||||
title="Muted reverse"
|
||||
/>
|
||||
</Section>
|
||||
|
||||
{/* ── secondary ── */}
|
||||
<Section label="secondary / inline">
|
||||
<Content
|
||||
icon={SvgSearch}
|
||||
sizePreset="secondary"
|
||||
variant="body"
|
||||
orientation="inline"
|
||||
title="Default inline"
|
||||
/>
|
||||
<Content
|
||||
icon={SvgSearch}
|
||||
sizePreset="secondary"
|
||||
variant="body"
|
||||
orientation="inline"
|
||||
prominence="muted"
|
||||
title="Muted inline"
|
||||
/>
|
||||
</Section>
|
||||
|
||||
<Section label="secondary / vertical">
|
||||
<Content
|
||||
icon={SvgSearch}
|
||||
sizePreset="secondary"
|
||||
variant="body"
|
||||
orientation="vertical"
|
||||
title="Default vertical"
|
||||
/>
|
||||
<Content
|
||||
icon={SvgSearch}
|
||||
sizePreset="secondary"
|
||||
variant="body"
|
||||
orientation="vertical"
|
||||
prominence="muted"
|
||||
title="Muted vertical"
|
||||
/>
|
||||
</Section>
|
||||
|
||||
<Section label="secondary / reverse">
|
||||
<Content
|
||||
icon={SvgSearch}
|
||||
sizePreset="secondary"
|
||||
variant="body"
|
||||
orientation="reverse"
|
||||
title="Default reverse"
|
||||
/>
|
||||
<Content
|
||||
icon={SvgSearch}
|
||||
sizePreset="secondary"
|
||||
variant="body"
|
||||
orientation="reverse"
|
||||
prominence="muted"
|
||||
title="Muted reverse"
|
||||
/>
|
||||
</Section>
|
||||
|
||||
{/* ── no icon ── */}
|
||||
<Section label="no icon">
|
||||
<Content
|
||||
sizePreset="main-ui"
|
||||
variant="body"
|
||||
title="Title only (no icon)"
|
||||
/>
|
||||
<Content
|
||||
sizePreset="main-ui"
|
||||
variant="body"
|
||||
prominence="muted"
|
||||
title="Title only muted (no icon)"
|
||||
/>
|
||||
</Section>
|
||||
</LayoutGroup>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Storybook helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function LayoutGroup({
|
||||
label,
|
||||
children,
|
||||
}: {
|
||||
label: string;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
border: "2px solid #ccc",
|
||||
borderRadius: 12,
|
||||
padding: "1.5rem",
|
||||
marginBottom: "2.5rem",
|
||||
}}
|
||||
>
|
||||
<h2 style={{ fontSize: 20, fontWeight: 700, marginBottom: "1.5rem" }}>
|
||||
{label}
|
||||
</h2>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Section({
|
||||
label,
|
||||
children,
|
||||
}: {
|
||||
label: string;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<section style={{ marginBottom: "2rem" }}>
|
||||
<h3
|
||||
style={{
|
||||
fontSize: 13,
|
||||
fontWeight: 600,
|
||||
textTransform: "uppercase",
|
||||
letterSpacing: 1,
|
||||
opacity: 0.5,
|
||||
marginBottom: "0.75rem",
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</h3>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "0.75rem",
|
||||
border: "1px solid #e0e0e0",
|
||||
borderRadius: 8,
|
||||
padding: "1rem",
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user