@papernote/ui 1.13.0 → 2.0.0
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/dist/components/FilterBar.d.ts +1 -1
- package/dist/components/FilterBar.d.ts.map +1 -1
- package/dist/components/FilterPills.d.ts +14 -0
- package/dist/components/FilterPills.d.ts.map +1 -0
- package/dist/components/LetterNav.d.ts +8 -0
- package/dist/components/LetterNav.d.ts.map +1 -0
- package/dist/components/Pagination.d.ts +11 -1
- package/dist/components/Pagination.d.ts.map +1 -1
- package/dist/components/index.d.ts +4 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/index.d.ts +36 -4
- package/dist/index.esm.js +346 -280
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +346 -278
- package/dist/index.js.map +1 -1
- package/dist/styles.css +6228 -6713
- package/package.json +6 -4
- package/src/components/AdminModal.tsx +1 -1
- package/src/components/Autocomplete.tsx +1 -1
- package/src/components/CommandPalette.tsx +2 -2
- package/src/components/DataTable.tsx +1 -1
- package/src/components/Drawer.tsx +1 -1
- package/src/components/FilterBar.tsx +116 -3
- package/src/components/FilterPills.tsx +58 -0
- package/src/components/LetterNav.tsx +67 -0
- package/src/components/MarkdownEditor.tsx +1 -1
- package/src/components/MaskedInput.tsx +1 -1
- package/src/components/Modal.tsx +1 -1
- package/src/components/NotificationBar.tsx +2 -2
- package/src/components/Pagination.tsx +49 -1
- package/src/components/PasswordInput.tsx +1 -1
- package/src/components/SearchBar.tsx +1 -1
- package/src/components/Textarea.tsx +1 -1
- package/src/components/index.ts +6 -0
- package/src/styles/index.css +50 -160
- package/src/styles/theme.css +302 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@papernote/ui",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "A modern React component library with a paper notebook aesthetic - minimal, professional, and expressive",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -20,6 +20,8 @@
|
|
|
20
20
|
},
|
|
21
21
|
"./styles": "./dist/styles.css",
|
|
22
22
|
"./styles.css": "./dist/styles.css",
|
|
23
|
+
"./theme": "./src/styles/theme.css",
|
|
24
|
+
"./theme.css": "./src/styles/theme.css",
|
|
23
25
|
"./tailwind-config": "./tailwind.config.js"
|
|
24
26
|
},
|
|
25
27
|
"scripts": {
|
|
@@ -60,7 +62,8 @@
|
|
|
60
62
|
"@types/react-dom": "^19.2.2",
|
|
61
63
|
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
|
62
64
|
"@typescript-eslint/parser": "^6.0.0",
|
|
63
|
-
"
|
|
65
|
+
"@tailwindcss/postcss": "^4.0.0",
|
|
66
|
+
"@tailwindcss/vite": "^4.0.0",
|
|
64
67
|
"eslint": "^8.0.0",
|
|
65
68
|
"eslint-plugin-react": "^7.0.0",
|
|
66
69
|
"eslint-plugin-react-hooks": "^4.0.0",
|
|
@@ -69,7 +72,6 @@
|
|
|
69
72
|
"jest-environment-jsdom": "^30.2.0",
|
|
70
73
|
"lucide-react": "^0.554.0",
|
|
71
74
|
"postcss": "^8.4.0",
|
|
72
|
-
"postcss-import": "^16.1.1",
|
|
73
75
|
"react": "^19.2.0",
|
|
74
76
|
"react-dom": "^19.2.0",
|
|
75
77
|
"react-router-dom": "^7.9.6",
|
|
@@ -78,7 +80,7 @@
|
|
|
78
80
|
"rollup-plugin-peer-deps-external": "^2.2.4",
|
|
79
81
|
"rollup-plugin-postcss": "^4.0.0",
|
|
80
82
|
"storybook": "^10.1.11",
|
|
81
|
-
"tailwindcss": "^
|
|
83
|
+
"tailwindcss": "^4.0.0",
|
|
82
84
|
"ts-jest": "^29.4.5",
|
|
83
85
|
"tslib": "^2.6.0",
|
|
84
86
|
"typescript": "^5.0.0",
|
|
@@ -63,7 +63,7 @@ export function AdminModal({
|
|
|
63
63
|
};
|
|
64
64
|
|
|
65
65
|
return (
|
|
66
|
-
<div className="fixed inset-0 bg-black
|
|
66
|
+
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4 admin-modal-overlay">
|
|
67
67
|
<div
|
|
68
68
|
className={`bg-white rounded-lg w-full ${sizeClasses[size]} flex flex-col overflow-hidden shadow-2xl admin-modal-content`}
|
|
69
69
|
style={{ height: height }}
|
|
@@ -301,7 +301,7 @@ const Autocomplete = forwardRef<AutocompleteHandle, AutocompleteProps>(({
|
|
|
301
301
|
disabled={disabled}
|
|
302
302
|
className={`
|
|
303
303
|
w-full pl-9 pr-9 py-2
|
|
304
|
-
text-sm text-ink-900 placeholder-ink-400
|
|
304
|
+
text-sm text-ink-900 placeholder:text-ink-400
|
|
305
305
|
bg-white border rounded-lg
|
|
306
306
|
focus:outline-none focus:ring-2 focus:ring-accent-400 focus:border-accent-400
|
|
307
307
|
disabled:bg-paper-100 disabled:cursor-not-allowed
|
|
@@ -167,7 +167,7 @@ export default function CommandPalette({
|
|
|
167
167
|
<div className="fixed inset-0 z-50 flex items-start justify-center pt-[20vh] animate-fade-in">
|
|
168
168
|
{/* Backdrop */}
|
|
169
169
|
<div
|
|
170
|
-
className="absolute inset-0 bg-ink-900
|
|
170
|
+
className="absolute inset-0 bg-ink-900/50 backdrop-blur-sm"
|
|
171
171
|
onClick={() => onOpenChange(false)}
|
|
172
172
|
/>
|
|
173
173
|
|
|
@@ -185,7 +185,7 @@ export default function CommandPalette({
|
|
|
185
185
|
setSelectedIndex(0);
|
|
186
186
|
}}
|
|
187
187
|
placeholder={placeholder}
|
|
188
|
-
className="flex-1 text-base text-ink-900 placeholder-ink-400 bg-transparent border-none outline-none"
|
|
188
|
+
className="flex-1 text-base text-ink-900 placeholder:text-ink-400 bg-transparent border-none outline-none"
|
|
189
189
|
/>
|
|
190
190
|
{trigger && (
|
|
191
191
|
<kbd className="hidden sm:inline-block px-2 py-1 text-xs font-mono text-ink-500 bg-paper-100 border border-paper-300 rounded">
|
|
@@ -1646,7 +1646,7 @@ export default function DataTable<T extends BaseDataItem = BaseDataItem>({
|
|
|
1646
1646
|
{/* Loading overlay for when data is being refreshed */}
|
|
1647
1647
|
{loading && data.length > 0 && (
|
|
1648
1648
|
<div
|
|
1649
|
-
className="absolute inset-0 bg-white
|
|
1649
|
+
className="absolute inset-0 bg-white/75 flex items-center justify-center z-20"
|
|
1650
1650
|
style={{ backdropFilter: 'blur(2px)' }}
|
|
1651
1651
|
>
|
|
1652
1652
|
<div className="flex flex-col items-center gap-3">
|
|
@@ -131,7 +131,7 @@ export default function Drawer({
|
|
|
131
131
|
{/* Overlay */}
|
|
132
132
|
{showOverlay && (
|
|
133
133
|
<div
|
|
134
|
-
className="fixed inset-0 bg-ink-900
|
|
134
|
+
className="fixed inset-0 bg-ink-900/50 backdrop-blur-sm animate-fade-in"
|
|
135
135
|
onClick={handleOverlayClick}
|
|
136
136
|
aria-hidden="true"
|
|
137
137
|
/>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { X } from 'lucide-react';
|
|
1
|
+
import { X, Search } from 'lucide-react';
|
|
2
2
|
import Input from './Input';
|
|
3
3
|
import Select, { type SelectOption } from './Select';
|
|
4
4
|
import Button from './Button';
|
|
@@ -6,7 +6,7 @@ import Button from './Button';
|
|
|
6
6
|
export interface FilterConfig {
|
|
7
7
|
key: string;
|
|
8
8
|
label: string;
|
|
9
|
-
type: 'text' | 'select' | 'date' | 'number' | 'boolean';
|
|
9
|
+
type: 'text' | 'search' | 'select' | 'date' | 'number' | 'boolean' | 'dateRange' | 'toggle' | 'multiSelect';
|
|
10
10
|
placeholder?: string;
|
|
11
11
|
options?: Array<{ label: string; value: unknown }>;
|
|
12
12
|
}
|
|
@@ -42,7 +42,15 @@ export default function FilterBar({
|
|
|
42
42
|
// Default clear: set all values to null/empty
|
|
43
43
|
const clearedValues: Record<string, unknown> = {};
|
|
44
44
|
filters.forEach(filter => {
|
|
45
|
-
|
|
45
|
+
if (filter.type === 'text' || filter.type === 'search') {
|
|
46
|
+
clearedValues[filter.key] = '';
|
|
47
|
+
} else if (filter.type === 'dateRange') {
|
|
48
|
+
clearedValues[filter.key] = { from: undefined, to: undefined };
|
|
49
|
+
} else if (filter.type === 'multiSelect') {
|
|
50
|
+
clearedValues[filter.key] = [];
|
|
51
|
+
} else {
|
|
52
|
+
clearedValues[filter.key] = null;
|
|
53
|
+
}
|
|
46
54
|
});
|
|
47
55
|
onChange(clearedValues);
|
|
48
56
|
}
|
|
@@ -127,6 +135,111 @@ export default function FilterBar({
|
|
|
127
135
|
);
|
|
128
136
|
}
|
|
129
137
|
|
|
138
|
+
case 'search':
|
|
139
|
+
return (
|
|
140
|
+
<div className="relative">
|
|
141
|
+
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
|
142
|
+
<Search className="h-4 w-4 text-ink-400" />
|
|
143
|
+
</div>
|
|
144
|
+
<input
|
|
145
|
+
type="text"
|
|
146
|
+
placeholder={filter.placeholder || `Search ${filter.label}...`}
|
|
147
|
+
value={(value as string) || ''}
|
|
148
|
+
onChange={(e) => handleFilterChange(filter.key, e.target.value)}
|
|
149
|
+
className="input pl-9"
|
|
150
|
+
/>
|
|
151
|
+
</div>
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
case 'dateRange': {
|
|
155
|
+
const rangeValue = (value as { from?: string; to?: string }) || {};
|
|
156
|
+
return (
|
|
157
|
+
<div className="flex items-center gap-2">
|
|
158
|
+
<input
|
|
159
|
+
type="date"
|
|
160
|
+
value={rangeValue.from || ''}
|
|
161
|
+
onChange={(e) =>
|
|
162
|
+
handleFilterChange(filter.key, { ...rangeValue, from: e.target.value || undefined })
|
|
163
|
+
}
|
|
164
|
+
className="input text-sm"
|
|
165
|
+
aria-label={`${filter.label} from`}
|
|
166
|
+
/>
|
|
167
|
+
<span className="text-ink-400 text-xs">to</span>
|
|
168
|
+
<input
|
|
169
|
+
type="date"
|
|
170
|
+
value={rangeValue.to || ''}
|
|
171
|
+
onChange={(e) =>
|
|
172
|
+
handleFilterChange(filter.key, { ...rangeValue, to: e.target.value || undefined })
|
|
173
|
+
}
|
|
174
|
+
className="input text-sm"
|
|
175
|
+
aria-label={`${filter.label} to`}
|
|
176
|
+
/>
|
|
177
|
+
</div>
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
case 'toggle': {
|
|
182
|
+
const toggleOptions: SelectOption[] = [
|
|
183
|
+
{ value: '', label: 'All' },
|
|
184
|
+
{ value: 'true', label: 'Yes' },
|
|
185
|
+
{ value: 'false', label: 'No' },
|
|
186
|
+
];
|
|
187
|
+
const currentVal = value === null || value === undefined ? '' : String(value);
|
|
188
|
+
return (
|
|
189
|
+
<div className="flex rounded-lg border border-paper-300 overflow-hidden" role="group">
|
|
190
|
+
{toggleOptions.map((opt) => (
|
|
191
|
+
<button
|
|
192
|
+
key={opt.value}
|
|
193
|
+
type="button"
|
|
194
|
+
onClick={() => handleFilterChange(filter.key, opt.value === '' ? null : opt.value === 'true')}
|
|
195
|
+
className={`px-3 py-1.5 text-xs font-medium transition-colors ${
|
|
196
|
+
currentVal === opt.value
|
|
197
|
+
? 'bg-accent-500 text-white'
|
|
198
|
+
: 'bg-white text-ink-600 hover:bg-paper-50'
|
|
199
|
+
} ${opt.value !== '' ? 'border-l border-paper-300' : ''}`}
|
|
200
|
+
>
|
|
201
|
+
{opt.label}
|
|
202
|
+
</button>
|
|
203
|
+
))}
|
|
204
|
+
</div>
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
case 'multiSelect': {
|
|
209
|
+
const selectedValues = Array.isArray(value) ? (value as string[]) : [];
|
|
210
|
+
const msOptions = filter.options || [];
|
|
211
|
+
return (
|
|
212
|
+
<div className="relative">
|
|
213
|
+
<Select
|
|
214
|
+
options={[{ value: '', label: `All ${filter.label}` }, ...msOptions.map(o => ({ value: String(o.value), label: o.label }))]}
|
|
215
|
+
value=""
|
|
216
|
+
onChange={(newValue) => {
|
|
217
|
+
if (!newValue) {
|
|
218
|
+
handleFilterChange(filter.key, []);
|
|
219
|
+
} else if (!selectedValues.includes(newValue)) {
|
|
220
|
+
handleFilterChange(filter.key, [...selectedValues, newValue]);
|
|
221
|
+
}
|
|
222
|
+
}}
|
|
223
|
+
/>
|
|
224
|
+
{selectedValues.length > 0 && (
|
|
225
|
+
<div className="flex flex-wrap gap-1 mt-1">
|
|
226
|
+
{selectedValues.map((sv) => {
|
|
227
|
+
const opt = msOptions.find(o => String(o.value) === sv);
|
|
228
|
+
return (
|
|
229
|
+
<span key={sv} className="inline-flex items-center gap-1 px-2 py-0.5 text-xs bg-accent-100 text-accent-700 rounded-full">
|
|
230
|
+
{opt?.label || sv}
|
|
231
|
+
<button type="button" onClick={() => handleFilterChange(filter.key, selectedValues.filter(v => v !== sv))} className="hover:text-accent-900">
|
|
232
|
+
<X className="h-3 w-3" />
|
|
233
|
+
</button>
|
|
234
|
+
</span>
|
|
235
|
+
);
|
|
236
|
+
})}
|
|
237
|
+
</div>
|
|
238
|
+
)}
|
|
239
|
+
</div>
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
|
|
130
243
|
default:
|
|
131
244
|
return null;
|
|
132
245
|
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import Chip from './Chip';
|
|
2
|
+
import { Filter } from 'lucide-react';
|
|
3
|
+
|
|
4
|
+
export interface FilterPill {
|
|
5
|
+
key: string;
|
|
6
|
+
label: string;
|
|
7
|
+
displayValue: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface FilterPillsProps {
|
|
11
|
+
pills: FilterPill[];
|
|
12
|
+
onRemove: (key: string) => void;
|
|
13
|
+
onClearAll: () => void;
|
|
14
|
+
totalCount?: number;
|
|
15
|
+
className?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export default function FilterPills({
|
|
19
|
+
pills,
|
|
20
|
+
onRemove,
|
|
21
|
+
onClearAll,
|
|
22
|
+
totalCount,
|
|
23
|
+
className = '',
|
|
24
|
+
}: FilterPillsProps) {
|
|
25
|
+
if (pills.length === 0) return null;
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<div className={`flex items-center gap-2 px-4 py-2 border-b border-paper-200 bg-paper-50 ${className}`}>
|
|
29
|
+
<Filter className="h-3.5 w-3.5 text-ink-400 shrink-0" />
|
|
30
|
+
<div className="flex items-center gap-1.5 flex-wrap flex-1">
|
|
31
|
+
{pills.map((pill) => (
|
|
32
|
+
<Chip
|
|
33
|
+
key={pill.key}
|
|
34
|
+
size="sm"
|
|
35
|
+
variant="primary"
|
|
36
|
+
onClose={() => onRemove(pill.key)}
|
|
37
|
+
>
|
|
38
|
+
{pill.label}: {pill.displayValue}
|
|
39
|
+
</Chip>
|
|
40
|
+
))}
|
|
41
|
+
{pills.length >= 2 && (
|
|
42
|
+
<button
|
|
43
|
+
type="button"
|
|
44
|
+
onClick={onClearAll}
|
|
45
|
+
className="text-xs text-ink-500 hover:text-ink-700 underline underline-offset-2 ml-1"
|
|
46
|
+
>
|
|
47
|
+
Clear all
|
|
48
|
+
</button>
|
|
49
|
+
)}
|
|
50
|
+
</div>
|
|
51
|
+
{totalCount !== undefined && (
|
|
52
|
+
<span className="text-xs text-ink-500 shrink-0 tabular-nums">
|
|
53
|
+
{totalCount.toLocaleString()} {totalCount === 1 ? 'record' : 'records'}
|
|
54
|
+
</span>
|
|
55
|
+
)}
|
|
56
|
+
</div>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
const LETTERS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('');
|
|
2
|
+
|
|
3
|
+
export interface LetterNavProps {
|
|
4
|
+
activeLetter: string | null;
|
|
5
|
+
onChange: (letter: string | null) => void;
|
|
6
|
+
availableLetters?: string[];
|
|
7
|
+
className?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export default function LetterNav({
|
|
11
|
+
activeLetter,
|
|
12
|
+
onChange,
|
|
13
|
+
availableLetters,
|
|
14
|
+
className = '',
|
|
15
|
+
}: LetterNavProps) {
|
|
16
|
+
const hasAvailability = availableLetters && availableLetters.length > 0;
|
|
17
|
+
const availableSet = hasAvailability
|
|
18
|
+
? new Set(availableLetters!.map((l) => l.toUpperCase()))
|
|
19
|
+
: null;
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<div className={`flex items-center gap-0.5 px-4 py-1.5 border-b border-paper-200 bg-white overflow-x-auto ${className}`}>
|
|
23
|
+
<button
|
|
24
|
+
type="button"
|
|
25
|
+
onClick={() => onChange(null)}
|
|
26
|
+
className={`px-2 py-1 text-xs font-medium rounded transition-colors ${
|
|
27
|
+
activeLetter === null
|
|
28
|
+
? 'bg-accent-500 text-white'
|
|
29
|
+
: 'text-ink-600 hover:bg-paper-100'
|
|
30
|
+
}`}
|
|
31
|
+
>
|
|
32
|
+
All
|
|
33
|
+
</button>
|
|
34
|
+
{LETTERS.map((letter) => {
|
|
35
|
+
const isActive = activeLetter === letter;
|
|
36
|
+
const isAvailable = !availableSet || availableSet.has(letter);
|
|
37
|
+
return (
|
|
38
|
+
<button
|
|
39
|
+
key={letter}
|
|
40
|
+
type="button"
|
|
41
|
+
onClick={() => onChange(isActive ? null : letter)}
|
|
42
|
+
className={`w-7 h-7 text-xs font-medium rounded transition-colors ${
|
|
43
|
+
isActive
|
|
44
|
+
? 'bg-accent-500 text-white'
|
|
45
|
+
: isAvailable
|
|
46
|
+
? 'text-ink-600 hover:bg-paper-100'
|
|
47
|
+
: 'text-ink-300'
|
|
48
|
+
}`}
|
|
49
|
+
>
|
|
50
|
+
{letter}
|
|
51
|
+
</button>
|
|
52
|
+
);
|
|
53
|
+
})}
|
|
54
|
+
<button
|
|
55
|
+
type="button"
|
|
56
|
+
onClick={() => onChange(activeLetter === '#' ? null : '#')}
|
|
57
|
+
className={`px-2 py-1 text-xs font-medium rounded transition-colors ${
|
|
58
|
+
activeLetter === '#'
|
|
59
|
+
? 'bg-accent-500 text-white'
|
|
60
|
+
: 'text-ink-600 hover:bg-paper-100'
|
|
61
|
+
}`}
|
|
62
|
+
>
|
|
63
|
+
#
|
|
64
|
+
</button>
|
|
65
|
+
</div>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
@@ -187,7 +187,7 @@ export default function MarkdownEditor({
|
|
|
187
187
|
disabled={disabled}
|
|
188
188
|
className={`
|
|
189
189
|
w-full p-3 outline-none resize-none
|
|
190
|
-
text-sm text-ink-900 font-mono placeholder-ink-400
|
|
190
|
+
text-sm text-ink-900 font-mono placeholder:text-ink-400
|
|
191
191
|
${disabled ? 'bg-paper-100 cursor-not-allowed' : 'bg-white'}
|
|
192
192
|
`}
|
|
193
193
|
style={{ minHeight, maxHeight }}
|
|
@@ -161,7 +161,7 @@ const MaskedInput = forwardRef<MaskedInputHandle, MaskedInputProps>(({
|
|
|
161
161
|
disabled={disabled}
|
|
162
162
|
className={`
|
|
163
163
|
w-full px-3 py-2
|
|
164
|
-
text-sm text-ink-900 placeholder-ink-400
|
|
164
|
+
text-sm text-ink-900 placeholder:text-ink-400
|
|
165
165
|
bg-white border rounded-lg
|
|
166
166
|
focus:outline-none focus:ring-2 focus:ring-accent-400 focus:border-accent-400
|
|
167
167
|
disabled:bg-paper-100 disabled:cursor-not-allowed
|
package/src/components/Modal.tsx
CHANGED
|
@@ -259,7 +259,7 @@ export default function Modal({
|
|
|
259
259
|
// Render as standard modal on desktop
|
|
260
260
|
const modalContent = (
|
|
261
261
|
<div
|
|
262
|
-
className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-ink-900
|
|
262
|
+
className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-ink-900/50 backdrop-blur-sm animate-fade-in"
|
|
263
263
|
onMouseDown={handleBackdropMouseDown}
|
|
264
264
|
onClick={handleBackdropClick}
|
|
265
265
|
>
|
|
@@ -54,7 +54,7 @@ export default function NotificationBar({ notifications, onDismiss }: Notificati
|
|
|
54
54
|
className={`border-2 rounded-xl p-4 backdrop-blur-sm transform transition-all duration-300 ease-in-out animate-slide-in ${notificationStyles[notification.type]}`}
|
|
55
55
|
>
|
|
56
56
|
<div className="flex items-start space-x-3">
|
|
57
|
-
<div className={`flex-shrink-0 p-1 rounded-full bg-white
|
|
57
|
+
<div className={`flex-shrink-0 p-1 rounded-full bg-white/50 ${iconStyles[notification.type]}`}>
|
|
58
58
|
<IconComponent className="h-5 w-5" />
|
|
59
59
|
</div>
|
|
60
60
|
<div className="flex-1 min-w-0">
|
|
@@ -68,7 +68,7 @@ export default function NotificationBar({ notifications, onDismiss }: Notificati
|
|
|
68
68
|
{notification.dismissible && onDismiss && (
|
|
69
69
|
<button
|
|
70
70
|
onClick={() => onDismiss(notification.id)}
|
|
71
|
-
className={`flex-shrink-0 p-1.5 hover:bg-white
|
|
71
|
+
className={`flex-shrink-0 p-1.5 hover:bg-white/60 rounded-full transition-all duration-200 ${iconStyles[notification.type]} hover:scale-110`}
|
|
72
72
|
title="Dismiss notification"
|
|
73
73
|
>
|
|
74
74
|
<X className="h-4 w-4" />
|
|
@@ -10,6 +10,16 @@ export interface PaginationProps {
|
|
|
10
10
|
maxPageNumbers?: number;
|
|
11
11
|
/** Show page jump input field */
|
|
12
12
|
showPageJump?: boolean;
|
|
13
|
+
/** Total number of items across all pages */
|
|
14
|
+
totalItems?: number;
|
|
15
|
+
/** Current page size */
|
|
16
|
+
pageSize?: number;
|
|
17
|
+
/** Available page size options */
|
|
18
|
+
pageSizeOptions?: number[];
|
|
19
|
+
/** Callback when page size changes */
|
|
20
|
+
onPageSizeChange?: (size: number) => void;
|
|
21
|
+
/** Show "Showing X-Y of Z records" text */
|
|
22
|
+
showRecordCount?: boolean;
|
|
13
23
|
}
|
|
14
24
|
|
|
15
25
|
export default function Pagination({
|
|
@@ -19,6 +29,11 @@ export default function Pagination({
|
|
|
19
29
|
showPageNumbers = true,
|
|
20
30
|
maxPageNumbers = 5,
|
|
21
31
|
showPageJump = false,
|
|
32
|
+
totalItems,
|
|
33
|
+
pageSize,
|
|
34
|
+
pageSizeOptions,
|
|
35
|
+
onPageSizeChange,
|
|
36
|
+
showRecordCount = false,
|
|
22
37
|
}: PaginationProps) {
|
|
23
38
|
const [jumpValue, setJumpValue] = useState('');
|
|
24
39
|
const getPageNumbers = () => {
|
|
@@ -67,8 +82,23 @@ export default function Pagination({
|
|
|
67
82
|
}
|
|
68
83
|
};
|
|
69
84
|
|
|
85
|
+
const showLeftSection = showRecordCount && totalItems !== undefined && pageSize;
|
|
86
|
+
const showRightSection = onPageSizeChange && pageSizeOptions && pageSizeOptions.length > 0;
|
|
87
|
+
|
|
88
|
+
const rangeStart = totalItems ? (currentPage - 1) * (pageSize || 0) + 1 : 0;
|
|
89
|
+
const rangeEnd = totalItems ? Math.min(currentPage * (pageSize || 0), totalItems) : 0;
|
|
90
|
+
|
|
70
91
|
return (
|
|
71
|
-
<nav className=
|
|
92
|
+
<nav className={`flex items-center gap-2 ${showLeftSection || showRightSection ? 'justify-between' : 'justify-center'}`} aria-label="Pagination">
|
|
93
|
+
{/* Record Count (left) */}
|
|
94
|
+
{showLeftSection ? (
|
|
95
|
+
<span className="text-sm text-ink-500 tabular-nums shrink-0">
|
|
96
|
+
Showing {rangeStart.toLocaleString()}–{rangeEnd.toLocaleString()} of {totalItems!.toLocaleString()}
|
|
97
|
+
</span>
|
|
98
|
+
) : showRightSection ? <div /> : null}
|
|
99
|
+
|
|
100
|
+
{/* Center: nav buttons */}
|
|
101
|
+
<div className="flex items-center gap-2">
|
|
72
102
|
{/* Previous Button */}
|
|
73
103
|
<button
|
|
74
104
|
onClick={() => onPageChange(currentPage - 1)}
|
|
@@ -148,6 +178,24 @@ export default function Pagination({
|
|
|
148
178
|
</button>
|
|
149
179
|
</form>
|
|
150
180
|
)}
|
|
181
|
+
</div>
|
|
182
|
+
|
|
183
|
+
{/* Page Size Selector (right) */}
|
|
184
|
+
{showRightSection ? (
|
|
185
|
+
<div className="flex items-center gap-2 shrink-0">
|
|
186
|
+
<span className="text-sm text-ink-500 hidden sm:inline">Per page:</span>
|
|
187
|
+
<select
|
|
188
|
+
value={pageSize || pageSizeOptions![0]}
|
|
189
|
+
onChange={(e) => onPageSizeChange!(Number(e.target.value))}
|
|
190
|
+
className="px-2 py-1.5 text-sm border border-paper-300 rounded-lg bg-white text-ink-700 focus:outline-none focus:ring-2 focus:ring-accent-400 focus:border-accent-400 cursor-pointer"
|
|
191
|
+
aria-label="Items per page"
|
|
192
|
+
>
|
|
193
|
+
{pageSizeOptions!.map((size) => (
|
|
194
|
+
<option key={size} value={size}>{size}</option>
|
|
195
|
+
))}
|
|
196
|
+
</select>
|
|
197
|
+
</div>
|
|
198
|
+
) : showLeftSection ? <div /> : null}
|
|
151
199
|
</nav>
|
|
152
200
|
);
|
|
153
201
|
}
|
|
@@ -164,7 +164,7 @@ const PasswordInput = forwardRef<PasswordInputHandle, PasswordInputProps>(({
|
|
|
164
164
|
disabled={disabled}
|
|
165
165
|
className={`
|
|
166
166
|
w-full px-3 py-2 pr-10
|
|
167
|
-
text-sm text-ink-900 placeholder-ink-400
|
|
167
|
+
text-sm text-ink-900 placeholder:text-ink-400
|
|
168
168
|
bg-white border rounded-lg
|
|
169
169
|
focus:outline-none focus:ring-2 focus:ring-accent-400 focus:border-accent-400
|
|
170
170
|
disabled:bg-paper-100 disabled:cursor-not-allowed
|
|
@@ -35,7 +35,7 @@ export default function SearchBar({
|
|
|
35
35
|
onChange={(e) => onChange(e.target.value)}
|
|
36
36
|
onKeyDown={handleKeyDown}
|
|
37
37
|
disabled={disabled}
|
|
38
|
-
className="block w-full pl-11 pr-4 py-3 border border-paper-300 rounded-lg leading-5 bg-white placeholder-ink-400 text-ink-800 focus:outline-none focus:placeholder-ink-300 focus:ring-2 focus:ring-accent-400 focus:border-accent-400 hover:border-paper-400 transition-all sm:text-sm disabled:bg-paper-100 disabled:cursor-not-allowed"
|
|
38
|
+
className="block w-full pl-11 pr-4 py-3 border border-paper-300 rounded-lg leading-5 bg-white placeholder:text-ink-400 text-ink-800 focus:outline-none focus:placeholder:text-ink-300 focus:ring-2 focus:ring-accent-400 focus:border-accent-400 hover:border-paper-400 transition-all sm:text-sm disabled:bg-paper-100 disabled:cursor-not-allowed"
|
|
39
39
|
placeholder={placeholder}
|
|
40
40
|
/>
|
|
41
41
|
</div>
|
|
@@ -177,7 +177,7 @@ const Textarea = forwardRef<HTMLTextAreaElement, TextareaProps>(
|
|
|
177
177
|
rows={autoExpand ? minRows : rows}
|
|
178
178
|
enterKeyHint={enterKeyHint}
|
|
179
179
|
className={`
|
|
180
|
-
block w-full border rounded-lg text-ink-800 placeholder-ink-400
|
|
180
|
+
block w-full border rounded-lg text-ink-800 placeholder:text-ink-400
|
|
181
181
|
bg-white bg-subtle-grain transition-all duration-200
|
|
182
182
|
focus:outline-none focus:ring-2 ${getResizeClass()}
|
|
183
183
|
disabled:bg-paper-100 disabled:text-ink-400 disabled:cursor-not-allowed disabled:opacity-60
|
package/src/components/index.ts
CHANGED
|
@@ -54,6 +54,12 @@ export type { FormControlProps } from './FormControl';
|
|
|
54
54
|
export { default as FilterBar } from './FilterBar';
|
|
55
55
|
export type { FilterBarProps, FilterConfig } from './FilterBar';
|
|
56
56
|
|
|
57
|
+
export { default as FilterPills } from './FilterPills';
|
|
58
|
+
export type { FilterPillsProps, FilterPill } from './FilterPills';
|
|
59
|
+
|
|
60
|
+
export { default as LetterNav } from './LetterNav';
|
|
61
|
+
export type { LetterNavProps } from './LetterNav';
|
|
62
|
+
|
|
57
63
|
export { default as StatCard } from './StatCard';
|
|
58
64
|
export type { StatCardProps } from './StatCard';
|
|
59
65
|
export { default as StatsGrid, StatItem } from './StatsGrid';
|