windowpp 0.1.2 → 0.1.3

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.
@@ -0,0 +1,267 @@
1
+ import { Show, Index, createSignal, createMemo, createEffect, onMount, onCleanup, splitProps, JSX } from 'solid-js';
2
+
3
+ // ============================================================================
4
+ // InfiniteScrollList.tsx - Virtualized infinite scroll component for Solid
5
+ // ============================================================================
6
+
7
+ export interface InfiniteScrollListProps<T> {
8
+ // Data source
9
+ items: () => readonly T[];
10
+ // Has more items to load?
11
+ hasMore: () => boolean;
12
+ // Load more callback
13
+ onLoadMore: () => void | Promise<void>;
14
+ // Currently loading?
15
+ isLoading: () => boolean;
16
+ // Item height estimate (pixels) for virtualization
17
+ itemHeight?: number;
18
+ // Container height (pixels)
19
+ height?: string | number;
20
+ // Item renderer
21
+ children: (item: T, index: () => number) => JSX.Element;
22
+ // Loading indicator
23
+ loadingFallback?: JSX.Element;
24
+ // Empty state
25
+ emptyFallback?: JSX.Element;
26
+ // End of list message
27
+ endOfListMessage?: string;
28
+ // Overscan (extra items to render above/below viewport)
29
+ overscan?: number;
30
+ // Visible range callback for external lazy work like metadata fetches
31
+ onVisibleRangeChange?: (range: { start: number; end: number; items: readonly T[] }) => void;
32
+ // Class name for the container
33
+ class?: string;
34
+ }
35
+
36
+ export function InfiniteScrollList<T>(props: InfiniteScrollListProps<T>) {
37
+ const [local, others] = splitProps(props, [
38
+ 'items',
39
+ 'hasMore',
40
+ 'onLoadMore',
41
+ 'isLoading',
42
+ 'itemHeight',
43
+ 'height',
44
+ 'children',
45
+ 'loadingFallback',
46
+ 'emptyFallback',
47
+ 'endOfListMessage',
48
+ 'overscan',
49
+ 'onVisibleRangeChange',
50
+ 'class',
51
+ ]);
52
+
53
+ const [scrollPosition, setScrollPosition] = createSignal(0);
54
+ const [containerHeight, setContainerHeight] = createSignal(0);
55
+ const [viewportHeight, setViewportHeight] = createSignal(0);
56
+
57
+ let containerRef: HTMLDivElement | undefined;
58
+ let sentinelRef: HTMLDivElement | undefined;
59
+
60
+ const itemHeight = () => local.itemHeight ?? 50;
61
+ const overscan = () => local.overscan ?? 5;
62
+
63
+ // Calculate visible range
64
+ const visibleRange = createMemo(() => {
65
+ const scrollTop = scrollPosition();
66
+ const vpHeight = viewportHeight();
67
+ const height = itemHeight();
68
+ const os = overscan();
69
+
70
+ const start = Math.max(0, Math.floor(scrollTop / height) - os);
71
+ const end = Math.min(
72
+ local.items().length,
73
+ Math.ceil((scrollTop + vpHeight) / height) + os
74
+ );
75
+
76
+ return { start, end };
77
+ });
78
+
79
+ // Intersection Observer for infinite scroll trigger
80
+ onMount(() => {
81
+ const observer = new IntersectionObserver(
82
+ (entries) => {
83
+ if (entries[0]?.isIntersecting) {
84
+ if (local.hasMore() && !local.isLoading()) {
85
+ local.onLoadMore();
86
+ }
87
+ }
88
+ },
89
+ { root: containerRef, rootMargin: '100px', threshold: 0.1 }
90
+ );
91
+
92
+ createEffect(() => {
93
+ const sentinel = sentinelRef;
94
+ if (!sentinel) {
95
+ return;
96
+ }
97
+ observer.disconnect();
98
+ observer.observe(sentinel);
99
+ });
100
+
101
+ onCleanup(() => observer.disconnect());
102
+ });
103
+
104
+ const handleScroll = () => {
105
+ if (containerRef) {
106
+ setScrollPosition(containerRef.scrollTop);
107
+ }
108
+ };
109
+
110
+ const updateDimensions = () => {
111
+ if (containerRef) {
112
+ setViewportHeight(containerRef.clientHeight);
113
+ const totalHeight = local.items().length * itemHeight();
114
+ setContainerHeight(totalHeight);
115
+ }
116
+ };
117
+
118
+ onMount(() => {
119
+ updateDimensions();
120
+ window.addEventListener('resize', updateDimensions);
121
+ onCleanup(() => window.removeEventListener('resize', updateDimensions));
122
+ });
123
+
124
+ // Re-calculate when items change
125
+ createEffect(() => {
126
+ local.items();
127
+ updateDimensions();
128
+ });
129
+
130
+ const visibleItems = createMemo(() => {
131
+ const range = visibleRange();
132
+ return local.items().slice(range.start, range.end);
133
+ });
134
+ const offsetY = createMemo(() => visibleRange().start * itemHeight());
135
+ const bottomSpacerHeight = createMemo(() => {
136
+ const remaining = local.items().length - visibleRange().end;
137
+ return Math.max(0, remaining * itemHeight());
138
+ });
139
+
140
+ createEffect(() => {
141
+ local.onVisibleRangeChange?.({
142
+ start: visibleRange().start,
143
+ end: visibleRange().end,
144
+ items: visibleItems(),
145
+ });
146
+ });
147
+
148
+ const mergedClass = () =>
149
+ `overflow-y-auto ${local.class ?? ''}`;
150
+
151
+ const mergedHeight = () => {
152
+ if (local.height === undefined) return '100%';
153
+ return typeof local.height === 'number' ? `${local.height}px` : local.height;
154
+ };
155
+
156
+ return (
157
+ <div
158
+ ref={containerRef}
159
+ class={mergedClass()}
160
+ style={{ height: mergedHeight() }}
161
+ onScroll={handleScroll}
162
+ {...others}
163
+ >
164
+ <Show
165
+ when={local.items().length > 0}
166
+ fallback={
167
+ local.emptyFallback ?? (
168
+ <div class="flex items-center justify-center p-8 text-slate-500">
169
+ No items to display
170
+ </div>
171
+ )
172
+ }
173
+ >
174
+ {/* Spacer for virtualization */}
175
+ <div style={{ height: `${offsetY()}px`, flex: 'none' }} />
176
+
177
+ {/* Visible items */}
178
+ <Index each={visibleItems()}>
179
+ {(item, i) => local.children(item(), () => visibleRange().start + i())}
180
+ </Index>
181
+
182
+ <div style={{ height: `${bottomSpacerHeight()}px`, flex: 'none' }} />
183
+
184
+ {/* Sentinel element for intersection observer */}
185
+ <Show when={local.hasMore() || local.isLoading()}>
186
+ <div
187
+ ref={sentinelRef}
188
+ class="flex items-center justify-center p-4"
189
+ style={{ minHeight: '60px' }}
190
+ >
191
+ <Show
192
+ when={local.isLoading()}
193
+ fallback={
194
+ <span class="text-sm text-slate-400">
195
+ {local.endOfListMessage ?? 'End of list'}
196
+ </span>
197
+ }
198
+ >
199
+ {local.loadingFallback ?? (
200
+ <div class="flex items-center gap-2 text-sm text-slate-500">
201
+ <svg class="animate-spin h-4 w-4" viewBox="0 0 24 24">
202
+ <circle
203
+ class="opacity-25"
204
+ cx="12"
205
+ cy="12"
206
+ r="10"
207
+ stroke="currentColor"
208
+ stroke-width="4"
209
+ fill="none"
210
+ />
211
+ <path
212
+ class="opacity-75"
213
+ fill="currentColor"
214
+ d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
215
+ />
216
+ </svg>
217
+ <span>Loading more...</span>
218
+ </div>
219
+ )}
220
+ </Show>
221
+ </div>
222
+ </Show>
223
+ </Show>
224
+ </div>
225
+ );
226
+ }
227
+
228
+ // ============================================================================
229
+ // SimpleList - Non-virtualized version for smaller datasets
230
+ // ============================================================================
231
+
232
+ export interface SimpleListProps<T> {
233
+ items: () => readonly T[];
234
+ children: (item: T, index: () => number) => JSX.Element;
235
+ emptyFallback?: JSX.Element;
236
+ class?: string;
237
+ }
238
+
239
+ export function SimpleList<T>(props: SimpleListProps<T>) {
240
+ const [local, others] = splitProps(props, [
241
+ 'items',
242
+ 'children',
243
+ 'emptyFallback',
244
+ 'class',
245
+ ]);
246
+
247
+ const mergedClass = () => `space-y-2 ${local.class ?? ''}`;
248
+
249
+ return (
250
+ <div class={mergedClass()} {...others}>
251
+ <Show
252
+ when={local.items().length > 0}
253
+ fallback={
254
+ local.emptyFallback ?? (
255
+ <div class="rounded-3xl border border-dashed border-slate-300 bg-slate-50 px-5 py-10 text-center text-sm text-slate-500">
256
+ No items to display
257
+ </div>
258
+ )
259
+ }
260
+ >
261
+ <Index each={local.items()}>
262
+ {(item, i) => local.children(item(), i)}
263
+ </Index>
264
+ </Show>
265
+ </div>
266
+ );
267
+ }
@@ -0,0 +1,13 @@
1
+ export { InfiniteScrollList, SimpleList } from './InfiniteScrollList';
2
+ export { ClipboardToast } from './ClipboardToast';
3
+ export {
4
+ FileSearch,
5
+ FileSearchButton,
6
+ PresetsPanel,
7
+ type SearchPreset,
8
+ type SearchMode,
9
+ type FileSearchResult,
10
+ type FileSearchProps,
11
+ type FileSearchButtonProps,
12
+ type PresetsPanelProps,
13
+ } from './FileSearch';
@@ -0,0 +1,421 @@
1
+ /**
2
+ * FileDrop Styles - Drop zone styling for drag & drop functionality
3
+ *
4
+ * Usage:
5
+ * <div class="filedrop-zone">
6
+ * Drop files here
7
+ * </div>
8
+ *
9
+ * States automatically applied via JavaScript:
10
+ * - .wpp-dropzone-active: When drag enters the zone
11
+ * - .wpp-dropzone-disabled: When file drop is disabled
12
+ */
13
+
14
+ /* ============================================================================
15
+ * BASE DROP ZONE STYLES
16
+ * ============================================================================ */
17
+
18
+ .wpp-dropzone {
19
+ position: relative;
20
+ display: flex;
21
+ flex-direction: column;
22
+ align-items: center;
23
+ justify-content: center;
24
+ min-height: 200px;
25
+ padding: 2rem;
26
+ border: 2px dashed #cbd5e1;
27
+ border-radius: 0.75rem;
28
+ background-color: #f8fafc;
29
+ transition: all 0.2s ease-in-out;
30
+ cursor: pointer;
31
+ user-select: none;
32
+ }
33
+
34
+ .wpp-dropzone:hover {
35
+ border-color: #94a3b8;
36
+ background-color: #f1f5f9;
37
+ }
38
+
39
+ /* ============================================================================
40
+ * ACTIVE STATE (Drag Over)
41
+ * ============================================================================ */
42
+
43
+ .wpp-dropzone.wpp-dropzone-active {
44
+ border-color: #3b82f6;
45
+ background-color: #eff6ff;
46
+ border-width: 3px;
47
+ transform: scale(1.02);
48
+ box-shadow: 0 0 0 4px rgba(59, 130, 246, 0.1);
49
+ }
50
+
51
+ .wpp-dropzone.wpp-dropzone-active::before {
52
+ content: '';
53
+ position: absolute;
54
+ inset: 0;
55
+ border-radius: 0.5rem;
56
+ background: linear-gradient(135deg, rgba(59, 130, 246, 0.05), rgba(147, 51, 234, 0.05));
57
+ pointer-events: none;
58
+ }
59
+
60
+ /* ============================================================================
61
+ * DISABLED STATE
62
+ * ============================================================================ */
63
+
64
+ .wpp-dropzone.wpp-dropzone-disabled {
65
+ opacity: 0.5;
66
+ cursor: not-allowed;
67
+ border-color: #e2e8f0;
68
+ background-color: #f8fafc;
69
+ }
70
+
71
+ .wpp-dropzone.wpp-dropzone-disabled:hover {
72
+ border-color: #e2e8f0;
73
+ background-color: #f8fafc;
74
+ transform: none;
75
+ }
76
+
77
+ /* ============================================================================
78
+ * DROP ZONE CONTENT
79
+ * ============================================================================ */
80
+
81
+ .wpp-dropzone-content {
82
+ display: flex;
83
+ flex-direction: column;
84
+ align-items: center;
85
+ gap: 1rem;
86
+ text-align: center;
87
+ pointer-events: none;
88
+ }
89
+
90
+ .wpp-dropzone-icon {
91
+ width: 3rem;
92
+ height: 3rem;
93
+ color: #94a3b8;
94
+ transition: all 0.2s ease-in-out;
95
+ }
96
+
97
+ .wpp-dropzone.wpp-dropzone-active .wpp-dropzone-icon {
98
+ color: #3b82f6;
99
+ transform: scale(1.2);
100
+ }
101
+
102
+ .wpp-dropzone.wpp-dropzone-disabled .wpp-dropzone-icon {
103
+ color: #cbd5e1;
104
+ }
105
+
106
+ .wpp-dropzone-text {
107
+ font-size: 1rem;
108
+ font-weight: 500;
109
+ color: #475569;
110
+ transition: color 0.2s ease-in-out;
111
+ }
112
+
113
+ .wpp-dropzone.wpp-dropzone-active .wpp-dropzone-text {
114
+ color: #3b82f6;
115
+ font-weight: 600;
116
+ }
117
+
118
+ .wpp-dropzone.wpp-dropzone-disabled .wpp-dropzone-text {
119
+ color: #cbd5e1;
120
+ }
121
+
122
+ .wpp-dropzone-hint {
123
+ font-size: 0.875rem;
124
+ color: #94a3b8;
125
+ transition: color 0.2s ease-in-out;
126
+ }
127
+
128
+ .wpp-dropzone.wpp-dropzone-active .wpp-dropzone-hint {
129
+ color: #60a5fa;
130
+ }
131
+
132
+ .wpp-dropzone.wpp-dropzone-disabled .wpp-dropzone-hint {
133
+ color: #cbd5e1;
134
+ }
135
+
136
+ /* ============================================================================
137
+ * COMPACT VARIANT
138
+ * ============================================================================ */
139
+
140
+ .wpp-dropzone.wpp-dropzone-compact {
141
+ min-height: 120px;
142
+ padding: 1.5rem;
143
+ }
144
+
145
+ .wpp-dropzone.wpp-dropzone-compact .wpp-dropzone-icon {
146
+ width: 2rem;
147
+ height: 2rem;
148
+ }
149
+
150
+ .wpp-dropzone.wpp-dropzone-compact .wpp-dropzone-text {
151
+ font-size: 0.875rem;
152
+ }
153
+
154
+ .wpp-dropzone.wpp-dropzone-compact .wpp-dropzone-hint {
155
+ font-size: 0.75rem;
156
+ }
157
+
158
+ /* ============================================================================
159
+ * INLINE VARIANT (Full Width)
160
+ * ============================================================================ */
161
+
162
+ .wpp-dropzone.wpp-dropzone-inline {
163
+ min-height: 80px;
164
+ padding: 1rem;
165
+ flex-direction: row;
166
+ justify-content: flex-start;
167
+ gap: 1rem;
168
+ }
169
+
170
+ .wpp-dropzone.wpp-dropzone-inline .wpp-dropzone-content {
171
+ flex-direction: row;
172
+ align-items: center;
173
+ text-align: left;
174
+ gap: 0.75rem;
175
+ }
176
+
177
+ .wpp-dropzone.wpp-dropzone-inline .wpp-dropzone-icon {
178
+ width: 2rem;
179
+ height: 2rem;
180
+ }
181
+
182
+ /* ============================================================================
183
+ * FILE LIST DISPLAY
184
+ * ============================================================================ */
185
+
186
+ .wpp-dropzone-files {
187
+ margin-top: 1rem;
188
+ width: 100%;
189
+ }
190
+
191
+ .wpp-dropzone-file-item {
192
+ display: flex;
193
+ align-items: center;
194
+ gap: 0.75rem;
195
+ padding: 0.75rem;
196
+ background-color: white;
197
+ border: 1px solid #e2e8f0;
198
+ border-radius: 0.5rem;
199
+ margin-bottom: 0.5rem;
200
+ transition: all 0.2s ease-in-out;
201
+ }
202
+
203
+ .wpp-dropzone-file-item:hover {
204
+ border-color: #cbd5e1;
205
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
206
+ }
207
+
208
+ .wpp-dropzone-file-icon {
209
+ width: 2rem;
210
+ height: 2rem;
211
+ flex-shrink: 0;
212
+ display: flex;
213
+ align-items: center;
214
+ justify-content: center;
215
+ background-color: #f1f5f9;
216
+ border-radius: 0.375rem;
217
+ color: #64748b;
218
+ }
219
+
220
+ .wpp-dropzone-file-info {
221
+ flex: 1;
222
+ min-width: 0;
223
+ }
224
+
225
+ .wpp-dropzone-file-name {
226
+ font-size: 0.875rem;
227
+ font-weight: 500;
228
+ color: #334155;
229
+ white-space: nowrap;
230
+ overflow: hidden;
231
+ text-overflow: ellipsis;
232
+ }
233
+
234
+ .wpp-dropzone-file-meta {
235
+ font-size: 0.75rem;
236
+ color: #94a3b8;
237
+ margin-top: 0.125rem;
238
+ }
239
+
240
+ .wpp-dropzone-file-remove {
241
+ flex-shrink: 0;
242
+ width: 1.5rem;
243
+ height: 1.5rem;
244
+ display: flex;
245
+ align-items: center;
246
+ justify-content: center;
247
+ border-radius: 0.25rem;
248
+ color: #94a3b8;
249
+ cursor: pointer;
250
+ transition: all 0.2s ease-in-out;
251
+ pointer-events: auto;
252
+ }
253
+
254
+ .wpp-dropzone-file-remove:hover {
255
+ background-color: #fee2e2;
256
+ color: #ef4444;
257
+ }
258
+
259
+ /* ============================================================================
260
+ * DARK MODE SUPPORT
261
+ * ============================================================================ */
262
+
263
+ @media (prefers-color-scheme: dark) {
264
+ .wpp-dropzone {
265
+ border-color: #475569;
266
+ background-color: #1e293b;
267
+ }
268
+
269
+ .wpp-dropzone:hover {
270
+ border-color: #64748b;
271
+ background-color: #334155;
272
+ }
273
+
274
+ .wpp-dropzone.wpp-dropzone-active {
275
+ border-color: #60a5fa;
276
+ background-color: #1e3a8a;
277
+ box-shadow: 0 0 0 4px rgba(96, 165, 250, 0.1);
278
+ }
279
+
280
+ .wpp-dropzone.wpp-dropzone-active::before {
281
+ background: linear-gradient(135deg, rgba(96, 165, 250, 0.1), rgba(168, 85, 247, 0.1));
282
+ }
283
+
284
+ .wpp-dropzone.wpp-dropzone-disabled {
285
+ border-color: #334155;
286
+ background-color: #1e293b;
287
+ }
288
+
289
+ .wpp-dropzone-icon {
290
+ color: #64748b;
291
+ }
292
+
293
+ .wpp-dropzone.wpp-dropzone-active .wpp-dropzone-icon {
294
+ color: #60a5fa;
295
+ }
296
+
297
+ .wpp-dropzone.wpp-dropzone-disabled .wpp-dropzone-icon {
298
+ color: #475569;
299
+ }
300
+
301
+ .wpp-dropzone-text {
302
+ color: #cbd5e1;
303
+ }
304
+
305
+ .wpp-dropzone.wpp-dropzone-active .wpp-dropzone-text {
306
+ color: #93c5fd;
307
+ }
308
+
309
+ .wpp-dropzone.wpp-dropzone-disabled .wpp-dropzone-text {
310
+ color: #64748b;
311
+ }
312
+
313
+ .wpp-dropzone-hint {
314
+ color: #64748b;
315
+ }
316
+
317
+ .wpp-dropzone.wpp-dropzone-active .wpp-dropzone-hint {
318
+ color: #93c5fd;
319
+ }
320
+
321
+ .wpp-dropzone-file-item {
322
+ background-color: #334155;
323
+ border-color: #475569;
324
+ }
325
+
326
+ .wpp-dropzone-file-item:hover {
327
+ border-color: #64748b;
328
+ }
329
+
330
+ .wpp-dropzone-file-icon {
331
+ background-color: #1e293b;
332
+ color: #94a3b8;
333
+ }
334
+
335
+ .wpp-dropzone-file-name {
336
+ color: #e2e8f0;
337
+ }
338
+
339
+ .wpp-dropzone-file-meta {
340
+ color: #64748b;
341
+ }
342
+
343
+ .wpp-dropzone-file-remove:hover {
344
+ background-color: #7f1d1d;
345
+ color: #fca5a5;
346
+ }
347
+ }
348
+
349
+ /* ============================================================================
350
+ * ANIMATION UTILITIES
351
+ * ============================================================================ */
352
+
353
+ @keyframes filedrop-pulse {
354
+ 0%, 100% {
355
+ opacity: 1;
356
+ }
357
+ 50% {
358
+ opacity: 0.7;
359
+ }
360
+ }
361
+
362
+ .wpp-dropzone.wpp-dropzone-processing {
363
+ animation: filedrop-pulse 1.5s ease-in-out infinite;
364
+ pointer-events: none;
365
+ }
366
+
367
+ @keyframes filedrop-shake {
368
+ 0%, 100% {
369
+ transform: translateX(0);
370
+ }
371
+ 25% {
372
+ transform: translateX(-4px);
373
+ }
374
+ 75% {
375
+ transform: translateX(4px);
376
+ }
377
+ }
378
+
379
+ .wpp-dropzone.wpp-dropzone-error {
380
+ border-color: #ef4444;
381
+ background-color: #fee2e2;
382
+ animation: filedrop-shake 0.3s ease-in-out;
383
+ }
384
+
385
+ @media (prefers-color-scheme: dark) {
386
+ .wpp-dropzone.wpp-dropzone-error {
387
+ border-color: #f87171;
388
+ background-color: #7f1d1d;
389
+ }
390
+ }
391
+
392
+ /* ============================================================================
393
+ * CUSTOM VARIANTS
394
+ * ============================================================================ */
395
+
396
+ /* Minimal variant - subtle styling */
397
+ .wpp-dropzone.wpp-dropzone-minimal {
398
+ border-style: solid;
399
+ border-width: 1px;
400
+ background-color: transparent;
401
+ }
402
+
403
+ .wpp-dropzone.wpp-dropzone-minimal:hover {
404
+ background-color: rgba(0, 0, 0, 0.02);
405
+ }
406
+
407
+ @media (prefers-color-scheme: dark) {
408
+ .wpp-dropzone.wpp-dropzone-minimal:hover {
409
+ background-color: rgba(255, 255, 255, 0.02);
410
+ }
411
+ }
412
+
413
+ /* Bold variant - prominent styling */
414
+ .wpp-dropzone.wpp-dropzone-bold {
415
+ border-width: 3px;
416
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
417
+ }
418
+
419
+ .wpp-dropzone.wpp-dropzone-bold.wpp-dropzone-active {
420
+ box-shadow: 0 8px 16px rgba(59, 130, 246, 0.2);
421
+ }
@@ -0,0 +1 @@
1
+ @import 'tailwindcss';