@ibobbyts/svelte-ui-utils 0.2.5 → 0.2.6

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/README.md CHANGED
@@ -401,14 +401,17 @@ can clip the menu even when the menu has a high `z-index`. Keep the nearest
401
401
  filter container at `overflow: visible`, or move the clipping/scrolling behavior
402
402
  to a parent that does not wrap the dropdown menu directly.
403
403
 
404
- `dateRange` renders two browser date inputs plus preset buttons:
405
- `last 24 hours`, `last 7 days`, `last 30 days`, `today`, `this week`,
406
- `this month`, and `this year`. Manual changes emit `{ startDate, endDate,
407
- preset: null }`. Pass `defaultPreset` to apply a preset on mount when the
408
- current value is empty. Clicking the currently active preset clears both date
409
- inputs and emits an empty range. The `last24Hours` preset also emits
410
- `startDateTime` and `endDateTime` so a consuming app can run an exact timestamp
411
- query while still showing the covered dates in the inputs.
404
+ `dateRange` renders two browser date inputs, preset buttons, and quick month/year
405
+ selects. The presets are `last 24 hours`, `last 7 days`, `last 30 days`,
406
+ `today`, `this week`, `this month`, and `this year`. Manual changes and quick
407
+ month/year changes emit `{ startDate, endDate, preset: null }`. When the month is
408
+ selected first, the year select fills with the most recent matching year. For
409
+ example, in July 2026, July resolves to 2026 and August resolves to 2025. Pass
410
+ `defaultPreset` to apply a preset on mount when the current value is empty.
411
+ Clicking the currently active preset clears both date inputs and emits an empty
412
+ range. The `last24Hours` preset also emits `startDateTime` and `endDateTime` so
413
+ a consuming app can run an exact timestamp query while still showing the covered
414
+ dates in the inputs.
412
415
 
413
416
  `numberRange` renders min/max number inputs and supports `prefixLabel`, for
414
417
  example `$` for currency filters.
@@ -28,10 +28,16 @@
28
28
  'thisMonth',
29
29
  'thisYear'
30
30
  ];
31
+ const monthNumbers = Array.from({ length: 12 }, (_, index) => index + 1);
32
+
33
+ let quickYear = '';
34
+ let quickMonth = '';
31
35
 
32
36
  $: messages = getUiMessages(language);
33
37
  $: resolvedStartLabel = startLabel ?? messages.dateRange.startLabel;
34
38
  $: resolvedEndLabel = endLabel ?? messages.dateRange.endLabel;
39
+ $: currentYear = startOfDay(now()).getFullYear();
40
+ $: quickYearOptions = Array.from({ length: 21 }, (_, index) => currentYear - index);
35
41
 
36
42
  function pad(value: number) {
37
43
  return String(value).padStart(2, '0');
@@ -62,6 +68,31 @@
62
68
  return addDays(start, -offset);
63
69
  }
64
70
 
71
+ function endOfMonth(year: number, month: number) {
72
+ return new Date(year, month, 0);
73
+ }
74
+
75
+ function resolveYearForMonth(month: number) {
76
+ const today = startOfDay(now());
77
+ return month <= today.getMonth() + 1 ? today.getFullYear() : today.getFullYear() - 1;
78
+ }
79
+
80
+ function resolveQuickRange(year: number, month: number | null): DateRangeFilterValue {
81
+ const start = month === null ? new Date(year, 0, 1) : new Date(year, month - 1, 1);
82
+ const end = month === null ? new Date(year, 11, 31) : endOfMonth(year, month);
83
+
84
+ return {
85
+ startDate: formatDate(start),
86
+ endDate: formatDate(end),
87
+ preset: null
88
+ };
89
+ }
90
+
91
+ function clearQuickSelection() {
92
+ quickYear = '';
93
+ quickMonth = '';
94
+ }
95
+
65
96
  function resolvePreset(preset: DateRangePreset): DateRangeFilterValue {
66
97
  const current = now();
67
98
  const today = startOfDay(current);
@@ -142,6 +173,7 @@
142
173
  }
143
174
 
144
175
  function updateDate(part: 'startDate' | 'endDate', nextValue: string) {
176
+ clearQuickSelection();
145
177
  emit({
146
178
  startDate: part === 'startDate' ? nextValue : value.startDate,
147
179
  endDate: part === 'endDate' ? nextValue : value.endDate,
@@ -149,7 +181,39 @@
149
181
  });
150
182
  }
151
183
 
184
+ function updateQuickYear(nextValue: string) {
185
+ quickYear = nextValue;
186
+
187
+ if (!nextValue) {
188
+ clearQuickSelection();
189
+ emit(emptyRange());
190
+ return;
191
+ }
192
+
193
+ emit(resolveQuickRange(Number(nextValue), quickMonth ? Number(quickMonth) : null));
194
+ }
195
+
196
+ function updateQuickMonth(nextValue: string) {
197
+ quickMonth = nextValue;
198
+
199
+ if (!nextValue) {
200
+ if (quickYear) {
201
+ emit(resolveQuickRange(Number(quickYear), null));
202
+ } else {
203
+ emit(emptyRange());
204
+ }
205
+ return;
206
+ }
207
+
208
+ if (!quickYear) {
209
+ quickYear = String(resolveYearForMonth(Number(nextValue)));
210
+ }
211
+
212
+ emit(resolveQuickRange(Number(quickYear), Number(nextValue)));
213
+ }
214
+
152
215
  function applyPreset(preset: DateRangePreset) {
216
+ clearQuickSelection();
153
217
  if (value.preset === preset) {
154
218
  emit(emptyRange());
155
219
  return;
@@ -162,6 +226,10 @@
162
226
  return presetLabels[preset] ?? messages.dateRange.presetLabels[preset];
163
227
  }
164
228
 
229
+ function monthLabel(month: number) {
230
+ return messages.dateRange.monthLabels[month - 1] ?? String(month);
231
+ }
232
+
165
233
  onMount(() => {
166
234
  if (defaultPreset && isEmptyRange(value)) {
167
235
  emit(resolvePreset(defaultPreset));
@@ -196,6 +264,33 @@
196
264
  >
197
265
  {labelFor(preset)}
198
266
  </button>
267
+ {#if preset === 'last30Days' || preset === 'thisYear'}
268
+ <span class="suu-filter-preset-divider" aria-hidden="true"></span>
269
+ {/if}
199
270
  {/each}
271
+ <label class="suu-filter-preset-select">
272
+ <span class="suu-visually-hidden">{messages.dateRange.quickMonthLabel}</span>
273
+ <select
274
+ aria-label={messages.dateRange.quickMonthLabel}
275
+ on:change={(event) => updateQuickMonth((event.currentTarget as HTMLSelectElement).value)}
276
+ >
277
+ <option value="" selected={quickMonth === ''}>{messages.dateRange.quickMonthPlaceholder}</option>
278
+ {#each monthNumbers as month}
279
+ <option value={month} selected={quickMonth === String(month)}>{monthLabel(month)}</option>
280
+ {/each}
281
+ </select>
282
+ </label>
283
+ <label class="suu-filter-preset-select">
284
+ <span class="suu-visually-hidden">{messages.dateRange.quickYearLabel}</span>
285
+ <select
286
+ aria-label={messages.dateRange.quickYearLabel}
287
+ on:change={(event) => updateQuickYear((event.currentTarget as HTMLSelectElement).value)}
288
+ >
289
+ <option value="" selected={quickYear === ''}>{messages.dateRange.quickYearPlaceholder}</option>
290
+ {#each quickYearOptions as year}
291
+ <option value={year} selected={quickYear === String(year)}>{year}</option>
292
+ {/each}
293
+ </select>
294
+ </label>
200
295
  </div>
201
296
  </div>
@@ -1 +1 @@
1
- {"version":3,"file":"DateRangeFilter.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/data-table/DateRangeFilter.svelte.ts"],"names":[],"mappings":"AAIA,OAAO,EAAiB,KAAK,UAAU,EAAE,MAAM,YAAY,CAAC;AAC5D,OAAO,KAAK,EAAE,oBAAoB,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAiMxE,UAAU,kCAAkC,CAAC,KAAK,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,GAAG,EAAE,MAAM,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,GAAG,EAAE,KAAK,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,GAAG,EAAE,OAAO,GAAG,EAAE,EAAE,QAAQ,GAAG,MAAM;IACpM,KAAK,OAAO,EAAE,OAAO,QAAQ,EAAE,2BAA2B,CAAC,KAAK,CAAC,GAAG,OAAO,QAAQ,EAAE,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,CAAC,GAAG;QAAE,UAAU,CAAC,EAAE,QAAQ,CAAA;KAAE,GAAG,OAAO,CAAC;IACjK,CAAC,QAAQ,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,GAAG;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,KAAK,CAAA;KAAC,GAAG,OAAO,GAAG;QAAE,IAAI,CAAC,EAAE,GAAG,CAAC;QAAC,GAAG,CAAC,EAAE,GAAG,CAAA;KAAE,CAAC;IAC9G,YAAY,CAAC,EAAE,QAAQ,CAAC;CAC3B;AAKD,QAAA,MAAM,eAAe;YAVkN,oBAAoB;eAAa,UAAU;iBAAe,MAAM,GAAG,SAAS;eAAa,MAAM,GAAG,SAAS;mBAAiB,OAAO,CAAC,MAAM,CAAC,eAAe,EAAE,MAAM,CAAC,CAAC;oBAAkB,eAAe,GAAG,SAAS;UAAQ,MAAM,IAAI;mBAAiB,CAAC,GAAG,CAAC;eAAa,CAAC,CAAC,KAAK,EAAE,oBAAoB,KAAK,IAAI,CAAC,GAAG,SAAS;;;kBAU5b,CAAC;AACnF,KAAK,eAAe,GAAG,YAAY,CAAC,OAAO,eAAe,CAAC,CAAC;AAC9D,eAAe,eAAe,CAAC"}
1
+ {"version":3,"file":"DateRangeFilter.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/data-table/DateRangeFilter.svelte.ts"],"names":[],"mappings":"AAIA,OAAO,EAAiB,KAAK,UAAU,EAAE,MAAM,YAAY,CAAC;AAC5D,OAAO,KAAK,EAAE,oBAAoB,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AA0RxE,UAAU,kCAAkC,CAAC,KAAK,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,GAAG,EAAE,MAAM,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,GAAG,EAAE,KAAK,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,GAAG,EAAE,OAAO,GAAG,EAAE,EAAE,QAAQ,GAAG,MAAM;IACpM,KAAK,OAAO,EAAE,OAAO,QAAQ,EAAE,2BAA2B,CAAC,KAAK,CAAC,GAAG,OAAO,QAAQ,EAAE,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,CAAC,GAAG;QAAE,UAAU,CAAC,EAAE,QAAQ,CAAA;KAAE,GAAG,OAAO,CAAC;IACjK,CAAC,QAAQ,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,GAAG;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,KAAK,CAAA;KAAC,GAAG,OAAO,GAAG;QAAE,IAAI,CAAC,EAAE,GAAG,CAAC;QAAC,GAAG,CAAC,EAAE,GAAG,CAAA;KAAE,CAAC;IAC9G,YAAY,CAAC,EAAE,QAAQ,CAAC;CAC3B;AAKD,QAAA,MAAM,eAAe;YAVkN,oBAAoB;eAAa,UAAU;iBAAe,MAAM,GAAG,SAAS;eAAa,MAAM,GAAG,SAAS;mBAAiB,OAAO,CAAC,MAAM,CAAC,eAAe,EAAE,MAAM,CAAC,CAAC;oBAAkB,eAAe,GAAG,SAAS;UAAQ,MAAM,IAAI;mBAAiB,CAAC,GAAG,CAAC;eAAa,CAAC,CAAC,KAAK,EAAE,oBAAoB,KAAK,IAAI,CAAC,GAAG,SAAS;;;kBAU5b,CAAC;AACnF,KAAK,eAAe,GAAG,YAAY,CAAC,OAAO,eAAe,CAAC,CAAC;AAC9D,eAAe,eAAe,CAAC"}
package/dist/i18n.d.ts CHANGED
@@ -19,6 +19,11 @@ export interface UiMessages {
19
19
  startLabel: string;
20
20
  endLabel: string;
21
21
  presetLabels: Record<DateRangePresetKey, string>;
22
+ quickMonthLabel: string;
23
+ quickMonthPlaceholder: string;
24
+ quickYearLabel: string;
25
+ quickYearPlaceholder: string;
26
+ monthLabels: string[];
22
27
  };
23
28
  numberRange: {
24
29
  minLabel: string;
@@ -1 +1 @@
1
- {"version":3,"file":"i18n.d.ts","sourceRoot":"","sources":["../src/lib/i18n.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,WAAW,sCAAuC,CAAC;AAEhE,MAAM,MAAM,UAAU,GAAG,CAAC,OAAO,WAAW,CAAC,CAAC,MAAM,CAAC,CAAC;AAEtD,KAAK,kBAAkB,GACnB,aAAa,GACb,WAAW,GACX,YAAY,GACZ,OAAO,GACP,UAAU,GACV,WAAW,GACX,UAAU,CAAC;AAEf,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE;QACL,UAAU,EAAE,MAAM,CAAC;KACpB,CAAC;IACF,cAAc,EAAE;QACd,aAAa,EAAE,MAAM,CAAC;QACtB,WAAW,EAAE,MAAM,CAAC;QACpB,UAAU,EAAE,MAAM,CAAC;KACpB,CAAC;IACF,KAAK,EAAE;QACL,SAAS,EAAE,MAAM,CAAC;QAClB,aAAa,EAAE,MAAM,CAAC;QACtB,eAAe,EAAE,MAAM,CAAC;KACzB,CAAC;IACF,SAAS,EAAE;QACT,UAAU,EAAE,MAAM,CAAC;QACnB,QAAQ,EAAE,MAAM,CAAC;QACjB,YAAY,EAAE,MAAM,CAAC,kBAAkB,EAAE,MAAM,CAAC,CAAC;KAClD,CAAC;IACF,WAAW,EAAE;QACX,QAAQ,EAAE,MAAM,CAAC;QACjB,QAAQ,EAAE,MAAM,CAAC;KAClB,CAAC;CACH;AAqGD,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,UAAU,GAAG,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,UAAU,CAiB9F;AAED,wBAAgB,aAAa,CAAC,QAAQ,EAAE,UAAU,GAAG,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,UAAU,CAE1F"}
1
+ {"version":3,"file":"i18n.d.ts","sourceRoot":"","sources":["../src/lib/i18n.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,WAAW,sCAAuC,CAAC;AAEhE,MAAM,MAAM,UAAU,GAAG,CAAC,OAAO,WAAW,CAAC,CAAC,MAAM,CAAC,CAAC;AAEtD,KAAK,kBAAkB,GACnB,aAAa,GACb,WAAW,GACX,YAAY,GACZ,OAAO,GACP,UAAU,GACV,WAAW,GACX,UAAU,CAAC;AAEf,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE;QACL,UAAU,EAAE,MAAM,CAAC;KACpB,CAAC;IACF,cAAc,EAAE;QACd,aAAa,EAAE,MAAM,CAAC;QACtB,WAAW,EAAE,MAAM,CAAC;QACpB,UAAU,EAAE,MAAM,CAAC;KACpB,CAAC;IACF,KAAK,EAAE;QACL,SAAS,EAAE,MAAM,CAAC;QAClB,aAAa,EAAE,MAAM,CAAC;QACtB,eAAe,EAAE,MAAM,CAAC;KACzB,CAAC;IACF,SAAS,EAAE;QACT,UAAU,EAAE,MAAM,CAAC;QACnB,QAAQ,EAAE,MAAM,CAAC;QACjB,YAAY,EAAE,MAAM,CAAC,kBAAkB,EAAE,MAAM,CAAC,CAAC;QACjD,eAAe,EAAE,MAAM,CAAC;QACxB,qBAAqB,EAAE,MAAM,CAAC;QAC9B,cAAc,EAAE,MAAM,CAAC;QACvB,oBAAoB,EAAE,MAAM,CAAC;QAC7B,WAAW,EAAE,MAAM,EAAE,CAAC;KACvB,CAAC;IACF,WAAW,EAAE;QACX,QAAQ,EAAE,MAAM,CAAC;QACjB,QAAQ,EAAE,MAAM,CAAC;KAClB,CAAC;CACH;AAoHD,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,UAAU,GAAG,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,UAAU,CAiB9F;AAED,wBAAgB,aAAa,CAAC,QAAQ,EAAE,UAAU,GAAG,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,UAAU,CAE1F"}
package/dist/i18n.js CHANGED
@@ -25,7 +25,12 @@ const messages = {
25
25
  thisWeek: 'This Week',
26
26
  thisMonth: 'This Month',
27
27
  thisYear: 'This Year'
28
- }
28
+ },
29
+ quickMonthLabel: 'Month',
30
+ quickMonthPlaceholder: 'Month',
31
+ quickYearLabel: 'Year',
32
+ quickYearPlaceholder: 'Year',
33
+ monthLabels: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
29
34
  },
30
35
  numberRange: {
31
36
  minLabel: 'Min',
@@ -57,7 +62,12 @@ const messages = {
57
62
  thisWeek: '本周',
58
63
  thisMonth: '本月',
59
64
  thisYear: '今年'
60
- }
65
+ },
66
+ quickMonthLabel: '月份',
67
+ quickMonthPlaceholder: '月份',
68
+ quickYearLabel: '年份',
69
+ quickYearPlaceholder: '年份',
70
+ monthLabels: ['1 月', '2 月', '3 月', '4 月', '5 月', '6 月', '7 月', '8 月', '9 月', '10 月', '11 月', '12 月']
61
71
  },
62
72
  numberRange: {
63
73
  minLabel: '最小值',
@@ -89,7 +99,12 @@ const messages = {
89
99
  thisWeek: '本週',
90
100
  thisMonth: '本月',
91
101
  thisYear: '今年'
92
- }
102
+ },
103
+ quickMonthLabel: '月份',
104
+ quickMonthPlaceholder: '月份',
105
+ quickYearLabel: '年份',
106
+ quickYearPlaceholder: '年份',
107
+ monthLabels: ['1 月', '2 月', '3 月', '4 月', '5 月', '6 月', '7 月', '8 月', '9 月', '10 月', '11 月', '12 月']
93
108
  },
94
109
  numberRange: {
95
110
  minLabel: '最小值',
package/dist/style.css CHANGED
@@ -33,6 +33,18 @@
33
33
  --suu-shadow: 0 16px 34px rgb(0 0 0 / 0.32);
34
34
  }
35
35
 
36
+ .suu-visually-hidden {
37
+ position: absolute;
38
+ overflow: hidden;
39
+ width: 1px;
40
+ height: 1px;
41
+ padding: 0;
42
+ border: 0;
43
+ margin: -1px;
44
+ clip: rect(0 0 0 0);
45
+ white-space: nowrap;
46
+ }
47
+
36
48
  .suu-toast-region {
37
49
  position: fixed;
38
50
  z-index: 1000;
@@ -551,6 +563,7 @@
551
563
  .suu-filter-preset-row {
552
564
  display: flex;
553
565
  flex-wrap: wrap;
566
+ align-items: center;
554
567
  gap: 6px;
555
568
  }
556
569
 
@@ -582,6 +595,34 @@
582
595
  background: color-mix(in srgb, var(--suu-color-accent) 12%, var(--suu-color-bg));
583
596
  }
584
597
 
598
+ .suu-filter-preset-divider {
599
+ width: 1px;
600
+ height: 22px;
601
+ margin: 0 3px;
602
+ background: var(--suu-color-border);
603
+ }
604
+
605
+ .suu-filter-preset-select select {
606
+ min-height: 30px;
607
+ border: 1px solid var(--suu-color-border);
608
+ border-radius: var(--suu-radius);
609
+ background: var(--suu-color-bg);
610
+ color: var(--suu-color-text);
611
+ cursor: pointer;
612
+ font: inherit;
613
+ font-size: 0.8125rem;
614
+ }
615
+
616
+ .suu-filter-preset-select select {
617
+ padding: 4px 28px 4px 8px;
618
+ }
619
+
620
+ .suu-filter-preset-select select:focus {
621
+ border-color: var(--suu-color-accent);
622
+ outline: 2px solid color-mix(in srgb, var(--suu-color-accent) 22%, transparent);
623
+ outline-offset: 1px;
624
+ }
625
+
585
626
  .suu-input-affix {
586
627
  display: flex;
587
628
  align-items: stretch;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ibobbyts/svelte-ui-utils",
3
- "version": "0.2.5",
3
+ "version": "0.2.6",
4
4
  "description": "Reusable Svelte 5 UI utilities for dialogs, toast notifications, dropdown search, and data tables.",
5
5
  "type": "module",
6
6
  "license": "MIT",