@papernote/ui 2.0.3 → 2.0.4
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/index.d.ts +1 -1
- package/dist/index.esm.js +105 -75
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +105 -75
- package/dist/index.js.map +1 -1
- package/dist/styles.css +8 -0
- package/package.json +1 -1
- package/src/components/FilterBar.tsx +130 -53
package/dist/styles.css
CHANGED
|
@@ -1424,6 +1424,10 @@
|
|
|
1424
1424
|
--tw-translate-x: calc(var(--spacing) * 0);
|
|
1425
1425
|
translate: var(--tw-translate-x) var(--tw-translate-y);
|
|
1426
1426
|
}
|
|
1427
|
+
.translate-x-1 {
|
|
1428
|
+
--tw-translate-x: calc(var(--spacing) * 1);
|
|
1429
|
+
translate: var(--tw-translate-x) var(--tw-translate-y);
|
|
1430
|
+
}
|
|
1427
1431
|
.translate-x-4 {
|
|
1428
1432
|
--tw-translate-x: calc(var(--spacing) * 4);
|
|
1429
1433
|
translate: var(--tw-translate-x) var(--tw-translate-y);
|
|
@@ -1432,6 +1436,10 @@
|
|
|
1432
1436
|
--tw-translate-x: calc(var(--spacing) * 5);
|
|
1433
1437
|
translate: var(--tw-translate-x) var(--tw-translate-y);
|
|
1434
1438
|
}
|
|
1439
|
+
.translate-x-6 {
|
|
1440
|
+
--tw-translate-x: calc(var(--spacing) * 6);
|
|
1441
|
+
translate: var(--tw-translate-x) var(--tw-translate-y);
|
|
1442
|
+
}
|
|
1435
1443
|
.translate-x-7 {
|
|
1436
1444
|
--tw-translate-x: calc(var(--spacing) * 7);
|
|
1437
1445
|
translate: var(--tw-translate-x) var(--tw-translate-y);
|
package/package.json
CHANGED
|
@@ -1,12 +1,22 @@
|
|
|
1
|
-
import { X, Search } from
|
|
2
|
-
import Input from
|
|
3
|
-
import Select, { type SelectOption } from
|
|
4
|
-
import Button from
|
|
1
|
+
import { X, Search } from "lucide-react";
|
|
2
|
+
import Input from "./Input";
|
|
3
|
+
import Select, { type SelectOption } from "./Select";
|
|
4
|
+
import Button from "./Button";
|
|
5
5
|
|
|
6
6
|
export interface FilterConfig {
|
|
7
7
|
key: string;
|
|
8
8
|
label: string;
|
|
9
|
-
type:
|
|
9
|
+
type:
|
|
10
|
+
| "text"
|
|
11
|
+
| "search"
|
|
12
|
+
| "select"
|
|
13
|
+
| "date"
|
|
14
|
+
| "number"
|
|
15
|
+
| "boolean"
|
|
16
|
+
| "dateRange"
|
|
17
|
+
| "toggle"
|
|
18
|
+
| "switch"
|
|
19
|
+
| "multiSelect";
|
|
10
20
|
placeholder?: string;
|
|
11
21
|
options?: Array<{ label: string; value: unknown }>;
|
|
12
22
|
}
|
|
@@ -24,7 +34,7 @@ export default function FilterBar({
|
|
|
24
34
|
filters,
|
|
25
35
|
values,
|
|
26
36
|
onChange,
|
|
27
|
-
className =
|
|
37
|
+
className = "",
|
|
28
38
|
onClear,
|
|
29
39
|
showClearButton = false,
|
|
30
40
|
}: FilterBarProps) {
|
|
@@ -41,13 +51,15 @@ export default function FilterBar({
|
|
|
41
51
|
} else {
|
|
42
52
|
// Default clear: set all values to null/empty
|
|
43
53
|
const clearedValues: Record<string, unknown> = {};
|
|
44
|
-
filters.forEach(filter => {
|
|
45
|
-
if (filter.type ===
|
|
46
|
-
clearedValues[filter.key] =
|
|
47
|
-
} else if (filter.type ===
|
|
54
|
+
filters.forEach((filter) => {
|
|
55
|
+
if (filter.type === "text" || filter.type === "search") {
|
|
56
|
+
clearedValues[filter.key] = "";
|
|
57
|
+
} else if (filter.type === "dateRange") {
|
|
48
58
|
clearedValues[filter.key] = { from: undefined, to: undefined };
|
|
49
|
-
} else if (filter.type ===
|
|
59
|
+
} else if (filter.type === "multiSelect") {
|
|
50
60
|
clearedValues[filter.key] = [];
|
|
61
|
+
} else if (filter.type === "switch") {
|
|
62
|
+
clearedValues[filter.key] = false;
|
|
51
63
|
} else {
|
|
52
64
|
clearedValues[filter.key] = null;
|
|
53
65
|
}
|
|
@@ -60,20 +72,20 @@ export default function FilterBar({
|
|
|
60
72
|
const value = values[filter.key];
|
|
61
73
|
|
|
62
74
|
switch (filter.type) {
|
|
63
|
-
case
|
|
75
|
+
case "text":
|
|
64
76
|
return (
|
|
65
77
|
<Input
|
|
66
78
|
type="text"
|
|
67
79
|
placeholder={filter.placeholder || `Filter by ${filter.label}`}
|
|
68
|
-
value={(value as string) ||
|
|
80
|
+
value={(value as string) || ""}
|
|
69
81
|
onChange={(e) => handleFilterChange(filter.key, e.target.value)}
|
|
70
82
|
/>
|
|
71
83
|
);
|
|
72
84
|
|
|
73
|
-
case
|
|
85
|
+
case "select": {
|
|
74
86
|
const selectOptions: SelectOption[] = [
|
|
75
|
-
{ value:
|
|
76
|
-
...(filter.options?.map(opt => ({
|
|
87
|
+
{ value: "", label: `All ${filter.label}` },
|
|
88
|
+
...(filter.options?.map((opt) => ({
|
|
77
89
|
value: String(opt.value),
|
|
78
90
|
label: opt.label,
|
|
79
91
|
})) || []),
|
|
@@ -82,60 +94,62 @@ export default function FilterBar({
|
|
|
82
94
|
return (
|
|
83
95
|
<Select
|
|
84
96
|
options={selectOptions}
|
|
85
|
-
value={String(value ||
|
|
86
|
-
onChange={(newValue) =>
|
|
97
|
+
value={String(value || "")}
|
|
98
|
+
onChange={(newValue) =>
|
|
99
|
+
handleFilterChange(filter.key, newValue || null)
|
|
100
|
+
}
|
|
87
101
|
/>
|
|
88
102
|
);
|
|
89
103
|
}
|
|
90
104
|
|
|
91
|
-
case
|
|
105
|
+
case "date":
|
|
92
106
|
return (
|
|
93
107
|
<input
|
|
94
108
|
type="date"
|
|
95
|
-
value={(value as string) ||
|
|
109
|
+
value={(value as string) || ""}
|
|
96
110
|
onChange={(e) => handleFilterChange(filter.key, e.target.value)}
|
|
97
111
|
className="input"
|
|
98
112
|
/>
|
|
99
113
|
);
|
|
100
114
|
|
|
101
|
-
case
|
|
115
|
+
case "number":
|
|
102
116
|
return (
|
|
103
117
|
<input
|
|
104
118
|
type="number"
|
|
105
119
|
placeholder={filter.placeholder || `Filter by ${filter.label}`}
|
|
106
|
-
value={value !== null && value !== undefined ? String(value) :
|
|
120
|
+
value={value !== null && value !== undefined ? String(value) : ""}
|
|
107
121
|
onChange={(e) =>
|
|
108
122
|
handleFilterChange(
|
|
109
123
|
filter.key,
|
|
110
|
-
e.target.value ? Number(e.target.value) : null
|
|
124
|
+
e.target.value ? Number(e.target.value) : null,
|
|
111
125
|
)
|
|
112
126
|
}
|
|
113
127
|
className="input"
|
|
114
128
|
/>
|
|
115
129
|
);
|
|
116
130
|
|
|
117
|
-
case
|
|
131
|
+
case "boolean": {
|
|
118
132
|
const boolOptions: SelectOption[] = [
|
|
119
|
-
{ value:
|
|
120
|
-
{ value:
|
|
121
|
-
{ value:
|
|
133
|
+
{ value: "", label: "All" },
|
|
134
|
+
{ value: "true", label: "Yes" },
|
|
135
|
+
{ value: "false", label: "No" },
|
|
122
136
|
];
|
|
123
137
|
|
|
124
138
|
return (
|
|
125
139
|
<Select
|
|
126
140
|
options={boolOptions}
|
|
127
|
-
value={value === null || value === undefined ?
|
|
141
|
+
value={value === null || value === undefined ? "" : String(value)}
|
|
128
142
|
onChange={(newValue) =>
|
|
129
143
|
handleFilterChange(
|
|
130
144
|
filter.key,
|
|
131
|
-
newValue ===
|
|
145
|
+
newValue === "" ? null : newValue === "true",
|
|
132
146
|
)
|
|
133
147
|
}
|
|
134
148
|
/>
|
|
135
149
|
);
|
|
136
150
|
}
|
|
137
151
|
|
|
138
|
-
case
|
|
152
|
+
case "search":
|
|
139
153
|
return (
|
|
140
154
|
<div className="relative">
|
|
141
155
|
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
|
@@ -144,22 +158,25 @@ export default function FilterBar({
|
|
|
144
158
|
<input
|
|
145
159
|
type="text"
|
|
146
160
|
placeholder={filter.placeholder || `Search ${filter.label}...`}
|
|
147
|
-
value={(value as string) ||
|
|
161
|
+
value={(value as string) || ""}
|
|
148
162
|
onChange={(e) => handleFilterChange(filter.key, e.target.value)}
|
|
149
163
|
className="input pl-9"
|
|
150
164
|
/>
|
|
151
165
|
</div>
|
|
152
166
|
);
|
|
153
167
|
|
|
154
|
-
case
|
|
168
|
+
case "dateRange": {
|
|
155
169
|
const rangeValue = (value as { from?: string; to?: string }) || {};
|
|
156
170
|
return (
|
|
157
171
|
<div className="flex items-center gap-2">
|
|
158
172
|
<input
|
|
159
173
|
type="date"
|
|
160
|
-
value={rangeValue.from ||
|
|
174
|
+
value={rangeValue.from || ""}
|
|
161
175
|
onChange={(e) =>
|
|
162
|
-
handleFilterChange(filter.key, {
|
|
176
|
+
handleFilterChange(filter.key, {
|
|
177
|
+
...rangeValue,
|
|
178
|
+
from: e.target.value || undefined,
|
|
179
|
+
})
|
|
163
180
|
}
|
|
164
181
|
className="input text-sm"
|
|
165
182
|
aria-label={`${filter.label} from`}
|
|
@@ -167,9 +184,12 @@ export default function FilterBar({
|
|
|
167
184
|
<span className="text-ink-400 text-xs">to</span>
|
|
168
185
|
<input
|
|
169
186
|
type="date"
|
|
170
|
-
value={rangeValue.to ||
|
|
187
|
+
value={rangeValue.to || ""}
|
|
171
188
|
onChange={(e) =>
|
|
172
|
-
handleFilterChange(filter.key, {
|
|
189
|
+
handleFilterChange(filter.key, {
|
|
190
|
+
...rangeValue,
|
|
191
|
+
to: e.target.value || undefined,
|
|
192
|
+
})
|
|
173
193
|
}
|
|
174
194
|
className="input text-sm"
|
|
175
195
|
aria-label={`${filter.label} to`}
|
|
@@ -178,25 +198,34 @@ export default function FilterBar({
|
|
|
178
198
|
);
|
|
179
199
|
}
|
|
180
200
|
|
|
181
|
-
case
|
|
201
|
+
case "toggle": {
|
|
182
202
|
const toggleOptions: SelectOption[] = [
|
|
183
|
-
{ value:
|
|
184
|
-
{ value:
|
|
185
|
-
{ value:
|
|
203
|
+
{ value: "", label: "All" },
|
|
204
|
+
{ value: "true", label: "Yes" },
|
|
205
|
+
{ value: "false", label: "No" },
|
|
186
206
|
];
|
|
187
|
-
const currentVal =
|
|
207
|
+
const currentVal =
|
|
208
|
+
value === null || value === undefined ? "" : String(value);
|
|
188
209
|
return (
|
|
189
|
-
<div
|
|
210
|
+
<div
|
|
211
|
+
className="flex rounded-lg border border-paper-300 overflow-hidden"
|
|
212
|
+
role="group"
|
|
213
|
+
>
|
|
190
214
|
{toggleOptions.map((opt) => (
|
|
191
215
|
<button
|
|
192
216
|
key={opt.value}
|
|
193
217
|
type="button"
|
|
194
|
-
onClick={() =>
|
|
218
|
+
onClick={() =>
|
|
219
|
+
handleFilterChange(
|
|
220
|
+
filter.key,
|
|
221
|
+
opt.value === "" ? null : opt.value === "true",
|
|
222
|
+
)
|
|
223
|
+
}
|
|
195
224
|
className={`px-3 py-1.5 text-xs font-medium transition-colors ${
|
|
196
225
|
currentVal === opt.value
|
|
197
|
-
?
|
|
198
|
-
:
|
|
199
|
-
} ${opt.value !==
|
|
226
|
+
? "bg-accent-500 text-white"
|
|
227
|
+
: "bg-white text-ink-600 hover:bg-paper-50"
|
|
228
|
+
} ${opt.value !== "" ? "border-l border-paper-300" : ""}`}
|
|
200
229
|
>
|
|
201
230
|
{opt.label}
|
|
202
231
|
</button>
|
|
@@ -205,13 +234,44 @@ export default function FilterBar({
|
|
|
205
234
|
);
|
|
206
235
|
}
|
|
207
236
|
|
|
208
|
-
case
|
|
237
|
+
case "switch": {
|
|
238
|
+
// Single binary toggle — use when the filter is naturally on/off
|
|
239
|
+
// (e.g. "Mine only", "Archived"), unlike `boolean` / `toggle` which
|
|
240
|
+
// present an All/Yes/No tri-state. Stored value is a plain boolean.
|
|
241
|
+
const checked = value === true;
|
|
242
|
+
return (
|
|
243
|
+
<button
|
|
244
|
+
type="button"
|
|
245
|
+
role="switch"
|
|
246
|
+
aria-checked={checked}
|
|
247
|
+
onClick={() => handleFilterChange(filter.key, !checked)}
|
|
248
|
+
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-accent-400 focus:ring-offset-2 ${
|
|
249
|
+
checked ? "bg-accent-500" : "bg-paper-300"
|
|
250
|
+
}`}
|
|
251
|
+
>
|
|
252
|
+
<span
|
|
253
|
+
className={`inline-block h-4 w-4 transform rounded-full bg-white shadow transition-transform ${
|
|
254
|
+
checked ? "translate-x-6" : "translate-x-1"
|
|
255
|
+
}`}
|
|
256
|
+
/>
|
|
257
|
+
<span className="sr-only">{filter.label}</span>
|
|
258
|
+
</button>
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
case "multiSelect": {
|
|
209
263
|
const selectedValues = Array.isArray(value) ? (value as string[]) : [];
|
|
210
264
|
const msOptions = filter.options || [];
|
|
211
265
|
return (
|
|
212
266
|
<div className="relative">
|
|
213
267
|
<Select
|
|
214
|
-
options={[
|
|
268
|
+
options={[
|
|
269
|
+
{ value: "", label: `All ${filter.label}` },
|
|
270
|
+
...msOptions.map((o) => ({
|
|
271
|
+
value: String(o.value),
|
|
272
|
+
label: o.label,
|
|
273
|
+
})),
|
|
274
|
+
]}
|
|
215
275
|
value=""
|
|
216
276
|
onChange={(newValue) => {
|
|
217
277
|
if (!newValue) {
|
|
@@ -224,11 +284,23 @@ export default function FilterBar({
|
|
|
224
284
|
{selectedValues.length > 0 && (
|
|
225
285
|
<div className="flex flex-wrap gap-1 mt-1">
|
|
226
286
|
{selectedValues.map((sv) => {
|
|
227
|
-
const opt = msOptions.find(o => String(o.value) === sv);
|
|
287
|
+
const opt = msOptions.find((o) => String(o.value) === sv);
|
|
228
288
|
return (
|
|
229
|
-
<span
|
|
289
|
+
<span
|
|
290
|
+
key={sv}
|
|
291
|
+
className="inline-flex items-center gap-1 px-2 py-0.5 text-xs bg-accent-100 text-accent-700 rounded-full"
|
|
292
|
+
>
|
|
230
293
|
{opt?.label || sv}
|
|
231
|
-
<button
|
|
294
|
+
<button
|
|
295
|
+
type="button"
|
|
296
|
+
onClick={() =>
|
|
297
|
+
handleFilterChange(
|
|
298
|
+
filter.key,
|
|
299
|
+
selectedValues.filter((v) => v !== sv),
|
|
300
|
+
)
|
|
301
|
+
}
|
|
302
|
+
className="hover:text-accent-900"
|
|
303
|
+
>
|
|
232
304
|
<X className="h-3 w-3" />
|
|
233
305
|
</button>
|
|
234
306
|
</span>
|
|
@@ -248,12 +320,17 @@ export default function FilterBar({
|
|
|
248
320
|
if (filters.length === 0) return null;
|
|
249
321
|
|
|
250
322
|
return (
|
|
251
|
-
<div
|
|
323
|
+
<div
|
|
324
|
+
className={`bg-white bg-subtle-grain border border-paper-200 rounded-lg shadow-sm p-4 ${className}`}
|
|
325
|
+
>
|
|
252
326
|
<div className="flex items-start justify-between gap-4 flex-wrap">
|
|
253
327
|
{/* Filters */}
|
|
254
328
|
<div className="flex-1 flex flex-wrap gap-4">
|
|
255
329
|
{filters.map((filter) => (
|
|
256
|
-
<div
|
|
330
|
+
<div
|
|
331
|
+
key={filter.key}
|
|
332
|
+
className="flex flex-col space-y-1 min-w-[200px]"
|
|
333
|
+
>
|
|
257
334
|
<label className="label">{filter.label}</label>
|
|
258
335
|
{renderFilter(filter)}
|
|
259
336
|
</div>
|