@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/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,6 +1,6 @@
1
1
  {
2
2
  "name": "@papernote/ui",
3
- "version": "2.0.3",
3
+ "version": "2.0.4",
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",
@@ -1,12 +1,22 @@
1
- import { X, Search } from 'lucide-react';
2
- import Input from './Input';
3
- import Select, { type SelectOption } from './Select';
4
- import Button from './Button';
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: 'text' | 'search' | 'select' | 'date' | 'number' | 'boolean' | 'dateRange' | 'toggle' | 'multiSelect';
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 === 'text' || filter.type === 'search') {
46
- clearedValues[filter.key] = '';
47
- } else if (filter.type === 'dateRange') {
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 === 'multiSelect') {
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 'text':
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 'select': {
85
+ case "select": {
74
86
  const selectOptions: SelectOption[] = [
75
- { value: '', label: `All ${filter.label}` },
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) => handleFilterChange(filter.key, newValue || null)}
97
+ value={String(value || "")}
98
+ onChange={(newValue) =>
99
+ handleFilterChange(filter.key, newValue || null)
100
+ }
87
101
  />
88
102
  );
89
103
  }
90
104
 
91
- case 'date':
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 'number':
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 'boolean': {
131
+ case "boolean": {
118
132
  const boolOptions: SelectOption[] = [
119
- { value: '', label: 'All' },
120
- { value: 'true', label: 'Yes' },
121
- { value: 'false', label: 'No' },
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 ? '' : String(value)}
141
+ value={value === null || value === undefined ? "" : String(value)}
128
142
  onChange={(newValue) =>
129
143
  handleFilterChange(
130
144
  filter.key,
131
- newValue === '' ? null : newValue === 'true'
145
+ newValue === "" ? null : newValue === "true",
132
146
  )
133
147
  }
134
148
  />
135
149
  );
136
150
  }
137
151
 
138
- case 'search':
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 'dateRange': {
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, { ...rangeValue, from: e.target.value || undefined })
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, { ...rangeValue, to: e.target.value || undefined })
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 'toggle': {
201
+ case "toggle": {
182
202
  const toggleOptions: SelectOption[] = [
183
- { value: '', label: 'All' },
184
- { value: 'true', label: 'Yes' },
185
- { value: 'false', label: 'No' },
203
+ { value: "", label: "All" },
204
+ { value: "true", label: "Yes" },
205
+ { value: "false", label: "No" },
186
206
  ];
187
- const currentVal = value === null || value === undefined ? '' : String(value);
207
+ const currentVal =
208
+ value === null || value === undefined ? "" : String(value);
188
209
  return (
189
- <div className="flex rounded-lg border border-paper-300 overflow-hidden" role="group">
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={() => handleFilterChange(filter.key, opt.value === '' ? null : opt.value === 'true')}
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
- ? 'bg-accent-500 text-white'
198
- : 'bg-white text-ink-600 hover:bg-paper-50'
199
- } ${opt.value !== '' ? 'border-l border-paper-300' : ''}`}
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 'multiSelect': {
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={[{ value: '', label: `All ${filter.label}` }, ...msOptions.map(o => ({ value: String(o.value), label: o.label }))]}
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 key={sv} className="inline-flex items-center gap-1 px-2 py-0.5 text-xs bg-accent-100 text-accent-700 rounded-full">
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 type="button" onClick={() => handleFilterChange(filter.key, selectedValues.filter(v => v !== sv))} className="hover:text-accent-900">
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 className={`bg-white bg-subtle-grain border border-paper-200 rounded-lg shadow-sm p-4 ${className}`}>
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 key={filter.key} className="flex flex-col space-y-1 min-w-[200px]">
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>