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.
Files changed (66) hide show
  1. package/README.md +175 -23
  2. package/dist/SSRSafe.svelte +0 -3
  3. package/dist/atoms/Badge.svelte +32 -47
  4. package/dist/atoms/Badge.svelte.d.ts +6 -5
  5. package/dist/atoms/Button.svelte +18 -17
  6. package/dist/atoms/Button.svelte.d.ts +3 -2
  7. package/dist/atoms/Card.svelte +74 -23
  8. package/dist/atoms/Card.svelte.d.ts +3 -1
  9. package/dist/atoms/CardContent.svelte +24 -0
  10. package/dist/atoms/CardContent.svelte.d.ts +8 -0
  11. package/dist/atoms/CardDescription.svelte +24 -0
  12. package/dist/atoms/CardDescription.svelte.d.ts +8 -0
  13. package/dist/atoms/CardFooter.svelte +24 -0
  14. package/dist/atoms/CardFooter.svelte.d.ts +8 -0
  15. package/dist/atoms/CardHeader.svelte +24 -0
  16. package/dist/atoms/CardHeader.svelte.d.ts +8 -0
  17. package/dist/atoms/CardTitle.svelte +33 -0
  18. package/dist/atoms/CardTitle.svelte.d.ts +9 -0
  19. package/dist/atoms/Checkbox.svelte +19 -20
  20. package/dist/atoms/CodeBlock.svelte +4 -7
  21. package/dist/atoms/ColorPicker.svelte +33 -65
  22. package/dist/atoms/FeatureCard.svelte +2 -12
  23. package/dist/atoms/FeatureCard.svelte.d.ts +0 -8
  24. package/dist/atoms/Heading.svelte +0 -2
  25. package/dist/atoms/Input.svelte +8 -17
  26. package/dist/atoms/Input.svelte.d.ts +3 -2
  27. package/dist/atoms/Progress.svelte +1 -4
  28. package/dist/atoms/Select.svelte +37 -28
  29. package/dist/atoms/Skeleton.svelte +1 -1
  30. package/dist/atoms/Textarea.svelte +5 -11
  31. package/dist/atoms/Textarea.svelte.d.ts +3 -1
  32. package/dist/atoms/ThemeToggle.svelte +5 -12
  33. package/dist/atoms/Toast.svelte +1 -2
  34. package/dist/atoms/Toggle.svelte +2 -10
  35. package/dist/atoms/Tooltip.svelte +123 -26
  36. package/dist/atoms/index.d.ts +5 -0
  37. package/dist/atoms/index.js +5 -1
  38. package/dist/components/atoms/index.d.ts +5 -0
  39. package/dist/components/atoms/index.d.ts.map +1 -1
  40. package/dist/components/index.d.ts.map +1 -1
  41. package/dist/components/molecules/index.d.ts.map +1 -1
  42. package/dist/components/organisms/index.d.ts.map +1 -1
  43. package/dist/index.js +0 -23
  44. package/dist/molecules/Alert.svelte +9 -13
  45. package/dist/molecules/Alert.svelte.d.ts +2 -1
  46. package/dist/molecules/ComponentDemo.svelte +55 -64
  47. package/dist/molecules/ContactForm.svelte +21 -12
  48. package/dist/molecules/Dropdown.svelte +104 -8
  49. package/dist/molecules/Dropdown.svelte.d.ts +1 -0
  50. package/dist/molecules/Form.svelte +0 -1
  51. package/dist/molecules/ImageUpload.svelte +2 -6
  52. package/dist/molecules/Modal.svelte +30 -6
  53. package/dist/molecules/SlideUp.svelte +2 -4
  54. package/dist/molecules/Tabs.svelte +2 -4
  55. package/dist/molecules/index.js +0 -1
  56. package/dist/organisms/Navbar.svelte +1 -8
  57. package/dist/organisms/Navigation.svelte +0 -6
  58. package/dist/organisms/index.js +0 -1
  59. package/dist/routes/lib/variant-utils.ts +7 -5
  60. package/dist/zabi-components-colors.css +287 -211
  61. package/dist/zabi-components-theme-dark-only.css +158 -0
  62. package/dist/zabi-components-theme-dark.css +160 -0
  63. package/dist/zabi-components-theme-only.css +235 -0
  64. package/dist/zabi-components-theme.css +237 -0
  65. package/dist/zabi-components.css +575 -360
  66. package/package.json +15 -5
@@ -1,8 +1,12 @@
1
1
  <script lang="ts">
2
2
  import Dropdown from "../molecules/Dropdown.svelte";
3
- import { ChevronDown, CheckCircle, AlertTriangle, AlertCircle } from "@lucide/svelte";
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-brand-100 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";
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 ? String(selected.label) : String(placeholder || "Select an option");
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 (isOpen && !(event.target as HTMLElement).closest('.select-container')) {
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 isOpen={isOpen} placement="bottom-start">
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 class="text-left flex-1 {isEmpty() ? 'text-description' : 'text-body'}">
188
- {isEmpty() ? placeholder : (options.find((opt) => opt.value === value)?.label || placeholder)}
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 ? 'rotate-180' : ''}"
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-brand-50 transition-colors rounded-md my-0.5 {option.disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'} {value === option.value ? 'bg-brand-50' : ''}"
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>
@@ -14,7 +14,7 @@
14
14
  </script>
15
15
 
16
16
  <div
17
- class="animate-pulse bg-gray-200 rounded {className}"
17
+ class="animate-pulse bg-base-200 rounded {className}"
18
18
  style="width: {width}; height: {height};"
19
19
  {...restProps}
20
20
  ></div>
@@ -1,7 +1,7 @@
1
1
  <script lang="ts">
2
2
  import { CheckCircle, AlertTriangle, AlertCircle } from "@lucide/svelte";
3
-
4
- // SSR-safe ID generation
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
- variant?: "default" | "success" | "warning" | "error";
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-brand-100 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";
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
- variant?: "default" | "success" | "warning" | "error";
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-surface-hover border-0";
93
+ return "bg-transparent hover:bg-base-100 border-0";
99
94
  } else if (variant === "outline") {
100
- return "bg-surface-elevated hover:bg-surface-hover border border-border";
95
+ return "bg-base-50 hover:bg-base-100 border border-border";
101
96
  } else {
102
- // default
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-surface
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-surface-elevated rounded-lg flex items-center justify-center text-label cursor-pointer"
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}
@@ -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-gray-400 hover:text-gray-600"
48
+ class="ml-3 text-base-400 hover:text-base-600"
50
49
  onclick={closeToast}
51
50
  aria-label="Close notification"
52
51
  >
@@ -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-brand-600" : "bg-stone-300",
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-white transition-transform duration-200 ease-in-out";
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 group relative inline-block"
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
- {@render children?.()}
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
- class="tooltip group-hover:opacity-100 group-hover:visible group-focus-within:opacity-100 group-focus-within:visible"
86
+ id={tooltipId}
87
+ class="tooltip"
31
88
  role="tooltip"
32
- aria-hidden="true"
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: 50;
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
- /* Modern arrow implementation using CSS clip-path for better performance */
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 50%, 100% 0, 100% 100%);
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 0, 100% 50%, 0 100%);
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
- /* High contrast mode support */
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: none;
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>