@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.
- package/dist/components/ChipInput.svelte +120 -53
- package/dist/components/Pagination.svelte +93 -0
- package/dist/components/Pagination.svelte.d.ts +14 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/style/index.css +11 -3
- package/dist/style.css +12 -10
- package/package.json +1 -1
|
@@ -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
|
-
<
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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={
|
|
194
|
-
bind:value={
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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="
|
|
204
|
-
|
|
205
|
-
|
|
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
|
-
|
|
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
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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
|
-
.
|
|
280
|
+
.chip-row {
|
|
219
281
|
gap: var(--chip-input-gap);
|
|
220
282
|
}
|
|
221
283
|
|
|
222
|
-
.
|
|
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
|
-
.
|
|
294
|
+
.chip-selected {
|
|
232
295
|
border-color: var(--chip-input-highlight);
|
|
233
296
|
}
|
|
234
297
|
|
|
235
|
-
.
|
|
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
|
-
.
|
|
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';
|
package/dist/style/index.css
CHANGED
|
@@ -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 {
|