@rimelight/ui 0.0.20 → 0.0.22
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 +9 -3
- package/src/components/Head.astro +3 -0
- package/src/components/astro/RLAFooter.astro +69 -0
- package/src/components/astro/RLAHeader.astro +178 -75
- package/src/components/astro/RLAScrollToTop.astro +221 -0
- package/src/components/vue/RLVFooter.vue +81 -0
- package/src/components/vue/RLVHeader.vue +140 -18
- package/src/components/vue/RLVScrollToTop.vue +214 -0
- package/src/composables/index.ts +1 -0
- package/src/composables/useScrollToTop.ts +62 -0
- package/src/integrations/ui.ts +3 -2
- package/src/plugins/starlightAddons/PackageManagers.astro +4 -4
- package/src/plugins/starlightAddons/PageTitle.astro +112 -112
- package/src/plugins/starlightAddons/components/Gamepad.astro +180 -0
- package/src/plugins/starlightAddons/components/Keyboard.astro +321 -0
- package/src/plugins/starlightAddons/index.ts +1 -7
- package/src/styles/index.css +153 -0
- package/src/themes/footer.theme.ts +29 -0
- package/src/themes/header.theme.ts +32 -2
- package/src/themes/index.ts +7 -3
- package/src/themes/scroll-to-top.theme.ts +48 -0
- package/src/utils/headerStack.ts +88 -0
package/package.json
CHANGED
|
@@ -1,13 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rimelight/ui",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.22",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Rimelight Entertainment UI Library.",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"astro",
|
|
8
|
+
"typescript"
|
|
9
|
+
],
|
|
6
10
|
"homepage": "https://rimelight.com/docs",
|
|
7
11
|
"bugs": {
|
|
8
12
|
"url": "https://github.com/Rimelight-Entertainment/rimelight/issues"
|
|
9
13
|
},
|
|
10
14
|
"license": "MIT",
|
|
15
|
+
"author": {
|
|
16
|
+
"name": "Daniel Marchi"
|
|
17
|
+
},
|
|
11
18
|
"repository": {
|
|
12
19
|
"type": "git",
|
|
13
20
|
"url": "git+https://github.com/Rimelight-Entertainment/rimelight.git"
|
|
@@ -89,7 +96,6 @@
|
|
|
89
96
|
}
|
|
90
97
|
},
|
|
91
98
|
"scripts": {
|
|
92
|
-
"
|
|
93
|
-
"dev": "astro dev"
|
|
99
|
+
"astro": "astro"
|
|
94
100
|
}
|
|
95
101
|
}
|
|
@@ -82,6 +82,9 @@ const finalCanonical = canonical || (!is404 ? Astro.url.href : undefined);
|
|
|
82
82
|
<meta name="viewport" content={viewport} />
|
|
83
83
|
<meta name="generator" content={Astro.generator} />
|
|
84
84
|
|
|
85
|
+
<!-- Sitemap -->
|
|
86
|
+
<link rel="sitemap" href="/sitemap-index.xml" />
|
|
87
|
+
|
|
85
88
|
<!-- SEO -->
|
|
86
89
|
<title>{displayTitle}</title>
|
|
87
90
|
{safeDescription && <meta name="description" content={safeDescription} />}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { scv, cx } from "css-variants";
|
|
3
|
+
import { twMerge } from "tailwind-merge";
|
|
4
|
+
import defu from "defu";
|
|
5
|
+
import { footerTheme } from "../../themes/footer.theme.ts";
|
|
6
|
+
import { getUIConfig } from "virtual:rimelight-ui";
|
|
7
|
+
|
|
8
|
+
const footer = scv({
|
|
9
|
+
slots: [...footerTheme.slots],
|
|
10
|
+
base: footerTheme.base,
|
|
11
|
+
variants: footerTheme.variants,
|
|
12
|
+
defaultVariants: footerTheme.defaultVariants,
|
|
13
|
+
classNameResolver: (...args) => twMerge(cx(...args)),
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
export interface RLAFooterProps {
|
|
17
|
+
/**
|
|
18
|
+
* Whether to contain the footer content in a max-width container.
|
|
19
|
+
* @default false
|
|
20
|
+
*/
|
|
21
|
+
contain?: boolean;
|
|
22
|
+
/**
|
|
23
|
+
* UI overrides for individual slots, derived from the footer theme.
|
|
24
|
+
*/
|
|
25
|
+
ui?: Partial<Record<keyof typeof footerTheme.base, string>>;
|
|
26
|
+
/**
|
|
27
|
+
* External class — applied to the root element.
|
|
28
|
+
*/
|
|
29
|
+
class?: string;
|
|
30
|
+
/**
|
|
31
|
+
* Standard HTML attributes for <footer>
|
|
32
|
+
*/
|
|
33
|
+
[key: string]: unknown;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const {
|
|
37
|
+
contain = false,
|
|
38
|
+
class: className,
|
|
39
|
+
ui: uiProp,
|
|
40
|
+
...rest
|
|
41
|
+
} = Astro.props as RLAFooterProps
|
|
42
|
+
|
|
43
|
+
const globalConfig = getUIConfig();
|
|
44
|
+
|
|
45
|
+
const mergedUI = defu(
|
|
46
|
+
uiProp ?? {},
|
|
47
|
+
(globalConfig.footer as Record<string, unknown>) ?? {},
|
|
48
|
+
) as Record<keyof typeof footerTheme.base, string | undefined>;
|
|
49
|
+
|
|
50
|
+
const { root, container, left, center, right } = footer({ contain });
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
<footer
|
|
54
|
+
class:list={[twMerge(root, mergedUI.root, className)]}
|
|
55
|
+
data-slot="rla-footer"
|
|
56
|
+
{...rest}
|
|
57
|
+
>
|
|
58
|
+
<div class:list={[twMerge(container, mergedUI.container)]}>
|
|
59
|
+
<div class:list={[twMerge(left, mergedUI.left)]} data-slot="footer-left">
|
|
60
|
+
<slot name="left" />
|
|
61
|
+
</div>
|
|
62
|
+
<div class:list={[twMerge(center, mergedUI.center)]} data-slot="footer-center">
|
|
63
|
+
<slot name="center" />
|
|
64
|
+
</div>
|
|
65
|
+
<div class:list={[twMerge(right, mergedUI.right)]} data-slot="footer-right">
|
|
66
|
+
<slot name="right" />
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
</footer>
|
|
@@ -1,75 +1,178 @@
|
|
|
1
|
-
---
|
|
2
|
-
import { scv, cx } from "css-variants";
|
|
3
|
-
import { twMerge } from "tailwind-merge";
|
|
4
|
-
import defu from "defu";
|
|
5
|
-
import { headerTheme } from "
|
|
6
|
-
import { getUIConfig } from "virtual:rimelight-ui";
|
|
7
|
-
|
|
8
|
-
const header = scv({
|
|
9
|
-
slots: [...headerTheme.slots],
|
|
10
|
-
base: headerTheme.base,
|
|
11
|
-
variants: headerTheme.variants,
|
|
12
|
-
defaultVariants: headerTheme.defaultVariants,
|
|
13
|
-
classNameResolver: (...args) => twMerge(cx(...args)),
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
export interface RLAHeaderProps {
|
|
17
|
-
/**
|
|
18
|
-
* Whether to contain the header content in a max-width container.
|
|
19
|
-
* @default true
|
|
20
|
-
*/
|
|
21
|
-
contain?: boolean;
|
|
22
|
-
/**
|
|
23
|
-
*
|
|
24
|
-
* @default true
|
|
25
|
-
*/
|
|
26
|
-
sticky?: boolean;
|
|
27
|
-
/**
|
|
28
|
-
*
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
*
|
|
37
|
-
*/
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
1
|
+
---
|
|
2
|
+
import { scv, cx } from "css-variants";
|
|
3
|
+
import { twMerge } from "tailwind-merge";
|
|
4
|
+
import defu from "defu";
|
|
5
|
+
import { headerTheme } from "../../themes/header.theme.ts";
|
|
6
|
+
import { getUIConfig } from "virtual:rimelight-ui";
|
|
7
|
+
|
|
8
|
+
const header = scv({
|
|
9
|
+
slots: [...headerTheme.slots],
|
|
10
|
+
base: headerTheme.base,
|
|
11
|
+
variants: headerTheme.variants,
|
|
12
|
+
defaultVariants: headerTheme.defaultVariants,
|
|
13
|
+
classNameResolver: (...args) => twMerge(cx(...args)),
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
export interface RLAHeaderProps {
|
|
17
|
+
/**
|
|
18
|
+
* Whether to contain the header content in a max-width container.
|
|
19
|
+
* @default true
|
|
20
|
+
*/
|
|
21
|
+
contain?: boolean;
|
|
22
|
+
/**
|
|
23
|
+
* Sticky positioning when `fixed` is false.
|
|
24
|
+
* @default true
|
|
25
|
+
*/
|
|
26
|
+
sticky?: boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Switches to `position: fixed; left: 0; right: 0` (layer mode).
|
|
29
|
+
* When true, `stackIndex` controls the exact position and layer order.
|
|
30
|
+
* A module `<script>` is emitted to apply styles and attach scroll listeners.
|
|
31
|
+
* @default false
|
|
32
|
+
*/
|
|
33
|
+
fixed?: boolean;
|
|
34
|
+
/**
|
|
35
|
+
* Controls z-index when `fixed` is true: `z-index = 100 - stackIndex`.
|
|
36
|
+
* @default 0
|
|
37
|
+
*/
|
|
38
|
+
stackIndex?: number;
|
|
39
|
+
/**
|
|
40
|
+
* When true, hides the header on scroll-down and shows it on scroll-up.
|
|
41
|
+
* Implemented via a bundled Astro `<script>` — no Vue hydration needed.
|
|
42
|
+
* Only meaningful when `fixed` is true.
|
|
43
|
+
* @default false
|
|
44
|
+
*/
|
|
45
|
+
hideOnScroll?: boolean;
|
|
46
|
+
/**
|
|
47
|
+
* UI overrides for individual slots, derived from the header theme.
|
|
48
|
+
*/
|
|
49
|
+
ui?: Partial<Record<keyof typeof headerTheme.base, string>>;
|
|
50
|
+
/**
|
|
51
|
+
* External class — applied to the root element.
|
|
52
|
+
*/
|
|
53
|
+
class?: string;
|
|
54
|
+
/**
|
|
55
|
+
* Standard HTML attributes for <header>.
|
|
56
|
+
*/
|
|
57
|
+
[key: string]: unknown;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const {
|
|
61
|
+
contain = true,
|
|
62
|
+
sticky = true,
|
|
63
|
+
fixed = false,
|
|
64
|
+
stackIndex = 0,
|
|
65
|
+
hideOnScroll = false,
|
|
66
|
+
class: className,
|
|
67
|
+
ui: uiProp,
|
|
68
|
+
...rest
|
|
69
|
+
} = Astro.props as RLAHeaderProps;
|
|
70
|
+
|
|
71
|
+
const globalConfig = getUIConfig();
|
|
72
|
+
|
|
73
|
+
const mergedUI = defu(
|
|
74
|
+
uiProp ?? {},
|
|
75
|
+
(globalConfig.header as Record<string, unknown>) ?? {},
|
|
76
|
+
) as Record<keyof typeof headerTheme.base, string | undefined>;
|
|
77
|
+
|
|
78
|
+
// Resolve classes — note: sticky variant is suppressed when fixed is true,
|
|
79
|
+
// since the fixed variant's classes replace it.
|
|
80
|
+
const { root, content, container, left, center, right } = header({
|
|
81
|
+
contain,
|
|
82
|
+
sticky: fixed ? false : sticky,
|
|
83
|
+
fixed,
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// Build a unique ID so the script can target this specific header element.
|
|
87
|
+
const headerId = `rla-header-${Math.random().toString(36).slice(2, 8)}`;
|
|
88
|
+
const zIndex = 100 - stackIndex;
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
<header
|
|
92
|
+
id={headerId}
|
|
93
|
+
class:list={[twMerge(root, mergedUI.root, className)]}
|
|
94
|
+
data-rla-header={fixed ? "true" : undefined}
|
|
95
|
+
data-stack-index={stackIndex}
|
|
96
|
+
data-hide-on-scroll={String(hideOnScroll)}
|
|
97
|
+
style={fixed ? `z-index: ${zIndex};` : undefined}
|
|
98
|
+
{...rest}
|
|
99
|
+
>
|
|
100
|
+
<!--
|
|
101
|
+
content wrapper: present in all modes; in fixed+hideOnScroll mode the inline
|
|
102
|
+
script observes this element's offsetHeight for accurate measurement.
|
|
103
|
+
-->
|
|
104
|
+
<div class:list={[twMerge(content, mergedUI.content)]} data-header-content>
|
|
105
|
+
<div class:list={[twMerge(container, mergedUI.container)]}>
|
|
106
|
+
<div class:list={[twMerge(left, mergedUI.left)]} data-slot="header-left">
|
|
107
|
+
<slot name="left" />
|
|
108
|
+
</div>
|
|
109
|
+
<div class:list={[twMerge(center, mergedUI.center)]} data-slot="header-center">
|
|
110
|
+
<slot name="center" />
|
|
111
|
+
</div>
|
|
112
|
+
<div class:list={[twMerge(right, mergedUI.right)]} data-slot="header-right">
|
|
113
|
+
<slot name="right" />
|
|
114
|
+
</div>
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
</header>
|
|
118
|
+
|
|
119
|
+
{/*
|
|
120
|
+
Astro Module Script — automatically hydrates all static Astro headers
|
|
121
|
+
with the global Vanilla JS stack manager. It only executes once per page
|
|
122
|
+
load, managing all headers efficiently.
|
|
123
|
+
*/}
|
|
124
|
+
{fixed && (
|
|
125
|
+
<script>
|
|
126
|
+
import { getHeaderStack } from "../../utils/headerStack";
|
|
127
|
+
|
|
128
|
+
// This script runs exactly once per page load, managing all Astro headers
|
|
129
|
+
const stack = getHeaderStack();
|
|
130
|
+
const headers = document.querySelectorAll("[data-rla-header='true']");
|
|
131
|
+
|
|
132
|
+
headers.forEach(header => {
|
|
133
|
+
// Prevent double-initialization
|
|
134
|
+
if ((header as any)._rl_initialized) return;
|
|
135
|
+
(header as any)._rl_initialized = true;
|
|
136
|
+
|
|
137
|
+
const id = header.id;
|
|
138
|
+
const stackIndex = parseInt(header.getAttribute("data-stack-index") || "0", 10);
|
|
139
|
+
const hideOnScroll = header.getAttribute("data-hide-on-scroll") === "true";
|
|
140
|
+
const contentEl = header.querySelector("[data-header-content]") as HTMLElement;
|
|
141
|
+
|
|
142
|
+
let naturalHeight = contentEl ? contentEl.offsetHeight : 0;
|
|
143
|
+
let isVisible = true;
|
|
144
|
+
|
|
145
|
+
const updateStack = () => {
|
|
146
|
+
stack.register(id, stackIndex, naturalHeight, isVisible, header as HTMLElement);
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
if (typeof ResizeObserver !== "undefined" && contentEl) {
|
|
150
|
+
new ResizeObserver(entries => {
|
|
151
|
+
const h = entries[0]?.contentRect.height;
|
|
152
|
+
if (h && h > 0) {
|
|
153
|
+
naturalHeight = h;
|
|
154
|
+
updateStack();
|
|
155
|
+
}
|
|
156
|
+
}).observe(contentEl);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (hideOnScroll) {
|
|
160
|
+
let localLastScrollY = window.scrollY;
|
|
161
|
+
window.addEventListener("scroll", () => {
|
|
162
|
+
const current = window.scrollY;
|
|
163
|
+
const diff = current - localLastScrollY;
|
|
164
|
+
if (Math.abs(diff) >= 10) {
|
|
165
|
+
const newVisible = current <= 50 ? true : diff <= 0;
|
|
166
|
+
if (newVisible !== isVisible) {
|
|
167
|
+
isVisible = newVisible;
|
|
168
|
+
updateStack();
|
|
169
|
+
}
|
|
170
|
+
localLastScrollY = current;
|
|
171
|
+
}
|
|
172
|
+
}, { passive: true });
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
updateStack();
|
|
176
|
+
});
|
|
177
|
+
</script>
|
|
178
|
+
)}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { scv, cx } from "css-variants"
|
|
3
|
+
import { twMerge } from "tailwind-merge"
|
|
4
|
+
import defu from "defu"
|
|
5
|
+
import { scrollToTopTheme } from "../../themes/scroll-to-top.theme.ts"
|
|
6
|
+
import { getUIConfig } from "virtual:rimelight-ui"
|
|
7
|
+
|
|
8
|
+
export interface RLAScrollToTopProps {
|
|
9
|
+
/**
|
|
10
|
+
* Button color.
|
|
11
|
+
*
|
|
12
|
+
* @default "primary"
|
|
13
|
+
*/
|
|
14
|
+
color?: keyof typeof scrollToTopTheme.variants.color
|
|
15
|
+
/**
|
|
16
|
+
* Transition duration for the progress circle (in seconds).
|
|
17
|
+
*
|
|
18
|
+
* @default 0.1
|
|
19
|
+
*/
|
|
20
|
+
duration?: number
|
|
21
|
+
/**
|
|
22
|
+
* Scroll threshold (in pixels) before the button becomes visible.
|
|
23
|
+
*
|
|
24
|
+
* @default 200
|
|
25
|
+
*/
|
|
26
|
+
threshold?: number
|
|
27
|
+
/**
|
|
28
|
+
* Whether to show the scroll progress indicator.
|
|
29
|
+
*
|
|
30
|
+
* @default false
|
|
31
|
+
*/
|
|
32
|
+
showProgress?: boolean
|
|
33
|
+
/**
|
|
34
|
+
* Stroke width of the progress circle.
|
|
35
|
+
*
|
|
36
|
+
* @default 6
|
|
37
|
+
*/
|
|
38
|
+
progressWidth?: number
|
|
39
|
+
/**
|
|
40
|
+
* UI overrides for individual slots, derived from the scroll-to-top theme.
|
|
41
|
+
*/
|
|
42
|
+
ui?: Partial<Record<keyof typeof scrollToTopTheme.base, string>>
|
|
43
|
+
/**
|
|
44
|
+
* External class — applied to the root element.
|
|
45
|
+
*/
|
|
46
|
+
class?: string
|
|
47
|
+
/**
|
|
48
|
+
* Standard HTML attributes.
|
|
49
|
+
*/
|
|
50
|
+
[key: string]: unknown
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const scrollToTop = scv({
|
|
54
|
+
slots: [...scrollToTopTheme.slots],
|
|
55
|
+
base: scrollToTopTheme.base,
|
|
56
|
+
variants: scrollToTopTheme.variants,
|
|
57
|
+
compoundVariants: [...scrollToTopTheme.compoundVariants],
|
|
58
|
+
defaultVariants: scrollToTopTheme.defaultVariants,
|
|
59
|
+
classNameResolver: (...args) => twMerge(cx(...args))
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
const {
|
|
63
|
+
color = "primary",
|
|
64
|
+
progressWidth = 6,
|
|
65
|
+
duration = 0.1,
|
|
66
|
+
threshold = 200,
|
|
67
|
+
showProgress = false,
|
|
68
|
+
ui: uiProp,
|
|
69
|
+
class: className,
|
|
70
|
+
...rest
|
|
71
|
+
} = Astro.props as RLAScrollToTopProps
|
|
72
|
+
|
|
73
|
+
const globalConfig = getUIConfig()
|
|
74
|
+
|
|
75
|
+
const mergedUI = defu(
|
|
76
|
+
uiProp ?? {},
|
|
77
|
+
(globalConfig["scroll-to-top"] as Record<string, unknown>) ?? {}
|
|
78
|
+
) as Record<keyof typeof scrollToTopTheme.base, string | undefined>
|
|
79
|
+
|
|
80
|
+
const { root, button, progressBase, svg: svgClass, iconContainer, icon } = scrollToTop({
|
|
81
|
+
color
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
const circumference = 2 * Math.PI * 45
|
|
85
|
+
const percentPx = circumference / 100
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
<div
|
|
89
|
+
class:list={["rla-scroll-to-top", "fixed bottom-6 right-6 z-50", twMerge(root, mergedUI.root, className)]}
|
|
90
|
+
data-threshold={threshold}
|
|
91
|
+
data-duration={duration}
|
|
92
|
+
data-circle-stroke-width={progressWidth}
|
|
93
|
+
{...rest}
|
|
94
|
+
>
|
|
95
|
+
<button
|
|
96
|
+
type="button"
|
|
97
|
+
class={twMerge(button, mergedUI.button)}
|
|
98
|
+
aria-label="Scroll to top"
|
|
99
|
+
data-scroll-to-top-button
|
|
100
|
+
>
|
|
101
|
+
<div class={twMerge(progressBase, mergedUI.progressBase)}>
|
|
102
|
+
<svg class={twMerge(svgClass, mergedUI.svg)} viewBox="0 0 100 100" shape-rendering="geometricPrecision">
|
|
103
|
+
<circle
|
|
104
|
+
cx="50"
|
|
105
|
+
cy="50"
|
|
106
|
+
r="45"
|
|
107
|
+
fill="var(--color-primary-950)"
|
|
108
|
+
stroke-width={progressWidth}
|
|
109
|
+
stroke-dashoffset="0"
|
|
110
|
+
stroke-linecap="round"
|
|
111
|
+
class="gauge-secondary-stroke opacity-100"
|
|
112
|
+
/>
|
|
113
|
+
{showProgress && (
|
|
114
|
+
<circle
|
|
115
|
+
cx="50"
|
|
116
|
+
cy="50"
|
|
117
|
+
r="45"
|
|
118
|
+
fill="transparent"
|
|
119
|
+
stroke-width={progressWidth}
|
|
120
|
+
stroke-dashoffset="0"
|
|
121
|
+
stroke-linecap="round"
|
|
122
|
+
class="gauge-primary-stroke opacity-100"
|
|
123
|
+
data-progress-circle
|
|
124
|
+
style={`--stroke-percent: 0; --percent-to-px: ${percentPx}px; --circumference: ${circumference}; --duration: ${duration}s`}
|
|
125
|
+
/>
|
|
126
|
+
)}
|
|
127
|
+
</svg>
|
|
128
|
+
<div class={twMerge(iconContainer, mergedUI.iconContainer)}>
|
|
129
|
+
<svg
|
|
130
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
131
|
+
width="24"
|
|
132
|
+
height="24"
|
|
133
|
+
viewBox="0 0 24 24"
|
|
134
|
+
fill="none"
|
|
135
|
+
stroke="currentColor"
|
|
136
|
+
stroke-width="2"
|
|
137
|
+
stroke-linecap="round"
|
|
138
|
+
stroke-linejoin="round"
|
|
139
|
+
class={twMerge(icon, mergedUI.icon)}
|
|
140
|
+
>
|
|
141
|
+
<path d="m5 12 7-7 7 7" />
|
|
142
|
+
<path d="M12 19V5" />
|
|
143
|
+
</svg>
|
|
144
|
+
</div>
|
|
145
|
+
</div>
|
|
146
|
+
</button>
|
|
147
|
+
</div>
|
|
148
|
+
|
|
149
|
+
<script>
|
|
150
|
+
document.querySelectorAll('.rla-scroll-to-top').forEach((el) => {
|
|
151
|
+
const element = el as HTMLElement
|
|
152
|
+
const button = element.querySelector('[data-scroll-to-top-button]') as HTMLButtonElement | null
|
|
153
|
+
const progressCircle = element.querySelector('[data-progress-circle]') as SVGCircleElement | null
|
|
154
|
+
|
|
155
|
+
if (!button) return
|
|
156
|
+
|
|
157
|
+
const threshold = Number(element.dataset.threshold) || 200
|
|
158
|
+
|
|
159
|
+
function updateScrollState() {
|
|
160
|
+
const scrollTop = window.scrollY || document.documentElement.scrollTop
|
|
161
|
+
const scrollHeight = document.documentElement.scrollHeight - window.innerHeight
|
|
162
|
+
const scrollPercentage = scrollHeight > 0 ? (scrollTop / scrollHeight) * 100 : 0
|
|
163
|
+
|
|
164
|
+
if (scrollTop > threshold) {
|
|
165
|
+
element.classList.add('visible')
|
|
166
|
+
} else {
|
|
167
|
+
element.classList.remove('visible')
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (progressCircle) {
|
|
171
|
+
const circumference = 2 * Math.PI * 45
|
|
172
|
+
const percentPx = circumference / 100
|
|
173
|
+
const duration = element.dataset.duration || '0.1'
|
|
174
|
+
progressCircle.style.setProperty('--stroke-percent', scrollPercentage.toString())
|
|
175
|
+
progressCircle.style.setProperty('--percent-to-px', `${percentPx}px`)
|
|
176
|
+
progressCircle.style.setProperty('--circumference', circumference.toString())
|
|
177
|
+
progressCircle.style.setProperty('--duration', `${duration}s`)
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
button.addEventListener('click', () => {
|
|
182
|
+
window.scrollTo({ top: 0, behavior: 'smooth' })
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
window.addEventListener('scroll', updateScrollState, { passive: true })
|
|
186
|
+
updateScrollState()
|
|
187
|
+
})
|
|
188
|
+
</script>
|
|
189
|
+
|
|
190
|
+
<style>
|
|
191
|
+
.rla-scroll-to-top {
|
|
192
|
+
opacity: 0;
|
|
193
|
+
pointer-events: none;
|
|
194
|
+
transition: opacity 0.3s ease-in-out;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
.rla-scroll-to-top.visible {
|
|
198
|
+
opacity: 1;
|
|
199
|
+
pointer-events: auto;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
.progress-circle-base {
|
|
203
|
+
transform: translateZ(0);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
.gauge-primary-stroke {
|
|
207
|
+
stroke: var(--color-primary-500);
|
|
208
|
+
stroke-dasharray: calc(var(--stroke-percent, 0) * var(--percent-to-px, 2.826px)) var(--circumference, 282.6);
|
|
209
|
+
transition: stroke-dasharray var(--duration, 0.1s) ease, stroke var(--duration, 0.1s) ease;
|
|
210
|
+
transform: rotate(-90deg);
|
|
211
|
+
transform-origin: center;
|
|
212
|
+
shape-rendering: geometricPrecision;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
.gauge-secondary-stroke {
|
|
216
|
+
stroke: var(--color-primary-900);
|
|
217
|
+
stroke-dasharray: 282.6;
|
|
218
|
+
transform: rotate(-90deg);
|
|
219
|
+
transform-origin: center;
|
|
220
|
+
}
|
|
221
|
+
</style>
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed } from "vue"
|
|
3
|
+
import { scv, cx } from "css-variants"
|
|
4
|
+
import { twMerge } from "tailwind-merge"
|
|
5
|
+
import { useUi } from "../../composables"
|
|
6
|
+
import { footerTheme } from "../../themes/footer.theme.ts"
|
|
7
|
+
|
|
8
|
+
const footer = scv({
|
|
9
|
+
slots: [...footerTheme.slots],
|
|
10
|
+
base: footerTheme.base,
|
|
11
|
+
variants: footerTheme.variants,
|
|
12
|
+
defaultVariants: footerTheme.defaultVariants,
|
|
13
|
+
classNameResolver: (...args) => twMerge(cx(...args))
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
export interface RLVFooterProps {
|
|
17
|
+
/**
|
|
18
|
+
* Whether to contain the footer content in a max-width container.
|
|
19
|
+
*
|
|
20
|
+
* @default false
|
|
21
|
+
*/
|
|
22
|
+
contain?: boolean
|
|
23
|
+
/**
|
|
24
|
+
* UI overrides for individual slots, derived from the footer theme.
|
|
25
|
+
*/
|
|
26
|
+
ui?: Partial<Record<keyof typeof footerTheme.base, string>>
|
|
27
|
+
/**
|
|
28
|
+
* External class — applied to the root element.
|
|
29
|
+
*/
|
|
30
|
+
class?: string
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const { contain = false, class: className, ui: uiProp } = defineProps<RLVFooterProps>()
|
|
34
|
+
|
|
35
|
+
defineSlots<{
|
|
36
|
+
/**
|
|
37
|
+
* Content to display on the left side of the footer.
|
|
38
|
+
*/
|
|
39
|
+
left(props: {}): any
|
|
40
|
+
/**
|
|
41
|
+
* Content to display in the center of the footer.
|
|
42
|
+
*/
|
|
43
|
+
center(props: {}): any
|
|
44
|
+
/**
|
|
45
|
+
* Content to display on the right side of the footer.
|
|
46
|
+
*/
|
|
47
|
+
right(props: {}): any
|
|
48
|
+
}>()
|
|
49
|
+
|
|
50
|
+
const mergedUI = useUi("footer", uiProp as Record<string, unknown> | undefined)
|
|
51
|
+
|
|
52
|
+
const resolvedClasses = computed(() => {
|
|
53
|
+
const classes = footer({
|
|
54
|
+
contain
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
root: twMerge(classes.root, mergedUI.value.root as string | undefined),
|
|
59
|
+
container: twMerge(classes.container, mergedUI.value.container as string | undefined),
|
|
60
|
+
left: twMerge(classes.left, mergedUI.value.left as string | undefined),
|
|
61
|
+
center: twMerge(classes.center, mergedUI.value.center as string | undefined),
|
|
62
|
+
right: twMerge(classes.right, mergedUI.value.right as string | undefined)
|
|
63
|
+
}
|
|
64
|
+
})
|
|
65
|
+
</script>
|
|
66
|
+
|
|
67
|
+
<template>
|
|
68
|
+
<footer :class="[resolvedClasses.root, className]" data-slot="rlv-footer" v-bind="$attrs">
|
|
69
|
+
<div :class="resolvedClasses.container">
|
|
70
|
+
<div :class="resolvedClasses.left" data-slot="footer-left">
|
|
71
|
+
<slot name="left" />
|
|
72
|
+
</div>
|
|
73
|
+
<div :class="resolvedClasses.center" data-slot="footer-center">
|
|
74
|
+
<slot name="center" />
|
|
75
|
+
</div>
|
|
76
|
+
<div :class="resolvedClasses.right" data-slot="footer-right">
|
|
77
|
+
<slot name="right" />
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
</footer>
|
|
81
|
+
</template>
|