@spaethtech/svelte-ui 0.1.10
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/README.md +81 -0
- package/dist/components/Badge/Badge.svelte +80 -0
- package/dist/components/Badge/Badge.svelte.d.ts +14 -0
- package/dist/components/Badge/index.d.ts +1 -0
- package/dist/components/Badge/index.js +1 -0
- package/dist/components/Button/Button.svelte +172 -0
- package/dist/components/Button/Button.svelte.d.ts +32 -0
- package/dist/components/Button/index.d.ts +1 -0
- package/dist/components/Button/index.js +1 -0
- package/dist/components/Dialog/Dialog.svelte +101 -0
- package/dist/components/Dialog/Dialog.svelte.d.ts +18 -0
- package/dist/components/Dialog/index.d.ts +1 -0
- package/dist/components/Dialog/index.js +1 -0
- package/dist/components/ElementManager/ElementManager.svelte +397 -0
- package/dist/components/ElementManager/ElementManager.svelte.d.ts +43 -0
- package/dist/components/ElementManager/index.d.ts +1 -0
- package/dist/components/ElementManager/index.js +1 -0
- package/dist/components/EmailInput/EmailInput.svelte +47 -0
- package/dist/components/EmailInput/EmailInput.svelte.d.ts +14 -0
- package/dist/components/EmailInput/index.d.ts +1 -0
- package/dist/components/EmailInput/index.js +1 -0
- package/dist/components/Icon/Icon.svelte +74 -0
- package/dist/components/Icon/Icon.svelte.d.ts +13 -0
- package/dist/components/Icon/index.d.ts +1 -0
- package/dist/components/Icon/index.js +1 -0
- package/dist/components/Input/Input.svelte +268 -0
- package/dist/components/Input/Input.svelte.d.ts +18 -0
- package/dist/components/Input/index.d.ts +1 -0
- package/dist/components/Input/index.js +1 -0
- package/dist/components/List/List.svelte +580 -0
- package/dist/components/List/List.svelte.d.ts +38 -0
- package/dist/components/List/index.d.ts +1 -0
- package/dist/components/List/index.js +1 -0
- package/dist/components/ListItem/ListItem.svelte +175 -0
- package/dist/components/ListItem/ListItem.svelte.d.ts +24 -0
- package/dist/components/ListItem/index.d.ts +2 -0
- package/dist/components/ListItem/index.js +2 -0
- package/dist/components/ListView/ListView.svelte +463 -0
- package/dist/components/ListView/ListView.svelte.d.ts +37 -0
- package/dist/components/ListView/index.d.ts +2 -0
- package/dist/components/ListView/index.js +2 -0
- package/dist/components/NodeGraph/BaseNode.d.ts +57 -0
- package/dist/components/NodeGraph/BaseNode.js +30 -0
- package/dist/components/NodeGraph/Edge.svelte +60 -0
- package/dist/components/NodeGraph/Edge.svelte.d.ts +16 -0
- package/dist/components/NodeGraph/ExpressionEvaluator.d.ts +82 -0
- package/dist/components/NodeGraph/ExpressionEvaluator.js +152 -0
- package/dist/components/NodeGraph/Node.svelte +100 -0
- package/dist/components/NodeGraph/Node.svelte.d.ts +10 -0
- package/dist/components/NodeGraph/NodeGraph.svelte +52 -0
- package/dist/components/NodeGraph/NodeGraph.svelte.d.ts +14 -0
- package/dist/components/NodeGraph/NodeInstance.svelte.d.ts +80 -0
- package/dist/components/NodeGraph/NodeInstance.svelte.js +241 -0
- package/dist/components/NodeGraph/NodePort.svelte +75 -0
- package/dist/components/NodeGraph/NodePort.svelte.d.ts +11 -0
- package/dist/components/NodeGraph/decorators.d.ts +81 -0
- package/dist/components/NodeGraph/decorators.js +148 -0
- package/dist/components/NodeGraph/index.d.ts +9 -0
- package/dist/components/NodeGraph/index.js +9 -0
- package/dist/components/NodeGraph/types.d.ts +94 -0
- package/dist/components/NodeGraph/types.js +33 -0
- package/dist/components/NotesEditor/NotesEditor.svelte +203 -0
- package/dist/components/NotesEditor/NotesEditor.svelte.d.ts +9 -0
- package/dist/components/NotesEditor/index.d.ts +1 -0
- package/dist/components/NotesEditor/index.js +1 -0
- package/dist/components/NumberInput/NumberInput.svelte +353 -0
- package/dist/components/NumberInput/NumberInput.svelte.d.ts +26 -0
- package/dist/components/NumberInput/index.d.ts +1 -0
- package/dist/components/NumberInput/index.js +1 -0
- package/dist/components/PasswordInput/PasswordInput.svelte +41 -0
- package/dist/components/PasswordInput/PasswordInput.svelte.d.ts +13 -0
- package/dist/components/PasswordInput/index.d.ts +1 -0
- package/dist/components/PasswordInput/index.js +1 -0
- package/dist/components/Popup/index.d.ts +1 -0
- package/dist/components/Popup/index.js +1 -0
- package/dist/components/Rating/Rating.svelte +77 -0
- package/dist/components/Rating/Rating.svelte.d.ts +7 -0
- package/dist/components/Rating/index.d.ts +1 -0
- package/dist/components/Rating/index.js +1 -0
- package/dist/components/ScrollView/ScrollView.svelte +285 -0
- package/dist/components/ScrollView/ScrollView.svelte.d.ts +19 -0
- package/dist/components/ScrollView/index.d.ts +1 -0
- package/dist/components/ScrollView/index.js +1 -0
- package/dist/components/SearchInput/SearchInput.svelte +50 -0
- package/dist/components/SearchInput/SearchInput.svelte.d.ts +15 -0
- package/dist/components/SearchInput/index.d.ts +1 -0
- package/dist/components/SearchInput/index.js +1 -0
- package/dist/components/Select/Select.svelte +278 -0
- package/dist/components/Select/Select.svelte.d.ts +35 -0
- package/dist/components/Select/index.d.ts +1 -0
- package/dist/components/Select/index.js +1 -0
- package/dist/components/TextArea/TextArea.svelte +734 -0
- package/dist/components/TextArea/TextArea.svelte.d.ts +19 -0
- package/dist/components/TextArea/index.d.ts +1 -0
- package/dist/components/TextArea/index.js +1 -0
- package/dist/components/ThemeSelector/ThemeSelector.svelte +64 -0
- package/dist/components/ThemeSelector/ThemeSelector.svelte.d.ts +3 -0
- package/dist/components/ThemeSelector/index.d.ts +1 -0
- package/dist/components/ThemeSelector/index.js +1 -0
- package/dist/components/TooltipHandler/TooltipHandler.svelte +593 -0
- package/dist/components/TooltipHandler/TooltipHandler.svelte.d.ts +10 -0
- package/dist/components/TooltipHandler/index.d.ts +1 -0
- package/dist/components/TooltipHandler/index.js +1 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.js +26 -0
- package/dist/styles/sizes.d.ts +78 -0
- package/dist/styles/sizes.js +120 -0
- package/dist/styles/variants.d.ts +106 -0
- package/dist/styles/variants.js +194 -0
- package/dist/types/ManagerTypes.d.ts +42 -0
- package/dist/types/ManagerTypes.js +1 -0
- package/dist/types/sizes.d.ts +5 -0
- package/dist/types/sizes.js +1 -0
- package/dist/types/variants.d.ts +1 -0
- package/dist/types/variants.js +1 -0
- package/package.json +65 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { HTMLInputAttributes } from 'svelte/elements';
|
|
2
|
+
interface Props extends Omit<HTMLInputAttributes, 'type' | 'value'> {
|
|
3
|
+
value: number | null;
|
|
4
|
+
class?: string;
|
|
5
|
+
inputClass?: string;
|
|
6
|
+
validate?: (value: number | null) => boolean | string;
|
|
7
|
+
valid?: boolean;
|
|
8
|
+
touched?: boolean;
|
|
9
|
+
element?: HTMLInputElement;
|
|
10
|
+
min?: number;
|
|
11
|
+
max?: number;
|
|
12
|
+
step?: number;
|
|
13
|
+
allowDecimals?: boolean;
|
|
14
|
+
decimalPlaces?: number;
|
|
15
|
+
thousandsSeparator?: boolean;
|
|
16
|
+
prefix?: string;
|
|
17
|
+
suffix?: string;
|
|
18
|
+
currency?: string;
|
|
19
|
+
decimalSeparator?: string;
|
|
20
|
+
thousandsChar?: string;
|
|
21
|
+
roundingMode?: 'round' | 'floor' | 'ceil' | 'trunc';
|
|
22
|
+
negativeMode?: 'minus' | 'parentheses' | 'both';
|
|
23
|
+
}
|
|
24
|
+
declare const NumberInput: import("svelte").Component<Props, {}, "element" | "value" | "valid" | "touched">;
|
|
25
|
+
type NumberInput = ReturnType<typeof NumberInput>;
|
|
26
|
+
export default NumberInput;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from './NumberInput.svelte';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from './NumberInput.svelte';
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import Input from '../Input/Input.svelte';
|
|
3
|
+
import { faEye, faEyeSlash } from '@fortawesome/free-solid-svg-icons';
|
|
4
|
+
import type { HTMLInputAttributes } from 'svelte/elements';
|
|
5
|
+
|
|
6
|
+
interface Props extends Omit<HTMLInputAttributes, 'type'> {
|
|
7
|
+
value: string;
|
|
8
|
+
class?: string;
|
|
9
|
+
inputClass?: string;
|
|
10
|
+
validate?: (value: string) => boolean | string;
|
|
11
|
+
valid?: boolean;
|
|
12
|
+
touched?: boolean;
|
|
13
|
+
element?: HTMLInputElement;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
let {
|
|
17
|
+
value = $bindable(),
|
|
18
|
+
valid = $bindable(true),
|
|
19
|
+
touched = $bindable(false),
|
|
20
|
+
element = $bindable(),
|
|
21
|
+
...restProps
|
|
22
|
+
}: Props = $props();
|
|
23
|
+
|
|
24
|
+
let showPassword = $state(false);
|
|
25
|
+
|
|
26
|
+
function toggleVisibility() {
|
|
27
|
+
showPassword = !showPassword;
|
|
28
|
+
}
|
|
29
|
+
</script>
|
|
30
|
+
|
|
31
|
+
<Input
|
|
32
|
+
bind:value
|
|
33
|
+
bind:valid
|
|
34
|
+
bind:touched
|
|
35
|
+
bind:element
|
|
36
|
+
type={showPassword ? 'text' : 'password'}
|
|
37
|
+
icon={showPassword ? faEye : faEyeSlash}
|
|
38
|
+
iconClickable={true}
|
|
39
|
+
onIconClick={toggleVisibility}
|
|
40
|
+
{...restProps}
|
|
41
|
+
/>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { HTMLInputAttributes } from 'svelte/elements';
|
|
2
|
+
interface Props extends Omit<HTMLInputAttributes, 'type'> {
|
|
3
|
+
value: string;
|
|
4
|
+
class?: string;
|
|
5
|
+
inputClass?: string;
|
|
6
|
+
validate?: (value: string) => boolean | string;
|
|
7
|
+
valid?: boolean;
|
|
8
|
+
touched?: boolean;
|
|
9
|
+
element?: HTMLInputElement;
|
|
10
|
+
}
|
|
11
|
+
declare const PasswordInput: import("svelte").Component<Props, {}, "element" | "value" | "valid" | "touched">;
|
|
12
|
+
type PasswordInput = ReturnType<typeof PasswordInput>;
|
|
13
|
+
export default PasswordInput;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from './PasswordInput.svelte';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from './PasswordInput.svelte';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from './Popup.svelte';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from './Popup.svelte';
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
export type RatingProps = {
|
|
3
|
+
average: number;
|
|
4
|
+
count: number;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
let { average, count }: RatingProps = $props();
|
|
8
|
+
|
|
9
|
+
// Validate and sanitize the average rating
|
|
10
|
+
const validAverage =
|
|
11
|
+
isNaN(average) || average < 0 ? 0 : Math.min(average, 10);
|
|
12
|
+
|
|
13
|
+
// Convert TMDB 10-point scale to 5-star scale
|
|
14
|
+
const fiveStarRating = validAverage / 2;
|
|
15
|
+
|
|
16
|
+
// Calculate how many stars should be filled (ensure they're valid numbers)
|
|
17
|
+
const filledStars = Math.max(0, Math.min(5, Math.floor(fiveStarRating)));
|
|
18
|
+
const hasHalfStar = fiveStarRating % 1 >= 0.5;
|
|
19
|
+
const emptyStars = Math.max(0, 5 - filledStars - (hasHalfStar ? 1 : 0));
|
|
20
|
+
</script>
|
|
21
|
+
|
|
22
|
+
<div class="rating select-none">
|
|
23
|
+
<div class="stars">
|
|
24
|
+
{#each Array(filledStars) as _, i}
|
|
25
|
+
<span class="star filled">★</span>
|
|
26
|
+
{/each}
|
|
27
|
+
{#if hasHalfStar}
|
|
28
|
+
<span class="star half-filled">☆</span>
|
|
29
|
+
{/if}
|
|
30
|
+
{#each Array(emptyStars) as _, i}
|
|
31
|
+
<span class="star empty">☆</span>
|
|
32
|
+
{/each}
|
|
33
|
+
</div>
|
|
34
|
+
<span class="rating-value">{fiveStarRating.toFixed(1)}</span>
|
|
35
|
+
<span class="rating-count">({count})</span>
|
|
36
|
+
</div>
|
|
37
|
+
|
|
38
|
+
<style>
|
|
39
|
+
.rating {
|
|
40
|
+
display: flex;
|
|
41
|
+
align-items: center;
|
|
42
|
+
gap: 0.5rem;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.stars {
|
|
46
|
+
display: flex;
|
|
47
|
+
gap: 0.1rem;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.star {
|
|
51
|
+
font-size: 1.2rem;
|
|
52
|
+
line-height: 1;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.star.filled {
|
|
56
|
+
color: #ffd700;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.star.half-filled {
|
|
60
|
+
color: #ffd700;
|
|
61
|
+
opacity: 0.6;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.star.empty {
|
|
65
|
+
color: var(--color-secondary);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.rating-value {
|
|
69
|
+
font-weight: 600;
|
|
70
|
+
color: var(--color-text);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.rating-count {
|
|
74
|
+
font-size: 0.9rem;
|
|
75
|
+
color: var(--color-secondary);
|
|
76
|
+
}
|
|
77
|
+
</style>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from './Rating.svelte';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from './Rating.svelte';
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from "svelte";
|
|
3
|
+
import type { HTMLAttributes } from "svelte/elements";
|
|
4
|
+
|
|
5
|
+
interface Props extends HTMLAttributes<HTMLDivElement> {
|
|
6
|
+
showScrollbar?: boolean;
|
|
7
|
+
scrollbarWidth?: number;
|
|
8
|
+
class?: string;
|
|
9
|
+
disabled?: boolean;
|
|
10
|
+
children: Snippet;
|
|
11
|
+
view?: HTMLDivElement;
|
|
12
|
+
onScroll?: (data: {
|
|
13
|
+
scrollTop: number;
|
|
14
|
+
thumbTop: number;
|
|
15
|
+
scrollHeight: number;
|
|
16
|
+
clientHeight: number;
|
|
17
|
+
}) => void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
let {
|
|
21
|
+
showScrollbar = true,
|
|
22
|
+
scrollbarWidth = 6,
|
|
23
|
+
class: className = "",
|
|
24
|
+
style = "",
|
|
25
|
+
disabled = false,
|
|
26
|
+
children,
|
|
27
|
+
view = $bindable(),
|
|
28
|
+
onScroll,
|
|
29
|
+
...restProps
|
|
30
|
+
}: Props = $props();
|
|
31
|
+
|
|
32
|
+
let isDragging = $state(false);
|
|
33
|
+
let dragStartY = $state(0);
|
|
34
|
+
let dragStartScrollTop = $state(0);
|
|
35
|
+
let scrollTop = $state(0);
|
|
36
|
+
let previousScrollTop = $state(0);
|
|
37
|
+
let scrollHeight = $state(0);
|
|
38
|
+
let clientHeight = $state(0);
|
|
39
|
+
let isScrolling = $state(false);
|
|
40
|
+
let isHovering = $state(false);
|
|
41
|
+
let scrollTimeout = $state<ReturnType<typeof setTimeout>>();
|
|
42
|
+
|
|
43
|
+
// Inset from edges (0.25rem = 4px)
|
|
44
|
+
const inset = 4;
|
|
45
|
+
|
|
46
|
+
// Generate unique ID for aria-controls if not provided
|
|
47
|
+
const scrollableId =
|
|
48
|
+
restProps.id ?? `scrollview-${Math.random().toString(36).substr(2, 9)}`;
|
|
49
|
+
|
|
50
|
+
// Split classes: height/sizing/border/rounded classes go to wrapper, everything else to view
|
|
51
|
+
const wrapperClasses = $derived(
|
|
52
|
+
className
|
|
53
|
+
.match(
|
|
54
|
+
/\b(h-|min-h-|max-h-|flex-\d|flex-grow|flex-shrink|flex-none|flex-auto|basis-|border|rounded)\S*/g,
|
|
55
|
+
)
|
|
56
|
+
?.join(" ") || "",
|
|
57
|
+
);
|
|
58
|
+
const viewClasses = $derived(
|
|
59
|
+
className
|
|
60
|
+
.split(/\s+/)
|
|
61
|
+
.filter((cls) => cls && !wrapperClasses.includes(cls))
|
|
62
|
+
.join(" "),
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
// Initialize dimensions when view is mounted
|
|
66
|
+
$effect(() => {
|
|
67
|
+
if (view) {
|
|
68
|
+
scrollTop = view.scrollTop;
|
|
69
|
+
scrollHeight = view.scrollHeight;
|
|
70
|
+
clientHeight = view.clientHeight;
|
|
71
|
+
|
|
72
|
+
// Fire initial callback if provided
|
|
73
|
+
if (onScroll) {
|
|
74
|
+
onScroll({ scrollTop, thumbTop, scrollHeight, clientHeight });
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// Calculate thumb height based on view dimensions
|
|
80
|
+
const thumbHeight = $derived.by(() => {
|
|
81
|
+
if (scrollHeight === 0 || clientHeight === 0) return 20;
|
|
82
|
+
const visibleRatio = clientHeight / scrollHeight;
|
|
83
|
+
return Math.max(
|
|
84
|
+
20,
|
|
85
|
+
Math.min(clientHeight - inset * 2, visibleRatio * clientHeight),
|
|
86
|
+
);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// Calculate thumb position based on scroll position
|
|
90
|
+
const thumbTop = $derived.by(() => {
|
|
91
|
+
if (scrollHeight === 0 || clientHeight === 0) return inset;
|
|
92
|
+
const maxScrollTop = scrollHeight - clientHeight;
|
|
93
|
+
if (maxScrollTop <= 0) return inset;
|
|
94
|
+
const scrollRatio = scrollTop / maxScrollTop;
|
|
95
|
+
const trackHeight = clientHeight - thumbHeight - inset * 2;
|
|
96
|
+
return inset + scrollRatio * trackHeight;
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// Check if scrollbar is needed
|
|
100
|
+
const needsScrollbar = $derived(
|
|
101
|
+
showScrollbar &&
|
|
102
|
+
!disabled &&
|
|
103
|
+
scrollHeight > 0 &&
|
|
104
|
+
clientHeight > 0 &&
|
|
105
|
+
scrollHeight > clientHeight,
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
// Calculate scroll percentage for aria-valuenow
|
|
109
|
+
const scrollPercentage = $derived(() => {
|
|
110
|
+
if (!view) return 0;
|
|
111
|
+
const maxScrollTop = view.scrollHeight - view.clientHeight;
|
|
112
|
+
if (maxScrollTop <= 0) return 0;
|
|
113
|
+
return Math.round((scrollTop / maxScrollTop) * 100);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// Calculate opacity based on state
|
|
117
|
+
const thumbOpacity = $derived(isScrolling || isHovering ? "80%" : "30%");
|
|
118
|
+
|
|
119
|
+
// Handle scrollbar thumb mouse down
|
|
120
|
+
function handleThumbMouseDown(event: MouseEvent) {
|
|
121
|
+
if (!view) return;
|
|
122
|
+
event.preventDefault();
|
|
123
|
+
isDragging = true;
|
|
124
|
+
dragStartY = event.clientY;
|
|
125
|
+
dragStartScrollTop = view.scrollTop;
|
|
126
|
+
|
|
127
|
+
document.addEventListener("mousemove", handleThumbMouseMove);
|
|
128
|
+
document.addEventListener("mouseup", handleThumbMouseUp);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Handle scrollbar thumb mouse move
|
|
132
|
+
function handleThumbMouseMove(event: MouseEvent) {
|
|
133
|
+
if (!isDragging || !view) return;
|
|
134
|
+
|
|
135
|
+
const deltaY = event.clientY - dragStartY;
|
|
136
|
+
const scrollRatio = deltaY / (view.clientHeight - thumbHeight);
|
|
137
|
+
const maxScrollTop = view.scrollHeight - view.clientHeight;
|
|
138
|
+
|
|
139
|
+
view.scrollTop = Math.max(
|
|
140
|
+
0,
|
|
141
|
+
Math.min(
|
|
142
|
+
maxScrollTop,
|
|
143
|
+
dragStartScrollTop + scrollRatio * maxScrollTop,
|
|
144
|
+
),
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Handle scrollbar thumb mouse up
|
|
149
|
+
function handleThumbMouseUp() {
|
|
150
|
+
isDragging = false;
|
|
151
|
+
document.removeEventListener("mousemove", handleThumbMouseMove);
|
|
152
|
+
document.removeEventListener("mouseup", handleThumbMouseUp);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Handle keyboard events - prevent scroll chaining while preserving smooth scrolling
|
|
156
|
+
function handleKeyDown(event: KeyboardEvent) {
|
|
157
|
+
if (disabled) return;
|
|
158
|
+
|
|
159
|
+
// Only handle if this ScrollView has focus (not a child element)
|
|
160
|
+
if (event.target !== view) return;
|
|
161
|
+
|
|
162
|
+
// Check for scroll keys
|
|
163
|
+
const scrollKeys = [
|
|
164
|
+
"ArrowDown",
|
|
165
|
+
"ArrowUp",
|
|
166
|
+
"PageDown",
|
|
167
|
+
"PageUp",
|
|
168
|
+
"Home",
|
|
169
|
+
"End",
|
|
170
|
+
" ",
|
|
171
|
+
];
|
|
172
|
+
if (!scrollKeys.includes(event.key)) return;
|
|
173
|
+
|
|
174
|
+
if (!view) return;
|
|
175
|
+
|
|
176
|
+
// ALWAYS prevent default and stop propagation to have full control
|
|
177
|
+
event.preventDefault();
|
|
178
|
+
event.stopPropagation();
|
|
179
|
+
|
|
180
|
+
// Calculate scroll amount based on key
|
|
181
|
+
const {
|
|
182
|
+
clientHeight: currentClientHeight,
|
|
183
|
+
scrollTop: currentScrollTop,
|
|
184
|
+
scrollHeight: currentScrollHeight,
|
|
185
|
+
} = view;
|
|
186
|
+
let scrollAmount = 0;
|
|
187
|
+
|
|
188
|
+
if (event.key === "ArrowDown") scrollAmount = 40;
|
|
189
|
+
else if (event.key === "ArrowUp") scrollAmount = -40;
|
|
190
|
+
else if (event.key === "PageDown")
|
|
191
|
+
scrollAmount = currentClientHeight * 0.9;
|
|
192
|
+
else if (event.key === "PageUp")
|
|
193
|
+
scrollAmount = -currentClientHeight * 0.9;
|
|
194
|
+
else if (event.key === " ")
|
|
195
|
+
scrollAmount = event.shiftKey
|
|
196
|
+
? -currentClientHeight * 0.9
|
|
197
|
+
: currentClientHeight * 0.9;
|
|
198
|
+
else if (event.key === "Home") scrollAmount = -currentScrollTop;
|
|
199
|
+
else if (event.key === "End")
|
|
200
|
+
scrollAmount =
|
|
201
|
+
currentScrollHeight - currentClientHeight - currentScrollTop;
|
|
202
|
+
|
|
203
|
+
// Apply scroll instantly (smooth behavior feels laggy with keyboard)
|
|
204
|
+
view.scrollBy({ top: scrollAmount, behavior: "auto" });
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Handle scroll and update state
|
|
208
|
+
function handleScroll() {
|
|
209
|
+
if (!view) return;
|
|
210
|
+
|
|
211
|
+
// Update scroll state
|
|
212
|
+
previousScrollTop = scrollTop;
|
|
213
|
+
scrollTop = view.scrollTop;
|
|
214
|
+
scrollHeight = view.scrollHeight;
|
|
215
|
+
clientHeight = view.clientHeight;
|
|
216
|
+
|
|
217
|
+
// Show scrollbar during scrolling
|
|
218
|
+
isScrolling = true;
|
|
219
|
+
|
|
220
|
+
// Clear previous timeout
|
|
221
|
+
if (scrollTimeout) {
|
|
222
|
+
clearTimeout(scrollTimeout);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Hide scrollbar after scrolling stops (500ms delay)
|
|
226
|
+
scrollTimeout = setTimeout(() => {
|
|
227
|
+
isScrolling = false;
|
|
228
|
+
}, 500);
|
|
229
|
+
|
|
230
|
+
// Fire onScroll callback
|
|
231
|
+
if (onScroll) {
|
|
232
|
+
onScroll({ scrollTop, thumbTop, scrollHeight, clientHeight });
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
</script>
|
|
236
|
+
|
|
237
|
+
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
|
|
238
|
+
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
|
239
|
+
<div
|
|
240
|
+
class="relative overflow-hidden p-0 flex-1 min-h-8 {wrapperClasses}"
|
|
241
|
+
{style}
|
|
242
|
+
{...restProps}
|
|
243
|
+
>
|
|
244
|
+
<div
|
|
245
|
+
bind:this={view}
|
|
246
|
+
id={scrollableId}
|
|
247
|
+
class="scrollview absolute inset-0 {disabled
|
|
248
|
+
? 'overflow-y-hidden'
|
|
249
|
+
: 'overflow-y-auto'} overflow-x-hidden [scrollbar-width:none] [-ms-overflow-style:none] [overscroll-behavior:contain] {viewClasses} {disabled
|
|
250
|
+
? '[background-color:color-mix(in_srgb,var(--color-text)_5%,transparent)] cursor-not-allowed'
|
|
251
|
+
: '[background-color:var(--color-background)]'} focus-visible:[outline:2px_solid_var(--color-primary)] focus-visible:[outline-offset:0px]"
|
|
252
|
+
onscroll={handleScroll}
|
|
253
|
+
onkeydown={handleKeyDown}
|
|
254
|
+
onmouseenter={() => (isHovering = true)}
|
|
255
|
+
onmouseleave={() => (isHovering = false)}
|
|
256
|
+
tabindex={disabled ? -1 : 0}
|
|
257
|
+
role="region"
|
|
258
|
+
aria-label="Scrollable content"
|
|
259
|
+
>
|
|
260
|
+
{@render children()}
|
|
261
|
+
</div>
|
|
262
|
+
|
|
263
|
+
<!-- Custom scrollbar thumb -->
|
|
264
|
+
{#if needsScrollbar}
|
|
265
|
+
<div
|
|
266
|
+
class="absolute rounded cursor-pointer pointer-events-auto z-10"
|
|
267
|
+
style="top: {thumbTop}px; right: {inset}px; height: {thumbHeight}px; width: {scrollbarWidth}px; background-color: color-mix(in srgb, var(--color-primary) {thumbOpacity}, transparent);"
|
|
268
|
+
onmousedown={handleThumbMouseDown}
|
|
269
|
+
role="scrollbar"
|
|
270
|
+
aria-controls={scrollableId}
|
|
271
|
+
aria-orientation="vertical"
|
|
272
|
+
aria-valuenow={scrollPercentage()}
|
|
273
|
+
aria-valuemin="0"
|
|
274
|
+
aria-valuemax="100"
|
|
275
|
+
tabindex="-1"
|
|
276
|
+
aria-label="Scroll thumb"
|
|
277
|
+
></div>
|
|
278
|
+
{/if}
|
|
279
|
+
</div>
|
|
280
|
+
|
|
281
|
+
<style>
|
|
282
|
+
.scrollview::-webkit-scrollbar {
|
|
283
|
+
display: none;
|
|
284
|
+
}
|
|
285
|
+
</style>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Snippet } from "svelte";
|
|
2
|
+
import type { HTMLAttributes } from "svelte/elements";
|
|
3
|
+
interface Props extends HTMLAttributes<HTMLDivElement> {
|
|
4
|
+
showScrollbar?: boolean;
|
|
5
|
+
scrollbarWidth?: number;
|
|
6
|
+
class?: string;
|
|
7
|
+
disabled?: boolean;
|
|
8
|
+
children: Snippet;
|
|
9
|
+
view?: HTMLDivElement;
|
|
10
|
+
onScroll?: (data: {
|
|
11
|
+
scrollTop: number;
|
|
12
|
+
thumbTop: number;
|
|
13
|
+
scrollHeight: number;
|
|
14
|
+
clientHeight: number;
|
|
15
|
+
}) => void;
|
|
16
|
+
}
|
|
17
|
+
declare const ScrollView: import("svelte").Component<Props, {}, "view">;
|
|
18
|
+
type ScrollView = ReturnType<typeof ScrollView>;
|
|
19
|
+
export default ScrollView;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from './ScrollView.svelte';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from './ScrollView.svelte';
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import Input from '../Input/Input.svelte';
|
|
3
|
+
import { faSearch, faTimes } from '@fortawesome/free-solid-svg-icons';
|
|
4
|
+
import type { HTMLInputAttributes } from 'svelte/elements';
|
|
5
|
+
|
|
6
|
+
interface Props extends Omit<HTMLInputAttributes, 'type'> {
|
|
7
|
+
value: string;
|
|
8
|
+
class?: string;
|
|
9
|
+
inputClass?: string;
|
|
10
|
+
validate?: (value: string) => boolean | string;
|
|
11
|
+
valid?: boolean;
|
|
12
|
+
touched?: boolean;
|
|
13
|
+
element?: HTMLInputElement;
|
|
14
|
+
onClear?: () => void;
|
|
15
|
+
clearable?: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
let {
|
|
19
|
+
value = $bindable(),
|
|
20
|
+
valid = $bindable(true),
|
|
21
|
+
touched = $bindable(false),
|
|
22
|
+
element = $bindable(),
|
|
23
|
+
onClear,
|
|
24
|
+
clearable = true,
|
|
25
|
+
placeholder = 'Search...',
|
|
26
|
+
...restProps
|
|
27
|
+
}: Props = $props();
|
|
28
|
+
|
|
29
|
+
function handleClear() {
|
|
30
|
+
value = '';
|
|
31
|
+
onClear?.();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Show clear icon when there's content and clearable is true
|
|
35
|
+
let showClearIcon = $derived(clearable && value.length > 0);
|
|
36
|
+
</script>
|
|
37
|
+
|
|
38
|
+
<Input
|
|
39
|
+
bind:value
|
|
40
|
+
bind:valid
|
|
41
|
+
bind:touched
|
|
42
|
+
bind:element
|
|
43
|
+
type="search"
|
|
44
|
+
inputClass="[&::-webkit-search-cancel-button]:appearance-none [&::-webkit-search-decoration]:appearance-none"
|
|
45
|
+
icon={showClearIcon ? faTimes : faSearch}
|
|
46
|
+
iconClickable={showClearIcon}
|
|
47
|
+
onIconClick={showClearIcon ? handleClear : undefined}
|
|
48
|
+
{placeholder}
|
|
49
|
+
{...restProps}
|
|
50
|
+
/>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { HTMLInputAttributes } from 'svelte/elements';
|
|
2
|
+
interface Props extends Omit<HTMLInputAttributes, 'type'> {
|
|
3
|
+
value: string;
|
|
4
|
+
class?: string;
|
|
5
|
+
inputClass?: string;
|
|
6
|
+
validate?: (value: string) => boolean | string;
|
|
7
|
+
valid?: boolean;
|
|
8
|
+
touched?: boolean;
|
|
9
|
+
element?: HTMLInputElement;
|
|
10
|
+
onClear?: () => void;
|
|
11
|
+
clearable?: boolean;
|
|
12
|
+
}
|
|
13
|
+
declare const SearchInput: import("svelte").Component<Props, {}, "element" | "value" | "valid" | "touched">;
|
|
14
|
+
type SearchInput = ReturnType<typeof SearchInput>;
|
|
15
|
+
export default SearchInput;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from './SearchInput.svelte';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from './SearchInput.svelte';
|