@rimelight/ui 0.0.21 → 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 CHANGED
@@ -1,31 +1,31 @@
1
1
  {
2
- "version": "0.0.21",
3
- "type": "module",
4
- "private": false,
5
- "license": "MIT",
6
- "author": {
7
- "name": "Daniel Marchi"
8
- },
9
2
  "name": "@rimelight/ui",
3
+ "version": "0.0.22",
4
+ "private": false,
10
5
  "description": "Rimelight Entertainment UI Library.",
11
6
  "keywords": [
12
- "typescript",
13
- "astro"
7
+ "astro",
8
+ "typescript"
14
9
  ],
15
10
  "homepage": "https://rimelight.com/docs",
11
+ "bugs": {
12
+ "url": "https://github.com/Rimelight-Entertainment/rimelight/issues"
13
+ },
14
+ "license": "MIT",
15
+ "author": {
16
+ "name": "Daniel Marchi"
17
+ },
16
18
  "repository": {
17
19
  "type": "git",
18
20
  "url": "git+https://github.com/Rimelight-Entertainment/rimelight.git"
19
21
  },
20
- "bugs": {
21
- "url": "https://github.com/Rimelight-Entertainment/rimelight/issues"
22
- },
23
22
  "files": [
24
23
  "src",
25
24
  "worker-configuration.d.ts",
26
25
  "README.md",
27
26
  "LICENSE"
28
27
  ],
28
+ "type": "module",
29
29
  "exports": {
30
30
  ".": "./src/integrations/index.ts",
31
31
  "./styles/*": "./src/styles/*",
@@ -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 "@/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
- * Whether the header should be sticky at the top.
24
- * @default true
25
- */
26
- sticky?: boolean;
27
- /**
28
- * UI overrides for individual slots, derived from the header theme.
29
- */
30
- ui?: Partial<Record<keyof typeof headerTheme.base, string>>;
31
- /**
32
- * External class — applied to the root element.
33
- */
34
- class?: string;
35
- /**
36
- * Standard HTML attributes for <header>
37
- */
38
- [key: string]: unknown;
39
- }
40
-
41
- const {
42
- contain = true,
43
- sticky = true,
44
- class: className,
45
- ui: uiProp,
46
- ...rest
47
- } = Astro.props as RLAHeaderProps
48
-
49
- const globalConfig = getUIConfig();
50
-
51
- const mergedUI = defu(
52
- uiProp ?? {},
53
- (globalConfig.header as Record<string, unknown>) ?? {},
54
- ) as Record<keyof typeof headerTheme.base, string | undefined>;
55
-
56
- const { root, container, left, center, right } = header({ contain, sticky });
57
- ---
58
-
59
- <header
60
- class:list={[twMerge(root, mergedUI.root, className)]}
61
- data-slot="rla-header"
62
- {...rest}
63
- >
64
- <div class:list={[twMerge(container, mergedUI.container)]}>
65
- <div class:list={[twMerge(left, mergedUI.left)]} data-slot="header-left">
66
- <slot name="left" />
67
- </div>
68
- <div class:list={[twMerge(center, mergedUI.center)]} data-slot="header-center">
69
- <slot name="center" />
70
- </div>
71
- <div class:list={[twMerge(right, mergedUI.right)]} data-slot="header-right">
72
- <slot name="right" />
73
- </div>
74
- </div>
75
- </header>
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
+ )}
@@ -6,12 +6,47 @@ import { scrollToTopTheme } from "../../themes/scroll-to-top.theme.ts"
6
6
  import { getUIConfig } from "virtual:rimelight-ui"
7
7
 
8
8
  export interface RLAScrollToTopProps {
9
+ /**
10
+ * Button color.
11
+ *
12
+ * @default "primary"
13
+ */
9
14
  color?: keyof typeof scrollToTopTheme.variants.color
10
- circleStrokeWidth?: number
15
+ /**
16
+ * Transition duration for the progress circle (in seconds).
17
+ *
18
+ * @default 0.1
19
+ */
11
20
  duration?: number
21
+ /**
22
+ * Scroll threshold (in pixels) before the button becomes visible.
23
+ *
24
+ * @default 200
25
+ */
12
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
+ */
13
42
  ui?: Partial<Record<keyof typeof scrollToTopTheme.base, string>>
43
+ /**
44
+ * External class — applied to the root element.
45
+ */
14
46
  class?: string
47
+ /**
48
+ * Standard HTML attributes.
49
+ */
15
50
  [key: string]: unknown
16
51
  }
17
52
 
@@ -26,9 +61,10 @@ const scrollToTop = scv({
26
61
 
27
62
  const {
28
63
  color = "primary",
29
- circleStrokeWidth = 4,
64
+ progressWidth = 6,
30
65
  duration = 0.1,
31
66
  threshold = 200,
67
+ showProgress = false,
32
68
  ui: uiProp,
33
69
  class: className,
34
70
  ...rest
@@ -53,7 +89,7 @@ const percentPx = circumference / 100
53
89
  class:list={["rla-scroll-to-top", "fixed bottom-6 right-6 z-50", twMerge(root, mergedUI.root, className)]}
54
90
  data-threshold={threshold}
55
91
  data-duration={duration}
56
- data-circle-stroke-width={circleStrokeWidth}
92
+ data-circle-stroke-width={progressWidth}
57
93
  {...rest}
58
94
  >
59
95
  <button
@@ -63,28 +99,31 @@ const percentPx = circumference / 100
63
99
  data-scroll-to-top-button
64
100
  >
65
101
  <div class={twMerge(progressBase, mergedUI.progressBase)}>
66
- <svg class={twMerge(svgClass, mergedUI.svg)} viewBox="0 0 100 100">
102
+ <svg class={twMerge(svgClass, mergedUI.svg)} viewBox="0 0 100 100" shape-rendering="geometricPrecision">
67
103
  <circle
68
104
  cx="50"
69
105
  cy="50"
70
106
  r="45"
71
107
  fill="var(--color-primary-950)"
72
- stroke-width={circleStrokeWidth}
108
+ stroke-width={progressWidth}
73
109
  stroke-dashoffset="0"
74
110
  stroke-linecap="round"
75
111
  class="gauge-secondary-stroke opacity-100"
76
112
  />
113
+ {showProgress && (
77
114
  <circle
78
115
  cx="50"
79
116
  cy="50"
80
117
  r="45"
81
118
  fill="transparent"
82
- stroke-width={circleStrokeWidth}
119
+ stroke-width={progressWidth}
83
120
  stroke-dashoffset="0"
84
121
  stroke-linecap="round"
85
122
  class="gauge-primary-stroke opacity-100"
86
123
  data-progress-circle
124
+ style={`--stroke-percent: 0; --percent-to-px: ${percentPx}px; --circumference: ${circumference}; --duration: ${duration}s`}
87
125
  />
126
+ )}
88
127
  </svg>
89
128
  <div class={twMerge(iconContainer, mergedUI.iconContainer)}>
90
129
  <svg
@@ -109,11 +148,11 @@ const percentPx = circumference / 100
109
148
 
110
149
  <script>
111
150
  document.querySelectorAll('.rla-scroll-to-top').forEach((el) => {
112
- const element = el
113
- const button = element.querySelector('[data-scroll-to-top-button]')
114
- const progressCircle = element.querySelector('[data-progress-circle]')
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
115
154
 
116
- if (!button || !progressCircle) return
155
+ if (!button) return
117
156
 
118
157
  const threshold = Number(element.dataset.threshold) || 200
119
158
 
@@ -128,10 +167,15 @@ const percentPx = circumference / 100
128
167
  element.classList.remove('visible')
129
168
  }
130
169
 
131
- const circumference = 2 * Math.PI * 45
132
- const percentPx = circumference / 100
133
- const strokeDasharray = `${(scrollPercentage * percentPx)} ${circumference}`
134
- progressCircle.setAttribute('stroke-dasharray', strokeDasharray)
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
+ }
135
179
  }
136
180
 
137
181
  button.addEventListener('click', () => {
@@ -161,9 +205,11 @@ const percentPx = circumference / 100
161
205
 
162
206
  .gauge-primary-stroke {
163
207
  stroke: var(--color-primary-500);
164
- transition: stroke-dasharray 0.1s ease, stroke 0.1s ease;
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;
165
210
  transform: rotate(-90deg);
166
211
  transform-origin: center;
212
+ shape-rendering: geometricPrecision;
167
213
  }
168
214
 
169
215
  .gauge-secondary-stroke {
@@ -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>