feat: IACE Bibliotheks-Browser — 751 Normen, 1000 Patterns, 200 Massnahmen
Neue Seite /sdk/iace/library mit 3 Tabs: - Normen: Suche + Filter A/B/C + Pflicht + Beuth-Links - Patterns: Suche + Filter Kategorie/Prioritaet + Details aufklappbar - Massnahmen: Suche + Filter Design/Schutz/Information Alle mit Pagination (50/Seite) und Zaehler-Badges. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,128 @@
|
||||
'use client'
|
||||
|
||||
import React, { useState } from 'react'
|
||||
|
||||
// ---------- Pagination ----------
|
||||
interface PaginationProps {
|
||||
page: number
|
||||
totalPages: number
|
||||
onPageChange: (p: number) => void
|
||||
}
|
||||
|
||||
export function Pagination({ page, totalPages, onPageChange }: PaginationProps) {
|
||||
if (totalPages <= 1) return null
|
||||
return (
|
||||
<div className="flex items-center justify-center gap-3 pt-4 border-t border-gray-200 dark:border-gray-700">
|
||||
<button
|
||||
onClick={() => onPageChange(page - 1)}
|
||||
disabled={page <= 1}
|
||||
className="px-3 py-1.5 text-sm rounded-lg border border-gray-300 dark:border-gray-600 disabled:opacity-40 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
|
||||
>
|
||||
‹ Zurueck
|
||||
</button>
|
||||
<span className="text-sm text-gray-600 dark:text-gray-400">
|
||||
Seite {page} / {totalPages}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => onPageChange(page + 1)}
|
||||
disabled={page >= totalPages}
|
||||
className="px-3 py-1.5 text-sm rounded-lg border border-gray-300 dark:border-gray-600 disabled:opacity-40 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
|
||||
>
|
||||
Weiter ›
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ---------- Search ----------
|
||||
interface SearchInputProps {
|
||||
value: string
|
||||
onChange: (v: string) => void
|
||||
placeholder?: string
|
||||
}
|
||||
|
||||
export function SearchInput({ value, onChange, placeholder }: SearchInputProps) {
|
||||
return (
|
||||
<div className="relative">
|
||||
<svg className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||
</svg>
|
||||
<input
|
||||
type="text"
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
placeholder={placeholder || 'Suchen...'}
|
||||
className="w-full pl-10 pr-4 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:ring-2 focus:ring-purple-500 focus:border-transparent"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ---------- Filter Dropdown ----------
|
||||
interface FilterDropdownProps {
|
||||
label: string
|
||||
value: string
|
||||
options: { value: string; label: string }[]
|
||||
onChange: (v: string) => void
|
||||
}
|
||||
|
||||
export function FilterDropdown({ label, value, options, onChange }: FilterDropdownProps) {
|
||||
return (
|
||||
<select
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
aria-label={label}
|
||||
className="px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:ring-2 focus:ring-purple-500 focus:border-transparent"
|
||||
>
|
||||
{options.map((o) => (
|
||||
<option key={o.value} value={o.value}>{o.label}</option>
|
||||
))}
|
||||
</select>
|
||||
)
|
||||
}
|
||||
|
||||
// ---------- Expandable Row ----------
|
||||
interface ExpandableRowProps {
|
||||
cells: React.ReactNode[]
|
||||
expandedContent: React.ReactNode
|
||||
colSpan: number
|
||||
}
|
||||
|
||||
export function ExpandableRow({ cells, expandedContent, colSpan }: ExpandableRowProps) {
|
||||
const [open, setOpen] = useState(false)
|
||||
return (
|
||||
<>
|
||||
<tr
|
||||
onClick={() => setOpen(!open)}
|
||||
className="cursor-pointer hover:bg-purple-50/50 dark:hover:bg-purple-900/10 transition-colors even:bg-gray-50/50 dark:even:bg-gray-800/30"
|
||||
>
|
||||
{cells.map((cell, i) => (
|
||||
<td key={i} className="px-4 py-2.5 text-sm text-gray-700 dark:text-gray-300 whitespace-nowrap">
|
||||
{cell}
|
||||
</td>
|
||||
))}
|
||||
<td className="px-3 py-2.5 text-gray-400">
|
||||
<svg className={`w-4 h-4 transition-transform ${open ? 'rotate-90' : ''}`} fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</td>
|
||||
</tr>
|
||||
{open && (
|
||||
<tr className="bg-purple-50/30 dark:bg-purple-900/5">
|
||||
<td colSpan={colSpan + 1} className="px-6 py-3 text-sm text-gray-600 dark:text-gray-400">
|
||||
{expandedContent}
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
// ---------- External Link Icon ----------
|
||||
export function ExternalLinkIcon() {
|
||||
return (
|
||||
<svg className="w-3.5 h-3.5 inline-block ml-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user