atomic-design-organisms

安装量: 46
排名: #15929

安装

npx skills add https://github.com/thebushidocollective/han --skill atomic-design-organisms

Atomic Design: Organisms

Master the creation of organisms - complex, distinct sections of an interface composed of molecules and atoms. Organisms represent standalone UI sections that could exist independently.

What Are Organisms?

Organisms are relatively complex UI components that form distinct sections of an interface. They are:

Composed of molecules and atoms: May include both levels Standalone sections: Can exist independently on a page Context-aware: Often tied to specific business contexts Stateful: May manage significant internal state Reusable: Used across different templates and pages Common Organism Types Navigation Organisms Header (logo + navigation + user menu) Footer (links + social icons + legal) Sidebar (navigation + user info + actions) Breadcrumbs (full navigation path) Content Organisms Product cards (image + details + actions) Comment sections (comments + reply forms) Article previews (title + excerpt + meta) User profiles (avatar + bio + stats) Form Organisms Login forms (fields + actions + links) Registration forms (multi-step fields) Checkout forms (payment + shipping) Search with filters Data Display Organisms Data tables (header + rows + pagination) Dashboards (stats + charts + actions) Timelines (events + connectors) Galleries (images + navigation) Header Organism Example Complete Implementation // organisms/Header/Header.tsx import React, { useState } from 'react'; import { Icon } from '@/components/atoms/Icon'; import { Button } from '@/components/atoms/Button'; import { Avatar } from '@/components/atoms/Avatar'; import { NavItem } from '@/components/molecules/NavItem'; import { SearchForm } from '@/components/molecules/SearchForm'; import styles from './Header.module.css';

export interface NavLink { id: string; label: string; href: string; icon?: string; badge?: number; }

export interface User { id: string; name: string; email: string; avatar?: string; }

export interface HeaderProps { / Logo element or image */ logo: React.ReactNode; / Navigation links / navigation: NavLink[]; / Current active nav item / activeNavId?: string; / Authenticated user */ user?: User | null; / Show search form / showSearch?: boolean; / Search submit handler / onSearch?: (query: string) => void; / Login click handler */ onLogin?: () => void; / Logout click handler / onLogout?: () => void; / Profile click handler / onProfileClick?: () => void; }

export const Header: React.FC = ({ logo, navigation, activeNavId, user, showSearch = true, onSearch, onLogin, onLogout, onProfileClick, }) => { const [mobileMenuOpen, setMobileMenuOpen] = useState(false); const [userMenuOpen, setUserMenuOpen] = useState(false);

return (

{/ Logo /}
{logo}

    {/* Desktop Navigation */}
    <nav className={styles.nav} aria-label="Main navigation">
      <ul className={styles.navList}>
        {navigation.map((item) => (
          <li key={item.id}>
            <NavItem
              label={item.label}
              href={item.href}
              icon={item.icon}
              badge={item.badge}
              isActive={item.id === activeNavId}
            />
          </li>
        ))}
      </ul>
    </nav>

    {/* Search */}
    {showSearch && onSearch && (
      <div className={styles.search}>
        <SearchForm
          onSubmit={onSearch}
          placeholder="Search..."
          size="sm"
        />
      </div>
    )}

    {/* User Actions */}
    <div className={styles.actions}>
      {user ? (
        <div className={styles.userMenu}>
          <button
            className={styles.userButton}
            onClick={() => setUserMenuOpen(!userMenuOpen)}
            aria-expanded={userMenuOpen}
            aria-haspopup="true"
          >
            <Avatar
              src={user.avatar}
              alt={user.name}
              initials={user.name.slice(0, 2).toUpperCase()}
              size="sm"
            />
            <span className={styles.userName}>{user.name}</span>
            <Icon name="chevron-down" size="xs" />
          </button>

          {userMenuOpen && (
            <div className={styles.dropdown}>
              <button onClick={onProfileClick}>
                <Icon name="user" size="sm" />
                Profile
              </button>
              <button onClick={onLogout}>
                <Icon name="log-out" size="sm" />
                Logout
              </button>
            </div>
          )}
        </div>
      ) : (
        <Button variant="primary" size="sm" onClick={onLogin}>
          Login
        </Button>
      )}
    </div>

    {/* Mobile Menu Toggle */}
    <button
      className={styles.mobileToggle}
      onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
      aria-expanded={mobileMenuOpen}
      aria-label="Toggle menu"
    >
      <Icon name={mobileMenuOpen ? 'x' : 'menu'} size="md" />
    </button>
  </div>

  {/* Mobile Navigation */}
  {mobileMenuOpen && (
    <nav className={styles.mobileNav} aria-label="Mobile navigation">
      <ul>
        {navigation.map((item) => (
          <li key={item.id}>
            <NavItem
              label={item.label}
              href={item.href}
              icon={item.icon}
              badge={item.badge}
              isActive={item.id === activeNavId}
              onClick={() => setMobileMenuOpen(false)}
            />
          </li>
        ))}
      </ul>
    </nav>
  )}
</header>

); };

Header.displayName = 'Header';

/ organisms/Header/Header.module.css / .header { position: sticky; top: 0; z-index: 100; background-color: var(--color-white); border-bottom: 1px solid var(--color-neutral-200); }

.container { display: flex; align-items: center; gap: 24px; max-width: 1280px; margin: 0 auto; padding: 12px 24px; }

.logo { flex-shrink: 0; }

.nav { display: none; flex: 1; }

@media (min-width: 768px) { .nav { display: block; } }

.navList { display: flex; gap: 8px; list-style: none; margin: 0; padding: 0; }

.search { display: none; width: 280px; }

@media (min-width: 1024px) { .search { display: block; } }

.actions { display: flex; align-items: center; gap: 12px; }

.userMenu { position: relative; }

.userButton { display: flex; align-items: center; gap: 8px; padding: 6px 12px; background: transparent; border: 1px solid var(--color-neutral-200); border-radius: 6px; cursor: pointer; }

.userName { display: none; }

@media (min-width: 640px) { .userName { display: inline; } }

.dropdown { position: absolute; top: 100%; right: 0; margin-top: 8px; min-width: 160px; background: var(--color-white); border: 1px solid var(--color-neutral-200); border-radius: 8px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); overflow: hidden; }

.dropdown button { display: flex; align-items: center; gap: 8px; width: 100%; padding: 12px 16px; background: transparent; border: none; cursor: pointer; text-align: left; }

.dropdown button:hover { background-color: var(--color-neutral-50); }

.mobileToggle { display: flex; padding: 8px; background: transparent; border: none; cursor: pointer; }

@media (min-width: 768px) { .mobileToggle { display: none; } }

.mobileNav { border-top: 1px solid var(--color-neutral-200); padding: 16px; }

.mobileNav ul { display: flex; flex-direction: column; gap: 8px; list-style: none; margin: 0; padding: 0; }

Footer Organism Example // organisms/Footer/Footer.tsx import React from 'react'; import { Icon } from '@/components/atoms/Icon'; import { Text } from '@/components/atoms/Typography'; import styles from './Footer.module.css';

export interface FooterLink { label: string; href: string; }

export interface FooterSection { title: string; links: FooterLink[]; }

export interface SocialLink { platform: string; href: string; icon: string; }

export interface FooterProps { / Footer logo */ logo: React.ReactNode; / Tagline or description / tagline?: string; / Link sections / sections: FooterSection[]; / Social media links */ socialLinks?: SocialLink[]; / Copyright text / copyright?: string; / Legal links / legalLinks?: FooterLink[]; }

export const Footer: React.FC = ({ logo, tagline, sections, socialLinks = [], copyright, legalLinks = [], }) => { const currentYear = new Date().getFullYear(); const copyrightText = copyright || ${currentYear} Company. All rights reserved.;

return (