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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions docs/components/ACCEPTED_SLICE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Accepted component slice

This slice is backed by AgentReady and existing Core product needs. It intentionally excludes Menu and overlay primitives until a captured consumer flow proves them.

## Included

| Component | Layer | Key guidance |
|---|---|---|
| Button | Styled primitive | Use for actions; loading disables repeat submission |
| IconButton | Styled primitive | A non-empty accessible `label` is required |
| Input | Styled primitive | Pair with FormField; connect description/error IDs |
| Select | Styled primitive | Prefer native select until advanced behavior is proven |
| FormField | Product-neutral composition | Owns visible label, description and error layout |
| Surface | Styled primitive | Visual container; consumer chooses the semantic element |
| Card | Product-neutral composition | Use for grouped content, not every section |
| StatusBadge | Styled primitive | Always include text; color/dot are supplementary |
| Metric | Styled primitive | Format values in the consumer locale |
| PageHeader | Product-neutral composition | One page title with optional description/actions |
| EmptyState | Product-neutral composition | Explain the state and provide a useful next action |

## Accessibility contract

- Keyboard focus is visible.
- Controls retain native semantics.
- Disabled and loading states prevent duplicate action.
- Validation uses `aria-invalid`, `aria-describedby` and a live error.
- Status is never communicated by color alone.
- IconButton requires an accessible name.
- RTL/LTR use logical CSS properties.
- Dark theme uses semantic variables.
- Reduced-motion preferences remove non-essential transitions.

## Consumer boundary

AgentReady-specific AuditProgress, ReportSummary, FindingCard, EvidenceBlock, RecommendationCard and ReportFilters remain in AgentReady. They compose these primitives and must not be imported back into Core without new evidence.
47 changes: 25 additions & 22 deletions docs/components/COMPONENT_MATRIX.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,27 +4,30 @@ Updated: 2026-09-02

| Component/capability | Layer | Maturity | Owner | Consumer evidence | Accessibility contract | Next gate |
|---|---|---|---|---|---|---|
| Button | Styled primitive | Candidate | Design System | Existing package and Storybook | Native button, focus, disabled state | Validate in two products |
| Card | Styled primitive | Candidate | Design System | Existing package and Storybook | Semantic element selected by consumer | Usage guidance and adoption evidence |
| Badge | Styled primitive | Candidate | Design System | Existing package and Storybook | Text must carry meaning; color is supplementary | Status semantics and adoption evidence |
| ThemeProvider | Headless foundation | Experimental | Design System | Existing package | Preserve user preference; no content loss | Light/dark regression coverage |
| Input | Styled primitive | Proposed | Design System | AgentReady #7/#14; design-system #9 | Label, help, error, keyboard and focus | API proposal and implementation |
| FormField | Headless/styled composition | Proposed | Design System | AgentReady audit form | Label/description/error relationships | Validate with Input |
| AsyncStatus | Headless primitive | Proposed | Design System | AgentReady #14 | Announced state changes and stable vocabulary | Experimental implementation |
| Progress | Styled primitive | Proposed | Design System | AgentReady #14 | Accessible name/value; indeterminate support | Experimental implementation |
| StatusIndicator | Styled primitive | Proposed | Design System | AgentReady #14 | Icon/text semantics; not color-only | Experimental implementation |
| Disclosure/Accordion | Headless primitive | Proposed | Design System | AgentReady #8/#14 | Keyboard and expanded state | Consumer API example |
| Tabs/SegmentedControl | Headless/styled primitive | Proposed | Design System | AgentReady report filters | Roving focus and selected state | Choose correct semantics per job |
| Tooltip | Headless primitive | Proposed | Design System | AgentReady evidence help | Hover/focus, dismissal and non-essential content | API proposal |
| Dialog/Drawer/BottomSheet | Headless primitive | Deferred | Design System | Only one tentative AgentReady need | Focus trap, escape, return focus, labelling | Wait for a proven flow |
| Skeleton | Styled primitive | Proposed | Design System | AgentReady loading | Hidden or meaningful announcement; no layout shift | Pair with AsyncStatus |
| EmptyState/ErrorState | Product-neutral composition | Proposed | Design System | AgentReady #9/#14; Console candidate | Heading, action, recovery and live-region guidance | Validate across two consumers |
| Metric/AnimatedNumber | Styled primitive | Proposed | Design System | AgentReady score; Console metrics | Locale formatting; static reduced-motion fallback | Experimental implementation |
| CodeBlock | Styled primitive | Proposed | Design System | AgentReady remediation | Bidi isolation, language label and copy feedback | Validate developer workflow |
| Button | Styled primitive | Candidate | Design System | Package, Storybook, AgentReady #7/#14 | Native button, visible focus, disabled/loading | Production adoption in AgentReady |
| IconButton | Styled primitive | Candidate | Design System | AgentReady actions | Required accessible label and 44px default target | Production adoption in AgentReady |
| Input | Styled primitive | Candidate | Design System | AgentReady audit form | Native input, label/help/error wiring | Production adoption in AgentReady |
| Select | Styled primitive | Candidate | Design System | AgentReady vertical profiles | Native select and keyboard behavior | Second consumer evidence |
| FormField | Product-neutral composition | Candidate | Design System | AgentReady audit form | Visible label, description and live error | Production adoption in AgentReady |
| Surface | Styled primitive | Candidate | Design System | AgentReady and Console surfaces | Consumer-selected semantic element | Production adoption in AgentReady |
| Card | Product-neutral composition | Candidate | Design System | Existing package, AgentReady reports | Structured header/content/footer | Usage feedback |
| StatusBadge | Styled primitive | Candidate | Design System | AgentReady finding states | Text semantics; color/dot supplementary | Production adoption in AgentReady |
| Badge | Compatibility alias | Deprecated | Design System | Existing package consumers | Delegates to StatusBadge | Remove in next major after migration |
| Metric | Styled primitive | Candidate | Design System | AgentReady score; Console metrics | Locale formatting in consumer; reduced-motion safe | Validate in two products |
| PageHeader | Product-neutral composition | Candidate | Design System | AgentReady report and Console candidate | One h1; responsive actions | Validate in two products |
| EmptyState | Product-neutral composition | Candidate | Design System | AgentReady #9/#14; Console candidate | Heading, recovery and useful next action | Validate in two products |
| ThemeProvider | Headless foundation | Experimental | Design System | Existing package | Preserve preference and content | Light/dark regression coverage |
| AsyncStatus | Headless primitive | Proposed | Design System | AgentReady #14 | Announced state changes and stable vocabulary | Issue #12 |
| Progress | Styled primitive | Proposed | Design System | AgentReady #14 | Accessible name/value and indeterminate mode | Issue #12 |
| Disclosure/Accordion | Headless primitive | Proposed | Design System | AgentReady #8/#14 | Keyboard and expanded state | Future slice |
| Tabs/SegmentedControl | Headless/styled primitive | Proposed | Design System | AgentReady report filters | Focus and selected-state semantics | Future slice |
| Tooltip | Headless primitive | Proposed | Design System | AgentReady evidence help | Hover/focus/dismissal | Future slice |
| Dialog/Drawer/BottomSheet | Headless primitive | Deferred | Design System | No proven overlay flow | Focus trap, escape, return focus, labelling | Wait for captured flow |
| Skeleton | Styled primitive | Proposed | Design System | AgentReady loading | No layout shift; paired status announcement | Issue #12 |
| CodeBlock | Styled primitive | Proposed | Design System | AgentReady remediation | Bidi isolation and copy feedback | Future slice |
| AuditProgress | Product composition | Experimental-local | AgentReady | AgentReady #14 | Live status, elapsed time and recovery | Keep in AgentReady |
| Finding/EvidenceCard | Product composition | Experimental-local | AgentReady | AgentReady #8/#14 | Structured headings, disclosure and bidi-safe evidence | Keep in AgentReady |
| RecommendationCard | Product composition | Experimental-local | AgentReady | AgentReady #8/#14 | Priority and action cannot rely on color | Keep in AgentReady |
| Finding/EvidenceCard | Product composition | Experimental-local | AgentReady | AgentReady #8/#14 | Structured disclosure and bidi-safe evidence | Keep in AgentReady |
| RecommendationCard | Product composition | Experimental-local | AgentReady | AgentReady #8/#14 | Priority/action not color-only | Keep in AgentReady |
| AgentJourneyTimeline | Product composition | Deferred | AgentReady | AgentReady #2/#14 | Ordered steps and explicit outcomes | Wait for journey schema |
| Source/ToolChip | Product composition | Deferred | AgentReady/Core Signal | Beautiful UI reference only | Text label and removable/interactive semantics | Require implemented consumer evidence |
| AgentStatusOrb | Experimental adapter | Deferred | Design System | AgentReady idea; Thinking Orbs reference | Never sole cue; reduced motion; static fallback | Evaluate under motion issue #12 |
| Canvas/WebGL effects | Brand experiment | Deferred | Consumer | No production evidence | Static fallback, reduced motion and performance budget | Keep out of base package |
| AgentStatusOrb | Experimental adapter | Deferred | Design System | AgentReady idea | Never sole cue; reduced motion; static fallback | Issue #12 |
| Canvas/WebGL effects | Brand experiment | Deferred | Consumer | No production evidence | Static fallback and performance budget | Keep out of base package |
11 changes: 6 additions & 5 deletions src/components/Badge.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import type { HTMLAttributes } from 'react';
import type { ComponentProps } from 'react';
import { StatusBadge } from './StatusBadge';

export type BadgeVariant = 'success' | 'warning' | 'danger' | 'neutral';

export interface BadgeProps extends HTMLAttributes<HTMLSpanElement> {
export interface BadgeProps extends Omit<ComponentProps<typeof StatusBadge>, 'tone'> {
variant?: BadgeVariant;
}

export function Badge({ variant = 'neutral', children, ...props }: BadgeProps) {
return <span className={`core-badge core-badge-${variant}`} {...props}>{children}</span>;
/** @deprecated Prefer StatusBadge. Badge remains for backward compatibility. */
export function Badge({ variant = 'neutral', ...props }: BadgeProps) {
return <StatusBadge tone={variant} {...props} />;
}
37 changes: 33 additions & 4 deletions src/components/Button.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,40 @@
import type { ButtonHTMLAttributes } from 'react';
import type { ButtonHTMLAttributes, ReactNode } from 'react';

export type ButtonVariant = 'primary' | 'secondary' | 'danger';
export type ButtonVariant = 'primary' | 'secondary' | 'ghost' | 'danger';
export type ButtonSize = 'sm' | 'md' | 'lg';

export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
variant?: ButtonVariant;
size?: ButtonSize;
loading?: boolean;
leadingIcon?: ReactNode;
trailingIcon?: ReactNode;
}

export function Button({ variant = 'primary', className = '', ...props }: ButtonProps) {
return <button className={`core-button core-button-${variant} ${className}`} {...props} />;
export function Button({
variant = 'primary',
size = 'md',
loading = false,
leadingIcon,
trailingIcon,
disabled,
className = '',
children,
type = 'button',
...props
}: ButtonProps) {
return (
<button
type={type}
className={`core-button core-button--${variant} core-button--${size} ${className}`.trim()}
disabled={disabled || loading}
aria-busy={loading || undefined}
{...props}
>
{loading && <span className="core-spinner" aria-hidden="true" />}
{!loading && leadingIcon && <span className="core-button__icon" aria-hidden="true">{leadingIcon}</span>}
<span>{children}</span>
{!loading && trailingIcon && <span className="core-button__icon" aria-hidden="true">{trailingIcon}</span>}
</button>
);
}
33 changes: 28 additions & 5 deletions src/components/Card.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,32 @@
import type { HTMLAttributes } from 'react';
import type { HTMLAttributes, ReactNode } from 'react';
import { Surface, type SurfaceVariant } from './Surface';

export interface CardProps extends HTMLAttributes<HTMLDivElement> {
title?: string;
export interface CardProps extends Omit<HTMLAttributes<HTMLElement>, 'title'> {
title?: ReactNode;
description?: ReactNode;
footer?: ReactNode;
variant?: SurfaceVariant;
}

export function Card({ title, children, className = '', ...props }: CardProps) {
return <section className={`core-card ${className}`} {...props}>{title && <h3>{title}</h3>}{children}</section>;
export function Card({
title,
description,
footer,
children,
variant = 'outlined',
className = '',
...props
}: CardProps) {
return (
<Surface as="section" variant={variant} className={`core-card ${className}`.trim()} {...props}>
{(title || description) && (
<header className="core-card__header">
{title && <h3 className="core-card__title">{title}</h3>}
{description && <p className="core-card__description">{description}</p>}
</header>
)}
<div className="core-card__content">{children}</div>
{footer && <footer className="core-card__footer">{footer}</footer>}
</Surface>
);
}
118 changes: 118 additions & 0 deletions src/components/ComponentSlice.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import type { Meta, StoryObj } from '@storybook/react';
import { Button } from './Button';
import { IconButton } from './IconButton';
import { Input } from './Input';
import { Select } from './Select';
import { FormField } from './FormField';
import { Surface } from './Surface';
import { Card } from './Card';
import { StatusBadge } from './StatusBadge';
import { Metric } from './Metric';
import { PageHeader } from './PageHeader';
import { EmptyState } from './EmptyState';

const meta = {
title: 'Components/Accepted slice',
parameters: { layout: 'padded' },
} satisfies Meta;

export default meta;
type Story = StoryObj<typeof meta>;

const Icon = () => <span aria-hidden="true">◆</span>;

export const PersianRTL: Story = {
render: () => (
<div className="core-root" dir="rtl" lang="fa" style={{ display: 'grid', gap: 24, padding: 24 }}>
<PageHeader
eyebrow="Core Signal"
title="گزارش آمادگی عامل هوشمند"
description="کامپوننت‌های مشترک با پشتیبانی کامل از رابط راست‌به‌چپ."
actions={<Button>شروع ارزیابی</Button>}
/>
<Surface variant="subtle">
<FormField
controlId="domain-fa"
label="دامنهٔ وب‌سایت"
description="نشانی را بدون مسیر وارد کنید."
required
>
<Input id="domain-fa" dir="ltr" placeholder="example.com" required />
</FormField>
</Surface>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 8 }}>
<Button>اصلی</Button>
<Button variant="secondary">ثانویه</Button>
<Button variant="ghost">بدون زمینه</Button>
<Button variant="danger">حذف</Button>
<Button loading>در حال بررسی</Button>
<IconButton label="تنظیمات" icon={<Icon />} />
</div>
<FormField controlId="profile-fa" label="نوع سایت">
<Select id="profile-fa" defaultValue="media">
<option value="media">رسانه</option>
<option value="commerce">فروشگاه</option>
</Select>
</FormField>
<Card title="کشف‌پذیری" description="وضعیت سیگنال‌های قابل استفاده برای عامل‌ها" footer={<Button variant="secondary">مشاهدهٔ شواهد</Button>}>
<div style={{ display: 'flex', gap: 8 }}>
<StatusBadge tone="success" dot>آماده</StatusBadge>
<StatusBadge tone="warning" dot>نیازمند بهبود</StatusBadge>
</div>
<Metric label="امتیاز آمادگی" value="۸۲" trend="+۴٪" supportingText="در مقایسه با ارزیابی پیشین" />
</Card>
<EmptyState
icon={<Icon />}
title="هنوز گزارشی وجود ندارد"
description="برای مشاهدهٔ شواهد و پیشنهادهای اصلاحی، نخستین ارزیابی را اجرا کنید."
action={<Button>شروع ارزیابی</Button>}
/>
</div>
),
};

export const EnglishLTR: Story = {
render: () => (
<div className="core-root" dir="ltr" lang="en" style={{ display: 'grid', gap: 24, padding: 24 }}>
<PageHeader
eyebrow="Core Signal"
title="Agent readiness report"
description="The same component contract in a left-to-right interface."
actions={<Button>Run audit</Button>}
/>
<FormField controlId="domain-en" label="Website domain" description="Enter a hostname without a path.">
<Input id="domain-en" placeholder="example.com" />
</FormField>
<Card title="Discoverability" description="Machine-readable signals">
<StatusBadge tone="info" dot>Running</StatusBadge>
<Metric label="Readiness score" value="82" supportingText="Ruleset v1" />
</Card>
</div>
),
};

export const DarkTheme: Story = {
render: () => (
<div data-theme="dark" style={{ background: '#0c111d', padding: 24 }}>
<div className="core-root" dir="rtl" lang="fa" style={{ display: 'grid', gap: 16, padding: 24 }}>
<Card title="وضعیت سرویس" variant="raised">
<StatusBadge tone="success" dot>متصل</StatusBadge>
<Metric label="در دسترس بودن" value="۹۹٫۹٪" />
<Button>مشاهدهٔ جزئیات</Button>
</Card>
</div>
</div>
),
};

export const ValidationAndDisabled: Story = {
render: () => (
<div className="core-root" dir="rtl" lang="fa" style={{ display: 'grid', gap: 20, maxWidth: 520, padding: 24 }}>
<FormField controlId="invalid-domain" label="دامنه" error="دامنه معتبر نیست.">
<Input id="invalid-domain" invalid aria-describedby="invalid-domain-error" defaultValue="not a domain" />
</FormField>
<Input disabled value="ورودی غیرفعال" readOnly />
<Button disabled>عملیات غیرفعال</Button>
</div>
),
};
35 changes: 35 additions & 0 deletions src/components/EmptyState.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import type { HTMLAttributes, ReactNode } from 'react';

export interface EmptyStateProps extends HTMLAttributes<HTMLDivElement> {
title: ReactNode;
description?: ReactNode;
icon?: ReactNode;
action?: ReactNode;
secondaryAction?: ReactNode;
size?: 'sm' | 'md';
}

export function EmptyState({
title,
description,
icon,
action,
secondaryAction,
size = 'md',
className = '',
...props
}: EmptyStateProps) {
return (
<div className={`core-empty-state core-empty-state--${size} ${className}`.trim()} {...props}>
{icon && <div className="core-empty-state__icon" aria-hidden="true">{icon}</div>}
<h2 className="core-empty-state__title">{title}</h2>
{description && <p className="core-empty-state__description">{description}</p>}
{(action || secondaryAction) && (
<div className="core-empty-state__actions">
{action}
{secondaryAction}
</div>
)}
</div>
);
}
Loading
Loading