From 52e55af84d169ebcb8751bd350af94d85b154f39 Mon Sep 17 00:00:00 2001 From: IamLRBA Date: Wed, 17 Dec 2025 00:55:31 +0300 Subject: [PATCH] feat(components): add reusable design system components Add comprehensive set of reusable components built with ODE design tokens: - Alert, Badge, Button, ButtonGroup, CodeBlock, Container, Divider, Spacer, Tag, Tooltip - All components support dark mode and use design tokens - Includes component documentation and centralized exports Closes #2 --- src/components/Alert/index.tsx | 52 +++ src/components/Alert/styles.module.css | 109 +++++++ src/components/Badge/index.tsx | 34 ++ src/components/Badge/styles.module.css | 99 ++++++ src/components/Button/index.tsx | 73 +++++ src/components/Button/styles.module.css | 321 +++++++++++++++++++ src/components/ButtonGroup/index.tsx | 19 ++ src/components/ButtonGroup/styles.module.css | 16 + src/components/CodeBlock/index.tsx | 44 +++ src/components/CodeBlock/styles.module.css | 110 +++++++ src/components/Container/index.tsx | 29 ++ src/components/Container/styles.module.css | 45 +++ src/components/Divider/index.tsx | 52 +++ src/components/Divider/styles.module.css | 81 +++++ src/components/README.md | 251 +++++++++++++++ src/components/Spacer/index.tsx | 31 ++ src/components/Spacer/styles.module.css | 115 +++++++ src/components/Tag/index.tsx | 48 +++ src/components/Tag/styles.module.css | 93 ++++++ src/components/Tooltip/index.tsx | 63 ++++ src/components/Tooltip/styles.module.css | 122 +++++++ src/components/index.ts | 18 ++ 22 files changed, 1825 insertions(+) create mode 100644 src/components/Alert/index.tsx create mode 100644 src/components/Alert/styles.module.css create mode 100644 src/components/Badge/index.tsx create mode 100644 src/components/Badge/styles.module.css create mode 100644 src/components/Button/index.tsx create mode 100644 src/components/Button/styles.module.css create mode 100644 src/components/ButtonGroup/index.tsx create mode 100644 src/components/ButtonGroup/styles.module.css create mode 100644 src/components/CodeBlock/index.tsx create mode 100644 src/components/CodeBlock/styles.module.css create mode 100644 src/components/Container/index.tsx create mode 100644 src/components/Container/styles.module.css create mode 100644 src/components/Divider/index.tsx create mode 100644 src/components/Divider/styles.module.css create mode 100644 src/components/README.md create mode 100644 src/components/Spacer/index.tsx create mode 100644 src/components/Spacer/styles.module.css create mode 100644 src/components/Tag/index.tsx create mode 100644 src/components/Tag/styles.module.css create mode 100644 src/components/Tooltip/index.tsx create mode 100644 src/components/Tooltip/styles.module.css create mode 100644 src/components/index.ts diff --git a/src/components/Alert/index.tsx b/src/components/Alert/index.tsx new file mode 100644 index 0000000..5d44f1f --- /dev/null +++ b/src/components/Alert/index.tsx @@ -0,0 +1,52 @@ +import type { ReactNode } from 'react'; +import clsx from 'clsx'; +import styles from './styles.module.css'; + +type AlertVariant = 'info' | 'success' | 'warning' | 'error'; +type AlertSize = 'sm' | 'md'; + +interface AlertProps { + variant?: AlertVariant; + size?: AlertSize; + title?: string; + children: ReactNode; + className?: string; + icon?: ReactNode; +} + +export default function Alert({ + variant = 'info', + size = 'md', + title, + children, + className, + icon, +}: AlertProps): ReactNode { + const defaultIcons = { + info: 'ℹ️', + success: '✓', + warning: '⚠', + error: '✕', + }; + + return ( +
+
+ {icon || {defaultIcons[variant]}} +
+
+ {title &&
{title}
} +
{children}
+
+
+ ); +} + diff --git a/src/components/Alert/styles.module.css b/src/components/Alert/styles.module.css new file mode 100644 index 0000000..f5abc27 --- /dev/null +++ b/src/components/Alert/styles.module.css @@ -0,0 +1,109 @@ +.alert { + display: flex; + gap: var(--ode-spacing-4); + padding: var(--ode-spacing-4); + border-radius: var(--ode-border-radius-md); + border: var(--ode-border-width-thin) solid; + font-family: var(--ode-font-family-sans); +} + +.alert--sm { + padding: var(--ode-spacing-3); + gap: var(--ode-spacing-3); +} + +.alert--md { + padding: var(--ode-spacing-4); + gap: var(--ode-spacing-4); +} + +.alertIcon { + flex-shrink: 0; + display: flex; + align-items: flex-start; + padding-top: var(--ode-spacing-0_5); +} + +.iconEmoji { + font-size: var(--ode-font-size-lg); + line-height: 1; +} + +.alertContent { + flex: 1; + display: flex; + flex-direction: column; + gap: var(--ode-spacing-2); +} + +.alertTitle { + font-size: var(--ode-font-size-base); + font-weight: var(--ode-font-weight-semibold); + line-height: var(--ode-line-height-tight); +} + +.alertMessage { + font-size: var(--ode-font-size-sm); + line-height: var(--ode-line-height-relaxed); +} + +.alert--sm .alertTitle { + font-size: var(--ode-font-size-sm); +} + +.alert--sm .alertMessage { + font-size: var(--ode-font-size-xs); +} + +/* Info Variant */ +.alert--info { + background-color: var(--ode-color-semantic-info-50); + border-color: var(--ode-color-semantic-info-500); + color: var(--ode-color-semantic-info-600); +} + +[data-theme='dark'] .alert--info { + background-color: rgba(33, 150, 243, 0.1); + border-color: var(--ode-color-semantic-info-500); + color: #90caf9; +} + +/* Success Variant */ +.alert--success { + background-color: var(--ode-color-semantic-success-50); + border-color: var(--ode-color-semantic-success-500); + color: var(--ode-color-semantic-success-600); +} + +[data-theme='dark'] .alert--success { + background-color: rgba(52, 199, 89, 0.1); + border-color: var(--ode-color-semantic-success-500); + color: #81c784; +} + +/* Warning Variant */ +.alert--warning { + background-color: var(--ode-color-semantic-warning-50); + border-color: var(--ode-color-semantic-warning-500); + color: var(--ode-color-semantic-warning-600); +} + +[data-theme='dark'] .alert--warning { + background-color: rgba(255, 149, 0, 0.1); + border-color: var(--ode-color-semantic-warning-500); + color: #ffb74d; +} + +/* Error Variant */ +.alert--error { + background-color: var(--ode-color-semantic-error-50); + border-color: var(--ode-color-semantic-error-500); + color: var(--ode-color-semantic-error-600); +} + +[data-theme='dark'] .alert--error { + background-color: rgba(244, 67, 54, 0.1); + border-color: var(--ode-color-semantic-error-500); + color: #e57373; +} + diff --git a/src/components/Badge/index.tsx b/src/components/Badge/index.tsx new file mode 100644 index 0000000..23f00cf --- /dev/null +++ b/src/components/Badge/index.tsx @@ -0,0 +1,34 @@ +import type { ReactNode } from 'react'; +import clsx from 'clsx'; +import styles from './styles.module.css'; + +type BadgeVariant = 'primary' | 'secondary' | 'success' | 'warning' | 'error' | 'info' | 'neutral'; +type BadgeSize = 'sm' | 'md'; + +interface BadgeProps { + variant?: BadgeVariant; + size?: BadgeSize; + children: ReactNode; + className?: string; +} + +export default function Badge({ + variant = 'primary', + size = 'md', + children, + className, +}: BadgeProps): ReactNode { + return ( + + {children} + + ); +} + diff --git a/src/components/Badge/styles.module.css b/src/components/Badge/styles.module.css new file mode 100644 index 0000000..7bde82b --- /dev/null +++ b/src/components/Badge/styles.module.css @@ -0,0 +1,99 @@ +.badge { + display: inline-flex; + align-items: center; + justify-content: center; + font-family: var(--ode-font-family-sans); + font-weight: var(--ode-font-weight-medium); + line-height: var(--ode-line-height-none); + border-radius: var(--ode-border-radius-full); + white-space: nowrap; +} + +/* Size Variants */ +.badge--sm { + padding: var(--ode-spacing-0_5) var(--ode-spacing-2); + font-size: var(--ode-font-size-xs); +} + +.badge--md { + padding: var(--ode-spacing-1) var(--ode-spacing-3); + font-size: var(--ode-font-size-sm); +} + +/* Primary Variant */ +.badge--primary { + background-color: var(--ode-color-brand-primary-100); + color: var(--ode-color-brand-primary-700); +} + +[data-theme='dark'] .badge--primary { + background-color: var(--ode-color-brand-primary-900); + color: var(--ode-color-brand-primary-200); +} + +/* Secondary Variant */ +.badge--secondary { + background-color: var(--ode-color-brand-secondary-100); + color: var(--ode-color-brand-secondary-800); +} + +[data-theme='dark'] .badge--secondary { + background-color: var(--ode-color-brand-secondary-900); + color: var(--ode-color-brand-secondary-200); +} + +/* Success Variant */ +.badge--success { + background-color: var(--ode-color-semantic-success-50); + color: var(--ode-color-semantic-success-600); +} + +[data-theme='dark'] .badge--success { + background-color: rgba(46, 125, 50, 0.2); + color: #81c784; +} + +/* Warning Variant */ +.badge--warning { + background-color: var(--ode-color-semantic-warning-50); + color: var(--ode-color-semantic-warning-600); +} + +[data-theme='dark'] .badge--warning { + background-color: rgba(217, 119, 6, 0.2); + color: #ffb74d; +} + +/* Error Variant */ +.badge--error { + background-color: var(--ode-color-semantic-error-50); + color: var(--ode-color-semantic-error-600); +} + +[data-theme='dark'] .badge--error { + background-color: rgba(220, 38, 38, 0.2); + color: #e57373; +} + +/* Info Variant */ +.badge--info { + background-color: var(--ode-color-semantic-info-50); + color: var(--ode-color-semantic-info-600); +} + +[data-theme='dark'] .badge--info { + background-color: rgba(37, 99, 235, 0.2); + color: #64b5f6; +} + +/* Neutral Variant */ +.badge--neutral { + background-color: var(--ode-color-neutral-100); + color: var(--ode-color-neutral-700); +} + +[data-theme='dark'] .badge--neutral { + background-color: var(--ode-color-neutral-800); + color: var(--ode-color-neutral-200); +} + diff --git a/src/components/Button/index.tsx b/src/components/Button/index.tsx new file mode 100644 index 0000000..58ae1b0 --- /dev/null +++ b/src/components/Button/index.tsx @@ -0,0 +1,73 @@ +import type { ReactNode, ButtonHTMLAttributes } from 'react'; +import Link from '@docusaurus/Link'; +import clsx from 'clsx'; +import styles from './styles.module.css'; + +type ButtonVariant = 'primary' | 'secondary' | 'outline' | 'ghost' | 'pill-light' | 'pill-dark'; +type ButtonSize = 'sm' | 'md' | 'lg'; + +interface BaseButtonProps { + variant?: ButtonVariant; + size?: ButtonSize; + children: ReactNode; + className?: string; + disabled?: boolean; +} + +interface ButtonAsButton extends BaseButtonProps, Omit, 'className' | 'children'> { + href?: never; + to?: never; + onClick?: () => void; +} + +interface ButtonAsLink extends BaseButtonProps { + href?: string; + to?: string; + onClick?: never; +} + +type ButtonProps = ButtonAsButton | ButtonAsLink; + +export default function Button({ + variant = 'primary', + size = 'md', + children, + className, + disabled = false, + href, + to, + onClick, + ...props +}: ButtonProps): ReactNode { + const classes = clsx( + styles.button, + styles[`button--${variant}`], + styles[`button--${size}`], + disabled && styles['button--disabled'], + className + ); + + if (href || to) { + return ( + + {children} + + ); + } + + return ( + + ); +} + diff --git a/src/components/Button/styles.module.css b/src/components/Button/styles.module.css new file mode 100644 index 0000000..023233f --- /dev/null +++ b/src/components/Button/styles.module.css @@ -0,0 +1,321 @@ +.button { + position: relative; + display: inline-flex !important; + align-items: center !important; + justify-content: center !important; + font-family: var(--ode-font-family-sans) !important; + font-weight: var(--ode-font-weight-button) !important; + line-height: var(--ode-line-height-tight) !important; + text-decoration: none !important; + border: var(--ode-border-width-thin) solid transparent !important; + border-radius: var(--ode-border-radius-md) !important; + cursor: pointer !important; + transition: all var(--ode-duration-normal) var(--ode-easing-ease-out) !important; + white-space: nowrap !important; + user-select: none !important; + background-color: transparent !important; + overflow: visible !important; + z-index: 0; + text-transform: none; /* Default - can be overridden per variant */ +} + +.button:hover, +.button:focus, +.button:active { + text-decoration: none !important; +} + +.button > * { + position: relative; + z-index: 2; +} + +.button:focus { + outline: var(--ode-border-width-medium) solid var(--ifm-color-primary); + outline-offset: var(--ode-spacing-1); +} + +.button--disabled { + opacity: var(--ode-opacity-50); + cursor: not-allowed; + pointer-events: none; +} + +/* Primary Variant */ +.button--primary { + background-color: var(--ode-color-brand-primary-500) !important; + color: var(--ode-color-neutral-white) !important; + border-color: var(--ode-color-brand-primary-500) !important; +} + +.button--primary:hover:not(.button--disabled) { + background-color: var(--ode-color-brand-primary-600) !important; + border-color: var(--ode-color-brand-primary-600) !important; + box-shadow: var(--ode-shadow-sm) !important; +} + +.button--primary:active:not(.button--disabled) { + background-color: var(--ode-color-brand-primary-700) !important; + border-color: var(--ode-color-brand-primary-700) !important; +} + +/* Secondary Variant */ +.button--secondary { + background-color: var(--ode-color-brand-secondary-500) !important; + color: var(--ode-color-neutral-900) !important; + border-color: var(--ode-color-brand-secondary-500) !important; +} + +.button--secondary:hover:not(.button--disabled) { + background-color: var(--ode-color-brand-secondary-600) !important; + border-color: var(--ode-color-brand-secondary-600) !important; + box-shadow: var(--ode-shadow-sm) !important; +} + +.button--secondary:active:not(.button--disabled) { + background-color: var(--ode-color-brand-secondary-700) !important; + border-color: var(--ode-color-brand-secondary-700) !important; +} + +/* Outline Variant */ +.button--outline { + background-color: transparent; + color: var(--ode-color-brand-primary-500); + border-color: var(--ode-color-brand-primary-500); +} + +.button--outline:hover:not(.button--disabled) { + background-color: var(--ode-color-brand-primary-50); + color: var(--ode-color-brand-primary-600); + border-color: var(--ode-color-brand-primary-600); +} + +[data-theme='dark'] .button--outline { + color: var(--ode-color-brand-primary-400); + border-color: var(--ode-color-brand-primary-400); +} + +[data-theme='dark'] .button--outline:hover:not(.button--disabled) { + background-color: rgba(79, 127, 78, 0.1); + color: var(--ode-color-brand-primary-300); + border-color: var(--ode-color-brand-primary-300); +} + +/* Ghost Variant */ +.button--ghost { + background-color: transparent; + color: var(--ode-color-brand-primary-500); + border-color: transparent; +} + +.button--ghost:hover:not(.button--disabled) { + background-color: var(--ode-color-neutral-100); + color: var(--ode-color-brand-primary-600); +} + +[data-theme='dark'] .button--ghost { + color: var(--ode-color-brand-primary-400); +} + +[data-theme='dark'] .button--ghost:hover:not(.button--disabled) { + background-color: var(--ode-color-neutral-800); + color: var(--ode-color-brand-primary-300); +} + +/* Size Variants */ +.button--sm { + padding: var(--ode-spacing-2) var(--ode-spacing-4); + font-size: var(--ode-font-size-sm); + gap: var(--ode-spacing-2); +} + +.button--md { + padding: var(--ode-spacing-3) var(--ode-spacing-6); + font-size: var(--ode-font-size-base); + gap: var(--ode-spacing-2); +} + +.button--lg { + padding: var(--ode-spacing-4) var(--ode-spacing-8); + font-size: var(--ode-font-size-lg); + gap: var(--ode-spacing-3); +} + +/* Pill Light Variant - Secondary color bg, primary color border/text, fade on left */ +.button--pill-light { + border-radius: var(--ode-border-radius-full) !important; + background-color: var(--ode-color-brand-secondary-500) !important; /* #E9B85B */ + color: var(--ode-color-brand-primary-500) !important; /* #4F7F4E */ + border: var(--ode-border-width-thin) solid var(--ode-color-brand-primary-500) !important; + position: relative; + overflow: visible; + text-transform: uppercase !important; + letter-spacing: 0.02em !important; +} + +/* Create fading border effect on left side - overlay that fades the border */ +.button--pill-light::after { + content: ''; + position: absolute; + top: -1px; + left: -1px; + width: 30%; + height: calc(100% + 2px); + border-radius: var(--ode-border-radius-full) 0 0 var(--ode-border-radius-full); + background: linear-gradient( + to right, + var(--ode-color-brand-secondary-500) 0%, + rgba(233, 184, 91, 0.99) 8%, + rgba(233, 184, 91, 0.95) 16%, + rgba(233, 184, 91, 0.88) 25%, + rgba(233, 184, 91, 0.75) 35%, + rgba(233, 184, 91, 0.6) 50%, + rgba(233, 184, 91, 0.4) 65%, + rgba(233, 184, 91, 0.2) 80%, + transparent 100% + ); + pointer-events: none; + z-index: 1; +} + +.button--pill-light:hover:not(.button--disabled) { + background-color: var(--ode-color-brand-secondary-400) !important; + color: var(--ode-color-brand-primary-600) !important; + border-color: var(--ode-color-brand-primary-600) !important; +} + +.button--pill-light:hover:not(.button--disabled)::after { + background: linear-gradient( + to right, + var(--ode-color-brand-secondary-400) 0%, + rgba(240, 184, 77, 0.99) 8%, + rgba(240, 184, 77, 0.95) 16%, + rgba(240, 184, 77, 0.88) 25%, + rgba(240, 184, 77, 0.75) 35%, + rgba(240, 184, 77, 0.6) 50%, + rgba(240, 184, 77, 0.4) 65%, + rgba(240, 184, 77, 0.2) 80%, + transparent 100% + ); +} + +[data-theme='dark'] .button--pill-light { + background-color: var(--ode-color-brand-secondary-500) !important; + color: var(--ode-color-brand-primary-400) !important; + border-color: var(--ode-color-brand-primary-400) !important; +} + +[data-theme='dark'] .button--pill-light::after { + background: linear-gradient( + to right, + var(--ode-color-brand-secondary-500) 0%, + rgba(233, 184, 91, 0.99) 8%, + rgba(233, 184, 91, 0.95) 16%, + rgba(233, 184, 91, 0.88) 25%, + rgba(233, 184, 91, 0.75) 35%, + rgba(233, 184, 91, 0.6) 50%, + rgba(233, 184, 91, 0.4) 65%, + rgba(233, 184, 91, 0.2) 80%, + transparent 100% + ); +} + +[data-theme='dark'] .button--pill-light:hover:not(.button--disabled) { + background-color: var(--ode-color-brand-secondary-400) !important; + color: var(--ode-color-brand-primary-300) !important; + border-color: var(--ode-color-brand-primary-300) !important; +} + +/* Pill Dark Variant - Primary color bg, white text, secondary color border, fade on right */ +.button--pill-dark { + border-radius: var(--ode-border-radius-full) !important; + background-color: var(--ode-color-brand-primary-500) !important; /* #4F7F4E */ + color: var(--ode-color-neutral-white) !important; + border: var(--ode-border-width-thin) solid var(--ode-color-brand-secondary-500) !important; /* #E9B85B */ + position: relative; + overflow: visible; + text-transform: uppercase !important; + letter-spacing: 0.02em !important; +} + +/* Create fading border effect on right side - overlay that fades the border */ +.button--pill-dark::after { + content: ''; + position: absolute; + top: -1px; + right: -1px; + width: 30%; + height: calc(100% + 2px); + border-radius: 0 var(--ode-border-radius-full) var(--ode-border-radius-full) 0; + background: linear-gradient( + to left, + var(--ode-color-brand-primary-500) 0%, + rgba(79, 127, 78, 0.99) 8%, + rgba(79, 127, 78, 0.95) 16%, + rgba(79, 127, 78, 0.88) 25%, + rgba(79, 127, 78, 0.75) 35%, + rgba(79, 127, 78, 0.6) 50%, + rgba(79, 127, 78, 0.4) 65%, + rgba(79, 127, 78, 0.2) 80%, + transparent 100% + ); + pointer-events: none; + z-index: 1; +} + +.button--pill-dark:hover:not(.button--disabled) { + background-color: var(--ode-color-brand-primary-600) !important; + color: var(--ode-color-neutral-white) !important; + border-color: var(--ode-color-brand-secondary-400) !important; +} + +.button--pill-dark:hover:not(.button--disabled)::after { + background: linear-gradient( + to left, + var(--ode-color-brand-primary-600) 0%, + rgba(63, 106, 62, 0.99) 8%, + rgba(63, 106, 62, 0.95) 16%, + rgba(63, 106, 62, 0.88) 25%, + rgba(63, 106, 62, 0.75) 35%, + rgba(63, 106, 62, 0.6) 50%, + rgba(63, 106, 62, 0.4) 65%, + rgba(63, 106, 62, 0.2) 80%, + transparent 100% + ); +} + +[data-theme='dark'] .button--pill-dark { + background-color: var(--ode-color-brand-primary-500) !important; + color: var(--ode-color-neutral-white) !important; + border-color: var(--ode-color-brand-secondary-500) !important; +} + +[data-theme='dark'] .button--pill-dark::after { + background: linear-gradient( + to left, + var(--ode-color-brand-primary-500) 0%, + rgba(79, 127, 78, 0.99) 8%, + rgba(79, 127, 78, 0.95) 16%, + rgba(79, 127, 78, 0.88) 25%, + rgba(79, 127, 78, 0.75) 35%, + rgba(79, 127, 78, 0.6) 50%, + rgba(79, 127, 78, 0.4) 65%, + rgba(79, 127, 78, 0.2) 80%, + transparent 100% + ); +} + +[data-theme='dark'] .button--pill-dark:hover:not(.button--disabled) { + background-color: var(--ode-color-brand-primary-400) !important; + color: var(--ode-color-neutral-white) !important; + border-color: var(--ode-color-brand-secondary-400) !important; +} + +/* Responsive */ +@media (max-width: 768px) { + .button--lg { + padding: var(--ode-spacing-3) var(--ode-spacing-6); + font-size: var(--ode-font-size-base); + } +} + diff --git a/src/components/ButtonGroup/index.tsx b/src/components/ButtonGroup/index.tsx new file mode 100644 index 0000000..1fec749 --- /dev/null +++ b/src/components/ButtonGroup/index.tsx @@ -0,0 +1,19 @@ +import type { ReactNode } from 'react'; +import clsx from 'clsx'; +import styles from './styles.module.css'; + +interface ButtonGroupProps { + children: ReactNode; + className?: string; +} + +export default function ButtonGroup({ + children, + className, +}: ButtonGroupProps): ReactNode { + return ( +
+ {children} +
+ ); +} diff --git a/src/components/ButtonGroup/styles.module.css b/src/components/ButtonGroup/styles.module.css new file mode 100644 index 0000000..90fb998 --- /dev/null +++ b/src/components/ButtonGroup/styles.module.css @@ -0,0 +1,16 @@ +.buttonGroup { + display: inline-flex; + gap: var(--ode-spacing-4); + align-items: center; +} + +@media (max-width: 768px) { + .buttonGroup { + flex-direction: column; + width: 100%; + } + + .buttonGroup > * { + width: 100%; + } +} diff --git a/src/components/CodeBlock/index.tsx b/src/components/CodeBlock/index.tsx new file mode 100644 index 0000000..1967f13 --- /dev/null +++ b/src/components/CodeBlock/index.tsx @@ -0,0 +1,44 @@ +import type { ReactNode } from 'react'; +import clsx from 'clsx'; +import styles from './styles.module.css'; + +interface CodeBlockProps { + children: ReactNode; + language?: string; + title?: string; + className?: string; + showLineNumbers?: boolean; +} + +export default function CodeBlock({ + children, + language, + title, + className, + showLineNumbers = false, +}: CodeBlockProps): ReactNode { + return ( +
+ {title && ( +
+ {language && ( + {language} + )} + {title} +
+ )} +
+
+          
+            {children}
+          
+        
+
+
+ ); +} diff --git a/src/components/CodeBlock/styles.module.css b/src/components/CodeBlock/styles.module.css new file mode 100644 index 0000000..183dd10 --- /dev/null +++ b/src/components/CodeBlock/styles.module.css @@ -0,0 +1,110 @@ +.codeBlockWrapper { + margin: var(--ode-spacing-6) 0; + border-radius: var(--ode-border-radius-md); + overflow: hidden; + border: var(--ode-border-width-thin) solid var(--ode-color-neutral-300); + background-color: var(--ode-color-neutral-50); +} + +[data-theme='dark'] .codeBlockWrapper { + border-color: var(--ode-color-neutral-700); + background-color: var(--ode-color-neutral-900); +} + +.codeBlockHeader { + display: flex; + align-items: center; + justify-content: space-between; + padding: var(--ode-spacing-3) var(--ode-spacing-4); + background-color: var(--ode-color-neutral-100); + border-bottom: var(--ode-border-width-thin) solid var(--ode-color-neutral-300); + font-family: var(--ode-font-family-sans); + font-size: var(--ode-font-size-sm); +} + +[data-theme='dark'] .codeBlockHeader { + background-color: var(--ode-color-neutral-800); + border-bottom-color: var(--ode-color-neutral-700); +} + +.codeBlockLanguage { + font-weight: var(--ode-font-weight-medium); + color: var(--ode-color-neutral-600); + text-transform: uppercase; + letter-spacing: 0.05em; + font-size: var(--ode-font-size-xs); +} + +[data-theme='dark'] .codeBlockLanguage { + color: var(--ode-color-neutral-400); +} + +.codeBlockTitle { + font-weight: var(--ode-font-weight-regular); + color: var(--ode-color-neutral-700); + flex: 1; + text-align: right; +} + +[data-theme='dark'] .codeBlockTitle { + color: var(--ode-color-neutral-300); +} + +.codeBlockContainer { + overflow-x: auto; + background-color: var(--ode-color-neutral-50); +} + +[data-theme='dark'] .codeBlockContainer { + background-color: var(--ode-color-neutral-900); +} + +.codeBlock { + margin: 0; + padding: var(--ode-spacing-4); + font-family: var(--ode-font-family-mono); + font-size: var(--ode-font-size-sm); + line-height: var(--ode-line-height-relaxed); + color: var(--ode-color-neutral-900); + background: transparent; + overflow-x: auto; +} + +[data-theme='dark'] .codeBlock { + color: var(--ode-color-neutral-100); +} + +.codeBlock code { + font-family: inherit; + font-size: inherit; + color: inherit; + background: transparent; + padding: 0; + border-radius: 0; +} + +.codeBlock--lineNumbers { + counter-reset: line; +} + +.codeBlock--lineNumbers code { + display: block; + padding-left: var(--ode-spacing-12); + position: relative; +} + +.codeBlock--lineNumbers code::before { + counter-increment: line; + content: counter(line); + position: absolute; + left: 0; + width: var(--ode-spacing-8); + text-align: right; + padding-right: var(--ode-spacing-4); + color: var(--ode-color-neutral-500); + user-select: none; +} + +[data-theme='dark'] .codeBlock--lineNumbers code::before { + color: var(--ode-color-neutral-600); +} diff --git a/src/components/Container/index.tsx b/src/components/Container/index.tsx new file mode 100644 index 0000000..7d4867f --- /dev/null +++ b/src/components/Container/index.tsx @@ -0,0 +1,29 @@ +import type { ReactNode } from 'react'; +import clsx from 'clsx'; +import styles from './styles.module.css'; + +type ContainerSize = 'sm' | 'md' | 'lg' | 'xl' | '2xl' | 'full'; + +interface ContainerProps { + size?: ContainerSize; + children: ReactNode; + className?: string; +} + +export default function Container({ + size = 'lg', + children, + className, +}: ContainerProps): ReactNode { + return ( +
+ {children} +
+ ); +} diff --git a/src/components/Container/styles.module.css b/src/components/Container/styles.module.css new file mode 100644 index 0000000..e258fc3 --- /dev/null +++ b/src/components/Container/styles.module.css @@ -0,0 +1,45 @@ +.container { + width: 100%; + margin-left: auto; + margin-right: auto; + padding-left: var(--ode-spacing-4); + padding-right: var(--ode-spacing-4); +} + +.container--sm { + max-width: var(--ode-container-sm); +} + +.container--md { + max-width: var(--ode-container-md); +} + +.container--lg { + max-width: var(--ode-container-lg); +} + +.container--xl { + max-width: var(--ode-container-xl); +} + +.container--2xl { + max-width: var(--ode-container-2xl); +} + +.container--full { + max-width: 100%; +} + +@media (min-width: 640px) { + .container { + padding-left: var(--ode-spacing-6); + padding-right: var(--ode-spacing-6); + } +} + +@media (min-width: 1024px) { + .container { + padding-left: var(--ode-spacing-8); + padding-right: var(--ode-spacing-8); + } +} diff --git a/src/components/Divider/index.tsx b/src/components/Divider/index.tsx new file mode 100644 index 0000000..14e7b61 --- /dev/null +++ b/src/components/Divider/index.tsx @@ -0,0 +1,52 @@ +import type { ReactNode } from 'react'; +import clsx from 'clsx'; +import styles from './styles.module.css'; + +type DividerVariant = 'solid' | 'dashed' | 'dotted'; +type DividerOrientation = 'horizontal' | 'vertical'; + +interface DividerProps { + variant?: DividerVariant; + orientation?: DividerOrientation; + spacing?: 'none' | 'sm' | 'md' | 'lg'; + className?: string; + children?: ReactNode; +} + +export default function Divider({ + variant = 'solid', + orientation = 'horizontal', + spacing = 'md', + className, + children, +}: DividerProps): ReactNode { + if (children) { + return ( +
+
+ {children} +
+
+ ); + } + + return ( +
+ ); +} + diff --git a/src/components/Divider/styles.module.css b/src/components/Divider/styles.module.css new file mode 100644 index 0000000..b25f778 --- /dev/null +++ b/src/components/Divider/styles.module.css @@ -0,0 +1,81 @@ +.divider, +.dividerLine { + border: none; + border-top: var(--ode-border-width-thin) solid var(--ode-color-neutral-300); + margin: 0; +} + +[data-theme='dark'] .divider, +[data-theme='dark'] .dividerLine { + border-top-color: var(--ode-color-neutral-700); +} + +.divider--dashed { + border-style: dashed; +} + +.divider--dotted { + border-style: dotted; +} + +.divider--horizontal { + width: 100%; + height: 0; +} + +.divider--vertical { + width: 0; + height: 100%; + border-top: none; + border-left: var(--ode-border-width-thin) solid var(--ode-color-neutral-300); +} + +[data-theme='dark'] .divider--vertical { + border-left-color: var(--ode-color-neutral-700); +} + +/* Spacing Variants */ +.divider--spacing-none { + margin: 0; +} + +.divider--spacing-sm { + margin: var(--ode-spacing-4) 0; +} + +.divider--spacing-md { + margin: var(--ode-spacing-8) 0; +} + +.divider--spacing-lg { + margin: var(--ode-spacing-12) 0; +} + +.dividerWithText { + display: flex; + align-items: center; + width: 100%; + gap: var(--ode-spacing-4); +} + +.dividerWithText .dividerLine { + flex: 1; + border-top: var(--ode-border-width-thin) solid var(--ode-color-neutral-300); +} + +[data-theme='dark'] .dividerWithText .dividerLine { + border-top-color: var(--ode-color-neutral-700); +} + +.dividerText { + font-family: var(--ode-font-family-sans); + font-size: var(--ode-font-size-sm); + color: var(--ode-color-neutral-600); + white-space: nowrap; + flex-shrink: 0; +} + +[data-theme='dark'] .dividerText { + color: var(--ode-color-neutral-400); +} + diff --git a/src/components/README.md b/src/components/README.md new file mode 100644 index 0000000..7d9f1d0 --- /dev/null +++ b/src/components/README.md @@ -0,0 +1,251 @@ +# ODE Design System Components + +A collection of reusable React components built with ODE design tokens. All components support dark mode, are fully accessible, and follow the ODE design system specifications. + +## Components + +### Alert +Display contextual feedback messages with semantic variants. + +```tsx +import { Alert } from '@site/src/components'; + + + This is an informational message. + + +Operation completed successfully! +Please review your input. +Something went wrong. +``` + +**Props:** +- `variant`: `'info' | 'success' | 'warning' | 'error'` (default: `'info'`) +- `size`: `'sm' | 'md'` (default: `'md'`) +- `title`: Optional title text +- `icon`: Optional custom icon element +- `children`: Alert message content + +--- + +### Badge +Small status indicators and labels. + +```tsx +import { Badge } from '@site/src/components'; + +New +Active +Pending +``` + +**Props:** +- `variant`: `'primary' | 'secondary' | 'success' | 'warning' | 'error' | 'info' | 'neutral'` (default: `'primary'`) +- `size`: `'sm' | 'md'` (default: `'md'`) + +--- + +### Button +Interactive button component with multiple variants and sizes. + +```tsx +import { Button, ButtonGroup } from '@site/src/components'; + + + + + + +{/* Pill buttons with fading borders - perfect for pairs */} + + + + + + +``` + +**Props:** +- `variant`: `'primary' | 'secondary' | 'outline' | 'ghost' | 'pill-light' | 'pill-dark'` (default: `'primary'`) + - `pill-light`: Light style with transparent background, dark border/text, fade on left side + - `pill-dark`: Dark style with filled background, light text, fade on right side +- `size`: `'sm' | 'md' | 'lg'` (default: `'md'`) +- `disabled`: Boolean +- `href` or `to`: Renders as a link instead of button +- `onClick`: Click handler (when used as button) + +**Pill Button Behavior:** +- `pill-light`: Default state has transparent background with dark border/text. On hover, fills with light background. +- `pill-dark`: Default state has dark filled background with light text. On hover, darkens further. +- When paired together, they create opposite visual states perfect for toggle or action pairs. + +--- + +### CodeBlock +Enhanced code block with optional title and line numbers. + +```tsx +import { CodeBlock } from '@site/src/components'; + + +{`function greet(name: string) { + return \`Hello, \${name}!\`; +}`} + +``` + +**Props:** +- `language`: Code language for syntax highlighting +- `title`: Optional title shown in header +- `showLineNumbers`: Boolean (default: `false`) + +--- + +### Container +Responsive container with max-width constraints. + +```tsx +import { Container } from '@site/src/components'; + + +

Content

+
+``` + +**Props:** +- `size`: `'sm' | 'md' | 'lg' | 'xl' | '2xl' | 'full'` (default: `'lg'`) + +--- + +### Divider +Visual separator with optional text. + +```tsx +import { Divider } from '@site/src/components'; + + + +Or + +``` + +**Props:** +- `variant`: `'solid' | 'dashed' | 'dotted'` (default: `'solid'`) +- `orientation`: `'horizontal' | 'vertical'` (default: `'horizontal'`) +- `spacing`: `'none' | 'sm' | 'md' | 'lg'` (default: `'md'`) +- `children`: Optional text to display in center + +--- + +### Spacer +Consistent spacing utility component. + +```tsx +import { Spacer } from '@site/src/components'; + + + + +``` + +**Props:** +- `size`: `0 | 1 | 2 | 3 | 4 | 5 | 6 | 8 | 10 | 12 | 16 | 20 | 24` +- `axis`: `'x' | 'y' | 'both'` (default: `'both'`) + +--- + +### Tag +Categorization tags with optional remove functionality. + +```tsx +import { Tag } from '@site/src/components'; + +React +TypeScript + console.log('removed')}> + Removable + +``` + +**Props:** +- `variant`: `'primary' | 'secondary' | 'neutral'` (default: `'neutral'`) +- `size`: `'sm' | 'md'` (default: `'md'`) +- `removable`: Boolean (default: `false`) +- `onRemove`: Callback when remove button is clicked + +--- + +### Tooltip +Contextual information on hover/focus. + +```tsx +import { Tooltip } from '@site/src/components'; + + + + +``` + +**Props:** +- `content`: Tooltip content +- `position`: `'top' | 'bottom' | 'left' | 'right'` (default: `'top'`) +- `delay`: Delay in milliseconds before showing (default: `200`) + +--- + +### ButtonGroup +Container for grouping buttons together, especially useful for pill button pairs. + +```tsx +import { ButtonGroup, Button } from '@site/src/components'; + + + + + +``` + +**Props:** +- `children`: Button elements to group together +- `className`: Optional additional CSS classes + +--- + +## Design Tokens + +All components use ODE design tokens defined in `src/css/ode-tokens.css`. These include: + +- **Colors**: Brand (primary/secondary), semantic (success/error/warning/info), neutral +- **Spacing**: 4px base unit scale (0-24) +- **Typography**: Font families, sizes, weights, line heights +- **Borders**: Radius and width variants +- **Shadows**: Elevation levels +- **Motion**: Duration and easing functions +- **Layout**: Breakpoints and container max-widths + +## Dark Mode + +All components automatically support dark mode through the `[data-theme='dark']` selector. No additional configuration needed. + +## Accessibility + +Components follow accessibility best practices: +- Proper ARIA attributes +- Keyboard navigation support +- Focus management +- Semantic HTML elements +- Color contrast compliance + +## Usage in MDX + +Components can be imported and used directly in MDX files: + +```mdx +import { Alert, Button, Badge } from '@site/src/components'; + + + This works in MDX! + + + +``` diff --git a/src/components/Spacer/index.tsx b/src/components/Spacer/index.tsx new file mode 100644 index 0000000..4407aaf --- /dev/null +++ b/src/components/Spacer/index.tsx @@ -0,0 +1,31 @@ +import type { ReactElement } from 'react'; +import clsx from 'clsx'; +import styles from './styles.module.css'; + +type SpacerSize = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 8 | 10 | 12 | 16 | 20 | 24; +type SpacerAxis = 'x' | 'y' | 'both'; + +interface SpacerProps { + size: SpacerSize; + axis?: SpacerAxis; + className?: string; +} + +export default function Spacer({ + size, + axis = 'both', + className, +}: SpacerProps): React.ReactElement { + return ( +