zabi-components 5.0.15 → 5.0.18
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 +175 -23
- package/dist/SSRSafe.svelte +0 -3
- package/dist/atoms/Badge.svelte +32 -47
- package/dist/atoms/Badge.svelte.d.ts +6 -5
- package/dist/atoms/Button.svelte +18 -17
- package/dist/atoms/Button.svelte.d.ts +3 -2
- package/dist/atoms/Card.svelte +74 -23
- package/dist/atoms/Card.svelte.d.ts +3 -1
- package/dist/atoms/CardContent.svelte +24 -0
- package/dist/atoms/CardContent.svelte.d.ts +8 -0
- package/dist/atoms/CardDescription.svelte +24 -0
- package/dist/atoms/CardDescription.svelte.d.ts +8 -0
- package/dist/atoms/CardFooter.svelte +24 -0
- package/dist/atoms/CardFooter.svelte.d.ts +8 -0
- package/dist/atoms/CardHeader.svelte +24 -0
- package/dist/atoms/CardHeader.svelte.d.ts +8 -0
- package/dist/atoms/CardTitle.svelte +33 -0
- package/dist/atoms/CardTitle.svelte.d.ts +9 -0
- package/dist/atoms/Checkbox.svelte +19 -20
- package/dist/atoms/CodeBlock.svelte +4 -7
- package/dist/atoms/ColorPicker.svelte +33 -65
- package/dist/atoms/FeatureCard.svelte +2 -12
- package/dist/atoms/FeatureCard.svelte.d.ts +0 -8
- package/dist/atoms/Heading.svelte +0 -2
- package/dist/atoms/Input.svelte +8 -17
- package/dist/atoms/Input.svelte.d.ts +3 -2
- package/dist/atoms/Progress.svelte +1 -4
- package/dist/atoms/Select.svelte +37 -28
- package/dist/atoms/Skeleton.svelte +1 -1
- package/dist/atoms/Textarea.svelte +5 -11
- package/dist/atoms/Textarea.svelte.d.ts +3 -1
- package/dist/atoms/ThemeToggle.svelte +5 -12
- package/dist/atoms/Toast.svelte +1 -2
- package/dist/atoms/Toggle.svelte +2 -10
- package/dist/atoms/Tooltip.svelte +123 -26
- package/dist/atoms/index.d.ts +5 -0
- package/dist/atoms/index.js +5 -1
- package/dist/components/atoms/index.d.ts +5 -0
- package/dist/components/atoms/index.d.ts.map +1 -1
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/molecules/index.d.ts.map +1 -1
- package/dist/components/organisms/index.d.ts.map +1 -1
- package/dist/index.js +0 -23
- package/dist/molecules/Alert.svelte +9 -13
- package/dist/molecules/Alert.svelte.d.ts +2 -1
- package/dist/molecules/ComponentDemo.svelte +55 -64
- package/dist/molecules/ContactForm.svelte +21 -12
- package/dist/molecules/Dropdown.svelte +104 -8
- package/dist/molecules/Dropdown.svelte.d.ts +1 -0
- package/dist/molecules/Form.svelte +0 -1
- package/dist/molecules/ImageUpload.svelte +2 -6
- package/dist/molecules/Modal.svelte +30 -6
- package/dist/molecules/SlideUp.svelte +2 -4
- package/dist/molecules/Tabs.svelte +2 -4
- package/dist/molecules/index.js +0 -1
- package/dist/organisms/Navbar.svelte +1 -8
- package/dist/organisms/Navigation.svelte +0 -6
- package/dist/organisms/index.js +0 -1
- package/dist/routes/lib/variant-utils.ts +7 -5
- package/dist/zabi-components-colors.css +287 -211
- package/dist/zabi-components-theme-dark-only.css +158 -0
- package/dist/zabi-components-theme-dark.css +160 -0
- package/dist/zabi-components-theme-only.css +235 -0
- package/dist/zabi-components-theme.css +237 -0
- package/dist/zabi-components.css +575 -360
- package/package.json +15 -5
package/dist/atoms/Select.svelte
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import Dropdown from "../molecules/Dropdown.svelte";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
ChevronDown,
|
|
5
|
+
CheckCircle,
|
|
6
|
+
AlertTriangle,
|
|
7
|
+
AlertCircle,
|
|
8
|
+
} from "@lucide/svelte";
|
|
4
9
|
|
|
5
|
-
// SSR-safe ID generation
|
|
6
10
|
function generateId(prefix: string = "id"): string {
|
|
7
11
|
if (typeof window !== "undefined") {
|
|
8
12
|
return `${prefix}-${Math.random().toString(36).substr(2, 9)}`;
|
|
@@ -42,31 +46,28 @@
|
|
|
42
46
|
|
|
43
47
|
let isOpen = $state(false);
|
|
44
48
|
|
|
45
|
-
// Size classes matching M3 design specifications
|
|
46
49
|
const sizeClass = $derived(() => {
|
|
47
50
|
if (size === "sm") {
|
|
48
51
|
return {
|
|
49
52
|
padding: "px-4 py-2",
|
|
50
53
|
text: "text-sm",
|
|
51
|
-
leading: "leading-5"
|
|
54
|
+
leading: "leading-5",
|
|
52
55
|
};
|
|
53
56
|
} else if (size === "lg") {
|
|
54
57
|
return {
|
|
55
58
|
padding: "px-4 py-3",
|
|
56
59
|
text: "text-base",
|
|
57
|
-
leading: "leading-6"
|
|
60
|
+
leading: "leading-6",
|
|
58
61
|
};
|
|
59
62
|
} else {
|
|
60
|
-
// default md
|
|
61
63
|
return {
|
|
62
64
|
padding: "px-4 py-2.5",
|
|
63
65
|
text: "text-base",
|
|
64
|
-
leading: "leading-6"
|
|
66
|
+
leading: "leading-6",
|
|
65
67
|
};
|
|
66
68
|
}
|
|
67
69
|
});
|
|
68
70
|
|
|
69
|
-
// Variant classes using semantic colors
|
|
70
71
|
const variantClass = $derived(() => {
|
|
71
72
|
return variant === "success"
|
|
72
73
|
? "border-success focus:border-success focus:ring-success"
|
|
@@ -77,21 +78,18 @@
|
|
|
77
78
|
: "border-0 focus:ring-2 focus:ring-brand-500"; // default - no border
|
|
78
79
|
});
|
|
79
80
|
|
|
80
|
-
// Trigger button classes matching M3 design
|
|
81
81
|
const triggerClasses = $derived(() => {
|
|
82
82
|
const sizeStyles = sizeClass();
|
|
83
83
|
const baseClasses =
|
|
84
|
-
"w-full bg-
|
|
84
|
+
"w-full bg-input hover:bg-input-hover focus:bg-input-focus disabled:bg-input-disabled rounded-lg transition-all duration-200 text-body focus:outline-none focus:ring-offset-0 disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-between";
|
|
85
85
|
|
|
86
86
|
return `${baseClasses} ${sizeStyles.padding} ${sizeStyles.text} ${sizeStyles.leading} ${variantClass()}`.trim();
|
|
87
87
|
});
|
|
88
88
|
|
|
89
|
-
// Label classes using semantic text colors
|
|
90
89
|
const labelClasses = $derived(
|
|
91
90
|
() => "block text-sm font-medium text-label mb-1",
|
|
92
91
|
);
|
|
93
92
|
|
|
94
|
-
// Message classes based on variant
|
|
95
93
|
const messageClasses = $derived(() => {
|
|
96
94
|
if (variant === "error") {
|
|
97
95
|
return "text-error text-sm mt-1 flex items-center gap-1.5";
|
|
@@ -103,7 +101,6 @@
|
|
|
103
101
|
return "text-description text-sm mt-1 flex items-center gap-1.5";
|
|
104
102
|
});
|
|
105
103
|
|
|
106
|
-
// Get icon component based on variant
|
|
107
104
|
const getIcon = $derived(() => {
|
|
108
105
|
if (variant === "error") return AlertCircle;
|
|
109
106
|
if (variant === "success") return CheckCircle;
|
|
@@ -111,19 +108,18 @@
|
|
|
111
108
|
return null;
|
|
112
109
|
});
|
|
113
110
|
|
|
114
|
-
// Generate unique ID - SSR safe
|
|
115
111
|
const selectId = generateId("select");
|
|
116
112
|
|
|
117
|
-
// Get selected option label
|
|
118
113
|
const selectedLabel = $derived(() => {
|
|
119
114
|
if (isEmpty()) {
|
|
120
115
|
return String(placeholder || "Select an option");
|
|
121
116
|
}
|
|
122
117
|
const selected = options.find((opt) => opt.value === value);
|
|
123
|
-
return selected?.label
|
|
118
|
+
return selected?.label
|
|
119
|
+
? String(selected.label)
|
|
120
|
+
: String(placeholder || "Select an option");
|
|
124
121
|
});
|
|
125
122
|
|
|
126
|
-
// Check if value is empty
|
|
127
123
|
const isEmpty = $derived(() => {
|
|
128
124
|
return value === undefined || value === null || value === "";
|
|
129
125
|
});
|
|
@@ -133,12 +129,11 @@
|
|
|
133
129
|
value = optionValue;
|
|
134
130
|
isOpen = false;
|
|
135
131
|
|
|
136
|
-
// Create a synthetic event for onchange
|
|
137
132
|
if (onchange) {
|
|
138
133
|
const syntheticEvent = new Event("change", { bubbles: true });
|
|
139
134
|
Object.defineProperty(syntheticEvent, "target", {
|
|
140
135
|
value: { value: optionValue },
|
|
141
|
-
enumerable: true
|
|
136
|
+
enumerable: true,
|
|
142
137
|
});
|
|
143
138
|
(onchange as (event: Event) => void)(syntheticEvent);
|
|
144
139
|
}
|
|
@@ -150,14 +145,15 @@
|
|
|
150
145
|
isOpen = !isOpen;
|
|
151
146
|
}
|
|
152
147
|
|
|
153
|
-
// Close dropdown when clicking outside
|
|
154
148
|
function handleClickOutside(event: MouseEvent) {
|
|
155
|
-
if (
|
|
149
|
+
if (
|
|
150
|
+
isOpen &&
|
|
151
|
+
!(event.target as HTMLElement).closest(".select-container")
|
|
152
|
+
) {
|
|
156
153
|
isOpen = false;
|
|
157
154
|
}
|
|
158
155
|
}
|
|
159
156
|
|
|
160
|
-
// Handle escape key
|
|
161
157
|
function handleKeydown(event: KeyboardEvent) {
|
|
162
158
|
if (event.key === "Escape" && isOpen) {
|
|
163
159
|
isOpen = false;
|
|
@@ -172,7 +168,7 @@
|
|
|
172
168
|
<label for={selectId} class={labelClasses()}>{label}</label>
|
|
173
169
|
{/if}
|
|
174
170
|
|
|
175
|
-
<Dropdown
|
|
171
|
+
<Dropdown {isOpen} placement="bottom-start">
|
|
176
172
|
{#snippet trigger()}
|
|
177
173
|
<button
|
|
178
174
|
type="button"
|
|
@@ -184,12 +180,21 @@
|
|
|
184
180
|
aria-expanded={isOpen}
|
|
185
181
|
aria-describedby={message ? `${selectId}-message` : undefined}
|
|
186
182
|
>
|
|
187
|
-
<span
|
|
188
|
-
{isEmpty()
|
|
183
|
+
<span
|
|
184
|
+
class="text-left flex-1 {isEmpty()
|
|
185
|
+
? 'text-description'
|
|
186
|
+
: 'text-body'}"
|
|
187
|
+
>
|
|
188
|
+
{isEmpty()
|
|
189
|
+
? placeholder
|
|
190
|
+
: options.find((opt) => opt.value === value)?.label ||
|
|
191
|
+
placeholder}
|
|
189
192
|
</span>
|
|
190
193
|
<ChevronDown
|
|
191
194
|
size={20}
|
|
192
|
-
class="text-description transition-transform duration-200 {isOpen
|
|
195
|
+
class="text-description transition-transform duration-200 {isOpen
|
|
196
|
+
? 'rotate-180'
|
|
197
|
+
: ''}"
|
|
193
198
|
/>
|
|
194
199
|
</button>
|
|
195
200
|
{/snippet}
|
|
@@ -198,7 +203,11 @@
|
|
|
198
203
|
{#each options as option (option.value)}
|
|
199
204
|
<button
|
|
200
205
|
type="button"
|
|
201
|
-
class="w-full text-left px-4 py-2 text-body hover:bg-
|
|
206
|
+
class="w-full text-left px-4 py-2 text-body hover:bg-base-100 transition-colors rounded-md my-0.5 {option.disabled
|
|
207
|
+
? 'opacity-50 cursor-not-allowed'
|
|
208
|
+
: 'cursor-pointer'} {value === option.value
|
|
209
|
+
? 'bg-base-100'
|
|
210
|
+
: ''}"
|
|
202
211
|
onclick={() => handleOptionClick(option.value)}
|
|
203
212
|
disabled={option.disabled}
|
|
204
213
|
>
|
|
@@ -217,4 +226,4 @@
|
|
|
217
226
|
<span>{message}</span>
|
|
218
227
|
</p>
|
|
219
228
|
{/if}
|
|
220
|
-
</div>
|
|
229
|
+
</div>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { CheckCircle, AlertTriangle, AlertCircle } from "@lucide/svelte";
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
import type { SemanticVariant, SizeVariant } from "../../types/variants.js";
|
|
4
|
+
|
|
5
5
|
function generateId(prefix: string = "id"): string {
|
|
6
6
|
if (typeof window !== "undefined") {
|
|
7
7
|
return `${prefix}-${Math.random().toString(36).substr(2, 9)}`;
|
|
@@ -17,7 +17,8 @@
|
|
|
17
17
|
placeholder?: string;
|
|
18
18
|
disabled?: boolean;
|
|
19
19
|
rows?: number;
|
|
20
|
-
|
|
20
|
+
size?: SizeVariant;
|
|
21
|
+
variant?: SemanticVariant;
|
|
21
22
|
message?: string;
|
|
22
23
|
oninput?: (event: Event) => void;
|
|
23
24
|
}
|
|
@@ -35,10 +36,8 @@
|
|
|
35
36
|
...restProps
|
|
36
37
|
}: Props = $props();
|
|
37
38
|
|
|
38
|
-
// Generate unique ID - SSR safe (call directly, not in $state)
|
|
39
39
|
const textareaId = generateId("textarea");
|
|
40
40
|
|
|
41
|
-
// Variant classes using semantic colors
|
|
42
41
|
const variantClass = $derived(() => {
|
|
43
42
|
return variant === "success"
|
|
44
43
|
? "border-success focus:border-success focus:ring-success"
|
|
@@ -49,20 +48,17 @@
|
|
|
49
48
|
: "border-0 focus:ring-2 focus:ring-brand-500"; // default - no border
|
|
50
49
|
});
|
|
51
50
|
|
|
52
|
-
// Textarea classes matching M3 design
|
|
53
51
|
const textareaClasses = $derived(() => {
|
|
54
52
|
const baseClasses =
|
|
55
|
-
"w-full bg-
|
|
53
|
+
"w-full bg-input hover:bg-input-hover focus:bg-input-focus disabled:bg-input-disabled rounded-lg transition-all duration-200 placeholder:text-description text-body focus:outline-none focus:ring-offset-0 disabled:opacity-50 disabled:cursor-not-allowed resize-y px-4 py-2.5 text-base leading-6";
|
|
56
54
|
|
|
57
55
|
return `${baseClasses} ${variantClass()}`.trim();
|
|
58
56
|
});
|
|
59
57
|
|
|
60
|
-
// Label classes using semantic text colors
|
|
61
58
|
const labelClasses = $derived(
|
|
62
59
|
() => "block text-sm font-medium text-label mb-1",
|
|
63
60
|
);
|
|
64
61
|
|
|
65
|
-
// Message classes based on variant
|
|
66
62
|
const messageClasses = $derived(() => {
|
|
67
63
|
if (variant === "error") {
|
|
68
64
|
return "text-error text-sm mt-1 flex items-center gap-1.5";
|
|
@@ -74,7 +70,6 @@
|
|
|
74
70
|
return "text-description text-sm mt-1 flex items-center gap-1.5";
|
|
75
71
|
});
|
|
76
72
|
|
|
77
|
-
// Get icon component based on variant
|
|
78
73
|
const getIcon = $derived(() => {
|
|
79
74
|
if (variant === "error") return AlertCircle;
|
|
80
75
|
if (variant === "success") return CheckCircle;
|
|
@@ -86,7 +81,6 @@
|
|
|
86
81
|
const target = event.target as HTMLTextAreaElement;
|
|
87
82
|
value = target.value;
|
|
88
83
|
|
|
89
|
-
// Call the parent's oninput handler if provided
|
|
90
84
|
if (oninput) {
|
|
91
85
|
oninput(event);
|
|
92
86
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { SemanticVariant, SizeVariant } from "../../types/variants.js";
|
|
1
2
|
interface Props {
|
|
2
3
|
value?: string;
|
|
3
4
|
name?: string;
|
|
@@ -5,7 +6,8 @@ interface Props {
|
|
|
5
6
|
placeholder?: string;
|
|
6
7
|
disabled?: boolean;
|
|
7
8
|
rows?: number;
|
|
8
|
-
|
|
9
|
+
size?: SizeVariant;
|
|
10
|
+
variant?: SemanticVariant;
|
|
9
11
|
message?: string;
|
|
10
12
|
oninput?: (event: Event) => void;
|
|
11
13
|
}
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
import { onMount } from "svelte";
|
|
3
3
|
import { Sun, Moon } from "@lucide/svelte";
|
|
4
4
|
|
|
5
|
-
// SSR-safe utilities
|
|
6
5
|
function safeLocalStorage(): Storage | undefined {
|
|
7
6
|
return typeof window !== "undefined" ? localStorage : undefined;
|
|
8
7
|
}
|
|
@@ -28,7 +27,6 @@
|
|
|
28
27
|
|
|
29
28
|
onMount(() => {
|
|
30
29
|
mounted = true;
|
|
31
|
-
// Check for saved theme preference or default to light mode
|
|
32
30
|
const storage = safeLocalStorage();
|
|
33
31
|
if (storage) {
|
|
34
32
|
const savedTheme = storage.getItem("theme");
|
|
@@ -71,7 +69,6 @@
|
|
|
71
69
|
}
|
|
72
70
|
}
|
|
73
71
|
|
|
74
|
-
// Size classes matching M3 design
|
|
75
72
|
const sizeClass = $derived(() => {
|
|
76
73
|
if (size === "sm") {
|
|
77
74
|
return {
|
|
@@ -84,7 +81,6 @@
|
|
|
84
81
|
icon: 24
|
|
85
82
|
};
|
|
86
83
|
} else {
|
|
87
|
-
// default md
|
|
88
84
|
return {
|
|
89
85
|
button: "w-10 h-10",
|
|
90
86
|
icon: 20
|
|
@@ -92,15 +88,13 @@
|
|
|
92
88
|
}
|
|
93
89
|
});
|
|
94
90
|
|
|
95
|
-
// Variant classes using semantic colors
|
|
96
91
|
const variantClass = $derived(() => {
|
|
97
92
|
if (variant === "ghost") {
|
|
98
|
-
return "bg-transparent hover:bg-
|
|
93
|
+
return "bg-transparent hover:bg-base-100 border-0";
|
|
99
94
|
} else if (variant === "outline") {
|
|
100
|
-
return "bg-
|
|
95
|
+
return "bg-base-50 hover:bg-base-100 border border-border";
|
|
101
96
|
} else {
|
|
102
|
-
|
|
103
|
-
return "bg-surface-elevated hover:bg-surface-hover border-0";
|
|
97
|
+
return "bg-base-50 hover:bg-base-100 border-0";
|
|
104
98
|
}
|
|
105
99
|
});
|
|
106
100
|
|
|
@@ -121,7 +115,7 @@
|
|
|
121
115
|
focus:ring-2
|
|
122
116
|
focus:ring-brand-500
|
|
123
117
|
focus:ring-offset-2
|
|
124
|
-
focus:ring-offset-
|
|
118
|
+
focus:ring-offset-base-50
|
|
125
119
|
`.trim().replace(/\s+/g, " ");
|
|
126
120
|
});
|
|
127
121
|
</script>
|
|
@@ -141,9 +135,8 @@
|
|
|
141
135
|
{/if}
|
|
142
136
|
</button>
|
|
143
137
|
{:else}
|
|
144
|
-
<!-- SSR fallback -->
|
|
145
138
|
<button
|
|
146
|
-
class="w-10 h-10 bg-
|
|
139
|
+
class="w-10 h-10 bg-base-50 rounded-lg flex items-center justify-center text-label cursor-pointer"
|
|
147
140
|
aria-label="Theme toggle"
|
|
148
141
|
type="button"
|
|
149
142
|
{...restProps}
|
package/dist/atoms/Toast.svelte
CHANGED
|
@@ -16,7 +16,6 @@
|
|
|
16
16
|
|
|
17
17
|
let isVisible = $state(true);
|
|
18
18
|
|
|
19
|
-
// Simple type classes
|
|
20
19
|
const typeClasses = {
|
|
21
20
|
success: "bg-green-100 border-green-300 text-success",
|
|
22
21
|
error: "bg-red-100 border-red-300 text-error",
|
|
@@ -46,7 +45,7 @@
|
|
|
46
45
|
{#if closable}
|
|
47
46
|
<button
|
|
48
47
|
type="button"
|
|
49
|
-
class="ml-3 text-
|
|
48
|
+
class="ml-3 text-base-400 hover:text-base-600"
|
|
50
49
|
onclick={closeToast}
|
|
51
50
|
aria-label="Close notification"
|
|
52
51
|
>
|
package/dist/atoms/Toggle.svelte
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
// SSR-safe ID generation
|
|
3
2
|
function generateId(prefix: string = "id"): string {
|
|
4
3
|
if (typeof window !== "undefined") {
|
|
5
4
|
return `${prefix}-${Math.random().toString(36).substr(2, 9)}`;
|
|
@@ -8,7 +7,6 @@
|
|
|
8
7
|
}
|
|
9
8
|
}
|
|
10
9
|
|
|
11
|
-
// Props using Svelte 5 runes
|
|
12
10
|
interface Props {
|
|
13
11
|
checked?: boolean;
|
|
14
12
|
disabled?: boolean;
|
|
@@ -26,40 +24,34 @@
|
|
|
26
24
|
...restProps
|
|
27
25
|
}: Props = $props();
|
|
28
26
|
|
|
29
|
-
// SSR-safe ID generation (call directly, not in onMount)
|
|
30
27
|
const toggleId = generateId("toggle");
|
|
31
28
|
|
|
32
|
-
// Handle click
|
|
33
29
|
function handleClick(event: MouseEvent) {
|
|
34
30
|
if (disabled) return;
|
|
35
31
|
checked = !checked;
|
|
36
32
|
|
|
37
|
-
// Call parent handlers if provided
|
|
38
33
|
if (onclick) onclick(event);
|
|
39
34
|
if (onchange) onchange({ checked });
|
|
40
35
|
}
|
|
41
36
|
|
|
42
|
-
// Toggle button classes matching M3 design
|
|
43
37
|
const toggleButtonClasses = $derived(() =>
|
|
44
38
|
[
|
|
45
39
|
"relative inline-flex w-10 h-6 flex-shrink-0 cursor-pointer rounded-full border-0 transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-brand-500 focus:ring-offset-2",
|
|
46
|
-
checked ? "bg-
|
|
40
|
+
checked ? "bg-action-primary" : "bg-base-400",
|
|
47
41
|
disabled && "opacity-50 cursor-not-allowed",
|
|
48
42
|
]
|
|
49
43
|
.filter(Boolean)
|
|
50
44
|
.join(" "),
|
|
51
45
|
);
|
|
52
46
|
|
|
53
|
-
// Toggle thumb classes matching M3 design
|
|
54
47
|
const toggleThumbClasses = $derived(() => {
|
|
55
48
|
const baseClasses =
|
|
56
|
-
"pointer-events-none absolute top-0.5 left-0.5 w-5 h-5 rounded-full bg-
|
|
49
|
+
"pointer-events-none absolute top-0.5 left-0.5 w-5 h-5 rounded-full bg-card transition-transform duration-200 ease-in-out";
|
|
57
50
|
const positionClasses = checked ? "translate-x-4" : "translate-x-0";
|
|
58
51
|
|
|
59
52
|
return `${baseClasses} ${positionClasses}`.trim();
|
|
60
53
|
});
|
|
61
54
|
|
|
62
|
-
// Handle keyboard
|
|
63
55
|
function handleKeydown(event: KeyboardEvent) {
|
|
64
56
|
if (disabled) return;
|
|
65
57
|
if (event.key === " " || event.key === "Enter") {
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { onMount } from "svelte";
|
|
3
|
+
|
|
2
4
|
interface Props {
|
|
3
5
|
content?: string;
|
|
4
6
|
placement?: "top" | "bottom" | "left" | "right";
|
|
@@ -14,22 +16,79 @@
|
|
|
14
16
|
children,
|
|
15
17
|
...restProps
|
|
16
18
|
}: Props & { children?: any } = $props();
|
|
19
|
+
|
|
20
|
+
let triggerId = `tooltip-trigger-${Math.random().toString(36).substr(2, 9)}`;
|
|
21
|
+
let tooltipId = `tooltip-${Math.random().toString(36).substr(2, 9)}`;
|
|
22
|
+
let isVisible = $state(false);
|
|
23
|
+
let triggerElement: HTMLElement | null = $state(null);
|
|
24
|
+
|
|
25
|
+
function handleKeydown(event: KeyboardEvent) {
|
|
26
|
+
if (event.key === "Escape" && isVisible) {
|
|
27
|
+
isVisible = false;
|
|
28
|
+
triggerElement?.focus();
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function handleFocus() {
|
|
33
|
+
if (!disabled && content) {
|
|
34
|
+
isVisible = true;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function handleBlur() {
|
|
39
|
+
setTimeout(() => {
|
|
40
|
+
isVisible = false;
|
|
41
|
+
}, 100);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function handleMouseEnter() {
|
|
45
|
+
if (!disabled && content) {
|
|
46
|
+
if (delay > 0) {
|
|
47
|
+
setTimeout(() => {
|
|
48
|
+
isVisible = true;
|
|
49
|
+
}, delay);
|
|
50
|
+
} else {
|
|
51
|
+
isVisible = true;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function handleMouseLeave() {
|
|
57
|
+
isVisible = false;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
onMount(() => {
|
|
61
|
+
if (typeof window !== "undefined") {
|
|
62
|
+
window.addEventListener("keydown", handleKeydown);
|
|
63
|
+
return () => {
|
|
64
|
+
window.removeEventListener("keydown", handleKeydown);
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
});
|
|
17
68
|
</script>
|
|
18
69
|
|
|
19
70
|
<div
|
|
20
|
-
class="tooltip-container
|
|
71
|
+
class="tooltip-container relative inline-block"
|
|
21
72
|
data-placement={placement}
|
|
22
|
-
data-delay={delay}
|
|
23
73
|
data-disabled={disabled}
|
|
74
|
+
onmouseenter={handleMouseEnter}
|
|
75
|
+
onmouseleave={handleMouseLeave}
|
|
76
|
+
onfocusin={handleFocus}
|
|
77
|
+
onfocusout={handleBlur}
|
|
24
78
|
{...restProps}
|
|
25
79
|
>
|
|
26
|
-
{
|
|
80
|
+
<div bind:this={triggerElement} id={triggerId} aria-describedby={isVisible ? tooltipId : undefined}>
|
|
81
|
+
{@render children?.()}
|
|
82
|
+
</div>
|
|
27
83
|
|
|
28
84
|
{#if content && !disabled}
|
|
29
85
|
<div
|
|
30
|
-
|
|
86
|
+
id={tooltipId}
|
|
87
|
+
class="tooltip"
|
|
31
88
|
role="tooltip"
|
|
32
|
-
aria-hidden=
|
|
89
|
+
aria-hidden={!isVisible}
|
|
90
|
+
data-visible={isVisible}
|
|
91
|
+
data-placement={placement}
|
|
33
92
|
>
|
|
34
93
|
{content}
|
|
35
94
|
</div>
|
|
@@ -48,7 +107,7 @@
|
|
|
48
107
|
--tooltip-gap: 0.5rem;
|
|
49
108
|
--tooltip-arrow-size: 4px;
|
|
50
109
|
--tooltip-transition: opacity 0.2s ease-in-out,
|
|
51
|
-
visibility 0.2s ease-in-out;
|
|
110
|
+
visibility 0.2s ease-in-out, transform 0.2s ease-in-out;
|
|
52
111
|
}
|
|
53
112
|
|
|
54
113
|
.tooltip-container {
|
|
@@ -58,7 +117,7 @@
|
|
|
58
117
|
|
|
59
118
|
.tooltip {
|
|
60
119
|
position: absolute;
|
|
61
|
-
z-index:
|
|
120
|
+
z-index: var(--z-tooltip, 1070);
|
|
62
121
|
padding: var(--tooltip-padding);
|
|
63
122
|
background-color: var(--tooltip-bg);
|
|
64
123
|
color: var(--tooltip-color);
|
|
@@ -72,42 +131,60 @@
|
|
|
72
131
|
visibility: hidden;
|
|
73
132
|
transition: var(--tooltip-transition);
|
|
74
133
|
pointer-events: none;
|
|
75
|
-
/* Use logical properties for better internationalization */
|
|
76
|
-
inset-inline-start: 50%;
|
|
77
|
-
inset-block-start: 50%;
|
|
78
|
-
transform: translate(-50%, -50%);
|
|
79
134
|
}
|
|
80
135
|
|
|
81
|
-
/* Simplified positioning using CSS custom properties and logical properties */
|
|
82
136
|
.tooltip-container[data-placement="top"] .tooltip {
|
|
83
137
|
inset-block-end: 100%;
|
|
84
138
|
inset-inline-start: 50%;
|
|
85
|
-
transform: translateX(-50%);
|
|
139
|
+
transform: translateX(-50%) translateY(4px) scale(0.95);
|
|
86
140
|
margin-block-end: var(--tooltip-gap);
|
|
87
141
|
}
|
|
88
142
|
|
|
143
|
+
.tooltip-container[data-placement="top"] .tooltip[data-visible="true"] {
|
|
144
|
+
opacity: 1;
|
|
145
|
+
visibility: visible;
|
|
146
|
+
transform: translateX(-50%) translateY(0) scale(1);
|
|
147
|
+
}
|
|
148
|
+
|
|
89
149
|
.tooltip-container[data-placement="bottom"] .tooltip {
|
|
90
150
|
inset-block-start: 100%;
|
|
91
151
|
inset-inline-start: 50%;
|
|
92
|
-
transform: translateX(-50%);
|
|
152
|
+
transform: translateX(-50%) translateY(-4px) scale(0.95);
|
|
93
153
|
margin-block-start: var(--tooltip-gap);
|
|
94
154
|
}
|
|
95
155
|
|
|
156
|
+
.tooltip-container[data-placement="bottom"] .tooltip[data-visible="true"] {
|
|
157
|
+
opacity: 1;
|
|
158
|
+
visibility: visible;
|
|
159
|
+
transform: translateX(-50%) translateY(0) scale(1);
|
|
160
|
+
}
|
|
161
|
+
|
|
96
162
|
.tooltip-container[data-placement="left"] .tooltip {
|
|
97
163
|
inset-inline-end: 100%;
|
|
98
164
|
inset-block-start: 50%;
|
|
99
|
-
transform: translateY(-50%);
|
|
165
|
+
transform: translateY(-50%) translateX(4px) scale(0.95);
|
|
100
166
|
margin-inline-end: var(--tooltip-gap);
|
|
101
167
|
}
|
|
102
168
|
|
|
169
|
+
.tooltip-container[data-placement="left"] .tooltip[data-visible="true"] {
|
|
170
|
+
opacity: 1;
|
|
171
|
+
visibility: visible;
|
|
172
|
+
transform: translateY(-50%) translateX(0) scale(1);
|
|
173
|
+
}
|
|
174
|
+
|
|
103
175
|
.tooltip-container[data-placement="right"] .tooltip {
|
|
104
176
|
inset-inline-start: 100%;
|
|
105
177
|
inset-block-start: 50%;
|
|
106
|
-
transform: translateY(-50%);
|
|
178
|
+
transform: translateY(-50%) translateX(-4px) scale(0.95);
|
|
107
179
|
margin-inline-start: var(--tooltip-gap);
|
|
108
180
|
}
|
|
109
181
|
|
|
110
|
-
|
|
182
|
+
.tooltip-container[data-placement="right"] .tooltip[data-visible="true"] {
|
|
183
|
+
opacity: 1;
|
|
184
|
+
visibility: visible;
|
|
185
|
+
transform: translateY(-50%) translateX(0) scale(1);
|
|
186
|
+
}
|
|
187
|
+
|
|
111
188
|
.tooltip::before {
|
|
112
189
|
content: "";
|
|
113
190
|
position: absolute;
|
|
@@ -134,55 +211,75 @@
|
|
|
134
211
|
inset-inline-start: 100%;
|
|
135
212
|
inset-block-start: 50%;
|
|
136
213
|
transform: translateY(-50%);
|
|
137
|
-
clip-path: polygon(0
|
|
214
|
+
clip-path: polygon(0 0, 100% 50%, 0 100%);
|
|
138
215
|
}
|
|
139
216
|
|
|
140
217
|
.tooltip-container[data-placement="right"] .tooltip::before {
|
|
141
218
|
inset-inline-end: 100%;
|
|
142
219
|
inset-block-start: 50%;
|
|
143
220
|
transform: translateY(-50%);
|
|
144
|
-
clip-path: polygon(0
|
|
221
|
+
clip-path: polygon(0 50%, 100% 0, 100% 100%);
|
|
145
222
|
}
|
|
146
223
|
|
|
147
|
-
/* Delay support using CSS custom properties */
|
|
148
224
|
.tooltip-container[data-delay] .tooltip {
|
|
149
225
|
transition-delay: calc(var(--tooltip-delay, 0) * 1ms);
|
|
150
226
|
}
|
|
151
227
|
|
|
152
|
-
/* Responsive design with modern CSS */
|
|
153
228
|
@media (max-width: 640px) {
|
|
154
229
|
.tooltip {
|
|
155
230
|
--tooltip-max-width: calc(100vw - 2rem);
|
|
156
231
|
inset-inline-start: 50% !important;
|
|
157
232
|
inset-inline-end: auto !important;
|
|
158
|
-
transform: translateX(-50%) !important;
|
|
233
|
+
transform: translateX(-50%) scale(0.95) !important;
|
|
159
234
|
margin: var(--tooltip-gap) 0 !important;
|
|
160
235
|
}
|
|
161
236
|
|
|
237
|
+
.tooltip[data-visible="true"] {
|
|
238
|
+
transform: translateX(-50%) scale(1) !important;
|
|
239
|
+
}
|
|
240
|
+
|
|
162
241
|
.tooltip::before {
|
|
163
242
|
display: none;
|
|
164
243
|
}
|
|
165
244
|
}
|
|
166
245
|
|
|
167
|
-
/* Dark mode support */
|
|
168
246
|
@media (prefers-color-scheme: dark) {
|
|
169
247
|
:root {
|
|
170
248
|
--tooltip-bg: #374151;
|
|
171
249
|
}
|
|
172
250
|
}
|
|
173
251
|
|
|
174
|
-
|
|
252
|
+
.dark :root {
|
|
253
|
+
--tooltip-bg: #374151;
|
|
254
|
+
}
|
|
255
|
+
|
|
175
256
|
@media (prefers-contrast: high) {
|
|
176
257
|
:root {
|
|
177
258
|
--tooltip-bg: #000000;
|
|
178
259
|
--tooltip-color: #ffffff;
|
|
260
|
+
--tooltip-padding: 0.75rem 1rem;
|
|
179
261
|
}
|
|
180
262
|
}
|
|
181
263
|
|
|
182
|
-
/* Reduced motion support */
|
|
183
264
|
@media (prefers-reduced-motion: reduce) {
|
|
184
265
|
.tooltip {
|
|
185
|
-
transition:
|
|
266
|
+
transition: opacity 0.1s ease-in-out, visibility 0.1s ease-in-out;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
.tooltip-container[data-placement="top"] .tooltip[data-visible="true"],
|
|
270
|
+
.tooltip-container[data-placement="bottom"] .tooltip[data-visible="true"] {
|
|
271
|
+
transform: translateX(-50%) !important;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
.tooltip-container[data-placement="left"] .tooltip[data-visible="true"],
|
|
275
|
+
.tooltip-container[data-placement="right"] .tooltip[data-visible="true"] {
|
|
276
|
+
transform: translateY(-50%) !important;
|
|
186
277
|
}
|
|
187
278
|
}
|
|
279
|
+
|
|
280
|
+
.tooltip-container:focus-visible {
|
|
281
|
+
outline: 2px solid var(--color-focus, #8fa8ff);
|
|
282
|
+
outline-offset: 2px;
|
|
283
|
+
border-radius: var(--tooltip-radius);
|
|
284
|
+
}
|
|
188
285
|
</style>
|