@rimelight/ui 0.0.35 → 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.
Files changed (44) hide show
  1. package/package.json +24 -20
  2. package/src/components/astro/RLAButton.astro +58 -26
  3. package/src/components/astro/RLACarousel.astro +202 -26
  4. package/src/components/astro/RLALink.astro +10 -12
  5. package/src/components/astro/RLALocaleSelector.astro +8 -8
  6. package/src/components/astro/RLAPopover.astro +99 -5
  7. package/src/components/astro/RLAScrollToTop.astro +10 -19
  8. package/src/components/astro/RLASelect.astro +52 -47
  9. package/src/components/astro/RLASidebar.astro +1 -1
  10. package/src/components/astro/RLATableOfContents.astro +76 -27
  11. package/src/components/astro/RLAThemeSelector.astro +1 -0
  12. package/src/components/astro/RLAToast.astro +2 -2
  13. package/src/components/vue/RLVScrollToTop.vue +12 -10
  14. package/src/components/vue/RLVToast.vue +1 -1
  15. package/src/components/vue/RLVToaster.vue +15 -3
  16. package/src/config/index.ts +0 -1
  17. package/src/integrations/index.ts +0 -1
  18. package/src/integrations/ui.ts +12 -11
  19. package/src/themes/link-group.theme.ts +1 -1
  20. package/src/themes/locale-selector.theme.ts +1 -1
  21. package/src/themes/popover.theme.ts +2 -5
  22. package/src/themes/select.theme.ts +1 -1
  23. package/src/themes/toast.theme.ts +9 -3
  24. package/src/types/components/button.ts +2 -1
  25. package/src/types/components/link.ts +0 -10
  26. package/src/types/components/sidebar.ts +1 -1
  27. package/src/types/components/table-of-contents.ts +5 -0
  28. package/src/types/components/toast.ts +1 -1
  29. package/src/utils/index.ts +2 -0
  30. package/src/utils/merge.ts +62 -0
  31. package/src/utils/scroll.ts +41 -0
  32. package/src/utils/toast.ts +37 -0
  33. package/src/utils/useUi.ts +2 -2
  34. package/src/components/HttpObservatory.astro +0 -103
  35. package/src/components/LighthouseScores.astro +0 -204
  36. package/src/composables/index.ts +0 -4
  37. package/src/composables/useHeaderStore.ts +0 -14
  38. package/src/composables/useScrollToTop.ts +0 -62
  39. package/src/composables/useShortcuts.ts +0 -11
  40. package/src/composables/useToast.ts +0 -43
  41. package/src/config/security.ts +0 -227
  42. package/src/integrations/sri.ts +0 -92
  43. package/src/middleware/index.ts +0 -1
  44. package/src/middleware/security.ts +0 -210
@@ -5,13 +5,10 @@ export const createPopoverTheme = (_colors: readonly string[] = defaultColors) =
5
5
  defineTheme({
6
6
  slots: ["root", "trigger", "content"],
7
7
  base: {
8
- root: "relative inline-block group/popover",
8
+ root: "relative inline-block",
9
9
  trigger: "",
10
10
  content: [
11
- "absolute z-50 w-72 rounded-lg border border-border bg-popover p-4 text-popover-foreground shadow-md outline-none",
12
- "transition-all duration-200 invisible opacity-0 scale-95 origin-center",
13
- "group-hover/popover:visible group-hover/popover:opacity-100 group-hover/popover:scale-100",
14
- "focus-within:visible focus-within:opacity-100 focus-within:scale-100"
11
+ "absolute z-50 w-72 rounded-xl border border-border bg-background p-4 text-foreground shadow-lg outline-none"
15
12
  ]
16
13
  },
17
14
  variants: {
@@ -9,7 +9,7 @@ export const createSelectTheme = () =>
9
9
  trigger:
10
10
  "flex h-9 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm shadow-sm transition-all focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
11
11
  content:
12
- "absolute z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md transition-all w-full mt-1 max-h-60 overflow-y-auto",
12
+ "absolute z-50 min-w-[8rem] overflow-hidden rounded-lg border border-border bg-background p-1 text-foreground shadow-lg transition-all w-full mt-1 max-h-60 overflow-y-auto",
13
13
  item: "relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 px-2 text-sm outline-none hover:bg-accent hover:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
14
14
  itemText: "truncate",
15
15
  itemIcon: "ml-auto size-4"
@@ -8,18 +8,24 @@ export const createToastTheme = () =>
8
8
  title: "text-sm font-semibold",
9
9
  description: "text-xs opacity-90",
10
10
  action:
11
- "inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-xs font-medium ring-offset-background transition-colors hover:bg-secondary focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive",
11
+ "inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-xs font-medium ring-offset-background transition-colors hover:bg-secondary focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 group-[.error]:border-muted/40 group-[.error]:hover:border-error-500/30 group-[.error]:hover:bg-error-500 group-[.error]:hover:text-white group-[.error]:focus:ring-error-500",
12
12
  close:
13
13
  "absolute right-2 top-2 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-2 group-hover:opacity-100"
14
14
  },
15
15
  variants: {
16
16
  variant: {
17
17
  default: { root: "bg-background border-border" },
18
- destructive: {
19
- root: "destructive group border-error-500/20 bg-error-500/10 text-error-700 dark:text-error-400"
18
+ error: {
19
+ root: "error group border-error-500/20 bg-error-500/10 text-error-700 dark:text-error-400"
20
20
  },
21
21
  success: {
22
22
  root: "border-success-500/20 bg-success-500/10 text-success-700 dark:text-success-400"
23
+ },
24
+ warning: {
25
+ root: "border-warning-500/20 bg-warning-500/10 text-warning-700 dark:text-warning-400"
26
+ },
27
+ info: {
28
+ root: "border-info-500/20 bg-info-500/10 text-info-700 dark:text-info-400"
23
29
  }
24
30
  }
25
31
  },
@@ -1,6 +1,7 @@
1
1
  import type { Theme, ThemeColor } from "../theme"
2
+ import type { LinkProps } from "./link"
2
3
 
3
- export interface ButtonProps {
4
+ export interface ButtonProps extends LinkProps {
4
5
  /**
5
6
  * The visual style variant of the button
6
7
  */
@@ -1,5 +1,3 @@
1
- import type { ThemeColor } from "../theme"
2
-
3
1
  export interface LinkProps {
4
2
  to?: string | object
5
3
  href?: string
@@ -13,14 +11,6 @@ export interface LinkProps {
13
11
  exactHash?: boolean
14
12
  inactiveClass?: string
15
13
  activeClass?: string
16
- variant?: "solid" | "outline" | "soft" | "subtle" | "ghost" | "link"
17
- color?: ThemeColor
18
- /**
19
- * Styles the link visually as a button
20
- *
21
- * @default false
22
- */
23
- isButton?: boolean
24
14
  /**
25
15
  * Screen reader accessibility label. Will automatically append " (opens in a new tab)" for
26
16
  * external links if not customized
@@ -8,7 +8,7 @@ export interface BadgeConfig {
8
8
  | "note"
9
9
  | "tip"
10
10
  | "caution"
11
- | "danger"
11
+ | "error"
12
12
  | "success"
13
13
  | "default"
14
14
  class?: string
@@ -21,6 +21,11 @@ export interface TableOfContentsProps {
21
21
  * @default ".markdown-content"
22
22
  */
23
23
  containerSelector?: string
24
+ /**
25
+ * Render as a collapsible dropdown (<details>/<summary>) with a scrollable list and a built-in
26
+ * bottom gap. Intended for small-screen / mobile contexts.
27
+ */
28
+ collapsible?: boolean
24
29
  /**
25
30
  * The component the root element should render as.
26
31
  *
@@ -11,7 +11,7 @@ export interface ToastProps {
11
11
  /**
12
12
  * Visual variant.
13
13
  */
14
- variant?: "default" | "destructive" | "success" | "warning" | "info"
14
+ variant?: "default" | "error" | "success" | "warning" | "info"
15
15
  /**
16
16
  * Action button label.
17
17
  */
@@ -5,3 +5,5 @@ export * from "./docs"
5
5
  export * from "./embed"
6
6
  export * from "./shortcuts"
7
7
  export * from "./color"
8
+ export * from "./scroll"
9
+ export * from "./toast"
@@ -0,0 +1,62 @@
1
+ export function isPlainObject(value: unknown): boolean {
2
+ if (value === null || typeof value !== "object") {
3
+ return false
4
+ }
5
+ const prototype = Object.getPrototypeOf(value)
6
+
7
+ if (
8
+ prototype !== null &&
9
+ prototype !== Object.prototype &&
10
+ Object.getPrototypeOf(prototype) !== null
11
+ ) {
12
+ return false
13
+ }
14
+
15
+ if (Symbol.iterator in value) {
16
+ return false
17
+ }
18
+
19
+ if (Symbol.toStringTag in value) {
20
+ return Object.prototype.toString.call(value) === "[object Module]"
21
+ }
22
+
23
+ return true
24
+ }
25
+
26
+ export function merge<T extends Record<string, any>>(base: T, defaults: Record<string, any>): T
27
+
28
+ export function merge(
29
+ base: Record<string, any>,
30
+ defaults: Record<string, any>
31
+ ): Record<string, any> {
32
+ if (!isPlainObject(defaults)) {
33
+ return merge(base, {})
34
+ }
35
+ if (!isPlainObject(base)) {
36
+ return { ...defaults }
37
+ }
38
+
39
+ const object = { ...defaults }
40
+
41
+ for (const key of Object.keys(base)) {
42
+ if (key === "__proto__" || key === "constructor") {
43
+ continue
44
+ }
45
+
46
+ const value = base[key]
47
+
48
+ if (value === null || value === undefined) {
49
+ continue
50
+ }
51
+
52
+ if (Array.isArray(value) && Array.isArray(object[key])) {
53
+ object[key] = [...value, ...object[key]]
54
+ } else if (isPlainObject(value) && isPlainObject(object[key])) {
55
+ object[key] = merge(value, object[key])
56
+ } else {
57
+ object[key] = value
58
+ }
59
+ }
60
+
61
+ return object
62
+ }
@@ -0,0 +1,41 @@
1
+ export interface ScrollState {
2
+ isVisible: boolean
3
+ scrollPercentage: number
4
+ }
5
+
6
+ export function trackScroll(
7
+ options: { threshold?: number } = {},
8
+ onUpdate: (state: ScrollState) => void
9
+ ): () => void {
10
+ if (typeof window === "undefined") {
11
+ onUpdate({ isVisible: false, scrollPercentage: 0 })
12
+ return () => {}
13
+ }
14
+
15
+ const { threshold = 200 } = options
16
+
17
+ function update() {
18
+ const scrollTop = window.scrollY || document.documentElement.scrollTop
19
+ const scrollHeight = document.documentElement.scrollHeight - window.innerHeight
20
+ onUpdate({
21
+ isVisible: scrollTop > threshold,
22
+ scrollPercentage: scrollHeight > 0 ? (scrollTop / scrollHeight) * 100 : 0
23
+ })
24
+ }
25
+
26
+ window.addEventListener("scroll", update, { passive: true })
27
+ update()
28
+
29
+ return () => {
30
+ window.removeEventListener("scroll", update)
31
+ }
32
+ }
33
+
34
+ export function scrollToTop() {
35
+ if (typeof window !== "undefined") {
36
+ window.scrollTo({
37
+ top: 0,
38
+ behavior: "smooth"
39
+ })
40
+ }
41
+ }
@@ -0,0 +1,37 @@
1
+ import { atom } from "nanostores"
2
+ import type { ThemeColor } from "../types"
3
+
4
+ export interface Toast {
5
+ id?: string | number
6
+ title?: string
7
+ description?: string
8
+ icon?: string
9
+ color?: ThemeColor
10
+ timeout?: number
11
+ callback?: () => void
12
+ }
13
+
14
+ export const toastsStore = atom<Toast[]>([])
15
+
16
+ export function addToast(toast: Toast) {
17
+ const id = toast.id || Date.now()
18
+ const newToast = { ...toast, id }
19
+ toastsStore.set([...toastsStore.get(), newToast])
20
+
21
+ if (toast.timeout !== 0) {
22
+ setTimeout(() => {
23
+ removeToast(id)
24
+ }, toast.timeout || 5000)
25
+ }
26
+
27
+ return newToast
28
+ }
29
+
30
+ export function removeToast(id: string | number) {
31
+ const currentToasts = toastsStore.get()
32
+ const target = currentToasts.find((t) => t.id === id)
33
+ if (target && target.callback) {
34
+ target.callback()
35
+ }
36
+ toastsStore.set(currentToasts.filter((t) => t.id !== id))
37
+ }
@@ -1,4 +1,4 @@
1
- import defu from "defu"
1
+ import { merge } from "./merge"
2
2
  import { getUIConfig } from "virtual:rimelight-ui"
3
3
  import type { DefaultUIConfig } from "../themes"
4
4
 
@@ -8,7 +8,7 @@ export function useUi(
8
8
  ): Record<string, string | undefined> {
9
9
  const globalConfig = getUIConfig()
10
10
  const defaults = globalConfig[componentName] ?? {}
11
- const merged = defu(uiProp ?? {}, defaults)
11
+ const merged = merge(uiProp ?? {}, defaults)
12
12
 
13
13
  return Object.fromEntries(
14
14
  Object.entries(merged).map(([key, value]) => [
@@ -1,103 +0,0 @@
1
- ---
2
- /**
3
- * HttpObservatory.astro
4
- * A callout component displaying an HTTP Observatory score
5
- * Designed to match the authentic Mozilla Observatory report aesthetic
6
- */
7
- export interface HttpObservatoryProps {
8
- grade?: string;
9
- score?: number;
10
- testsPassed?: number;
11
- totalTests?: number;
12
- }
13
-
14
- type Props = HttpObservatoryProps;
15
-
16
- const {
17
- grade = "A+",
18
- score = 145,
19
- testsPassed = 10,
20
- totalTests = 10,
21
- } = Astro.props;
22
- ---
23
-
24
- <div class="http-obs-container mt-6xl px-2xs sm:px-0">
25
- <!-- Header -->
26
- <div class="mb-4xl text-center">
27
- <h3 class="font-heading text-foreground text-3xl font-bold md:text-4xl">
28
- HTTP Observatory
29
- </h3>
30
- </div>
31
-
32
- <div
33
- class="mx-auto max-w-4xl overflow-hidden rounded-xl border border-border bg-[#181a1b] p-6xl text-white shadow-xl sm:p-4xl"
34
- >
35
- <div
36
- class="flex flex-col items-center gap-4xl sm:flex-row sm:items-start sm:justify-between"
37
- >
38
- <!-- Grade & Stats -->
39
- <div class="flex flex-col items-center gap-4xl sm:flex-row sm:items-center">
40
- <!-- Grade Box -->
41
- <div
42
- class="flex size-6xl shrink-0 items-center p-lg justify-center rounded-lg border-2 border-[#1eac4e] bg-[#1eac4e]/20 text-5xl font-bold text-[#1eac4e] sm:size-5xl sm:text-6xl"
43
- aria-label={`Grade ${grade}`}
44
- >
45
- {grade}
46
- </div>
47
-
48
- <!-- Stats -->
49
- <div class="flex flex-col gap-2xs text-center sm:text-left">
50
- <a
51
- href="https://developer.mozilla.org/en-US/observatory/analyze?host=danielmarchi.dev"
52
- target="_blank"
53
- rel="noopener noreferrer"
54
- class="text-sm font-semibold text-[#4fa9ff] hover:underline sm:text-base"
55
- >
56
- Run the scan.
57
- </a>
58
-
59
- <div class="flex flex-col gap-2xs font-medium text-gray-200">
60
- <p>
61
- <span
62
- class="text-white decoration-white/40 underline-offset-4 hover:underline"
63
- >Score</span
64
- >:
65
- <span class="text-white">{score} / 100</span>
66
- </p>
67
- <p>
68
- <span
69
- class="text-white decoration-white/40 underline-offset-4 hover:underline"
70
- >Tests Passed</span
71
- >:
72
- <span class="text-white">{testsPassed} / {totalTests}</span>
73
- </p>
74
- </div>
75
- </div>
76
- </div>
77
- </div>
78
- </div>
79
- </div>
80
-
81
- <style>
82
- .http-obs-container {
83
- animation: fade-up 0.8s cubic-bezier(0.16, 1, 0.3, 1) both;
84
- animation-delay: 0.4s;
85
- }
86
-
87
- @keyframes fade-up {
88
- from {
89
- opacity: 0;
90
- transform: translateY(20px);
91
- }
92
- to {
93
- opacity: 1;
94
- transform: translateY(0);
95
- }
96
- }
97
-
98
- @media (prefers-reduced-motion: reduce) {
99
- .http-obs-container {
100
- animation: none;
101
- }
102
- }
103
- </style>
@@ -1,204 +0,0 @@
1
- ---
2
- /**
3
- * LighthouseScores.astro
4
- * A callout component displaying perfect Lighthouse scores
5
- * Designed to match the authentic Lighthouse report aesthetic
6
- */
7
- export interface LighthouseScore {
8
- label: string;
9
- value: number;
10
- }
11
-
12
- export interface LighthouseScoresProps {
13
- performance?: number;
14
- accessibility?: number;
15
- bestPractices?: number;
16
- seo?: number;
17
- }
18
-
19
- const {
20
- performance = 100,
21
- accessibility = 100,
22
- bestPractices = 100,
23
- seo = 100,
24
- } = Astro.props;
25
-
26
- const scores = [
27
- { label: 'Performance', value: performance },
28
- { label: 'Accessibility', value: accessibility },
29
- { label: 'Best Practices', value: bestPractices },
30
- { label: 'SEO', value: seo },
31
- ];
32
- ---
33
-
34
- <div class="lighthouse-container">
35
- <!-- Header -->
36
- <div class="mb-4xl text-center">
37
- <h3 class="font-heading text-foreground text-3xl font-bold md:text-4xl">
38
- Lighthouse Report
39
- </h3>
40
- </div>
41
-
42
- <!-- Scores Grid -->
43
- <div
44
- class="lighthouse-scores mx-auto grid max-w-2xl grid-cols-2 gap-4xl sm:grid-cols-4"
45
- role="list"
46
- aria-label="Lighthouse scores"
47
- >
48
- {
49
- scores.map((score) => {
50
- const radius = 54;
51
- const circumference = 2 * Math.PI * radius;
52
- const offset = circumference - (score.value / 100) * circumference;
53
- const color = score.value >= 90 ? "#0cce6b" : score.value >= 50 ? "#ffa400" : "#ff4e42";
54
-
55
- return (
56
- <div
57
- class="lighthouse-score group flex flex-col items-center"
58
- role="listitem"
59
- >
60
- <div class="relative">
61
- <svg
62
- class="lighthouse-gauge"
63
- width="120"
64
- height="120"
65
- viewBox="0 0 120 120"
66
- aria-hidden="true"
67
- style="width: 96px; height: 96px;"
68
- >
69
- <style>
70
- @media (min-width: 640px) {
71
- .lighthouse-gauge {
72
- width: 112px !important;
73
- height: 112px !important;
74
- }
75
- }
76
- @media (min-width: 768px) {
77
- .lighthouse-gauge {
78
- width: 144px !important;
79
- height: 144px !important;
80
- }
81
- }
82
- </style>
83
- <circle
84
- cx="60"
85
- cy="60"
86
- r={radius}
87
- fill="none"
88
- stroke="#e0e0e0"
89
- stroke-width="8"
90
- />
91
- <circle
92
- cx="60"
93
- cy="60"
94
- r={radius}
95
- fill="none"
96
- stroke={color}
97
- stroke-width="8"
98
- stroke-linecap="round"
99
- stroke-dasharray={circumference}
100
- stroke-dashoffset={offset}
101
- class="lighthouse-progress"
102
- transform="rotate(-90 60 60)"
103
- style={`--target-offset: ${offset}`}
104
- />
105
- <text
106
- x="60"
107
- y="60"
108
- text-anchor="middle"
109
- dominant-baseline="central"
110
- class="lighthouse-number font-semibold font-sans"
111
- fill={color}
112
- style="font-size: 2rem;"
113
- >
114
- {score.value}
115
- </text>
116
- </svg>
117
- </div>
118
-
119
- <span class="text-muted-foreground mt-2xl text-center text-sm font-medium">
120
- {score.label}
121
- </span>
122
- </div>
123
- );
124
- })
125
- }
126
- </div>
127
-
128
- <!-- Footer note -->
129
- <p class="text-muted-foreground/60 mt-4xl text-center text-xs italic">
130
- *Results represent the starting template. Changes to styling and features may affect final results.
131
- </p>
132
- </div>
133
-
134
- <style>
135
- /* Entrance animations */
136
- .lighthouse-score {
137
- animation: score-enter 0.6s cubic-bezier(0.16, 1, 0.3, 1) both;
138
- }
139
-
140
- .lighthouse-progress {
141
- animation: progress-fill 1.2s cubic-bezier(0.16, 1, 0.3, 1) both;
142
- }
143
-
144
- /* Staggered delays via nth-child to avoid inline style attributes */
145
- .lighthouse-score:nth-child(1) { animation-delay: 100ms; }
146
- .lighthouse-score:nth-child(2) { animation-delay: 180ms; }
147
- .lighthouse-score:nth-child(3) { animation-delay: 260ms; }
148
- .lighthouse-score:nth-child(4) { animation-delay: 340ms; }
149
-
150
- .lighthouse-progress:nth-child(1) { animation-delay: 200ms; }
151
- .lighthouse-progress:nth-child(2) { animation-delay: 280ms; }
152
- .lighthouse-progress:nth-child(3) { animation-delay: 360ms; }
153
- .lighthouse-progress:nth-child(4) { animation-delay: 440ms; }
154
-
155
- @keyframes score-enter {
156
- from {
157
- opacity: 0;
158
- transform: scale(0.9) translateY(10px);
159
- }
160
- to {
161
- opacity: 1;
162
- transform: scale(1) translateY(0);
163
- }
164
- }
165
-
166
- @keyframes progress-fill {
167
- from {
168
- stroke-dashoffset: 339.292; /* Approximate circumference */
169
- }
170
- to {
171
- stroke-dashoffset: var(--target-offset, 0);
172
- }
173
- }
174
-
175
- @media (prefers-reduced-motion: reduce) {
176
- .lighthouse-score,
177
- .lighthouse-progress {
178
- animation: none;
179
- }
180
-
181
- .lighthouse-progress {
182
- stroke-dashoffset: 0;
183
- }
184
- }
185
- </style>
186
-
187
- <script>
188
- // Run animations immediately
189
- const initLighthouse = () => {
190
- const scoresContainer = document.querySelector('.lighthouse-scores');
191
- if (scoresContainer) {
192
- const elements = scoresContainer.querySelectorAll<HTMLElement>('.lighthouse-score, .lighthouse-progress');
193
- elements.forEach((el) => {
194
- el.style.animationPlayState = 'running';
195
- });
196
- }
197
- };
198
-
199
- // Run on initial load
200
- initLighthouse();
201
-
202
- // Run on View Transitions navigate
203
- document.addEventListener('astro:after-swap', initLighthouse);
204
- </script>
@@ -1,4 +0,0 @@
1
- export { useScrollToTop } from "./useScrollToTop"
2
- export { useToast } from "./useToast"
3
- export { headerLayersStore } from "./useHeaderStore"
4
- export { useShortcuts } from "./useShortcuts"
@@ -1,14 +0,0 @@
1
- import { map } from "nanostores"
2
-
3
- export interface HeaderLayerInfo {
4
- type: "banner" | "header"
5
- order: number
6
- isVisible: boolean
7
- height: number
8
- }
9
-
10
- /**
11
- * Global store tracking all registered header layers. This allows child components to subscribe to
12
- * header layer changes even across Vue/Astro boundaries, since nanostores is framework-agnostic.
13
- */
14
- export const headerLayersStore = map<Record<string, HeaderLayerInfo>>({})
@@ -1,62 +0,0 @@
1
- import { ref, onMounted, onUnmounted, type Ref } from "vue"
2
-
3
- export interface UseScrollToTopOptions {
4
- /**
5
- * The scroll threshold (in pixels) before the button becomes visible.
6
- *
7
- * @default 200
8
- */
9
- threshold?: number
10
- }
11
-
12
- export interface UseScrollToTopReturn {
13
- /**
14
- * Whether the scroll-to-top button should be visible.
15
- */
16
- isVisible: Ref<boolean>
17
- /**
18
- * The current scroll percentage (0-100).
19
- */
20
- scrollPercentage: Ref<number>
21
- /**
22
- * Scrolls the window to the top with a smooth animation.
23
- */
24
- scrollToTop: () => void
25
- }
26
-
27
- function scrollToTop() {
28
- window.scrollTo({
29
- top: 0,
30
- behavior: "smooth"
31
- })
32
- }
33
-
34
- export function useScrollToTop(options: UseScrollToTopOptions = {}): UseScrollToTopReturn {
35
- const { threshold = 200 } = options
36
-
37
- const isVisible = ref(false)
38
- const scrollPercentage = ref(0)
39
-
40
- function updateScrollState() {
41
- const scrollTop = window.scrollY || document.documentElement.scrollTop
42
- const scrollHeight = document.documentElement.scrollHeight - window.innerHeight
43
-
44
- isVisible.value = scrollTop > threshold
45
- scrollPercentage.value = scrollHeight > 0 ? (scrollTop / scrollHeight) * 100 : 0
46
- }
47
-
48
- onMounted(() => {
49
- window.addEventListener("scroll", updateScrollState, { passive: true })
50
- updateScrollState()
51
- })
52
-
53
- onUnmounted(() => {
54
- window.removeEventListener("scroll", updateScrollState)
55
- })
56
-
57
- return {
58
- isVisible,
59
- scrollPercentage,
60
- scrollToTop
61
- }
62
- }