@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.
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 +2 -1
  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
@@ -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
- }
@@ -1,11 +0,0 @@
1
- import { onUnmounted } from "vue"
2
- import { defineShortcuts } from "../utils/shortcuts"
3
- import type { ShortcutsConfig, ShortcutsOptions } from "../utils/shortcuts"
4
-
5
- export function useShortcuts(config: ShortcutsConfig, options?: ShortcutsOptions) {
6
- const cleanup = defineShortcuts(config, options)
7
- onUnmounted(() => {
8
- cleanup()
9
- })
10
- return cleanup
11
- }
@@ -1,43 +0,0 @@
1
- import { reactive } from "vue"
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
- const toasts = reactive<Toast[]>([])
15
-
16
- export function useToast() {
17
- const add = (toast: Toast) => {
18
- const id = toast.id || Date.now()
19
- const newToast = { ...toast, id }
20
- toasts.push(newToast)
21
-
22
- if (toast.timeout !== 0) {
23
- setTimeout(() => {
24
- remove(id)
25
- }, toast.timeout || 5000)
26
- }
27
-
28
- return newToast
29
- }
30
-
31
- const remove = (id: string | number) => {
32
- const index = toasts.findIndex((t) => t.id === id)
33
- if (index !== -1) {
34
- toasts.splice(index, 1)
35
- }
36
- }
37
-
38
- return {
39
- toasts,
40
- add,
41
- remove
42
- }
43
- }