@x33025/sveltely 0.0.31 → 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,86 +210,100 @@
157
210
  });
158
211
  </script>
159
212
 
160
- <div class="w-full max-w-lg">
161
- <div class="tag-row flex flex-wrap items-center">
162
- {#each tags as tag (tag)}
163
- {#if editingTag === tag}
164
- <input
165
- bind:this={editingEl}
166
- bind:value={editingValue}
167
- class="tag-surface tag-input-field min-w-20 outline-none"
168
- onblur={() => commitEdit('blur', tag)}
169
- onkeydown={(event) => onEditKeydown(event, tag)}
170
- />
171
- {:else if selectionEnabled}
172
- <button
173
- type="button"
174
- class="tag-surface inline-flex items-center gap-2"
175
- class:tag-selected={selection?.includes(tag)}
176
- onclick={() => toggleSelected(tag)}
177
- ondblclick={() => startEditing(tag)}
178
- aria-pressed={selection?.includes(tag)}
179
- >
180
- {tag}
181
- </button>
182
- {:else}
183
- <button
184
- type="button"
185
- class="tag-surface inline-flex items-center gap-2"
186
- ondblclick={() => startEditing(tag)}>{tag}</button
187
- >
188
- {/if}
189
- {/each}
190
-
191
- {#if showInput}
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)}
221
+ {#if editingTag === tag}
192
222
  <input
193
- bind:this={inputEl}
194
- bind:value={inputValue}
195
- class="tag-surface tag-input-field min-w-36 outline-none"
196
- {placeholder}
197
- onkeydown={onKeydown}
198
- onblur={onBlur}
223
+ bind:this={editingEl}
224
+ bind:value={editingValue}
225
+ size={Math.max(editingValue.length, 1)}
226
+ class="chip-surface chip-input-field outline-none"
227
+ onblur={() => commitEdit('blur', tag)}
228
+ onkeydown={(event) => onEditKeydown(event, tag)}
199
229
  />
200
- {:else}
230
+ {:else if selectionEnabled}
201
231
  <button
202
232
  type="button"
203
- class="tag-surface chip-input-action inline-flex items-center justify-center font-semibold"
204
- aria-label="Add tag"
205
- onclick={openInput}
233
+ class="chip-surface inline-flex items-center gap-2"
234
+ class:chip-selected={selection?.includes(tag)}
235
+ class:chip-hovered={isPointerSelecting && dragHoverIndex === index}
236
+ onpointerdown={(event) => beginRangeSelection(event, index)}
237
+ onpointerenter={() => extendRangeSelection(index)}
238
+ onclick={() => onChipClick(tag)}
239
+ ondblclick={() => startEditing(tag)}
240
+ aria-pressed={selection?.includes(tag)}
206
241
  >
207
- <Plus style="width: var(--chip-input-font-size); height: var(--chip-input-font-size);" />
242
+ {tag}
208
243
  </button>
244
+ {:else}
245
+ <button
246
+ type="button"
247
+ class="chip-surface inline-flex items-center gap-2"
248
+ ondblclick={() => startEditing(tag)}>{tag}</button
249
+ >
209
250
  {/if}
210
-
211
- {#if action}
212
- {@render action()}
213
- {/if}
214
- </div>
251
+ {/each}
252
+
253
+ {#if showInput}
254
+ <input
255
+ bind:this={inputEl}
256
+ bind:value={inputValue}
257
+ size={Math.max(inputValue.length, placeholder.length, 1)}
258
+ class="chip-surface chip-input-field outline-none"
259
+ {placeholder}
260
+ onkeydown={onKeydown}
261
+ onblur={onBlur}
262
+ />
263
+ {:else}
264
+ <button
265
+ type="button"
266
+ class="chip-surface chip-input-action inline-flex items-center justify-center font-semibold"
267
+ aria-label="Add tag"
268
+ onclick={openInput}
269
+ >
270
+ <Plus style="width: var(--chip-input-font-size); height: var(--chip-input-font-size);" />
271
+ </button>
272
+ {/if}
273
+
274
+ {#if action}
275
+ {@render action()}
276
+ {/if}
215
277
  </div>
216
278
 
217
279
  <style>
218
- .tag-row {
280
+ .chip-row {
219
281
  gap: var(--chip-input-gap);
220
282
  }
221
283
 
222
- .tag-surface {
284
+ .chip-surface {
223
285
  background: var(--chip-input-background);
224
286
  color: var(--chip-input-text);
225
287
  font-size: var(--chip-input-font-size);
226
288
  border-radius: var(--chip-input-border-radius);
227
289
  padding: var(--chip-input-padding);
228
290
  border: 1px solid var(--chip-input-border-color);
291
+ white-space: nowrap;
229
292
  }
230
293
 
231
- .tag-selected {
294
+ .chip-selected {
232
295
  border-color: var(--chip-input-highlight);
233
296
  }
234
297
 
235
- .tag-surface:hover {
298
+ .chip-surface:hover {
299
+ background: var(--chip-input-hover);
300
+ }
301
+
302
+ .chip-hovered {
236
303
  background: var(--chip-input-hover);
237
304
  }
238
305
 
239
- .tag-input-field:hover {
306
+ .chip-input-field:hover {
240
307
  background: var(--chip-input-background);
241
308
  }
242
309
  </style>
@@ -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);
@@ -22,7 +23,6 @@
22
23
  --color-black: #000;
23
24
  --color-white: #fff;
24
25
  --spacing: 0.25rem;
25
- --container-lg: 32rem;
26
26
  --text-sm: 0.875rem;
27
27
  --text-sm--line-height: calc(1.25 / 0.875);
28
28
  --font-weight-medium: 500;
@@ -234,6 +234,9 @@
234
234
  .flex {
235
235
  display: flex;
236
236
  }
237
+ .hidden {
238
+ display: none;
239
+ }
237
240
  .inline-block {
238
241
  display: inline-block;
239
242
  }
@@ -269,15 +272,6 @@
269
272
  .w-full {
270
273
  width: 100%;
271
274
  }
272
- .max-w-lg {
273
- max-width: var(--container-lg);
274
- }
275
- .min-w-20 {
276
- min-width: calc(var(--spacing) * 20);
277
- }
278
- .min-w-36 {
279
- min-width: calc(var(--spacing) * 36);
280
- }
281
275
  .flex-1 {
282
276
  flex: 1;
283
277
  }
@@ -374,6 +368,9 @@
374
368
  .bg-zinc-50 {
375
369
  background-color: var(--color-zinc-50);
376
370
  }
371
+ .bg-zinc-400 {
372
+ background-color: var(--color-zinc-400);
373
+ }
377
374
  .p-1\.5 {
378
375
  padding: calc(var(--spacing) * 1.5);
379
376
  }
@@ -550,6 +547,8 @@
550
547
  width: 100%;
551
548
  height: 100%;
552
549
  min-height: calc(var(--spacing) * 0);
550
+ min-width: calc(var(--spacing) * 0);
551
+ flex-shrink: 0;
553
552
  flex-direction: column;
554
553
  }
555
554
  .center {
@@ -559,11 +558,13 @@
559
558
  .vstack {
560
559
  display: flex;
561
560
  min-height: calc(var(--spacing) * 0);
561
+ flex-shrink: 0;
562
562
  flex-direction: column;
563
563
  }
564
564
  .hstack {
565
565
  display: flex;
566
566
  min-width: calc(var(--spacing) * 0);
567
+ flex-shrink: 0;
567
568
  flex-direction: row;
568
569
  }
569
570
  .spacer {
@@ -643,6 +644,7 @@
643
644
  --chip-input-border-radius: 9999px;
644
645
  --chip-input-border-color: transparent;
645
646
  --chip-input-highlight: var(--color-zinc-300);
647
+ --pagination-gap: calc(var(--spacing) * 4);
646
648
  }
647
649
  }
648
650
  @property --tw-rotate-x {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@x33025/sveltely",
3
- "version": "0.0.31",
3
+ "version": "0.0.34",
4
4
  "scripts": {
5
5
  "dev": "vite dev",
6
6
  "build": "vite build && npm run prepack",