@x33025/sveltely 0.0.32 → 0.0.34

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.
@@ -23,6 +23,12 @@
23
23
  let editingTag = $state<string | null>(null);
24
24
  let editingValue = $state('');
25
25
  let editingEl = $state<HTMLInputElement | null>(null);
26
+ let rootEl = $state<HTMLElement | null>(null);
27
+ let dragStartIndex = $state<number | null>(null);
28
+ let dragHoverIndex = $state<number | null>(null);
29
+ let isPointerSelecting = $state(false);
30
+ let rangeSelectionMoved = $state(false);
31
+ let suppressNextClick = $state(false);
26
32
  const selectionEnabled = $derived(selection !== undefined);
27
33
 
28
34
  const addTag = (rawValue: string) => {
@@ -42,6 +48,53 @@
42
48
  selection = [...selection, tag];
43
49
  };
44
50
 
51
+ const applySelectionRange = (start: number, end: number) => {
52
+ if (!selection) return;
53
+ const from = Math.min(start, end);
54
+ const to = Math.max(start, end);
55
+ selection = tags.slice(from, to + 1);
56
+ };
57
+
58
+ const beginRangeSelection = (event: PointerEvent, index: number) => {
59
+ if (!selectionEnabled || event.button !== 0) return;
60
+ dragStartIndex = index;
61
+ dragHoverIndex = index;
62
+ isPointerSelecting = true;
63
+ rangeSelectionMoved = false;
64
+ };
65
+
66
+ const extendRangeSelection = (index: number) => {
67
+ if (!selectionEnabled || !isPointerSelecting || dragStartIndex === null) return;
68
+ dragHoverIndex = index;
69
+ if (dragStartIndex === index && !rangeSelectionMoved) return;
70
+ rangeSelectionMoved = true;
71
+ applySelectionRange(dragStartIndex, index);
72
+ };
73
+
74
+ const endRangeSelection = () => {
75
+ if (!isPointerSelecting) return;
76
+ suppressNextClick = rangeSelectionMoved;
77
+ isPointerSelecting = false;
78
+ rangeSelectionMoved = false;
79
+ dragStartIndex = null;
80
+ dragHoverIndex = null;
81
+ };
82
+
83
+ const onChipClick = (tag: string) => {
84
+ if (suppressNextClick) {
85
+ suppressNextClick = false;
86
+ return;
87
+ }
88
+ toggleSelected(tag);
89
+ };
90
+
91
+ const handleWindowPointerDown = (event: PointerEvent) => {
92
+ if (!selectionEnabled || !selection || !rootEl) return;
93
+ const target = event.target as Node | null;
94
+ if (target && rootEl.contains(target)) return;
95
+ selection = [];
96
+ };
97
+
45
98
  const onKeydown = (event: KeyboardEvent) => {
46
99
  if ((event.key === 'Enter' || event.key === ',') && inputValue.trim()) {
47
100
  event.preventDefault();
@@ -157,8 +210,14 @@
157
210
  });
158
211
  </script>
159
212
 
160
- <div class="chip-row flex w-full flex-wrap items-center">
161
- {#each tags as tag (tag)}
213
+ <svelte:window
214
+ onpointerup={endRangeSelection}
215
+ onpointercancel={endRangeSelection}
216
+ onpointerdown={handleWindowPointerDown}
217
+ />
218
+
219
+ <div bind:this={rootEl} class="chip-row flex w-full flex-wrap items-center">
220
+ {#each tags as tag, index (tag)}
162
221
  {#if editingTag === tag}
163
222
  <input
164
223
  bind:this={editingEl}
@@ -173,7 +232,10 @@
173
232
  type="button"
174
233
  class="chip-surface inline-flex items-center gap-2"
175
234
  class:chip-selected={selection?.includes(tag)}
176
- onclick={() => toggleSelected(tag)}
235
+ class:chip-hovered={isPointerSelecting && dragHoverIndex === index}
236
+ onpointerdown={(event) => beginRangeSelection(event, index)}
237
+ onpointerenter={() => extendRangeSelection(index)}
238
+ onclick={() => onChipClick(tag)}
177
239
  ondblclick={() => startEditing(tag)}
178
240
  aria-pressed={selection?.includes(tag)}
179
241
  >
@@ -237,6 +299,10 @@
237
299
  background: var(--chip-input-hover);
238
300
  }
239
301
 
302
+ .chip-hovered {
303
+ background: var(--chip-input-hover);
304
+ }
305
+
240
306
  .chip-input-field:hover {
241
307
  background: var(--chip-input-background);
242
308
  }
@@ -0,0 +1,93 @@
1
+ <script lang="ts">
2
+ import { goto } from '$app/navigation';
3
+
4
+ type PaginationData = {
5
+ view: string;
6
+ page: number;
7
+ totalPages: number;
8
+ hasNext?: boolean;
9
+ };
10
+
11
+ let {
12
+ data,
13
+ onPageChange,
14
+ class: className = ''
15
+ }: {
16
+ data: PaginationData;
17
+ onPageChange?: (page: number, href: string) => void | Promise<void>;
18
+ class?: string;
19
+ } = $props();
20
+
21
+ const clampedPage = (targetPage: number) => Math.min(Math.max(targetPage, 1), maxPage());
22
+
23
+ const toQuery = (targetPage: number) => {
24
+ const params = new URLSearchParams({
25
+ view: data.view,
26
+ page: String(clampedPage(targetPage))
27
+ });
28
+ return `?${params.toString()}`;
29
+ };
30
+
31
+ const maxPage = () => Math.max(1, data.totalPages);
32
+ const safePage = () => Math.min(Math.max(data.page, 1), maxPage());
33
+ const hasNext = () => (typeof data.hasNext === 'boolean' ? data.hasNext : safePage() < maxPage());
34
+
35
+ const goToPage = (targetPage: number) => {
36
+ const clamped = clampedPage(targetPage);
37
+ const href = toQuery(clamped);
38
+ if (onPageChange) {
39
+ void onPageChange(clamped, href);
40
+ return;
41
+ }
42
+ void goto(href);
43
+ };
44
+
45
+ const handleSubmit = (event: SubmitEvent) => {
46
+ event.preventDefault();
47
+ const form = event.currentTarget as HTMLFormElement;
48
+ const input = form.elements.namedItem('page') as HTMLInputElement | null;
49
+ if (!input) return;
50
+
51
+ const parsed = Number.parseInt(input.value, 10);
52
+ const fallback = safePage();
53
+ const next = Number.isFinite(parsed) ? parsed : fallback;
54
+ const clamped = Math.min(Math.max(next, 1), maxPage());
55
+ input.value = String(clamped);
56
+ goToPage(clamped);
57
+ };
58
+ </script>
59
+
60
+ <div class={`hstack items-center ${className}`} style="gap: var(--pagination-gap);">
61
+ {#if safePage() > 1}
62
+ <button
63
+ type="button"
64
+ class="pagination-button action padding-sm"
65
+ onclick={() => goToPage(safePage() - 1)}
66
+ >
67
+ Previous
68
+ </button>
69
+ {:else}
70
+ <span class="pagination-button action padding-sm bg-zinc-400">Previous</span>
71
+ {/if}
72
+
73
+ <span>Page</span>
74
+
75
+ <form method="GET" style="display: inline;" novalidate onsubmit={handleSubmit}>
76
+ <input type="hidden" name="view" value={data.view} />
77
+ <input id="page" name="page" type="number" class="pagination-input" value={safePage()} />
78
+ </form>
79
+
80
+ <span>of {maxPage()}</span>
81
+
82
+ {#if hasNext()}
83
+ <button
84
+ type="button"
85
+ class="pagination-button action padding-sm"
86
+ onclick={() => goToPage(safePage() + 1)}
87
+ >
88
+ Next
89
+ </button>
90
+ {:else}
91
+ <span class="pagination-button action padding-sm bg-zinc-400">Next</span>
92
+ {/if}
93
+ </div>
@@ -0,0 +1,14 @@
1
+ type PaginationData = {
2
+ view: string;
3
+ page: number;
4
+ totalPages: number;
5
+ hasNext?: boolean;
6
+ };
7
+ type $$ComponentProps = {
8
+ data: PaginationData;
9
+ onPageChange?: (page: number, href: string) => void | Promise<void>;
10
+ class?: string;
11
+ };
12
+ declare const Pagination: import("svelte").Component<$$ComponentProps, {}, "">;
13
+ type Pagination = ReturnType<typeof Pagination>;
14
+ export default Pagination;
package/dist/index.d.ts CHANGED
@@ -4,6 +4,7 @@ export { default as AnimatedNumber } from './components/AnimatedNumber.svelte';
4
4
  export { default as GlowEffect } from './components/GlowEffect.svelte';
5
5
  export { default as NavigationStack } from './components/NavigationStack';
6
6
  export { default as SegmentedPicker } from './components/SegmentedPicker.svelte';
7
+ export { default as Pagination } from './components/Pagination.svelte';
7
8
  export { default as Slider } from './components/Slider.svelte';
8
9
  export { default as Sheet } from './components/Sheet';
9
10
  export { default as Spinner } from './components/Spinner.svelte';
package/dist/index.js CHANGED
@@ -4,6 +4,7 @@ export { default as AnimatedNumber } from './components/AnimatedNumber.svelte';
4
4
  export { default as GlowEffect } from './components/GlowEffect.svelte';
5
5
  export { default as NavigationStack } from './components/NavigationStack';
6
6
  export { default as SegmentedPicker } from './components/SegmentedPicker.svelte';
7
+ export { default as Pagination } from './components/Pagination.svelte';
7
8
  export { default as Slider } from './components/Slider.svelte';
8
9
  export { default as Sheet } from './components/Sheet';
9
10
  export { default as Spinner } from './components/Spinner.svelte';
@@ -9,7 +9,7 @@
9
9
 
10
10
  @layer utilities {
11
11
  .layout {
12
- @apply flex size-full min-h-0 flex-col;
12
+ @apply flex size-full min-h-0 min-w-0 shrink-0 flex-col;
13
13
  }
14
14
 
15
15
  .center {
@@ -17,11 +17,11 @@
17
17
  }
18
18
 
19
19
  .vstack {
20
- @apply flex min-h-0 flex-col;
20
+ @apply flex min-h-0 shrink-0 flex-col;
21
21
  }
22
22
 
23
23
  .hstack {
24
- @apply flex min-w-0 flex-row;
24
+ @apply flex min-w-0 shrink-0 flex-row;
25
25
  }
26
26
 
27
27
  .spacer {
@@ -55,6 +55,12 @@
55
55
  .dropdown-item:hover {
56
56
  background: var(--dropdown-item-highlight);
57
57
  }
58
+
59
+ .pagination-button {
60
+ }
61
+
62
+ .pagination-input {
63
+ }
58
64
  }
59
65
 
60
66
  @layer theme {
@@ -109,5 +115,7 @@
109
115
  --chip-input-border-radius: 9999px;
110
116
  --chip-input-border-color: transparent;
111
117
  --chip-input-highlight: var(--color-zinc-300);
118
+
119
+ --pagination-gap: calc(var(--spacing) * 4);
112
120
  }
113
121
  }
package/dist/style.css CHANGED
@@ -15,6 +15,7 @@
15
15
  --color-zinc-100: oklch(96.7% 0.001 286.375);
16
16
  --color-zinc-200: oklch(92% 0.004 286.32);
17
17
  --color-zinc-300: oklch(87.1% 0.006 286.286);
18
+ --color-zinc-400: oklch(70.5% 0.015 286.067);
18
19
  --color-zinc-500: oklch(55.2% 0.016 285.938);
19
20
  --color-zinc-700: oklch(37% 0.013 285.805);
20
21
  --color-zinc-900: oklch(21% 0.006 285.885);
@@ -233,6 +234,9 @@
233
234
  .flex {
234
235
  display: flex;
235
236
  }
237
+ .hidden {
238
+ display: none;
239
+ }
236
240
  .inline-block {
237
241
  display: inline-block;
238
242
  }
@@ -364,6 +368,9 @@
364
368
  .bg-zinc-50 {
365
369
  background-color: var(--color-zinc-50);
366
370
  }
371
+ .bg-zinc-400 {
372
+ background-color: var(--color-zinc-400);
373
+ }
367
374
  .p-1\.5 {
368
375
  padding: calc(var(--spacing) * 1.5);
369
376
  }
@@ -540,6 +547,8 @@
540
547
  width: 100%;
541
548
  height: 100%;
542
549
  min-height: calc(var(--spacing) * 0);
550
+ min-width: calc(var(--spacing) * 0);
551
+ flex-shrink: 0;
543
552
  flex-direction: column;
544
553
  }
545
554
  .center {
@@ -549,11 +558,13 @@
549
558
  .vstack {
550
559
  display: flex;
551
560
  min-height: calc(var(--spacing) * 0);
561
+ flex-shrink: 0;
552
562
  flex-direction: column;
553
563
  }
554
564
  .hstack {
555
565
  display: flex;
556
566
  min-width: calc(var(--spacing) * 0);
567
+ flex-shrink: 0;
557
568
  flex-direction: row;
558
569
  }
559
570
  .spacer {
@@ -633,6 +644,7 @@
633
644
  --chip-input-border-radius: 9999px;
634
645
  --chip-input-border-color: transparent;
635
646
  --chip-input-highlight: var(--color-zinc-300);
647
+ --pagination-gap: calc(var(--spacing) * 4);
636
648
  }
637
649
  }
638
650
  @property --tw-rotate-x {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@x33025/sveltely",
3
- "version": "0.0.32",
3
+ "version": "0.0.34",
4
4
  "scripts": {
5
5
  "dev": "vite dev",
6
6
  "build": "vite build && npm run prepack",