Add initial project structure with essential files

- Created .gitignore to exclude build artifacts and dependencies.
- Added index.html as the main entry point for the application.
- Included LICENSE file with Apache 2.0 terms.
- Initialized package.json and package-lock.json for project dependencies.
- Added pnpm-lock.yaml for package management.
- Created QUICKSTART.md for setup instructions.
- Added README.md and README.zh-CN.md for project documentation in English and Chinese.
This commit is contained in:
pftom
2026-04-28 12:25:59 +08:00
commit a98096a042
258 changed files with 67862 additions and 0 deletions
+129
View File
@@ -0,0 +1,129 @@
import { useMemo, useState } from 'react';
import { useT } from '../i18n';
import type { DesignSystemSummary } from '../types';
interface Props {
systems: DesignSystemSummary[];
selectedId: string | null;
onSelect: (id: string) => void;
onPreview: (id: string) => void;
}
const CATEGORY_ORDER = [
'Starter',
'AI & LLM',
'Developer Tools',
'Productivity & SaaS',
'Backend & Data',
'Design & Creative',
'Fintech & Crypto',
'E-Commerce & Retail',
'Media & Consumer',
'Automotive',
];
export function DesignSystemsTab({ systems, selectedId, onSelect, onPreview }: Props) {
const t = useT();
const [filter, setFilter] = useState('');
const [category, setCategory] = useState<string>('All');
const categories = useMemo(() => {
const cats = new Set<string>();
for (const s of systems) cats.add(s.category || 'Uncategorized');
const ordered: string[] = [];
for (const c of CATEGORY_ORDER) if (cats.has(c)) ordered.push(c);
for (const c of [...cats].sort()) if (!ordered.includes(c)) ordered.push(c);
return ['All', ...ordered];
}, [systems]);
const filtered = useMemo(() => {
const q = filter.trim().toLowerCase();
return systems.filter((s) => {
if (category !== 'All' && (s.category || 'Uncategorized') !== category) return false;
if (!q) return true;
return (
s.title.toLowerCase().includes(q) ||
s.summary.toLowerCase().includes(q)
);
});
}, [systems, filter, category]);
// The category metadata coming from each design system is authored in
// English. We translate the well-known buckets (All / Uncategorized) but
// pass the rest through unchanged so user-facing labels stay aligned with
// the underlying tags.
const renderCategory = (c: string) => {
if (c === 'All') return t('ds.categoryAll');
if (c === 'Uncategorized') return t('ds.categoryUncategorized');
return c;
};
return (
<div className="tab-panel">
<div className="tab-panel-toolbar">
<input
placeholder={t('ds.searchPlaceholder')}
value={filter}
onChange={(e) => setFilter(e.target.value)}
/>
<select value={category} onChange={(e) => setCategory(e.target.value)}>
{categories.map((c) => (
<option key={c} value={c}>
{renderCategory(c)}
</option>
))}
</select>
</div>
{filtered.length === 0 ? (
<div className="tab-empty">{t('ds.emptyNoMatch')}</div>
) : (
<div className="ds-list">
{filtered.map((s) => {
const active = s.id === selectedId;
return (
<div
key={s.id}
className={`ds-row ${active ? 'active' : ''}`}
onClick={() => onSelect(s.id)}
>
<div className="ds-row-body">
<div className="ds-row-title">
{s.title}
{active ? (
<span className="ds-row-default">
{t('ds.badgeDefault')}
</span>
) : null}
</div>
<div className="ds-row-summary">{s.summary || s.category}</div>
</div>
{s.swatches && s.swatches.length > 0 ? (
<div className="ds-row-swatches" aria-hidden>
{s.swatches.map((c, i) => (
<span
key={i}
className="ds-row-swatch"
style={{ background: c }}
title={c}
/>
))}
</div>
) : null}
<button
className="ghost"
onClick={(e) => {
e.stopPropagation();
onPreview(s.id);
}}
title={t('ds.previewTitle')}
>
{t('ds.preview')}
</button>
</div>
);
})}
</div>
)}
</div>
);
}