@makolabs/ripple 3.4.0 → 3.5.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/elements/pdf-viewer/PdfViewer.svelte +427 -0
- package/dist/elements/pdf-viewer/PdfViewer.svelte.d.ts +4 -0
- package/dist/elements/pdf-viewer/pdf-viewer-types.d.ts +38 -0
- package/dist/elements/pdf-viewer/pdf-viewer-types.js +1 -0
- package/dist/forms/DateRange.svelte +85 -460
- package/dist/forms/calendar/Calendar.svelte +369 -164
- package/dist/forms/calendar/calendar-types.d.ts +15 -5
- package/dist/forms/date-picker/DatePicker.svelte +0 -1
- package/dist/forms/month-picker/MonthPicker.svelte +119 -40
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/package.json +8 -1
|
@@ -28,7 +28,7 @@ export type CalendarProps = {
|
|
|
28
28
|
* Day the week starts on. `0` = Sunday, `1` = Monday. @default 1
|
|
29
29
|
*/
|
|
30
30
|
weekStartsOn?: 0 | 1;
|
|
31
|
-
/** Hide the
|
|
31
|
+
/** Hide the weekday header row above the scrolling month list. @default false */
|
|
32
32
|
hideHeader?: boolean;
|
|
33
33
|
/** Disable all interaction. */
|
|
34
34
|
disabled?: boolean;
|
|
@@ -39,6 +39,17 @@ export type CalendarProps = {
|
|
|
39
39
|
* oversized. `2xl` aliases `xl`. @default 'md'
|
|
40
40
|
*/
|
|
41
41
|
size?: VariantSizes;
|
|
42
|
+
/**
|
|
43
|
+
* Distance, in months (fractional allowed), of the open-time
|
|
44
|
+
* scroll-in flourish. The scroll animation starts this far above
|
|
45
|
+
* the anchor month and eases into it. Capped against `minDate`,
|
|
46
|
+
* so dates near the lower bound get a shorter flourish (or none).
|
|
47
|
+
* Set to `0` to disable. The default is shared by embedded
|
|
48
|
+
* Calendar and Calendars-inside-popovers (DatePicker, DateRange)
|
|
49
|
+
* so the open animation feels identical across all date pickers.
|
|
50
|
+
* @default 0.5
|
|
51
|
+
*/
|
|
52
|
+
scrollFlourish?: number;
|
|
42
53
|
/** Wrapper class. */
|
|
43
54
|
class?: ClassValue;
|
|
44
55
|
/**
|
|
@@ -53,11 +64,10 @@ export type CalendarProps = {
|
|
|
53
64
|
/**
|
|
54
65
|
* Test ID prefix. When set, the component emits these selectors:
|
|
55
66
|
* - `{testId}-calendar` — root wrapper
|
|
56
|
-
* - `{testId}-calendar-prev-month` — prev button
|
|
57
|
-
* - `{testId}-calendar-next-month` — next button
|
|
58
|
-
* - `{testId}-calendar-month-label` — month/year header
|
|
59
67
|
* - `{testId}-calendar-day-headers` — weekday row
|
|
60
|
-
* - `{testId}-calendar-
|
|
68
|
+
* - `{testId}-calendar-scroll` — scrollable month list
|
|
69
|
+
* - `{testId}-calendar-month-label-{yearMonthKey}` — sticky month/year header per month
|
|
70
|
+
* - `{testId}-calendar-day-{dayNumber}` — each day cell (repeats across months)
|
|
61
71
|
*/
|
|
62
72
|
testId?: string;
|
|
63
73
|
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { tick } from 'svelte';
|
|
2
3
|
import { cn } from '../../helper/cls.js';
|
|
3
4
|
import { buildTestId } from '../../helper/testid.js';
|
|
4
5
|
import { Size } from '../../variants.js';
|
|
@@ -25,6 +26,8 @@
|
|
|
25
26
|
|
|
26
27
|
let open = $state(false);
|
|
27
28
|
let listEl = $state<HTMLDivElement | undefined>();
|
|
29
|
+
let topSentinelEl = $state<HTMLDivElement | undefined>();
|
|
30
|
+
let bottomSentinelEl = $state<HTMLDivElement | undefined>();
|
|
28
31
|
const tokens = $derived(formSizeTokens[size]);
|
|
29
32
|
const hasErrors = $derived(errors.length > 0);
|
|
30
33
|
|
|
@@ -60,52 +63,68 @@
|
|
|
60
63
|
|
|
61
64
|
const display = $derived(value ? `${MONTH_NAMES[value.getMonth()]} ${value.getFullYear()}` : '');
|
|
62
65
|
|
|
63
|
-
//
|
|
64
|
-
//
|
|
65
|
-
|
|
66
|
-
|
|
66
|
+
// Lazy infinite-scroll window over years. The list starts as a small
|
|
67
|
+
// pad around the anchor (selected month or today) and grows toward
|
|
68
|
+
// either edge as IntersectionObserver sentinels enter the viewport,
|
|
69
|
+
// stopping at minDate/maxDate when those are set.
|
|
70
|
+
const INITIAL_PAD = 3;
|
|
71
|
+
const EXTEND_BY = 5;
|
|
67
72
|
|
|
68
|
-
const
|
|
69
|
-
const
|
|
70
|
-
const
|
|
73
|
+
const anchorYear = $derived((value ?? new Date()).getFullYear());
|
|
74
|
+
const minYear = $derived(minDate ? minDate.getFullYear() : null);
|
|
75
|
+
const maxYear = $derived(maxDate ? maxDate.getFullYear() : null);
|
|
71
76
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
77
|
+
let firstYear = $state<number>(new Date().getFullYear());
|
|
78
|
+
let lastYear = $state<number>(new Date().getFullYear());
|
|
79
|
+
// `extending` guards against re-entrant extends while a previous
|
|
80
|
+
// extension is still flushing through tick().
|
|
81
|
+
let extending = false;
|
|
82
|
+
|
|
83
|
+
// Initialize / reset the visible window whenever the anchor or bounds
|
|
84
|
+
// change. We deliberately do not depend on firstYear/lastYear here so
|
|
85
|
+
// extensions don't re-trigger this effect.
|
|
86
|
+
$effect(() => {
|
|
87
|
+
const a = anchorYear;
|
|
88
|
+
const lo = minYear;
|
|
89
|
+
const hi = maxYear;
|
|
90
|
+
let f = a - INITIAL_PAD;
|
|
91
|
+
let l = a + INITIAL_PAD;
|
|
92
|
+
if (lo !== null) f = Math.max(f, lo);
|
|
93
|
+
if (hi !== null) l = Math.min(l, hi);
|
|
94
|
+
if (f > l) {
|
|
95
|
+
f = l = a;
|
|
88
96
|
}
|
|
97
|
+
firstYear = f;
|
|
98
|
+
lastYear = l;
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
const years = $derived.by<number[]>(() => {
|
|
102
|
+
const out: number[] = [];
|
|
103
|
+
for (let y = firstYear; y <= lastYear; y++) out.push(y);
|
|
89
104
|
return out;
|
|
90
105
|
});
|
|
91
106
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
107
|
+
function isMonthDisabled(year: number, month: number): boolean {
|
|
108
|
+
const d = new Date(year, month, 1);
|
|
109
|
+
if (minDate && d < new Date(minDate.getFullYear(), minDate.getMonth(), 1)) return true;
|
|
110
|
+
if (maxDate && d > new Date(maxDate.getFullYear(), maxDate.getMonth(), 1)) return true;
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
95
113
|
|
|
96
|
-
function isSelected(
|
|
97
|
-
return !!value && value.getFullYear() ===
|
|
114
|
+
function isSelected(year: number, month: number): boolean {
|
|
115
|
+
return !!value && value.getFullYear() === year && value.getMonth() === month;
|
|
98
116
|
}
|
|
99
117
|
|
|
100
|
-
function isCurrent(
|
|
118
|
+
function isCurrent(year: number, month: number): boolean {
|
|
101
119
|
const now = new Date();
|
|
102
|
-
return
|
|
120
|
+
return year === now.getFullYear() && month === now.getMonth();
|
|
103
121
|
}
|
|
104
122
|
|
|
105
|
-
function pick(
|
|
106
|
-
if (
|
|
107
|
-
|
|
108
|
-
|
|
123
|
+
function pick(year: number, month: number) {
|
|
124
|
+
if (isMonthDisabled(year, month)) return;
|
|
125
|
+
const d = new Date(year, month, 1);
|
|
126
|
+
value = d;
|
|
127
|
+
onselect?.(d);
|
|
109
128
|
open = false;
|
|
110
129
|
}
|
|
111
130
|
|
|
@@ -126,6 +145,61 @@
|
|
|
126
145
|
open = false;
|
|
127
146
|
}
|
|
128
147
|
|
|
148
|
+
async function extendBackward() {
|
|
149
|
+
if (extending) return;
|
|
150
|
+
if (minYear !== null && firstYear <= minYear) return;
|
|
151
|
+
const el = listEl;
|
|
152
|
+
if (!el) return;
|
|
153
|
+
extending = true;
|
|
154
|
+
const prevHeight = el.scrollHeight;
|
|
155
|
+
const prevTop = el.scrollTop;
|
|
156
|
+
let next = firstYear - EXTEND_BY;
|
|
157
|
+
if (minYear !== null && next < minYear) next = minYear;
|
|
158
|
+
firstYear = next;
|
|
159
|
+
// Wait for the new year sections to render, then preserve the
|
|
160
|
+
// user's visual scroll position by adding the height delta.
|
|
161
|
+
await tick();
|
|
162
|
+
const newHeight = el.scrollHeight;
|
|
163
|
+
el.scrollTop = prevTop + (newHeight - prevHeight);
|
|
164
|
+
extending = false;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async function extendForward() {
|
|
168
|
+
if (extending) return;
|
|
169
|
+
if (maxYear !== null && lastYear >= maxYear) return;
|
|
170
|
+
extending = true;
|
|
171
|
+
let next = lastYear + EXTEND_BY;
|
|
172
|
+
if (maxYear !== null && next > maxYear) next = maxYear;
|
|
173
|
+
lastYear = next;
|
|
174
|
+
await tick();
|
|
175
|
+
extending = false;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Wire IntersectionObserver to the sentinels while the panel is open.
|
|
179
|
+
// Skipped where IO isn't available (SSR / jsdom tests) — the initial
|
|
180
|
+
// pad still renders, just without lazy extension.
|
|
181
|
+
$effect(() => {
|
|
182
|
+
if (!open) return;
|
|
183
|
+
if (typeof IntersectionObserver === 'undefined') return;
|
|
184
|
+
const root = listEl;
|
|
185
|
+
const top = topSentinelEl;
|
|
186
|
+
const bottom = bottomSentinelEl;
|
|
187
|
+
if (!root || !top || !bottom) return;
|
|
188
|
+
const io = new IntersectionObserver(
|
|
189
|
+
(entries) => {
|
|
190
|
+
for (const entry of entries) {
|
|
191
|
+
if (!entry.isIntersecting) continue;
|
|
192
|
+
if (entry.target === top) extendBackward();
|
|
193
|
+
else if (entry.target === bottom) extendForward();
|
|
194
|
+
}
|
|
195
|
+
},
|
|
196
|
+
{ root, rootMargin: '200px 0px' }
|
|
197
|
+
);
|
|
198
|
+
io.observe(top);
|
|
199
|
+
io.observe(bottom);
|
|
200
|
+
return () => io.disconnect();
|
|
201
|
+
});
|
|
202
|
+
|
|
129
203
|
// Auto-scroll to the selected (or current) month when the panel opens.
|
|
130
204
|
$effect(() => {
|
|
131
205
|
if (!open || !listEl) return;
|
|
@@ -133,7 +207,11 @@
|
|
|
133
207
|
const target =
|
|
134
208
|
listEl?.querySelector('[data-selected="true"]') ??
|
|
135
209
|
listEl?.querySelector('[data-current="true"]');
|
|
136
|
-
|
|
210
|
+
// jsdom doesn't implement scrollIntoView — guard so unit tests
|
|
211
|
+
// don't surface unhandled errors.
|
|
212
|
+
if (target && typeof (target as HTMLElement).scrollIntoView === 'function') {
|
|
213
|
+
(target as HTMLElement).scrollIntoView({ block: 'center' });
|
|
214
|
+
}
|
|
137
215
|
});
|
|
138
216
|
});
|
|
139
217
|
</script>
|
|
@@ -243,6 +321,7 @@
|
|
|
243
321
|
bind:this={listEl}
|
|
244
322
|
class="max-h-72 overflow-y-auto [scrollbar-width:none] sm:max-h-72 [&::-webkit-scrollbar]:hidden"
|
|
245
323
|
>
|
|
324
|
+
<div bind:this={topSentinelEl} aria-hidden="true" class="h-px"></div>
|
|
246
325
|
{#each years as year (year)}
|
|
247
326
|
{@const yearSelected = value?.getFullYear() === year}
|
|
248
327
|
<div
|
|
@@ -257,17 +336,16 @@
|
|
|
257
336
|
</div>
|
|
258
337
|
<div class="grid grid-cols-4 gap-0.5 p-1.5">
|
|
259
338
|
{#each MONTH_SHORT as monthLabel, m (m)}
|
|
260
|
-
{@const
|
|
261
|
-
{@const
|
|
262
|
-
{@const
|
|
263
|
-
{@const dis = entry?.disabled ?? true}
|
|
339
|
+
{@const sel = isSelected(year, m)}
|
|
340
|
+
{@const cur = isCurrent(year, m)}
|
|
341
|
+
{@const dis = isMonthDisabled(year, m)}
|
|
264
342
|
<button
|
|
265
343
|
type="button"
|
|
266
344
|
data-month={m}
|
|
267
345
|
data-testid={buildTestId('month-picker', 'month', testId, `${year}-${m}`)}
|
|
268
346
|
data-selected={sel || undefined}
|
|
269
347
|
data-current={cur || undefined}
|
|
270
|
-
onclick={() =>
|
|
348
|
+
onclick={() => pick(year, m)}
|
|
271
349
|
disabled={dis}
|
|
272
350
|
class={cn(
|
|
273
351
|
'cursor-pointer rounded px-1 py-2 text-xs font-medium transition-colors sm:py-1',
|
|
@@ -284,6 +362,7 @@
|
|
|
284
362
|
{/each}
|
|
285
363
|
</div>
|
|
286
364
|
{/each}
|
|
365
|
+
<div bind:this={bottomSentinelEl} aria-hidden="true" class="h-px"></div>
|
|
287
366
|
</div>
|
|
288
367
|
|
|
289
368
|
<div class="border-default-200 flex items-center justify-between border-t px-3 py-2">
|
package/dist/index.d.ts
CHANGED
|
@@ -46,6 +46,7 @@ export type { ProgressSegment, ProgressProps } from './elements/progress/progres
|
|
|
46
46
|
export type { SpinnerProps } from './elements/spinner/spinner-types.js';
|
|
47
47
|
export type { SkeletonProps, SkeletonVariant } from './elements/skeleton/skeleton-types.js';
|
|
48
48
|
export type { EmptyStateProps, EmptyStateSize } from './elements/empty-state/empty-state-types.js';
|
|
49
|
+
export type { PdfViewerProps } from './elements/pdf-viewer/pdf-viewer-types.js';
|
|
49
50
|
export type { TooltipProps, TooltipPlacement, TooltipSize, TooltipVariant } from './elements/tooltip/tooltip-types.js';
|
|
50
51
|
export type { PopoverProps, PopoverPlacement, PopoverTrigger } from './elements/popover/popover-types.js';
|
|
51
52
|
export type { CalendarProps, CalendarMode } from './forms/calendar/calendar-types.js';
|
|
@@ -99,6 +100,7 @@ export { default as Skeleton } from './elements/skeleton/Skeleton.svelte';
|
|
|
99
100
|
export { spinner as spinnerVariants } from './elements/spinner/spinner.js';
|
|
100
101
|
export { default as EmptyState } from './elements/empty-state/EmptyState.svelte';
|
|
101
102
|
export { emptyState as emptyStateVariants } from './elements/empty-state/empty-state.js';
|
|
103
|
+
export { default as PdfViewer } from './elements/pdf-viewer/PdfViewer.svelte';
|
|
102
104
|
export { default as Tooltip } from './elements/tooltip/Tooltip.svelte';
|
|
103
105
|
export { default as Popover } from './elements/popover/Popover.svelte';
|
|
104
106
|
export { default as ComboBox } from './elements/combobox/ComboBox.svelte';
|
package/dist/index.js
CHANGED
|
@@ -76,6 +76,8 @@ export { spinner as spinnerVariants } from './elements/spinner/spinner.js';
|
|
|
76
76
|
// Elements - EmptyState
|
|
77
77
|
export { default as EmptyState } from './elements/empty-state/EmptyState.svelte';
|
|
78
78
|
export { emptyState as emptyStateVariants } from './elements/empty-state/empty-state.js';
|
|
79
|
+
// Elements - PdfViewer
|
|
80
|
+
export { default as PdfViewer } from './elements/pdf-viewer/PdfViewer.svelte';
|
|
79
81
|
// Elements - Tooltip
|
|
80
82
|
export { default as Tooltip } from './elements/tooltip/Tooltip.svelte';
|
|
81
83
|
// Elements - Popover
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@makolabs/ripple",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.5.0",
|
|
4
4
|
"description": "Simple Svelte 5 powered component library ✨",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"repository": {
|
|
@@ -55,8 +55,14 @@
|
|
|
55
55
|
},
|
|
56
56
|
"peerDependencies": {
|
|
57
57
|
"@sveltejs/kit": "^2.0.0",
|
|
58
|
+
"pdfjs-dist": "^4.0.0 || ^5.0.0",
|
|
58
59
|
"svelte": "^5.0.0 || ^6.0.0"
|
|
59
60
|
},
|
|
61
|
+
"peerDependenciesMeta": {
|
|
62
|
+
"pdfjs-dist": {
|
|
63
|
+
"optional": true
|
|
64
|
+
}
|
|
65
|
+
},
|
|
60
66
|
"devDependencies": {
|
|
61
67
|
"@eslint/compat": "^1.4.1",
|
|
62
68
|
"@eslint/js": "^9.39.1",
|
|
@@ -81,6 +87,7 @@
|
|
|
81
87
|
"husky": "^9.1.7",
|
|
82
88
|
"jsdom": "^27.2.0",
|
|
83
89
|
"lint-staged": "^16.2.6",
|
|
90
|
+
"pdfjs-dist": "^5.4.394",
|
|
84
91
|
"prettier": "^3.4.2",
|
|
85
92
|
"prettier-plugin-svelte": "^3.3.3",
|
|
86
93
|
"prettier-plugin-tailwindcss": "^0.7.1",
|