@miozu/jera 0.5.0 → 0.6.0
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/package.json
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
<!--
|
|
2
2
|
@component Toast
|
|
3
3
|
|
|
4
|
-
A toast notification system
|
|
4
|
+
A toast notification system using native popover for top-layer rendering.
|
|
5
|
+
Provides stacking, auto-dismiss, and animations without z-index conflicts.
|
|
5
6
|
|
|
6
7
|
@example
|
|
7
8
|
// In your root layout
|
|
@@ -86,10 +87,11 @@
|
|
|
86
87
|
|
|
87
88
|
<script>
|
|
88
89
|
import { cn } from '../../utils/cn.svelte.js';
|
|
89
|
-
import { portal } from '../../actions/index.js';
|
|
90
90
|
|
|
91
91
|
const toast = getToastContext();
|
|
92
92
|
|
|
93
|
+
let containerEl = $state(null);
|
|
94
|
+
|
|
93
95
|
const icons = {
|
|
94
96
|
info: `<circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/>`,
|
|
95
97
|
success: `<circle cx="12" cy="12" r="10"/><path d="m9 12 2 2 4-4"/>`,
|
|
@@ -105,52 +107,82 @@
|
|
|
105
107
|
'bottom-center': 'toast-bottom-center',
|
|
106
108
|
'bottom-right': 'toast-bottom-right'
|
|
107
109
|
}[toast.position]);
|
|
110
|
+
|
|
111
|
+
// Show/hide popover based on toast count
|
|
112
|
+
$effect(() => {
|
|
113
|
+
if (!containerEl) return;
|
|
114
|
+
|
|
115
|
+
if (toast.toasts.length > 0 && !containerEl.matches(':popover-open')) {
|
|
116
|
+
containerEl.showPopover();
|
|
117
|
+
} else if (toast.toasts.length === 0 && containerEl.matches(':popover-open')) {
|
|
118
|
+
containerEl.hidePopover();
|
|
119
|
+
}
|
|
120
|
+
});
|
|
108
121
|
</script>
|
|
109
122
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
123
|
+
<!--
|
|
124
|
+
Using popover="manual" for:
|
|
125
|
+
- Automatic top-layer placement (no z-index wars)
|
|
126
|
+
- No portal action needed
|
|
127
|
+
- Native browser rendering optimization
|
|
128
|
+
-->
|
|
129
|
+
<div
|
|
130
|
+
bind:this={containerEl}
|
|
131
|
+
popover="manual"
|
|
132
|
+
class={cn('toast-container', positionClass)}
|
|
133
|
+
role="region"
|
|
134
|
+
aria-label="Notifications"
|
|
135
|
+
>
|
|
136
|
+
{#each toast.toasts as item (item.id)}
|
|
137
|
+
{@const remaining = item.duration - (Date.now() - item.createdAt)}
|
|
138
|
+
<div
|
|
139
|
+
class={cn('toast-item', `toast-${item.type}`)}
|
|
140
|
+
role="alert"
|
|
141
|
+
aria-live="polite"
|
|
142
|
+
onmouseenter={() => toast.pause(item.id)}
|
|
143
|
+
onmouseleave={() => toast.resume(item.id)}
|
|
144
|
+
>
|
|
145
|
+
<span class="toast-icon">
|
|
146
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
147
|
+
{@html icons[item.type]}
|
|
148
|
+
</svg>
|
|
149
|
+
</span>
|
|
150
|
+
<div class="toast-content">
|
|
151
|
+
{#if item.title}
|
|
152
|
+
<p class="toast-title">{item.title}</p>
|
|
139
153
|
{/if}
|
|
154
|
+
<p class={cn('toast-message', item.title && 'has-title')}>{item.message}</p>
|
|
140
155
|
</div>
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
156
|
+
<button type="button" class="toast-close" onclick={() => toast.dismiss(item.id)} aria-label="Dismiss">
|
|
157
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
158
|
+
<path d="M18 6 6 18" /><path d="m6 6 12 12" />
|
|
159
|
+
</svg>
|
|
160
|
+
</button>
|
|
161
|
+
{#if item.duration > 0 && !item.pausedAt}
|
|
162
|
+
{@const _ = setTimeout(() => toast.dismiss(item.id), remaining)}
|
|
163
|
+
{/if}
|
|
164
|
+
</div>
|
|
165
|
+
{/each}
|
|
166
|
+
</div>
|
|
144
167
|
|
|
145
168
|
<style>
|
|
169
|
+
/* Popover container - in top-layer, no z-index needed */
|
|
146
170
|
.toast-container {
|
|
147
171
|
position: fixed;
|
|
148
|
-
z-index: 9999;
|
|
149
172
|
display: flex;
|
|
150
173
|
gap: 0.5rem;
|
|
151
174
|
pointer-events: none;
|
|
175
|
+
/* Reset popover defaults */
|
|
176
|
+
border: none;
|
|
177
|
+
background: transparent;
|
|
178
|
+
padding: 0;
|
|
179
|
+
margin: 0;
|
|
180
|
+
overflow: visible;
|
|
181
|
+
/* Remove default popover positioning */
|
|
182
|
+
inset: unset;
|
|
152
183
|
}
|
|
153
184
|
|
|
185
|
+
/* Position variants */
|
|
154
186
|
.toast-top-left { top: 1rem; left: 1rem; flex-direction: column; }
|
|
155
187
|
.toast-top-center { top: 1rem; left: 50%; transform: translateX(-50%); flex-direction: column; }
|
|
156
188
|
.toast-top-right { top: 1rem; right: 1rem; flex-direction: column; }
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
<!--
|
|
2
2
|
@component Modal
|
|
3
3
|
|
|
4
|
-
A flexible modal dialog component
|
|
4
|
+
A flexible modal dialog component using native <dialog> element.
|
|
5
|
+
Provides built-in focus trap, ESC handling, backdrop, and accessibility.
|
|
5
6
|
|
|
6
7
|
@example
|
|
7
8
|
<Modal bind:open={showModal} title="Confirm Action">
|
|
@@ -13,9 +14,6 @@
|
|
|
13
14
|
</Modal>
|
|
14
15
|
-->
|
|
15
16
|
<script>
|
|
16
|
-
import { focusTrap, escapeKey, portal } from '../../actions/index.js';
|
|
17
|
-
import { cv } from '../../utils/cn.svelte.js';
|
|
18
|
-
|
|
19
17
|
let {
|
|
20
18
|
open = $bindable(false),
|
|
21
19
|
title = '',
|
|
@@ -31,6 +29,8 @@
|
|
|
31
29
|
class: className = ''
|
|
32
30
|
} = $props();
|
|
33
31
|
|
|
32
|
+
let dialogEl = $state(null);
|
|
33
|
+
|
|
34
34
|
// Variant styles for the icon container
|
|
35
35
|
const iconVariants = {
|
|
36
36
|
default: { bg: 'var(--color-base02)', color: 'var(--color-base05)' },
|
|
@@ -42,118 +42,170 @@
|
|
|
42
42
|
|
|
43
43
|
const iconStyle = $derived(iconVariants[variant] || iconVariants.default);
|
|
44
44
|
|
|
45
|
-
|
|
45
|
+
// Sync open state with native dialog
|
|
46
|
+
$effect(() => {
|
|
47
|
+
if (!dialogEl) return;
|
|
48
|
+
|
|
49
|
+
if (open && !dialogEl.open) {
|
|
50
|
+
dialogEl.showModal();
|
|
51
|
+
} else if (!open && dialogEl.open) {
|
|
52
|
+
dialogEl.close();
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// Handle native close event (ESC key, form[method=dialog], etc.)
|
|
57
|
+
function handleClose() {
|
|
46
58
|
open = false;
|
|
47
59
|
onclose();
|
|
48
60
|
}
|
|
49
61
|
|
|
62
|
+
// Handle cancel event (ESC key) - can be prevented
|
|
63
|
+
function handleCancel(e) {
|
|
64
|
+
if (!closeOnEscape) {
|
|
65
|
+
e.preventDefault();
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Handle backdrop click (click on dialog element itself, not content)
|
|
50
70
|
function handleBackdropClick(e) {
|
|
51
|
-
if (closeOnBackdrop && e.target ===
|
|
52
|
-
close();
|
|
71
|
+
if (closeOnBackdrop && e.target === dialogEl) {
|
|
72
|
+
dialogEl.close();
|
|
53
73
|
}
|
|
54
74
|
}
|
|
55
75
|
|
|
56
|
-
function
|
|
57
|
-
if (
|
|
58
|
-
close();
|
|
76
|
+
function close() {
|
|
77
|
+
if (dialogEl?.open) {
|
|
78
|
+
dialogEl.close();
|
|
59
79
|
}
|
|
60
80
|
}
|
|
61
81
|
</script>
|
|
62
82
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
<line x1="6" y1="6" x2="18" y2="18"></line>
|
|
86
|
-
</svg>
|
|
87
|
-
</button>
|
|
88
|
-
{/if}
|
|
89
|
-
|
|
90
|
-
<!-- Content -->
|
|
91
|
-
<div class="modal-content">
|
|
92
|
-
{#if icon || title}
|
|
93
|
-
<div class="modal-header">
|
|
94
|
-
{#if icon}
|
|
95
|
-
<div class="modal-icon" style="background: {iconStyle.bg}; color: {iconStyle.color};">
|
|
96
|
-
{@render icon()}
|
|
97
|
-
</div>
|
|
98
|
-
{/if}
|
|
83
|
+
<dialog
|
|
84
|
+
bind:this={dialogEl}
|
|
85
|
+
class="modal modal-{size} {className}"
|
|
86
|
+
aria-labelledby={title ? 'modal-title' : undefined}
|
|
87
|
+
onclose={handleClose}
|
|
88
|
+
oncancel={handleCancel}
|
|
89
|
+
onclick={handleBackdropClick}
|
|
90
|
+
>
|
|
91
|
+
<!-- Close button -->
|
|
92
|
+
{#if showClose}
|
|
93
|
+
<button
|
|
94
|
+
class="modal-close"
|
|
95
|
+
onclick={close}
|
|
96
|
+
aria-label="Close modal"
|
|
97
|
+
type="button"
|
|
98
|
+
>
|
|
99
|
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
100
|
+
<line x1="18" y1="6" x2="6" y2="18"></line>
|
|
101
|
+
<line x1="6" y1="6" x2="18" y2="18"></line>
|
|
102
|
+
</svg>
|
|
103
|
+
</button>
|
|
104
|
+
{/if}
|
|
99
105
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
{@render children()}
|
|
108
|
-
</div>
|
|
109
|
-
{/if}
|
|
110
|
-
</div>
|
|
111
|
-
{/if}
|
|
112
|
-
</div>
|
|
113
|
-
{:else if children}
|
|
114
|
-
<div class="modal-body">
|
|
115
|
-
{@render children()}
|
|
106
|
+
<!-- Content -->
|
|
107
|
+
<div class="modal-content">
|
|
108
|
+
{#if icon || title}
|
|
109
|
+
<div class="modal-header">
|
|
110
|
+
{#if icon}
|
|
111
|
+
<div class="modal-icon" style="background: {iconStyle.bg}; color: {iconStyle.color};">
|
|
112
|
+
{@render icon()}
|
|
116
113
|
</div>
|
|
117
114
|
{/if}
|
|
118
115
|
|
|
119
|
-
{#if
|
|
120
|
-
<div class="modal-
|
|
121
|
-
{
|
|
116
|
+
{#if title || children}
|
|
117
|
+
<div class="modal-text">
|
|
118
|
+
{#if title}
|
|
119
|
+
<h3 id="modal-title" class="modal-title">{title}</h3>
|
|
120
|
+
{/if}
|
|
121
|
+
{#if children}
|
|
122
|
+
<div class="modal-body">
|
|
123
|
+
{@render children()}
|
|
124
|
+
</div>
|
|
125
|
+
{/if}
|
|
122
126
|
</div>
|
|
123
127
|
{/if}
|
|
124
128
|
</div>
|
|
125
|
-
|
|
129
|
+
{:else if children}
|
|
130
|
+
<div class="modal-body">
|
|
131
|
+
{@render children()}
|
|
132
|
+
</div>
|
|
133
|
+
{/if}
|
|
134
|
+
|
|
135
|
+
{#if footer}
|
|
136
|
+
<div class="modal-footer">
|
|
137
|
+
{@render footer()}
|
|
138
|
+
</div>
|
|
139
|
+
{/if}
|
|
126
140
|
</div>
|
|
127
|
-
|
|
141
|
+
</dialog>
|
|
128
142
|
|
|
129
143
|
<style>
|
|
130
|
-
|
|
144
|
+
/* Native dialog element - automatically in top-layer */
|
|
145
|
+
dialog.modal {
|
|
131
146
|
position: fixed;
|
|
132
|
-
|
|
133
|
-
z-index: 100;
|
|
134
|
-
display: flex;
|
|
135
|
-
align-items: center;
|
|
136
|
-
justify-content: center;
|
|
137
|
-
background: color-mix(in srgb, var(--color-base00) 80%, transparent);
|
|
138
|
-
backdrop-filter: blur(4px);
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
.modal {
|
|
142
|
-
position: relative;
|
|
143
|
-
background: var(--color-base01);
|
|
147
|
+
border: none;
|
|
144
148
|
border-radius: 0.75rem;
|
|
149
|
+
background: var(--color-base01);
|
|
145
150
|
box-shadow: var(--shadow-2xl);
|
|
146
151
|
border: 1px solid var(--color-base03);
|
|
147
|
-
|
|
148
|
-
|
|
152
|
+
padding: 0;
|
|
153
|
+
margin: auto;
|
|
154
|
+
max-height: calc(100vh - 2rem);
|
|
155
|
+
overflow: auto;
|
|
149
156
|
}
|
|
150
157
|
|
|
151
158
|
/* Size variants */
|
|
152
|
-
.modal-sm { width: 100%; max-width: 20rem; }
|
|
153
|
-
.modal-md { width: 100%; max-width: 28rem; }
|
|
154
|
-
.modal-lg { width: 100%; max-width: 36rem; }
|
|
155
|
-
.modal-xl { width: 100%; max-width: 48rem; }
|
|
156
|
-
.modal-full { width:
|
|
159
|
+
dialog.modal-sm { width: 100%; max-width: 20rem; }
|
|
160
|
+
dialog.modal-md { width: 100%; max-width: 28rem; }
|
|
161
|
+
dialog.modal-lg { width: 100%; max-width: 36rem; }
|
|
162
|
+
dialog.modal-xl { width: 100%; max-width: 48rem; }
|
|
163
|
+
dialog.modal-full { width: calc(100vw - 2rem); max-width: calc(100vw - 2rem); }
|
|
164
|
+
|
|
165
|
+
/* Native backdrop - automatically handled by browser */
|
|
166
|
+
dialog.modal::backdrop {
|
|
167
|
+
background: color-mix(in srgb, var(--color-base00) 80%, transparent);
|
|
168
|
+
backdrop-filter: blur(4px);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/* Entry/exit animations using @starting-style */
|
|
172
|
+
dialog.modal[open] {
|
|
173
|
+
opacity: 1;
|
|
174
|
+
transform: scale(1) translateY(0);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
@starting-style {
|
|
178
|
+
dialog.modal[open] {
|
|
179
|
+
opacity: 0;
|
|
180
|
+
transform: scale(0.95) translateY(10px);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
dialog.modal[open]::backdrop {
|
|
185
|
+
opacity: 1;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
@starting-style {
|
|
189
|
+
dialog.modal[open]::backdrop {
|
|
190
|
+
opacity: 0;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/* Transitions for smooth open/close */
|
|
195
|
+
dialog.modal {
|
|
196
|
+
transition:
|
|
197
|
+
opacity 0.2s ease-out,
|
|
198
|
+
transform 0.2s ease-out,
|
|
199
|
+
overlay 0.2s ease-out allow-discrete,
|
|
200
|
+
display 0.2s ease-out allow-discrete;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
dialog.modal::backdrop {
|
|
204
|
+
transition:
|
|
205
|
+
opacity 0.2s ease-out,
|
|
206
|
+
overlay 0.2s ease-out allow-discrete,
|
|
207
|
+
display 0.2s ease-out allow-discrete;
|
|
208
|
+
}
|
|
157
209
|
|
|
158
210
|
.modal-close {
|
|
159
211
|
position: absolute;
|
|
@@ -166,6 +218,7 @@
|
|
|
166
218
|
color: var(--color-base05);
|
|
167
219
|
cursor: pointer;
|
|
168
220
|
transition: background 0.15s, color 0.15s;
|
|
221
|
+
z-index: 1;
|
|
169
222
|
}
|
|
170
223
|
|
|
171
224
|
.modal-close:hover {
|
|
@@ -218,15 +271,4 @@
|
|
|
218
271
|
margin-top: 1.5rem;
|
|
219
272
|
justify-content: flex-end;
|
|
220
273
|
}
|
|
221
|
-
|
|
222
|
-
@keyframes modal-enter {
|
|
223
|
-
from {
|
|
224
|
-
opacity: 0;
|
|
225
|
-
transform: scale(0.95) translateY(10px);
|
|
226
|
-
}
|
|
227
|
-
to {
|
|
228
|
-
opacity: 1;
|
|
229
|
-
transform: scale(1) translateY(0);
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
274
|
</style>
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
<!--
|
|
2
2
|
@component Popover
|
|
3
3
|
|
|
4
|
-
A tooltip/popover component with
|
|
4
|
+
A tooltip/popover component with CSS Anchor Positioning (with JS fallback).
|
|
5
|
+
Uses native popover API for top-layer rendering.
|
|
5
6
|
|
|
6
7
|
@example
|
|
7
8
|
<Popover content="This is helpful information" position="top">
|
|
@@ -28,14 +29,22 @@
|
|
|
28
29
|
class: className = ''
|
|
29
30
|
} = $props();
|
|
30
31
|
|
|
32
|
+
// Feature detection for CSS Anchor Positioning
|
|
33
|
+
const supportsAnchor = typeof CSS !== 'undefined' && CSS.supports('anchor-name', '--test');
|
|
34
|
+
|
|
31
35
|
let visible = $state(false);
|
|
32
36
|
let timeoutId = $state(null);
|
|
33
37
|
let popoverEl = $state(null);
|
|
34
38
|
let triggerEl = $state(null);
|
|
35
39
|
let isHoveringPopover = $state(false);
|
|
40
|
+
|
|
41
|
+
// Only needed for JS fallback
|
|
36
42
|
let windowWidth = $state(0);
|
|
37
43
|
let windowHeight = $state(0);
|
|
38
44
|
|
|
45
|
+
// Generate unique anchor name for this instance
|
|
46
|
+
const anchorName = `--popover-anchor-${Math.random().toString(36).slice(2, 9)}`;
|
|
47
|
+
|
|
39
48
|
// Animation configs based on position
|
|
40
49
|
const animations = {
|
|
41
50
|
top: { in: { y: 8, duration: 200 }, out: { y: -8, duration: 150 } },
|
|
@@ -51,11 +60,14 @@
|
|
|
51
60
|
triggerEl = event.currentTarget;
|
|
52
61
|
timeoutId = setTimeout(() => {
|
|
53
62
|
visible = true;
|
|
54
|
-
|
|
63
|
+
// For JS fallback, position after render
|
|
64
|
+
if (!supportsAnchor) {
|
|
55
65
|
requestAnimationFrame(() => {
|
|
56
|
-
|
|
66
|
+
requestAnimationFrame(() => {
|
|
67
|
+
positionPopover();
|
|
68
|
+
});
|
|
57
69
|
});
|
|
58
|
-
}
|
|
70
|
+
}
|
|
59
71
|
}, delay.show);
|
|
60
72
|
}
|
|
61
73
|
|
|
@@ -78,8 +90,9 @@
|
|
|
78
90
|
handleMouseLeave();
|
|
79
91
|
}
|
|
80
92
|
|
|
93
|
+
// JS fallback positioning (only used when CSS Anchor not supported)
|
|
81
94
|
function positionPopover() {
|
|
82
|
-
if (!popoverEl || !triggerEl) return;
|
|
95
|
+
if (!popoverEl || !triggerEl || supportsAnchor) return;
|
|
83
96
|
|
|
84
97
|
const triggerRect = triggerEl.getBoundingClientRect();
|
|
85
98
|
const popoverWidth = popoverEl.offsetWidth;
|
|
@@ -138,21 +151,25 @@
|
|
|
138
151
|
}
|
|
139
152
|
|
|
140
153
|
function handleScroll() {
|
|
141
|
-
if (visible && triggerEl && popoverEl) {
|
|
154
|
+
if (visible && triggerEl && popoverEl && !supportsAnchor) {
|
|
142
155
|
requestAnimationFrame(positionPopover);
|
|
143
156
|
}
|
|
144
157
|
}
|
|
145
158
|
</script>
|
|
146
159
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
160
|
+
<!-- Only bind window for JS fallback -->
|
|
161
|
+
{#if !supportsAnchor}
|
|
162
|
+
<svelte:window
|
|
163
|
+
bind:innerWidth={windowWidth}
|
|
164
|
+
bind:innerHeight={windowHeight}
|
|
165
|
+
onscroll={visible ? handleScroll : undefined}
|
|
166
|
+
onresize={visible ? handleScroll : undefined}
|
|
167
|
+
/>
|
|
168
|
+
{/if}
|
|
153
169
|
|
|
154
170
|
<div
|
|
155
171
|
class="popover-wrapper {className}"
|
|
172
|
+
style={supportsAnchor ? `anchor-name: ${anchorName};` : ''}
|
|
156
173
|
onmouseenter={handleMouseEnter}
|
|
157
174
|
onmouseleave={handleMouseLeave}
|
|
158
175
|
>
|
|
@@ -161,6 +178,10 @@
|
|
|
161
178
|
{#if visible}
|
|
162
179
|
<div
|
|
163
180
|
class="popover"
|
|
181
|
+
class:popover-anchor={supportsAnchor}
|
|
182
|
+
class:popover-js={!supportsAnchor}
|
|
183
|
+
data-position={position}
|
|
184
|
+
style={supportsAnchor ? `position-anchor: ${anchorName}; --offset: ${offset}px;` : ''}
|
|
164
185
|
role="tooltip"
|
|
165
186
|
in:fly={anim.in}
|
|
166
187
|
out:fly={anim.out}
|
|
@@ -186,8 +207,6 @@
|
|
|
186
207
|
}
|
|
187
208
|
|
|
188
209
|
.popover {
|
|
189
|
-
position: fixed;
|
|
190
|
-
z-index: 9999;
|
|
191
210
|
min-width: 8rem;
|
|
192
211
|
max-width: 18rem;
|
|
193
212
|
width: max-content;
|
|
@@ -203,4 +222,44 @@
|
|
|
203
222
|
word-wrap: break-word;
|
|
204
223
|
hyphens: auto;
|
|
205
224
|
}
|
|
225
|
+
|
|
226
|
+
/* CSS Anchor Positioning (Chrome 125+) */
|
|
227
|
+
.popover-anchor {
|
|
228
|
+
position: absolute;
|
|
229
|
+
inset: unset;
|
|
230
|
+
|
|
231
|
+
/* Position based on data-position attribute */
|
|
232
|
+
&[data-position='top'] {
|
|
233
|
+
bottom: calc(anchor(top) + var(--offset));
|
|
234
|
+
left: anchor(center);
|
|
235
|
+
translate: -50% 0;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
&[data-position='bottom'] {
|
|
239
|
+
top: calc(anchor(bottom) + var(--offset));
|
|
240
|
+
left: anchor(center);
|
|
241
|
+
translate: -50% 0;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
&[data-position='left'] {
|
|
245
|
+
right: calc(anchor(left) + var(--offset));
|
|
246
|
+
top: anchor(center);
|
|
247
|
+
translate: 0 -50%;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
&[data-position='right'] {
|
|
251
|
+
left: calc(anchor(right) + var(--offset));
|
|
252
|
+
top: anchor(center);
|
|
253
|
+
translate: 0 -50%;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/* Auto-flip when near viewport edges */
|
|
257
|
+
position-try-fallbacks: flip-block, flip-inline, flip-block flip-inline;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/* JS Fallback positioning */
|
|
261
|
+
.popover-js {
|
|
262
|
+
position: fixed;
|
|
263
|
+
z-index: 9999;
|
|
264
|
+
}
|
|
206
265
|
</style>
|