From c6527e41ef6a185ff79ffd1a6154500d65811338 Mon Sep 17 00:00:00 2001 From: Javid Date: Wed, 2 Sep 2026 16:32:59 +0200 Subject: [PATCH] feat(components): implement first consumer-proven slice Refs #9 --- docs/components/ACCEPTED_SLICE.md | 35 +++++++ docs/components/COMPONENT_MATRIX.md | 47 +++++---- src/components/Badge.tsx | 11 +- src/components/Button.tsx | 37 ++++++- src/components/Card.tsx | 33 +++++- src/components/ComponentSlice.stories.tsx | 118 +++++++++++++++++++++ src/components/EmptyState.tsx | 35 +++++++ src/components/FormField.tsx | 40 ++++++++ src/components/IconButton.tsx | 35 +++++++ src/components/Input.tsx | 15 +++ src/components/Metric.tsx | 30 ++++++ src/components/PageHeader.tsx | 28 +++++ src/components/Select.tsx | 17 +++ src/components/StatusBadge.tsx | 26 +++++ src/components/Surface.tsx | 30 ++++++ src/index.ts | 9 ++ src/styles/components.css | 120 ++++++++++++++++++++++ src/styles/index.css | 31 ++++-- src/tokens/semantic.css | 59 ++++++++++- 19 files changed, 708 insertions(+), 48 deletions(-) create mode 100644 docs/components/ACCEPTED_SLICE.md create mode 100644 src/components/ComponentSlice.stories.tsx create mode 100644 src/components/EmptyState.tsx create mode 100644 src/components/FormField.tsx create mode 100644 src/components/IconButton.tsx create mode 100644 src/components/Input.tsx create mode 100644 src/components/Metric.tsx create mode 100644 src/components/PageHeader.tsx create mode 100644 src/components/Select.tsx create mode 100644 src/components/StatusBadge.tsx create mode 100644 src/components/Surface.tsx create mode 100644 src/styles/components.css diff --git a/docs/components/ACCEPTED_SLICE.md b/docs/components/ACCEPTED_SLICE.md new file mode 100644 index 0000000..f93d927 --- /dev/null +++ b/docs/components/ACCEPTED_SLICE.md @@ -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. diff --git a/docs/components/COMPONENT_MATRIX.md b/docs/components/COMPONENT_MATRIX.md index 8966035..1d283e8 100644 --- a/docs/components/COMPONENT_MATRIX.md +++ b/docs/components/COMPONENT_MATRIX.md @@ -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 | diff --git a/src/components/Badge.tsx b/src/components/Badge.tsx index cb4a18d..b948002 100644 --- a/src/components/Badge.tsx +++ b/src/components/Badge.tsx @@ -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 { +export interface BadgeProps extends Omit, 'tone'> { variant?: BadgeVariant; } -export function Badge({ variant = 'neutral', children, ...props }: BadgeProps) { - return {children}; +/** @deprecated Prefer StatusBadge. Badge remains for backward compatibility. */ +export function Badge({ variant = 'neutral', ...props }: BadgeProps) { + return ; } diff --git a/src/components/Button.tsx b/src/components/Button.tsx index cf1a0fe..4e5ac93 100644 --- a/src/components/Button.tsx +++ b/src/components/Button.tsx @@ -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 { variant?: ButtonVariant; + size?: ButtonSize; + loading?: boolean; + leadingIcon?: ReactNode; + trailingIcon?: ReactNode; } -export function Button({ variant = 'primary', className = '', ...props }: ButtonProps) { - return + ); } diff --git a/src/components/Card.tsx b/src/components/Card.tsx index 0896e79..dadb65a 100644 --- a/src/components/Card.tsx +++ b/src/components/Card.tsx @@ -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 { - title?: string; +export interface CardProps extends Omit, 'title'> { + title?: ReactNode; + description?: ReactNode; + footer?: ReactNode; + variant?: SurfaceVariant; } -export function Card({ title, children, className = '', ...props }: CardProps) { - return
{title &&

{title}

}{children}
; +export function Card({ + title, + description, + footer, + children, + variant = 'outlined', + className = '', + ...props +}: CardProps) { + return ( + + {(title || description) && ( +
+ {title &&

{title}

} + {description &&

{description}

} +
+ )} +
{children}
+ {footer &&
{footer}
} +
+ ); } diff --git a/src/components/ComponentSlice.stories.tsx b/src/components/ComponentSlice.stories.tsx new file mode 100644 index 0000000..75f7176 --- /dev/null +++ b/src/components/ComponentSlice.stories.tsx @@ -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; + +const Icon = () => ; + +export const PersianRTL: Story = { + render: () => ( +
+ شروع ارزیابی} + /> + + + + + +
+ + + + + + } /> +
+ + + + مشاهدهٔ شواهد}> +
+ آماده + نیازمند بهبود +
+ +
+ } + title="هنوز گزارشی وجود ندارد" + description="برای مشاهدهٔ شواهد و پیشنهادهای اصلاحی، نخستین ارزیابی را اجرا کنید." + action={} + /> +
+ ), +}; + +export const EnglishLTR: Story = { + render: () => ( +
+ Run audit} + /> + + + + + Running + + +
+ ), +}; + +export const DarkTheme: Story = { + render: () => ( +
+
+ + متصل + + + +
+
+ ), +}; + +export const ValidationAndDisabled: Story = { + render: () => ( +
+ + + + + +
+ ), +}; diff --git a/src/components/EmptyState.tsx b/src/components/EmptyState.tsx new file mode 100644 index 0000000..169eaf6 --- /dev/null +++ b/src/components/EmptyState.tsx @@ -0,0 +1,35 @@ +import type { HTMLAttributes, ReactNode } from 'react'; + +export interface EmptyStateProps extends HTMLAttributes { + 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 ( +
+ {icon && } +

{title}

+ {description &&

{description}

} + {(action || secondaryAction) && ( +
+ {action} + {secondaryAction} +
+ )} +
+ ); +} diff --git a/src/components/FormField.tsx b/src/components/FormField.tsx new file mode 100644 index 0000000..f7b871a --- /dev/null +++ b/src/components/FormField.tsx @@ -0,0 +1,40 @@ +import type { ReactNode } from 'react'; + +export interface FormFieldProps { + controlId: string; + label: ReactNode; + description?: ReactNode; + error?: ReactNode; + required?: boolean; + children: ReactNode; + className?: string; +} + +export function FormField({ + controlId, + label, + description, + error, + required = false, + children, + className = '', +}: FormFieldProps) { + const descriptionId = description ? `${controlId}-description` : undefined; + const errorId = error ? `${controlId}-error` : undefined; + + return ( +
+ + {description &&
{description}
} + {children} + {error && } +
+ ); +} diff --git a/src/components/IconButton.tsx b/src/components/IconButton.tsx new file mode 100644 index 0000000..baff6a0 --- /dev/null +++ b/src/components/IconButton.tsx @@ -0,0 +1,35 @@ +import type { ButtonHTMLAttributes, ReactNode } from 'react'; +import type { ButtonSize, ButtonVariant } from './Button'; + +export interface IconButtonProps extends Omit, 'aria-label' | 'children'> { + label: string; + icon: ReactNode; + variant?: ButtonVariant; + size?: ButtonSize; + loading?: boolean; +} + +export function IconButton({ + label, + icon, + variant = 'ghost', + size = 'md', + loading = false, + disabled, + className = '', + type = 'button', + ...props +}: IconButtonProps) { + return ( + + ); +} diff --git a/src/components/Input.tsx b/src/components/Input.tsx new file mode 100644 index 0000000..0fb27a1 --- /dev/null +++ b/src/components/Input.tsx @@ -0,0 +1,15 @@ +import type { InputHTMLAttributes } from 'react'; + +export interface InputProps extends InputHTMLAttributes { + invalid?: boolean; +} + +export function Input({ invalid = false, className = '', ...props }: InputProps) { + return ( + + ); +} diff --git a/src/components/Metric.tsx b/src/components/Metric.tsx new file mode 100644 index 0000000..9cea13c --- /dev/null +++ b/src/components/Metric.tsx @@ -0,0 +1,30 @@ +import type { HTMLAttributes, ReactNode } from 'react'; + +export interface MetricProps extends HTMLAttributes { + label: ReactNode; + value: ReactNode; + supportingText?: ReactNode; + trend?: ReactNode; + size?: 'sm' | 'md' | 'lg'; +} + +export function Metric({ + label, + value, + supportingText, + trend, + size = 'md', + className = '', + ...props +}: MetricProps) { + return ( +
+
{label}
+
+ {value} + {trend && {trend}} +
+ {supportingText &&
{supportingText}
} +
+ ); +} diff --git a/src/components/PageHeader.tsx b/src/components/PageHeader.tsx new file mode 100644 index 0000000..ec7d9b6 --- /dev/null +++ b/src/components/PageHeader.tsx @@ -0,0 +1,28 @@ +import type { HTMLAttributes, ReactNode } from 'react'; + +export interface PageHeaderProps extends HTMLAttributes { + title: ReactNode; + description?: ReactNode; + eyebrow?: ReactNode; + actions?: ReactNode; +} + +export function PageHeader({ + title, + description, + eyebrow, + actions, + className = '', + ...props +}: PageHeaderProps) { + return ( +
+
+ {eyebrow &&
{eyebrow}
} +

{title}

+ {description &&

{description}

} +
+ {actions &&
{actions}
} +
+ ); +} diff --git a/src/components/Select.tsx b/src/components/Select.tsx new file mode 100644 index 0000000..856a909 --- /dev/null +++ b/src/components/Select.tsx @@ -0,0 +1,17 @@ +import type { SelectHTMLAttributes } from 'react'; + +export interface SelectProps extends SelectHTMLAttributes { + invalid?: boolean; +} + +export function Select({ invalid = false, className = '', children, ...props }: SelectProps) { + return ( + + ); +} diff --git a/src/components/StatusBadge.tsx b/src/components/StatusBadge.tsx new file mode 100644 index 0000000..b940e63 --- /dev/null +++ b/src/components/StatusBadge.tsx @@ -0,0 +1,26 @@ +import type { HTMLAttributes, ReactNode } from 'react'; + +export type StatusTone = 'neutral' | 'info' | 'success' | 'warning' | 'danger'; + +export interface StatusBadgeProps extends HTMLAttributes { + tone?: StatusTone; + dot?: boolean; + icon?: ReactNode; +} + +export function StatusBadge({ + tone = 'neutral', + dot = false, + icon, + className = '', + children, + ...props +}: StatusBadgeProps) { + return ( + + {dot && + ); +} diff --git a/src/components/Surface.tsx b/src/components/Surface.tsx new file mode 100644 index 0000000..3cc1fc5 --- /dev/null +++ b/src/components/Surface.tsx @@ -0,0 +1,30 @@ +import type { ElementType, HTMLAttributes, ReactNode } from 'react'; + +export type SurfaceVariant = 'default' | 'subtle' | 'raised' | 'outlined'; +export type SurfaceElement = 'div' | 'section' | 'article' | 'aside'; + +export interface SurfaceProps extends HTMLAttributes { + as?: SurfaceElement; + variant?: SurfaceVariant; + padding?: 'none' | 'sm' | 'md' | 'lg'; + children?: ReactNode; +} + +export function Surface({ + as = 'div', + variant = 'default', + padding = 'md', + className = '', + children, + ...props +}: SurfaceProps) { + const Component = as as ElementType; + return ( + + {children} + + ); +} diff --git a/src/index.ts b/src/index.ts index 7ed10a8..216927e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,15 @@ export * from './components/Button'; +export * from './components/IconButton'; +export * from './components/Input'; +export * from './components/Select'; +export * from './components/FormField'; +export * from './components/Surface'; export * from './components/Card'; +export * from './components/StatusBadge'; export * from './components/Badge'; +export * from './components/Metric'; +export * from './components/PageHeader'; +export * from './components/EmptyState'; export * from './tokens'; export * from './icons'; export * from './themes'; diff --git a/src/styles/components.css b/src/styles/components.css new file mode 100644 index 0000000..a0d9af1 --- /dev/null +++ b/src/styles/components.css @@ -0,0 +1,120 @@ +.core-button { + min-block-size: var(--core-control-md); + display: inline-flex; + align-items: center; + justify-content: center; + gap: var(--core-space-2); + border: 1px solid transparent; + border-radius: var(--core-radius-md); + padding-inline: var(--core-space-4); + background: transparent; + color: var(--core-color-text-primary); + font: inherit; + font-weight: 600; + line-height: 1; + cursor: pointer; + transition: background-color 140ms ease, border-color 140ms ease, color 140ms ease, box-shadow 140ms ease; +} +.core-button:focus-visible, +.core-control:focus-visible { + outline: 3px solid color-mix(in srgb, var(--core-color-focus) 32%, transparent); + outline-offset: 2px; +} +.core-button:disabled { cursor: not-allowed; opacity: .55; } +.core-button--primary { background: var(--core-color-action-primary); color: #fff; } +.core-button--primary:hover:not(:disabled) { background: var(--core-color-action-primary-hover); } +.core-button--secondary { border-color: var(--core-color-border-default); background: var(--core-color-surface-default); } +.core-button--secondary:hover:not(:disabled), +.core-button--ghost:hover:not(:disabled) { background: var(--core-color-surface-subtle); } +.core-button--danger { background: var(--core-color-action-danger); color: #fff; } +.core-button--danger:hover:not(:disabled) { background: var(--core-color-action-danger-hover); } +.core-button--sm { min-block-size: var(--core-control-sm); padding-inline: var(--core-space-3); font-size: .875rem; } +.core-button--lg { min-block-size: var(--core-control-lg); padding-inline: var(--core-space-6); } +.core-button__icon, .core-icon-button > span { display: inline-flex; } +.core-icon-button { inline-size: var(--core-control-md); padding: 0; } +.core-icon-button.core-button--sm { inline-size: var(--core-control-sm); } +.core-icon-button.core-button--lg { inline-size: var(--core-control-lg); } +.core-spinner { inline-size: 1em; block-size: 1em; border: 2px solid currentColor; border-inline-end-color: transparent; border-radius: 50%; animation: core-spin .7s linear infinite; } +@keyframes core-spin { to { transform: rotate(360deg); } } + +.core-field { display: grid; gap: var(--core-space-2); color: var(--core-color-text-primary); } +.core-field__label { font-weight: 600; } +.core-field__required, .core-field__error { color: var(--core-color-danger); } +.core-field__required { margin-inline-start: var(--core-space-1); } +.core-field__description { color: var(--core-color-text-secondary); font-size: .875rem; } +.core-field__error { font-size: .875rem; font-weight: 500; } +.core-control { + inline-size: 100%; + min-block-size: var(--core-control-md); + border: 1px solid var(--core-color-border-default); + border-radius: var(--core-radius-md); + padding-inline: var(--core-space-3); + background: var(--core-color-surface-default); + color: var(--core-color-text-primary); + font: inherit; +} +.core-control:hover:not(:disabled) { border-color: var(--core-color-border-strong); } +.core-control[aria-invalid='true'] { border-color: var(--core-color-danger); } +.core-control:disabled { background: var(--core-color-surface-subtle); color: var(--core-color-text-disabled); cursor: not-allowed; } +.core-select { appearance: auto; } + +.core-surface { border-radius: var(--core-radius-lg); color: var(--core-color-text-primary); } +.core-surface--default { background: var(--core-color-surface-default); } +.core-surface--subtle { background: var(--core-color-surface-subtle); } +.core-surface--raised { background: var(--core-color-surface-raised); box-shadow: var(--core-shadow-raised); } +.core-surface--outlined { background: var(--core-color-surface-default); border: 1px solid var(--core-color-border-default); } +.core-surface--padding-none { padding: 0; } +.core-surface--padding-sm { padding: var(--core-space-3); } +.core-surface--padding-md { padding: var(--core-space-4); } +.core-surface--padding-lg { padding: var(--core-space-6); } +.core-card { display: grid; gap: var(--core-space-4); } +.core-card__header, .core-card__content, .core-card__footer { min-inline-size: 0; } +.core-card__title { margin: 0; font-size: 1.125rem; } +.core-card__description { margin: var(--core-space-1) 0 0; color: var(--core-color-text-secondary); } +.core-card__footer { padding-block-start: var(--core-space-3); border-block-start: 1px solid var(--core-color-border-default); } + +.core-status-badge { + display: inline-flex; align-items: center; gap: var(--core-space-1); + max-inline-size: 100%; border-radius: 999px; padding: 3px var(--core-space-2); + background: var(--core-color-surface-subtle); color: var(--core-color-text-secondary); + font-size: .8125rem; font-weight: 600; line-height: 1.4; +} +.core-status-badge--info { background: var(--core-color-info-surface); color: var(--core-color-info); } +.core-status-badge--success { background: var(--core-color-success-surface); color: var(--core-color-success); } +.core-status-badge--warning { background: var(--core-color-warning-surface); color: var(--core-color-warning); } +.core-status-badge--danger { background: var(--core-color-danger-surface); color: var(--core-color-danger); } +.core-status-badge__dot { inline-size: 7px; block-size: 7px; border-radius: 50%; background: currentColor; flex: none; } +.core-status-badge__icon { display: inline-flex; } + +.core-metric { display: grid; gap: var(--core-space-1); } +.core-metric__label, .core-metric__supporting { color: var(--core-color-text-secondary); } +.core-metric__label { font-size: .875rem; font-weight: 600; } +.core-metric__row { display: flex; align-items: baseline; flex-wrap: wrap; gap: var(--core-space-2); } +.core-metric__value { font-family: var(--core-display-font-family); font-size: 2rem; line-height: 1; font-variant-numeric: tabular-nums; } +.core-metric--sm .core-metric__value { font-size: 1.5rem; } +.core-metric--lg .core-metric__value { font-size: 3rem; } +.core-metric__trend { font-size: .875rem; font-weight: 600; } +.core-metric__supporting { font-size: .8125rem; } + +.core-page-header { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--core-space-6); } +.core-page-header__copy { min-inline-size: 0; } +.core-page-header__eyebrow { margin-block-end: var(--core-space-1); color: var(--core-color-action-primary); font-size: .8125rem; font-weight: 700; text-transform: uppercase; letter-spacing: .08em; } +.core-page-header__title { margin: 0; font-size: clamp(1.75rem, 4vw, 2.5rem); line-height: 1.2; } +.core-page-header__description { max-inline-size: 70ch; margin: var(--core-space-2) 0 0; color: var(--core-color-text-secondary); } +.core-page-header__actions { display: flex; flex-wrap: wrap; gap: var(--core-space-2); } + +.core-empty-state { display: grid; justify-items: center; gap: var(--core-space-3); padding: var(--core-space-8); text-align: center; } +.core-empty-state--sm { padding: var(--core-space-4); } +.core-empty-state__icon { display: grid; place-items: center; inline-size: 48px; block-size: 48px; border-radius: var(--core-radius-lg); background: var(--core-color-surface-subtle); color: var(--core-color-action-primary); } +.core-empty-state__title { margin: 0; font-size: 1.25rem; } +.core-empty-state__description { max-inline-size: 52ch; margin: 0; color: var(--core-color-text-secondary); } +.core-empty-state__actions { display: flex; flex-wrap: wrap; justify-content: center; gap: var(--core-space-2); margin-block-start: var(--core-space-1); } + +@media (max-width: 640px) { + .core-page-header { flex-direction: column; } + .core-page-header__actions, .core-page-header__actions > * { inline-size: 100%; } +} +@media (prefers-reduced-motion: reduce) { + .core-button { transition: none; } + .core-spinner { animation-duration: 1.4s; } +} diff --git a/src/styles/index.css b/src/styles/index.css index bfee9cc..d40bb11 100644 --- a/src/styles/index.css +++ b/src/styles/index.css @@ -1,15 +1,32 @@ -@import url('https://fonts.googleapis.com/css2?family=Noto+Sans:wght@400;500;600;700&family=Handjet:wght@400;500;600&display=swap'); - -:root { - --core-font-family: 'Noto Sans', 'Noto Sans Arabic', system-ui, sans-serif; - --core-display-font-family: 'Handjet', 'Noto Sans', sans-serif; -} +@import '../fonts/index.css'; +@import '../tokens/semantic.css'; +@import './components.css'; .core-root { + box-sizing: border-box; + color: var(--core-color-text-primary); + background: var(--core-color-surface-page); font-family: var(--core-font-family); - direction: ltr; + color-scheme: light; +} + +.core-root *, +.core-root *::before, +.core-root *::after { + box-sizing: border-box; } +.core-root[dir='rtl'], .core-rtl { direction: rtl; } + +.core-root[dir='ltr'], +.core-ltr { + direction: ltr; +} + +[data-theme='dark'] .core-root, +.core-root[data-theme='dark'] { + color-scheme: dark; +} diff --git a/src/tokens/semantic.css b/src/tokens/semantic.css index c3dafe0..7c7150b 100644 --- a/src/tokens/semantic.css +++ b/src/tokens/semantic.css @@ -1,15 +1,64 @@ :root { - --core-color-action-primary: #0066FF; - --core-color-surface-default: #F9FAFB; - --core-color-text-primary: #111827; - + --core-color-action-primary: #0066ff; + --core-color-action-primary-hover: #0052cc; + --core-color-action-danger: #b42318; + --core-color-action-danger-hover: #912018; + --core-color-surface-page: #f9fafb; + --core-color-surface-default: #ffffff; + --core-color-surface-subtle: #f3f4f6; + --core-color-surface-raised: #ffffff; + --core-color-border-default: #d0d5dd; + --core-color-border-strong: #98a2b3; + --core-color-text-primary: #101828; + --core-color-text-secondary: #475467; + --core-color-text-disabled: #98a2b3; + --core-color-focus: #0066ff; + --core-color-info: #175cd3; + --core-color-info-surface: #eff8ff; + --core-color-success: #067647; + --core-color-success-surface: #ecfdf3; + --core-color-warning: #b54708; + --core-color-warning-surface: #fffaeb; + --core-color-danger: #b42318; + --core-color-danger-surface: #fef3f2; + --core-shadow-raised: 0 8px 24px rgba(16, 24, 40, 0.10); --core-radius-sm: 8px; --core-radius-md: 12px; --core-radius-lg: 16px; - --core-space-1: 4px; --core-space-2: 8px; --core-space-3: 12px; --core-space-4: 16px; + --core-space-5: 20px; --core-space-6: 24px; + --core-space-8: 32px; + --core-control-sm: 36px; + --core-control-md: 44px; + --core-control-lg: 52px; +} + +[data-theme='dark'] { + --core-color-action-primary: #66a3ff; + --core-color-action-primary-hover: #8fbbff; + --core-color-action-danger: #f97066; + --core-color-action-danger-hover: #fda29b; + --core-color-surface-page: #0c111d; + --core-color-surface-default: #161b26; + --core-color-surface-subtle: #1f242f; + --core-color-surface-raised: #1f242f; + --core-color-border-default: #333741; + --core-color-border-strong: #667085; + --core-color-text-primary: #f5f5f6; + --core-color-text-secondary: #cecfd2; + --core-color-text-disabled: #85888e; + --core-color-focus: #84adff; + --core-color-info: #84adff; + --core-color-info-surface: #102a56; + --core-color-success: #75e0a7; + --core-color-success-surface: #053321; + --core-color-warning: #fec84b; + --core-color-warning-surface: #4e1d09; + --core-color-danger: #fda29b; + --core-color-danger-surface: #55160c; + --core-shadow-raised: 0 12px 32px rgba(0, 0, 0, 0.32); }