@rimelight/ui 0.0.36 → 0.0.37
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 +24 -20
- package/src/components/astro/RLAButton.astro +58 -26
- package/src/components/astro/RLACarousel.astro +202 -26
- package/src/components/astro/RLALink.astro +10 -12
- package/src/components/astro/RLALocaleSelector.astro +2 -1
- package/src/components/astro/RLAPopover.astro +99 -5
- package/src/components/astro/RLAScrollToTop.astro +10 -19
- package/src/components/astro/RLASelect.astro +52 -47
- package/src/components/astro/RLASidebar.astro +1 -1
- package/src/components/astro/RLATableOfContents.astro +76 -27
- package/src/components/astro/RLAThemeSelector.astro +1 -0
- package/src/components/astro/RLAToast.astro +2 -2
- package/src/components/vue/RLVScrollToTop.vue +12 -10
- package/src/components/vue/RLVToast.vue +1 -1
- package/src/components/vue/RLVToaster.vue +15 -3
- package/src/config/index.ts +0 -1
- package/src/integrations/index.ts +0 -1
- package/src/integrations/ui.ts +12 -11
- package/src/themes/link-group.theme.ts +1 -1
- package/src/themes/locale-selector.theme.ts +1 -1
- package/src/themes/popover.theme.ts +2 -5
- package/src/themes/select.theme.ts +1 -1
- package/src/themes/toast.theme.ts +9 -3
- package/src/types/components/button.ts +2 -1
- package/src/types/components/link.ts +0 -10
- package/src/types/components/sidebar.ts +1 -1
- package/src/types/components/table-of-contents.ts +5 -0
- package/src/types/components/toast.ts +1 -1
- package/src/utils/index.ts +2 -0
- package/src/utils/merge.ts +62 -0
- package/src/utils/scroll.ts +41 -0
- package/src/utils/toast.ts +37 -0
- package/src/utils/useUi.ts +2 -2
- package/src/components/HttpObservatory.astro +0 -103
- package/src/components/LighthouseScores.astro +0 -204
- package/src/composables/index.ts +0 -4
- package/src/composables/useHeaderStore.ts +0 -14
- package/src/composables/useScrollToTop.ts +0 -62
- package/src/composables/useShortcuts.ts +0 -11
- package/src/composables/useToast.ts +0 -43
- package/src/config/security.ts +0 -227
- package/src/integrations/sri.ts +0 -92
- package/src/middleware/index.ts +0 -1
- package/src/middleware/security.ts +0 -210
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rimelight/ui",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.37",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Rimelight Entertainment UI Library.",
|
|
6
6
|
"keywords": [
|
|
@@ -34,8 +34,6 @@
|
|
|
34
34
|
"./components/vue/*": "./src/components/vue/*",
|
|
35
35
|
"./themes": "./src/themes/index.ts",
|
|
36
36
|
"./themes/*": "./src/themes/*",
|
|
37
|
-
"./composables": "./src/composables/index.ts",
|
|
38
|
-
"./composables/*": "./src/composables/*",
|
|
39
37
|
"./config": "./src/config/index.ts",
|
|
40
38
|
"./config/*": "./src/config/*",
|
|
41
39
|
"./utils": "./src/utils/index.ts",
|
|
@@ -46,8 +44,6 @@
|
|
|
46
44
|
"./docs/*": "./src/docs/*",
|
|
47
45
|
"./plugins": "./src/plugins/index.ts",
|
|
48
46
|
"./plugins/*": "./src/plugins/*",
|
|
49
|
-
"./middleware": "./src/middleware/index.ts",
|
|
50
|
-
"./middleware/*": "./src/middleware/*",
|
|
51
47
|
"./types": "./src/types/index.ts",
|
|
52
48
|
"./types/*": "./src/types/*",
|
|
53
49
|
"./presets": "./src/presets/index.ts",
|
|
@@ -62,38 +58,46 @@
|
|
|
62
58
|
"docs:meta": "tsx scripts/extract-meta.ts"
|
|
63
59
|
},
|
|
64
60
|
"dependencies": {
|
|
65
|
-
"@astrojs/vue": "6.0.1",
|
|
66
61
|
"@iconify-json/lucide": "1.2.113",
|
|
67
62
|
"@unocss/astro": "^66.7.2",
|
|
68
63
|
"css-variants": "2.3.5",
|
|
69
|
-
"
|
|
70
|
-
"embla-carousel": "8.6.0",
|
|
71
|
-
"nanostores": "1.3.0",
|
|
72
|
-
"picomatch": "^4.0.4",
|
|
73
|
-
"vite-plugin-virtual": "^0.5.0",
|
|
74
|
-
"vue": "3.5.38"
|
|
64
|
+
"nanostores": "1.3.0"
|
|
75
65
|
},
|
|
76
66
|
"devDependencies": {
|
|
77
67
|
"@astrojs/check": "0.9.9",
|
|
68
|
+
"@astrojs/solid-js": "7.0.1",
|
|
78
69
|
"@astrojs/ts-plugin": "1.10.9",
|
|
79
|
-
"@
|
|
80
|
-
"
|
|
81
|
-
"
|
|
82
|
-
"@unocss/eslint-plugin": "66.7.2",
|
|
83
|
-
"astro": "6.4.8",
|
|
84
|
-
"eslint-plugin-regexp": "3.1.0",
|
|
70
|
+
"@astrojs/vue": "7.0.1",
|
|
71
|
+
"astro": "7.0.2",
|
|
72
|
+
"solid-js": "1.9.13",
|
|
85
73
|
"tsx": "4.22.4",
|
|
86
74
|
"typescript": "6.0.3",
|
|
87
75
|
"unocss": "66.7.2",
|
|
88
|
-
"
|
|
76
|
+
"vue": "3.5.38"
|
|
89
77
|
},
|
|
90
78
|
"peerDependencies": {
|
|
79
|
+
"@astrojs/solid-js": ">=7.0.1",
|
|
80
|
+
"@astrojs/vue": ">=7.0.1",
|
|
91
81
|
"@rimelight/i18n": "workspace:^0.0.1",
|
|
92
|
-
"astro": ">=
|
|
82
|
+
"astro": ">=7.0.1",
|
|
83
|
+
"solid-js": ">=1.9.13",
|
|
84
|
+
"vue": ">=3.0.0"
|
|
93
85
|
},
|
|
94
86
|
"peerDependenciesMeta": {
|
|
87
|
+
"@astrojs/solid-js": {
|
|
88
|
+
"optional": true
|
|
89
|
+
},
|
|
90
|
+
"@astrojs/vue": {
|
|
91
|
+
"optional": true
|
|
92
|
+
},
|
|
95
93
|
"@rimelight/i18n": {
|
|
96
94
|
"optional": true
|
|
95
|
+
},
|
|
96
|
+
"solid-js": {
|
|
97
|
+
"optional": true
|
|
98
|
+
},
|
|
99
|
+
"vue": {
|
|
100
|
+
"optional": true
|
|
97
101
|
}
|
|
98
102
|
},
|
|
99
103
|
"packageManager": "pnpm@11.8.0"
|
|
@@ -5,6 +5,7 @@ import type { ButtonProps } from "../../types"
|
|
|
5
5
|
import buttonThemeDefault, { createButtonTheme } from "../../themes/button.theme"
|
|
6
6
|
import { getUIConfig } from "virtual:rimelight-ui"
|
|
7
7
|
import RLAAvatar from "./RLAAvatar.astro"
|
|
8
|
+
import RLALink from "./RLALink.astro"
|
|
8
9
|
|
|
9
10
|
const uiConfig = getUIConfig()
|
|
10
11
|
const buttonTheme = uiConfig.colors ? createButtonTheme(uiConfig.colors) : buttonThemeDefault
|
|
@@ -16,6 +17,7 @@ const {
|
|
|
16
17
|
color = "primary",
|
|
17
18
|
size = "md",
|
|
18
19
|
href,
|
|
20
|
+
to,
|
|
19
21
|
label,
|
|
20
22
|
leadingIcon,
|
|
21
23
|
trailingIcon,
|
|
@@ -38,7 +40,7 @@ const {
|
|
|
38
40
|
activeVariant?: string
|
|
39
41
|
}
|
|
40
42
|
|
|
41
|
-
const isLeading = !!(leadingIcon || avatar || Astro.slots.has("leading")
|
|
43
|
+
const isLeading = !!(leadingIcon || avatar || Astro.slots.has("leading") || loading)
|
|
42
44
|
const isTrailing = !!(trailingIcon || Astro.slots.has("trailing")) || (loading && !isLeading)
|
|
43
45
|
|
|
44
46
|
const showLeadingIcon = loading
|
|
@@ -66,14 +68,14 @@ const classes = resolveClasses(button, {
|
|
|
66
68
|
theme
|
|
67
69
|
}, useUi("button", uiProp), className);
|
|
68
70
|
|
|
69
|
-
const Tag = href ?
|
|
71
|
+
const Tag = (href || to) ? RLALink : "button";
|
|
70
72
|
|
|
71
73
|
const loadingAutoProps = loadingAuto ? { 'data-loading-auto': '' } : {};
|
|
72
74
|
---
|
|
73
75
|
<Tag
|
|
74
76
|
class={classes.root}
|
|
75
77
|
data-slot="root"
|
|
76
|
-
{...(Tag ===
|
|
78
|
+
{...(Tag === RLALink ? { href, to, noExternalIcon: true } : {})}
|
|
77
79
|
{...loadingAutoProps}
|
|
78
80
|
{...rest}
|
|
79
81
|
>
|
|
@@ -113,31 +115,61 @@ const loadingAutoProps = loadingAuto ? { 'data-loading-auto': '' } : {};
|
|
|
113
115
|
{loadingAuto && (
|
|
114
116
|
<script>
|
|
115
117
|
document.querySelectorAll('button[data-loading-auto]').forEach((button) => {
|
|
116
|
-
button.addEventListener('click', async (
|
|
117
|
-
// Find if there are any other click listeners that return a promise
|
|
118
|
-
// This is tricky in vanilla JS, but we can assume if it's data-loading-auto,
|
|
119
|
-
// the user wants us to track the click.
|
|
120
|
-
// If the button is already loading, don't do anything
|
|
118
|
+
button.addEventListener('click', async (event) => {
|
|
121
119
|
if (button.getAttribute('data-loading') === 'true') return;
|
|
122
120
|
|
|
123
|
-
//
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
121
|
+
// Extract onclick attribute if available
|
|
122
|
+
const onClickAttr = button.getAttribute('onclick');
|
|
123
|
+
if (onClickAttr) {
|
|
124
|
+
// Temporarily disable default onclick execution to run it through our promise tracker
|
|
125
|
+
event.preventDefault();
|
|
126
|
+
event.stopImmediatePropagation();
|
|
127
|
+
|
|
128
|
+
// Construct a function to await the promise
|
|
129
|
+
try {
|
|
130
|
+
button.setAttribute('data-loading', 'true');
|
|
131
|
+
button.setAttribute('disabled', 'true');
|
|
132
|
+
|
|
133
|
+
// To spin the icon dynamically:
|
|
134
|
+
let leadingIconSpan = button.querySelector('[data-slot="leading-icon"]');
|
|
135
|
+
let icon = leadingIconSpan ? leadingIconSpan.querySelector('span') : null;
|
|
136
|
+
let origClass = '';
|
|
137
|
+
let isDynamicallyAdded = false;
|
|
138
|
+
|
|
139
|
+
if (!leadingIconSpan) {
|
|
140
|
+
isDynamicallyAdded = true;
|
|
141
|
+
leadingIconSpan = document.createElement('span');
|
|
142
|
+
leadingIconSpan.className = 'flex items-center justify-center shrink-0';
|
|
143
|
+
leadingIconSpan.setAttribute('data-slot', 'leading-icon');
|
|
144
|
+
icon = document.createElement('span');
|
|
145
|
+
icon.className = 'i-lucide-loader-circle animate-spin';
|
|
146
|
+
leadingIconSpan.appendChild(icon);
|
|
147
|
+
button.insertBefore(leadingIconSpan, button.firstChild);
|
|
148
|
+
} else if (icon) {
|
|
149
|
+
origClass = icon.className;
|
|
150
|
+
icon.className = 'i-lucide-loader-circle animate-spin';
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Execute the promise
|
|
154
|
+
const promiseFn = new Function('event', `
|
|
155
|
+
const fn = () => { ${onClickAttr} };
|
|
156
|
+
return fn();
|
|
157
|
+
`);
|
|
158
|
+
await promiseFn.call(button, event);
|
|
159
|
+
|
|
160
|
+
// Restore state
|
|
161
|
+
button.removeAttribute('data-loading');
|
|
162
|
+
button.removeAttribute('disabled');
|
|
163
|
+
if (isDynamicallyAdded && leadingIconSpan) {
|
|
164
|
+
leadingIconSpan.remove();
|
|
165
|
+
} else if (icon) {
|
|
166
|
+
icon.className = origClass;
|
|
167
|
+
}
|
|
168
|
+
} catch (err) {
|
|
169
|
+
console.error(err);
|
|
170
|
+
button.removeAttribute('data-loading');
|
|
171
|
+
button.removeAttribute('disabled');
|
|
172
|
+
}
|
|
141
173
|
}
|
|
142
174
|
});
|
|
143
175
|
});
|
|
@@ -28,13 +28,15 @@ const carouselId = `rla-carousel-${Math.random().toString(36).substring(2, 9)}`
|
|
|
28
28
|
{slides ? (
|
|
29
29
|
slides.map((slide) => (
|
|
30
30
|
<div class={classes.slide} data-slot="slide">
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
31
|
+
<div class="relative w-full aspect-video">
|
|
32
|
+
{slide.imageSrc && <img src={slide.imageSrc} alt={slide.title || ""} class="w-full h-full object-cover rounded-lg" draggable="false" />}
|
|
33
|
+
{(slide.title || slide.description) && (
|
|
34
|
+
<div class="absolute bottom-4 left-4 right-4 p-4 rounded-lg bg-black/60 backdrop-blur-sm text-white">
|
|
35
|
+
{slide.title && <h4 class="text-sm font-semibold">{slide.title}</h4>}
|
|
36
|
+
{slide.description && <p class="text-xs opacity-95 mt-1">{slide.description}</p>}
|
|
37
|
+
</div>
|
|
38
|
+
)}
|
|
39
|
+
</div>
|
|
38
40
|
</div>
|
|
39
41
|
))
|
|
40
42
|
) : (
|
|
@@ -53,47 +55,221 @@ const carouselId = `rla-carousel-${Math.random().toString(36).substring(2, 9)}`
|
|
|
53
55
|
</rla-carousel>
|
|
54
56
|
|
|
55
57
|
<script>
|
|
56
|
-
import EmblaCarousel from "embla-carousel"
|
|
57
|
-
|
|
58
58
|
class RLACarousel extends HTMLElement {
|
|
59
|
-
private
|
|
59
|
+
private cleanup: (() => void) | null = null;
|
|
60
60
|
|
|
61
61
|
connectedCallback() {
|
|
62
62
|
this.style.display = 'contents';
|
|
63
63
|
const autoplay = this.getAttribute("data-autoplay") === "true";
|
|
64
64
|
const viewport = this.querySelector('[data-slot="viewport"]') as HTMLElement;
|
|
65
|
+
const container = this.querySelector('[data-slot="container"]') as HTMLElement;
|
|
65
66
|
const prevBtn = this.querySelector('[data-slot="prev-button"]') as HTMLButtonElement;
|
|
66
67
|
const nextBtn = this.querySelector('[data-slot="next-button"]') as HTMLButtonElement;
|
|
67
68
|
|
|
68
|
-
if (!viewport) return;
|
|
69
|
+
if (!viewport || !container) return;
|
|
70
|
+
|
|
71
|
+
const originalSlides = Array.from(container.children) as HTMLElement[];
|
|
72
|
+
const N = originalSlides.length;
|
|
73
|
+
if (N === 0) return;
|
|
74
|
+
|
|
75
|
+
// Clone first and last slides for seamless looping
|
|
76
|
+
const firstClone = originalSlides[0].cloneNode(true) as HTMLElement;
|
|
77
|
+
const lastClone = originalSlides[N - 1].cloneNode(true) as HTMLElement;
|
|
78
|
+
|
|
79
|
+
container.insertBefore(lastClone, originalSlides[0]);
|
|
80
|
+
container.appendChild(firstClone);
|
|
81
|
+
|
|
82
|
+
// Add draggable="false" to all images inside clones as well
|
|
83
|
+
container.querySelectorAll('img').forEach(img => {
|
|
84
|
+
img.setAttribute('draggable', 'false');
|
|
85
|
+
});
|
|
69
86
|
|
|
70
|
-
|
|
87
|
+
let currentIndex = 1; // Start at the first actual slide
|
|
88
|
+
let isTransitioning = false;
|
|
89
|
+
let startX = 0;
|
|
90
|
+
let startY = 0;
|
|
91
|
+
let isDragging = false;
|
|
92
|
+
let hasDragged = false;
|
|
93
|
+
let autoplayIntervalId: any = null;
|
|
94
|
+
|
|
95
|
+
const updateTransform = (withTransition: boolean, translatePercent?: number) => {
|
|
96
|
+
if (withTransition) {
|
|
97
|
+
container.style.transition = 'transform 500ms cubic-bezier(0.16, 1, 0.3, 1)';
|
|
98
|
+
} else {
|
|
99
|
+
container.style.transition = 'none';
|
|
100
|
+
}
|
|
101
|
+
const pct = translatePercent !== undefined ? translatePercent : -currentIndex * 100;
|
|
102
|
+
container.style.transform = `translate3d(${pct}%, 0px, 0px)`;
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
// Set initial position
|
|
106
|
+
updateTransform(false);
|
|
107
|
+
|
|
108
|
+
const handleTransitionEnd = () => {
|
|
109
|
+
isTransitioning = false;
|
|
110
|
+
if (currentIndex === 0) {
|
|
111
|
+
currentIndex = N;
|
|
112
|
+
updateTransform(false);
|
|
113
|
+
} else if (currentIndex === N + 1) {
|
|
114
|
+
currentIndex = 1;
|
|
115
|
+
updateTransform(false);
|
|
116
|
+
}
|
|
117
|
+
};
|
|
71
118
|
|
|
72
|
-
const
|
|
73
|
-
|
|
119
|
+
const goToIndex = (index: number) => {
|
|
120
|
+
if (isTransitioning) return;
|
|
121
|
+
isTransitioning = true;
|
|
122
|
+
currentIndex = index;
|
|
123
|
+
updateTransform(true);
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const scrollPrev = () => {
|
|
127
|
+
goToIndex(currentIndex - 1);
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const scrollNext = () => {
|
|
131
|
+
goToIndex(currentIndex + 1);
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
// Autoplay
|
|
135
|
+
const startAutoplay = () => {
|
|
136
|
+
if (autoplay && !autoplayIntervalId) {
|
|
137
|
+
autoplayIntervalId = setInterval(scrollNext, 5000);
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const stopAutoplay = () => {
|
|
142
|
+
if (autoplayIntervalId) {
|
|
143
|
+
clearInterval(autoplayIntervalId);
|
|
144
|
+
autoplayIntervalId = null;
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
startAutoplay();
|
|
149
|
+
|
|
150
|
+
// Button event listeners with throttle
|
|
151
|
+
let lastBtnClickTime = 0;
|
|
152
|
+
const handleBtnClick = (action: () => void) => {
|
|
153
|
+
const now = Date.now();
|
|
154
|
+
if (now - lastBtnClickTime < 500) return;
|
|
155
|
+
lastBtnClickTime = now;
|
|
156
|
+
stopAutoplay();
|
|
157
|
+
action();
|
|
158
|
+
startAutoplay();
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
const onPrevClick = (e: MouseEvent) => {
|
|
162
|
+
e.preventDefault();
|
|
163
|
+
handleBtnClick(scrollPrev);
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
const onNextClick = (e: MouseEvent) => {
|
|
167
|
+
e.preventDefault();
|
|
168
|
+
handleBtnClick(scrollNext);
|
|
169
|
+
};
|
|
74
170
|
|
|
75
171
|
if (prevBtn) prevBtn.addEventListener("click", onPrevClick);
|
|
76
172
|
if (nextBtn) nextBtn.addEventListener("click", onNextClick);
|
|
77
173
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
174
|
+
// Pointer events for drag/swipe
|
|
175
|
+
const onPointerDown = (e: PointerEvent) => {
|
|
176
|
+
if (isTransitioning) return;
|
|
177
|
+
stopAutoplay();
|
|
178
|
+
startX = e.clientX;
|
|
179
|
+
startY = e.clientY;
|
|
180
|
+
isDragging = true;
|
|
181
|
+
hasDragged = false;
|
|
182
|
+
try {
|
|
183
|
+
viewport.setPointerCapture(e.pointerId);
|
|
184
|
+
} catch (err) {}
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
const onPointerMove = (e: PointerEvent) => {
|
|
188
|
+
if (!isDragging) return;
|
|
189
|
+
const deltaX = e.clientX - startX;
|
|
190
|
+
const deltaY = e.clientY - startY;
|
|
191
|
+
|
|
192
|
+
if (!hasDragged) {
|
|
193
|
+
// If vertical movement is greater than horizontal early on, cancel drag to let page scroll
|
|
194
|
+
if (Math.abs(deltaY) > Math.abs(deltaX) && Math.abs(deltaY) > 5) {
|
|
195
|
+
isDragging = false;
|
|
196
|
+
try {
|
|
197
|
+
viewport.releasePointerCapture(e.pointerId);
|
|
198
|
+
} catch (err) {}
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
if (Math.abs(deltaX) > 5) {
|
|
202
|
+
hasDragged = true;
|
|
203
|
+
container.style.transition = 'none'; // Only disable transition when we actually start dragging
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (hasDragged) {
|
|
208
|
+
e.preventDefault();
|
|
209
|
+
const width = viewport.clientWidth;
|
|
210
|
+
const deltaPercent = (deltaX / width) * 100;
|
|
211
|
+
const basePercent = -currentIndex * 100;
|
|
212
|
+
updateTransform(false, basePercent + deltaPercent);
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
const onPointerUp = (e: PointerEvent) => {
|
|
217
|
+
if (!isDragging) return;
|
|
218
|
+
isDragging = false;
|
|
219
|
+
try {
|
|
220
|
+
viewport.releasePointerCapture(e.pointerId);
|
|
221
|
+
} catch (err) {}
|
|
222
|
+
|
|
223
|
+
if (hasDragged) {
|
|
224
|
+
const deltaX = e.clientX - startX;
|
|
225
|
+
const width = viewport.clientWidth;
|
|
226
|
+
const threshold = width * 0.15;
|
|
227
|
+
|
|
228
|
+
if (deltaX < -threshold) {
|
|
229
|
+
scrollNext();
|
|
230
|
+
} else if (deltaX > threshold) {
|
|
231
|
+
scrollPrev();
|
|
232
|
+
} else {
|
|
233
|
+
// Snap back
|
|
234
|
+
isTransitioning = true;
|
|
235
|
+
updateTransform(true);
|
|
236
|
+
setTimeout(() => {
|
|
237
|
+
isTransitioning = false;
|
|
238
|
+
}, 50);
|
|
239
|
+
}
|
|
240
|
+
} else {
|
|
241
|
+
// If it was just a click, make sure transition settings are restored
|
|
242
|
+
container.style.transition = 'transform 500ms cubic-bezier(0.16, 1, 0.3, 1)';
|
|
243
|
+
isTransitioning = false;
|
|
244
|
+
}
|
|
245
|
+
startAutoplay();
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
viewport.addEventListener("pointerdown", onPointerDown);
|
|
249
|
+
viewport.addEventListener("pointermove", onPointerMove, { passive: false });
|
|
250
|
+
viewport.addEventListener("pointerup", onPointerUp);
|
|
251
|
+
viewport.addEventListener("pointercancel", onPointerUp);
|
|
252
|
+
container.addEventListener("transitionend", handleTransitionEnd);
|
|
84
253
|
|
|
85
|
-
this.
|
|
254
|
+
this.cleanup = () => {
|
|
255
|
+
stopAutoplay();
|
|
86
256
|
if (prevBtn) prevBtn.removeEventListener("click", onPrevClick);
|
|
87
257
|
if (nextBtn) nextBtn.removeEventListener("click", onNextClick);
|
|
88
|
-
|
|
89
|
-
|
|
258
|
+
viewport.removeEventListener("pointerdown", onPointerDown);
|
|
259
|
+
viewport.removeEventListener("pointermove", onPointerMove);
|
|
260
|
+
viewport.removeEventListener("pointerup", onPointerUp);
|
|
261
|
+
viewport.removeEventListener("pointercancel", onPointerUp);
|
|
262
|
+
container.removeEventListener("transitionend", handleTransitionEnd);
|
|
263
|
+
|
|
264
|
+
if (container.contains(firstClone)) container.removeChild(firstClone);
|
|
265
|
+
if (container.contains(lastClone)) container.removeChild(lastClone);
|
|
90
266
|
};
|
|
91
267
|
}
|
|
92
268
|
|
|
93
269
|
disconnectedCallback() {
|
|
94
|
-
if (this.
|
|
95
|
-
this.
|
|
96
|
-
this.
|
|
270
|
+
if (this.cleanup) {
|
|
271
|
+
this.cleanup();
|
|
272
|
+
this.cleanup = null;
|
|
97
273
|
}
|
|
98
274
|
}
|
|
99
275
|
}
|
|
@@ -1,44 +1,42 @@
|
|
|
1
1
|
---
|
|
2
|
-
import RLAButton from "./RLAButton.astro"
|
|
3
2
|
import type { LinkProps } from "../../types"
|
|
4
3
|
|
|
5
4
|
const {
|
|
6
5
|
to,
|
|
7
6
|
href,
|
|
8
|
-
variant = "link",
|
|
9
|
-
color = "neutral",
|
|
10
7
|
external,
|
|
11
|
-
isButton = false,
|
|
12
8
|
ariaLabel,
|
|
13
9
|
externalIcon,
|
|
14
10
|
target,
|
|
15
11
|
rel,
|
|
16
12
|
trailingIcon,
|
|
13
|
+
noExternalIcon,
|
|
14
|
+
class: className,
|
|
17
15
|
...rest
|
|
18
|
-
} = Astro.props as LinkProps
|
|
16
|
+
} = Astro.props as LinkProps & { noExternalIcon?: boolean }
|
|
19
17
|
|
|
20
18
|
const linkHref = href || (typeof to === 'string' ? to : undefined);
|
|
21
19
|
|
|
22
20
|
const isExternal = !!(external || (linkHref && (linkHref.startsWith('http://') || linkHref.startsWith('https://') || linkHref.startsWith('//'))));
|
|
23
21
|
|
|
24
|
-
const linkVariant = isButton && variant === "link" ? "solid" : variant;
|
|
25
22
|
const linkTarget = target || (isExternal ? "_blank" : undefined);
|
|
26
23
|
const linkRel = rel || (isExternal ? "noopener noreferrer" : undefined);
|
|
27
24
|
|
|
28
|
-
const showExternalIcon = isExternal && !trailingIcon ? (externalIcon || "i-lucide-external-link") : trailingIcon;
|
|
25
|
+
const showExternalIcon = isExternal && !noExternalIcon && !trailingIcon ? (externalIcon || "i-lucide-external-link") : trailingIcon;
|
|
29
26
|
---
|
|
30
|
-
<
|
|
31
|
-
variant={linkVariant as any}
|
|
32
|
-
color={color as any}
|
|
27
|
+
<a
|
|
33
28
|
href={linkHref}
|
|
34
29
|
target={linkTarget}
|
|
35
30
|
rel={linkRel}
|
|
36
31
|
aria-label={ariaLabel}
|
|
37
|
-
|
|
32
|
+
class={className}
|
|
38
33
|
{...rest}
|
|
39
34
|
>
|
|
40
35
|
<slot />
|
|
36
|
+
{showExternalIcon && (
|
|
37
|
+
<span class={showExternalIcon} aria-hidden="true" />
|
|
38
|
+
)}
|
|
41
39
|
{isExternal && !ariaLabel && (
|
|
42
40
|
<span class="sr-only"> (opens in a new tab)</span>
|
|
43
41
|
)}
|
|
44
|
-
</
|
|
42
|
+
</a>
|
|
@@ -49,7 +49,8 @@ function getLocaleDisplayName(localeCode: string): string {
|
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
const items = processedLocales.map((item) => {
|
|
52
|
-
const
|
|
52
|
+
const basePath = Astro.url.pathname.replace(/^\/[^/]+/, '') || '/';
|
|
53
|
+
const targetUrl = getRelativeLocaleUrl(item.code, basePath) + Astro.url.search;
|
|
53
54
|
return {
|
|
54
55
|
label: item.label !== item.code ? item.label : getLocaleDisplayName(item.code),
|
|
55
56
|
value: targetUrl,
|
|
@@ -21,16 +21,110 @@ const {
|
|
|
21
21
|
const classes = resolveClasses(popover, {
|
|
22
22
|
placement
|
|
23
23
|
}, useUi("popover", uiProp), className)
|
|
24
|
-
const popoverId = `rla-popover-${Math.random().toString(36).substring(2, 9)}`
|
|
25
24
|
---
|
|
26
|
-
<div class={classes.root} data-slot="popover-container" {...rest}>
|
|
27
|
-
<div class={classes.trigger} data-slot="trigger">
|
|
25
|
+
<div class={classes.root} data-slot="popover-container" data-placement={placement} {...rest}>
|
|
26
|
+
<div class={`${classes.trigger} popover-trigger`} data-slot="trigger">
|
|
28
27
|
<slot name="trigger">
|
|
29
|
-
{triggerLabel && <button type="button" class="cursor-pointer"
|
|
28
|
+
{triggerLabel && <button type="button" class="cursor-pointer">{triggerLabel}</button>}
|
|
30
29
|
</slot>
|
|
31
30
|
</div>
|
|
32
31
|
|
|
33
|
-
<div
|
|
32
|
+
<div class={`${classes.content} popover-content hidden`} data-slot="content">
|
|
34
33
|
<slot />
|
|
35
34
|
</div>
|
|
36
35
|
</div>
|
|
36
|
+
|
|
37
|
+
<script>
|
|
38
|
+
function initPopovers() {
|
|
39
|
+
document.querySelectorAll('[data-slot="popover-container"]').forEach(container => {
|
|
40
|
+
const el = container as any;
|
|
41
|
+
if (el.dataset && el.dataset.popoverInitialized) return;
|
|
42
|
+
if (el.dataset) el.dataset.popoverInitialized = "true";
|
|
43
|
+
|
|
44
|
+
const trigger = container.querySelector('.popover-trigger');
|
|
45
|
+
const content = container.querySelector('.popover-content') as HTMLElement;
|
|
46
|
+
|
|
47
|
+
if (!trigger || !content) return;
|
|
48
|
+
|
|
49
|
+
trigger.addEventListener('click', (e) => {
|
|
50
|
+
e.stopPropagation();
|
|
51
|
+
const isOpen = !content.classList.contains('hidden');
|
|
52
|
+
|
|
53
|
+
document.querySelectorAll('.popover-content').forEach(otherContent => {
|
|
54
|
+
const other = otherContent as HTMLElement;
|
|
55
|
+
if (other !== content && !other.classList.contains('hidden')) {
|
|
56
|
+
other.classList.add('hidden');
|
|
57
|
+
other.style.left = '';
|
|
58
|
+
other.style.right = '';
|
|
59
|
+
other.style.transform = '';
|
|
60
|
+
other.dispatchEvent(new CustomEvent('popover-toggle', { detail: { open: false } }));
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
if (isOpen) {
|
|
65
|
+
content.classList.add('hidden');
|
|
66
|
+
content.style.left = '';
|
|
67
|
+
content.style.right = '';
|
|
68
|
+
content.style.transform = '';
|
|
69
|
+
content.dispatchEvent(new CustomEvent('popover-toggle', { detail: { open: false } }));
|
|
70
|
+
} else {
|
|
71
|
+
content.classList.remove('hidden');
|
|
72
|
+
|
|
73
|
+
// Reset styles to measure original computed bounds
|
|
74
|
+
content.style.left = '';
|
|
75
|
+
content.style.right = '';
|
|
76
|
+
content.style.transform = '';
|
|
77
|
+
|
|
78
|
+
const rect = content.getBoundingClientRect();
|
|
79
|
+
const viewportWidth = window.innerWidth;
|
|
80
|
+
const placement = container.getAttribute('data-placement') || 'bottom';
|
|
81
|
+
|
|
82
|
+
let shift = 0;
|
|
83
|
+
const safetyMargin = 24;
|
|
84
|
+
if (rect.right > viewportWidth - safetyMargin) {
|
|
85
|
+
shift = (viewportWidth - safetyMargin) - rect.right;
|
|
86
|
+
} else if (rect.left < safetyMargin) {
|
|
87
|
+
shift = safetyMargin - rect.left;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (shift !== 0) {
|
|
91
|
+
const isCentered = (placement === 'top' || placement === 'bottom') &&
|
|
92
|
+
!content.classList.contains('right-0') &&
|
|
93
|
+
!content.classList.contains('left-0');
|
|
94
|
+
if (isCentered) {
|
|
95
|
+
content.style.transform = `translateX(calc(-50% + ${shift}px))`;
|
|
96
|
+
} else {
|
|
97
|
+
content.style.transform = `translateX(${shift}px)`;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
content.dispatchEvent(new CustomEvent('popover-toggle', { detail: { open: true } }));
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
content.addEventListener('click', (e) => {
|
|
106
|
+
e.stopPropagation();
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const win = window as any;
|
|
112
|
+
if (!win.hasGlobalPopoverListener) {
|
|
113
|
+
win.hasGlobalPopoverListener = true;
|
|
114
|
+
document.addEventListener('click', () => {
|
|
115
|
+
document.querySelectorAll('.popover-content').forEach(content => {
|
|
116
|
+
const c = content as HTMLElement;
|
|
117
|
+
if (!c.classList.contains('hidden')) {
|
|
118
|
+
c.classList.add('hidden');
|
|
119
|
+
c.style.left = '';
|
|
120
|
+
c.style.right = '';
|
|
121
|
+
c.style.transform = '';
|
|
122
|
+
c.dispatchEvent(new CustomEvent('popover-toggle', { detail: { open: false } }));
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
document.addEventListener('astro:page-load', initPopovers);
|
|
129
|
+
initPopovers();
|
|
130
|
+
</script>
|