@umituz/web-design-system 2.3.0 → 2.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/web-design-system",
3
- "version": "2.3.0",
3
+ "version": "2.4.1",
4
4
  "private": false,
5
5
  "description": "Web Design System - Atomic Design components (Atoms, Molecules, Organisms, Templates) for React applications",
6
6
  "main": "./src/index.ts",
@@ -0,0 +1,83 @@
1
+ /**
2
+ * ActiveFilterTags Component (Molecule)
3
+ * @description Display active filters with remove buttons
4
+ */
5
+
6
+ import type { BaseProps } from '../../domain/types';
7
+
8
+ export interface ActiveFilterTagsProps extends BaseProps {
9
+ selectedCategory: string | null;
10
+ categories: Array<{ id: string; name: string }>;
11
+ selectedTags: string[];
12
+ onCategoryRemove: () => void;
13
+ onTagRemove: (tag: string) => void;
14
+ onClearAll: () => void;
15
+ }
16
+
17
+ export const ActiveFilterTags = ({
18
+ selectedCategory,
19
+ categories,
20
+ selectedTags,
21
+ onCategoryRemove,
22
+ onTagRemove,
23
+ onClearAll,
24
+ className,
25
+ }: ActiveFilterTagsProps) => {
26
+ const categoryName = selectedCategory
27
+ ? categories.find(c => c.id === selectedCategory)?.name
28
+ : null;
29
+
30
+ if (!selectedCategory && selectedTags.length === 0) {
31
+ return null;
32
+ }
33
+
34
+ return (
35
+ <div className={`hidden lg:flex flex-wrap items-center gap-2 mb-4 md:mb-6 ${className || ''}`}>
36
+ <span className="text-sm text-text-secondary">Active filters:</span>
37
+
38
+ {/* Category Tag */}
39
+ {categoryName && (
40
+ <span className="inline-flex items-center gap-1 px-3 py-1.5 bg-primary-light text-text-primary rounded-full text-xs font-medium">
41
+ {categoryName}
42
+ <button
43
+ onClick={onCategoryRemove}
44
+ className="ml-1 hover:opacity-70 transition-opacity"
45
+ aria-label={`Remove ${categoryName} filter`}
46
+ >
47
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
48
+ <path d="M18 6L6 18M6 6l12 12" />
49
+ </svg>
50
+ </button>
51
+ </span>
52
+ )}
53
+
54
+ {/* Tag Buttons */}
55
+ {selectedTags.map((tag) => (
56
+ <span
57
+ key={tag}
58
+ className="inline-flex items-center gap-1 px-3 py-1.5 bg-bg-secondary text-text-primary rounded-full text-xs font-medium border border-border"
59
+ >
60
+ {tag}
61
+ <button
62
+ onClick={() => onTagRemove(tag)}
63
+ className="ml-1 hover:opacity-70 transition-opacity"
64
+ aria-label={`Remove ${tag} filter`}
65
+ >
66
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
67
+ <path d="M18 6L6 18M6 6l12 12" />
68
+ </svg>
69
+ </button>
70
+ </span>
71
+ ))}
72
+
73
+ {/* Clear All Button */}
74
+ <button
75
+ onClick={onClearAll}
76
+ className="px-3 py-1.5 text-primary-light hover:underline text-xs font-medium"
77
+ type="button"
78
+ >
79
+ Clear all
80
+ </button>
81
+ </div>
82
+ );
83
+ };
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Breadcrumb Component (Molecule)
3
+ * @description Navigation breadcrumb with home icon
4
+ */
5
+
6
+ import { Link } from 'react-router-dom';
7
+ import type { BaseProps } from '../../domain/types';
8
+
9
+ export interface BreadcrumbItem {
10
+ label: string;
11
+ href?: string;
12
+ }
13
+
14
+ export interface BreadcrumbProps extends BaseProps {
15
+ items: BreadcrumbItem[];
16
+ }
17
+
18
+ export const Breadcrumb = ({ items, className }: BreadcrumbProps) => {
19
+ return (
20
+ <nav className={`flex items-center gap-2 text-sm mb-4 md:mb-6 ${className || ''}`} aria-label="Breadcrumb">
21
+ {/* Home */}
22
+ <Link
23
+ to="/"
24
+ className="flex items-center gap-1 text-text-secondary hover:text-primary-light transition-colors"
25
+ aria-label="Home"
26
+ >
27
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
28
+ <path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" />
29
+ <polyline points="9 22 9 12 15 12 15 22" />
30
+ </svg>
31
+ </Link>
32
+
33
+ {/* Breadcrumb items */}
34
+ {items.map((item, index) => (
35
+ <div key={index} className="flex items-center gap-2">
36
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="text-text-secondary">
37
+ <polyline points="9 18 15 12 9 6" />
38
+ </svg>
39
+ {item.href ? (
40
+ <Link
41
+ to={item.href}
42
+ className="text-text-secondary hover:text-primary-light transition-colors"
43
+ >
44
+ {item.label}
45
+ </Link>
46
+ ) : (
47
+ <span className="text-text-primary font-medium">{item.label}</span>
48
+ )}
49
+ </div>
50
+ ))}
51
+ </nav>
52
+ );
53
+ };
@@ -0,0 +1,144 @@
1
+ /**
2
+ * CodeBlock Component (Molecule)
3
+ * @description Syntax-highlighted code block with copy button and expand/collapse
4
+ */
5
+
6
+ import { useState, useEffect } from 'react';
7
+ import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
8
+ import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism';
9
+ import type { BaseProps } from '../../domain/types';
10
+
11
+ export interface CodeBlockProps extends BaseProps {
12
+ children: string;
13
+ language?: string;
14
+ filename?: string;
15
+ showLineNumbers?: boolean;
16
+ }
17
+
18
+ export const CodeBlock = ({
19
+ children,
20
+ language = 'javascript',
21
+ filename,
22
+ showLineNumbers = false,
23
+ className,
24
+ }: CodeBlockProps) => {
25
+ const [copied, setCopied] = useState(false);
26
+ const [isExpanded, setIsExpanded] = useState(false);
27
+
28
+ // Cleanup copy timeout
29
+ useEffect(() => {
30
+ if (copied) {
31
+ const timeout = setTimeout(() => setCopied(false), 2000);
32
+ return () => clearTimeout(timeout);
33
+ }
34
+ }, [copied]);
35
+
36
+ const handleCopy = async () => {
37
+ try {
38
+ await navigator.clipboard.writeText(children);
39
+ setCopied(true);
40
+ } catch (error) {
41
+ console.error('Failed to copy code:', error);
42
+ }
43
+ };
44
+
45
+ // Auto-detect language if not provided
46
+ const detectedLanguage = language || 'javascript';
47
+
48
+ // Check if code is long enough for collapse
49
+ const lineCount = children.split('\n').length;
50
+ const isLongCode = lineCount > 10;
51
+
52
+ return (
53
+ <div className={`my-6 rounded-lg border border-border overflow-hidden bg-bg-card transition-theme ${className || ''}`}>
54
+ {/* Header */}
55
+ <div className="flex items-center justify-between px-4 py-2 bg-bg-tertiary border-b border-border">
56
+ <div className="flex items-center gap-2">
57
+ {filename && (
58
+ <span className="text-xs font-medium text-text-secondary">{filename}</span>
59
+ )}
60
+ <span className="text-xs px-2 py-1 bg-bg-primary text-text-primary rounded-md font-medium">
61
+ {detectedLanguage}
62
+ </span>
63
+ </div>
64
+
65
+ <div className="flex items-center gap-2">
66
+ {isLongCode && (
67
+ <button
68
+ onClick={() => setIsExpanded(!isExpanded)}
69
+ className="p-1 text-text-secondary hover:text-text-primary rounded transition-colors"
70
+ title={isExpanded ? 'Collapse' : 'Expand'}
71
+ type="button"
72
+ aria-label={isExpanded ? 'Collapse code' : 'Expand code'}
73
+ >
74
+ {isExpanded ? (
75
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
76
+ <polyline points="18 15 12 9 6 15" />
77
+ </svg>
78
+ ) : (
79
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
80
+ <polyline points="6 9 12 15 18 9" />
81
+ </svg>
82
+ )}
83
+ </button>
84
+ )}
85
+
86
+ <button
87
+ onClick={handleCopy}
88
+ className={`flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium rounded-lg transition-all ${
89
+ copied
90
+ ? 'bg-green-500 text-white'
91
+ : 'bg-bg-secondary text-text-primary hover:bg-bg-tertiary border border-border'
92
+ }`}
93
+ title={copied ? 'Copied!' : 'Copy code'}
94
+ type="button"
95
+ aria-label={copied ? 'Code copied' : 'Copy code'}
96
+ >
97
+ {copied ? (
98
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
99
+ <polyline points="20 6 9 17 4 12" />
100
+ </svg>
101
+ ) : (
102
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
103
+ <rect x="9" y="9" width="13" height="13" rx="2" ry="2" />
104
+ <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
105
+ </svg>
106
+ )}
107
+ <span className="hidden sm:inline">{copied ? 'Copied!' : 'Copy'}</span>
108
+ </button>
109
+ </div>
110
+ </div>
111
+
112
+ {/* Code */}
113
+ <div className={isLongCode && !isExpanded ? 'max-h-80 overflow-hidden' : ''}>
114
+ <SyntaxHighlighter
115
+ language={detectedLanguage}
116
+ style={vscDarkPlus}
117
+ showLineNumbers={showLineNumbers}
118
+ customStyle={{
119
+ margin: 0,
120
+ borderRadius: 0,
121
+ fontSize: '14px',
122
+ background: 'transparent',
123
+ }}
124
+ >
125
+ {children}
126
+ </SyntaxHighlighter>
127
+ </div>
128
+
129
+ {/* Expand hint for collapsed code */}
130
+ {isLongCode && !isExpanded && (
131
+ <div className="px-4 py-2 bg-bg-secondary text-center border-t border-border">
132
+ <button
133
+ onClick={() => setIsExpanded(true)}
134
+ className="text-sm text-primary-light hover:underline font-medium"
135
+ type="button"
136
+ aria-label={`Show all ${lineCount} lines`}
137
+ >
138
+ Show all {lineCount} lines
139
+ </button>
140
+ </div>
141
+ )}
142
+ </div>
143
+ );
144
+ };
@@ -37,3 +37,13 @@ export { ScrollArea, ScrollBar } from './ScrollArea';
37
37
 
38
38
  export { ListItem } from './ListItem';
39
39
  export type { ListItemProps } from './ListItem';
40
+
41
+ // NEW: Filter & Navigation Components
42
+ export { ActiveFilterTags } from './ActiveFilterTags';
43
+ export type { ActiveFilterTagsProps } from './ActiveFilterTags';
44
+
45
+ export { Breadcrumb } from './Breadcrumb';
46
+ export type { BreadcrumbProps, BreadcrumbItem } from './Breadcrumb';
47
+
48
+ export { CodeBlock } from './CodeBlock';
49
+ export type { CodeBlockProps } from './CodeBlock';
@@ -0,0 +1,186 @@
1
+ /**
2
+ * FilterBar Component (Organism)
3
+ * @description Mobile filter bar with search, categories, tags, and sort
4
+ */
5
+
6
+ import { useState } from 'react';
7
+ import type { BaseProps } from '../../domain/types';
8
+
9
+ export interface Category {
10
+ id: string;
11
+ name: string;
12
+ count: number;
13
+ icon: React.ComponentType<{ className?: string; size?: number }>;
14
+ }
15
+
16
+ export interface SortOption {
17
+ id: string;
18
+ name: string;
19
+ }
20
+
21
+ export interface FilterBarProps extends BaseProps {
22
+ searchQuery: string;
23
+ setSearchQuery: (query: string) => void;
24
+ selectedCategory: string | null;
25
+ onCategoryChange: (category: string) => void;
26
+ categories: Category[];
27
+ popularTags: string[];
28
+ selectedTags: string[];
29
+ onTagToggle: (tag: string) => void;
30
+ sortBy: string;
31
+ onSortChange: (sort: string) => void;
32
+ sortOptions: readonly SortOption[] | SortOption[];
33
+ hasActiveFilters: boolean;
34
+ onClearFilters: () => void;
35
+ }
36
+
37
+ export const FilterBar = ({
38
+ searchQuery,
39
+ setSearchQuery,
40
+ selectedCategory,
41
+ onCategoryChange,
42
+ categories,
43
+ popularTags,
44
+ selectedTags,
45
+ onTagToggle,
46
+ sortBy,
47
+ onSortChange,
48
+ sortOptions,
49
+ hasActiveFilters,
50
+ onClearFilters,
51
+ className,
52
+ }: FilterBarProps) => {
53
+ const [isFilterOpen, setIsFilterOpen] = useState(false);
54
+
55
+ return (
56
+ <div className={`lg:hidden mb-4 space-y-3 ${className || ''}`}>
57
+ {/* Search */}
58
+ <div className="relative">
59
+ <svg
60
+ className="absolute left-3 top-1/2 -translate-y-1/2 text-text-secondary"
61
+ width="18"
62
+ height="18"
63
+ viewBox="0 0 24 24"
64
+ fill="none"
65
+ stroke="currentColor"
66
+ strokeWidth="2"
67
+ strokeLinecap="round"
68
+ strokeLinejoin="round"
69
+ >
70
+ <circle cx="11" cy="11" r="8" />
71
+ <path d="m21 21-4.35-4.35" />
72
+ </svg>
73
+ <input
74
+ type="text"
75
+ placeholder="Search..."
76
+ value={searchQuery}
77
+ onChange={(e) => setSearchQuery(e.target.value)}
78
+ className="w-full pl-10 pr-4 py-2.5 bg-bg-secondary text-text-primary rounded-lg border border-border focus:border-primary-light focus:outline-none placeholder-text-secondary/50 transition-theme text-sm"
79
+ aria-label="Search"
80
+ />
81
+ </div>
82
+
83
+ {/* Filter Toggle + Sort + Clear Filters */}
84
+ <div className="flex items-center gap-2 flex-wrap">
85
+ <button
86
+ onClick={() => setIsFilterOpen(!isFilterOpen)}
87
+ className="flex items-center gap-2 px-4 py-2.5 bg-bg-secondary text-text-primary rounded-lg border border-border hover:border-primary-light transition-all text-sm font-medium transition-theme"
88
+ type="button"
89
+ aria-expanded={isFilterOpen}
90
+ aria-label="Toggle filters"
91
+ >
92
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
93
+ <path d="M12 20v-6M6 20V10M18 20V4" />
94
+ </svg>
95
+ Filters
96
+ {(selectedCategory || selectedTags.length > 0) && (
97
+ <span className="ml-1 px-2 py-0.5 bg-primary-light text-text-primary text-xs rounded-full">
98
+ {[selectedCategory, ...selectedTags].filter(Boolean).length}
99
+ </span>
100
+ )}
101
+ </button>
102
+
103
+ {/* Sort Dropdown */}
104
+ <select
105
+ value={sortBy}
106
+ onChange={(e) => onSortChange(e.target.value)}
107
+ className="px-3 py-2.5 bg-bg-secondary text-text-primary rounded-lg border border-border focus:border-primary-light focus:outline-none transition-theme text-sm"
108
+ aria-label="Sort by"
109
+ >
110
+ {sortOptions.map((option) => (
111
+ <option key={option.id} value={option.id}>
112
+ {option.name}
113
+ </option>
114
+ ))}
115
+ </select>
116
+
117
+ {/* Clear Filters */}
118
+ {hasActiveFilters && (
119
+ <button
120
+ onClick={onClearFilters}
121
+ className="px-3 py-2.5 text-text-secondary hover:text-text-primary text-sm transition-colors"
122
+ type="button"
123
+ >
124
+ Clear All
125
+ </button>
126
+ )}
127
+ </div>
128
+
129
+ {/* Mobile Filter Drawer */}
130
+ {isFilterOpen && (
131
+ <div className="bg-bg-card rounded-xl p-4 border border-border space-y-4 transition-theme">
132
+ {/* Categories */}
133
+ <div>
134
+ <h4 className="text-sm font-semibold text-text-primary mb-3">Categories</h4>
135
+ <div className="grid grid-cols-2 gap-2">
136
+ {categories.map((category) => {
137
+ const Icon = category.icon;
138
+ const isActive = selectedCategory === category.id;
139
+ return (
140
+ <button
141
+ key={category.id}
142
+ onClick={() => {
143
+ onCategoryChange(category.id);
144
+ }}
145
+ className={`flex items-center gap-2 px-3 py-2 rounded-lg text-xs transition-all transition-theme ${
146
+ isActive
147
+ ? 'bg-primary-light text-text-primary font-medium'
148
+ : 'bg-bg-secondary text-text-secondary hover:bg-bg-tertiary hover:text-text-primary'
149
+ }`}
150
+ type="button"
151
+ aria-pressed={isActive}
152
+ >
153
+ <Icon size={14} />
154
+ <span>{category.name}</span>
155
+ </button>
156
+ );
157
+ })}
158
+ </div>
159
+ </div>
160
+
161
+ {/* Popular Tags */}
162
+ <div>
163
+ <h4 className="text-sm font-semibold text-text-primary mb-3">Popular Tags</h4>
164
+ <div className="flex flex-wrap gap-2">
165
+ {popularTags.map((tag) => (
166
+ <button
167
+ key={tag}
168
+ onClick={() => onTagToggle(tag)}
169
+ className={`px-3 py-1.5 rounded-full text-xs font-medium transition-all transition-theme ${
170
+ selectedTags.includes(tag)
171
+ ? 'bg-primary-light text-text-primary'
172
+ : 'bg-bg-secondary text-text-secondary hover:bg-bg-tertiary hover:text-text-primary'
173
+ }`}
174
+ type="button"
175
+ aria-pressed={selectedTags.includes(tag)}
176
+ >
177
+ {tag}
178
+ </button>
179
+ ))}
180
+ </div>
181
+ </div>
182
+ </div>
183
+ )}
184
+ </div>
185
+ );
186
+ };
@@ -0,0 +1,172 @@
1
+ /**
2
+ * FilterSidebar Component (Organism)
3
+ * @description Desktop sidebar with search, categories, tags, and sort options
4
+ */
5
+
6
+ import type { BaseProps } from '../../domain/types';
7
+ import type { ReactNode } from 'react';
8
+
9
+ export interface Category {
10
+ id: string;
11
+ name: string;
12
+ count: number;
13
+ icon: React.ComponentType<{ className?: string; size?: number }>;
14
+ }
15
+
16
+ export interface SortOption {
17
+ id: string;
18
+ name: string;
19
+ }
20
+
21
+ export interface FilterSidebarProps extends BaseProps {
22
+ searchQuery: string;
23
+ setSearchQuery: (query: string) => void;
24
+ selectedCategory: string | null;
25
+ onCategoryChange: (category: string) => void;
26
+ categories: Category[];
27
+ popularTags: string[];
28
+ selectedTags: string[];
29
+ onTagToggle: (tag: string) => void;
30
+ sortBy: string;
31
+ onSortChange: (sort: string) => void;
32
+ sortOptions: readonly SortOption[] | SortOption[];
33
+ hasActiveFilters: boolean;
34
+ onClearFilters: () => void;
35
+ children?: React.ReactNode;
36
+ }
37
+
38
+ export const FilterSidebar = ({
39
+ searchQuery,
40
+ setSearchQuery,
41
+ selectedCategory,
42
+ onCategoryChange,
43
+ categories,
44
+ popularTags,
45
+ selectedTags,
46
+ onTagToggle,
47
+ sortBy,
48
+ onSortChange,
49
+ sortOptions,
50
+ hasActiveFilters,
51
+ onClearFilters,
52
+ children,
53
+ className,
54
+ }: FilterSidebarProps) => {
55
+ return (
56
+ <aside className={`hidden lg:block w-64 flex-shrink-0 ${className || ''}`}>
57
+ <div className="sticky top-24 space-y-6">
58
+ {/* Search */}
59
+ <div className="relative">
60
+ <svg
61
+ className="absolute left-3 top-1/2 -translate-y-1/2 text-text-secondary"
62
+ width="18"
63
+ height="18"
64
+ viewBox="0 0 24 24"
65
+ fill="none"
66
+ stroke="currentColor"
67
+ strokeWidth="2"
68
+ strokeLinecap="round"
69
+ strokeLinejoin="round"
70
+ >
71
+ <circle cx="11" cy="11" r="8" />
72
+ <path d="m21 21-4.35-4.35" />
73
+ </svg>
74
+ <input
75
+ type="text"
76
+ placeholder="Search..."
77
+ value={searchQuery}
78
+ onChange={(e) => setSearchQuery(e.target.value)}
79
+ className="w-full pl-10 pr-4 py-2.5 bg-bg-secondary text-text-primary rounded-lg border border-border focus:border-primary-light focus:outline-none placeholder-text-secondary/50 transition-theme text-sm"
80
+ aria-label="Search"
81
+ />
82
+ </div>
83
+
84
+ {/* Categories */}
85
+ <div>
86
+ <h3 className="text-sm font-semibold text-text-primary mb-3">Categories</h3>
87
+ <div className="space-y-1">
88
+ {categories.map((category) => {
89
+ const Icon = category.icon;
90
+ const isActive = selectedCategory === category.id;
91
+ return (
92
+ <button
93
+ key={category.id}
94
+ onClick={() => onCategoryChange(category.id)}
95
+ className={`w-full flex items-center gap-3 px-3 py-2 rounded-lg text-sm transition-all transition-theme ${
96
+ isActive
97
+ ? 'bg-primary-light text-text-primary font-medium'
98
+ : 'text-text-secondary hover:bg-bg-secondary hover:text-text-primary'
99
+ }`}
100
+ type="button"
101
+ aria-pressed={isActive}
102
+ >
103
+ <Icon size={16} />
104
+ <span className="flex-1 text-left">{category.name}</span>
105
+ <span className={`text-xs ${isActive ? 'text-text-primary' : 'text-text-secondary'}`}>
106
+ {category.count}
107
+ </span>
108
+ </button>
109
+ );
110
+ })}
111
+ </div>
112
+ </div>
113
+
114
+ {/* Popular Tags */}
115
+ <div>
116
+ <h3 className="text-sm font-semibold text-text-primary mb-3">Popular Tags</h3>
117
+ <div className="flex flex-wrap gap-2">
118
+ {popularTags.map((tag) => (
119
+ <button
120
+ key={tag}
121
+ onClick={() => onTagToggle(tag)}
122
+ className={`px-3 py-1.5 rounded-full text-xs font-medium transition-all transition-theme ${
123
+ selectedTags.includes(tag)
124
+ ? 'bg-primary-light text-text-primary'
125
+ : 'bg-bg-secondary text-text-secondary hover:bg-bg-tertiary hover:text-text-primary'
126
+ }`}
127
+ type="button"
128
+ aria-pressed={selectedTags.includes(tag)}
129
+ >
130
+ {tag}
131
+ </button>
132
+ ))}
133
+ </div>
134
+ </div>
135
+
136
+ {/* Sort */}
137
+ <div>
138
+ <h3 className="text-sm font-semibold text-text-primary mb-3">Sort By</h3>
139
+ <select
140
+ value={sortBy}
141
+ onChange={(e) => onSortChange(e.target.value)}
142
+ className="w-full px-3 py-2.5 bg-bg-secondary text-text-primary rounded-lg border border-border focus:border-primary-light focus:outline-none transition-theme text-sm"
143
+ aria-label="Sort by"
144
+ >
145
+ {sortOptions.map((option) => (
146
+ <option key={option.id} value={option.id}>
147
+ {option.name}
148
+ </option>
149
+ ))}
150
+ </select>
151
+ </div>
152
+
153
+ {/* Additional Content */}
154
+ {children}
155
+
156
+ {/* Clear Filters */}
157
+ {hasActiveFilters && (
158
+ <button
159
+ onClick={onClearFilters}
160
+ className="w-full flex items-center justify-center gap-2 px-4 py-2.5 bg-bg-secondary text-text-secondary rounded-lg border border-border hover:border-primary-light hover:text-text-primary transition-all text-sm font-medium transition-theme"
161
+ type="button"
162
+ >
163
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
164
+ <path d="M18 6L6 18M6 6l12 12" />
165
+ </svg>
166
+ Clear All Filters
167
+ </button>
168
+ )}
169
+ </div>
170
+ </aside>
171
+ );
172
+ };
@@ -142,3 +142,10 @@ export type { NewsletterSignupProps } from './NewsletterSignup';
142
142
 
143
143
  export { Comments } from './Comments';
144
144
  export type { CommentsProps, GiscusConfig } from './Comments';
145
+
146
+ // NEW: Filter Components
147
+ export { FilterBar } from './FilterBar';
148
+ export type { FilterBarProps, Category, SortOption } from './FilterBar';
149
+
150
+ export { FilterSidebar } from './FilterSidebar';
151
+ export type { FilterSidebarProps } from './FilterSidebar';