@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
@@ -64,9 +64,7 @@ const classes = resolveClasses(scrollToTop, { color, theme }, useUi("scrollToTop
64
64
  </rla-scroll-to-top>
65
65
 
66
66
  <script>
67
- function onClick() {
68
- window.scrollTo({ top: 0, behavior: 'smooth' });
69
- }
67
+ import { trackScroll, scrollToTop } from "../../utils/scroll"
70
68
 
71
69
  class RLAScrollToTop extends HTMLElement {
72
70
  private cleanupScroll: (() => void) | null = null;
@@ -95,12 +93,12 @@ const classes = resolveClasses(scrollToTop, { color, theme }, useUi("scrollToTop
95
93
  return this.styleEl;
96
94
  };
97
95
 
98
- const updateStyles = () => {
99
- const scrollTop = window.scrollY || document.documentElement.scrollTop;
100
- const scrollHeight = document.documentElement.scrollHeight - window.innerHeight;
101
- const scrollPercentage = scrollHeight > 0 ? (scrollTop / scrollHeight) * 100 : 0;
96
+ innerElement.dataset.rlaInstance = String(id);
97
+
98
+ button.addEventListener('click', scrollToTop);
102
99
 
103
- if (scrollTop > threshold) {
100
+ const unsubscribe = trackScroll({ threshold }, (state) => {
101
+ if (state.isVisible) {
104
102
  innerElement.classList.add('visible');
105
103
  } else {
106
104
  innerElement.classList.remove('visible');
@@ -113,23 +111,16 @@ const classes = resolveClasses(scrollToTop, { color, theme }, useUi("scrollToTop
113
111
  background-color: #000;
114
112
  }
115
113
  .rla-scroll-to-top-inner[data-rla-instance="${id}"] .gauge-progress {
116
- --gauge-progress: ${scrollPercentage};
114
+ --gauge-progress: ${state.scrollPercentage};
117
115
  --gauge-stroke-width: ${gaugeBorderWidth}px;
118
116
  --gauge-color: var(--color-${color}-500);
119
117
  }
120
118
  `;
121
- };
122
-
123
- innerElement.dataset.rlaInstance = String(id);
124
-
125
- button.addEventListener('click', onClick);
126
-
127
- window.addEventListener('scroll', updateStyles, { passive: true });
128
- updateStyles();
119
+ });
129
120
 
130
121
  this.cleanupScroll = () => {
131
- button.removeEventListener('click', onClick);
132
- window.removeEventListener('scroll', updateStyles);
122
+ button.removeEventListener('click', scrollToTop);
123
+ unsubscribe();
133
124
  if (this.styleEl) {
134
125
  this.styleEl.remove();
135
126
  this.styleEl = null;
@@ -51,7 +51,7 @@ const initialLabel = selectedItem ? selectedItem.label : ""
51
51
  data-select-trigger
52
52
  >
53
53
  <span data-select-value>{initialLabel || placeholder}</span>
54
- <span class="i-lucide-chevron-down opacity-50" />
54
+ <span class="i-lucide-chevron-down ml-2 size-4 opacity-50" />
55
55
  </button>
56
56
 
57
57
  <div class={classes.content + " hidden"} data-select-dropdown data-slot="content">
@@ -78,61 +78,66 @@ const initialLabel = selectedItem ? selectedItem.label : ""
78
78
  <script>
79
79
  class RLASelect extends HTMLElement {
80
80
  private selectCleanup: (() => void) | null = null;
81
+ private initTimeout: any = null;
81
82
 
82
83
  connectedCallback() {
83
84
  this.style.display = 'contents';
84
- const wrapper = this.querySelector("[data-select-wrapper]") as HTMLElement | null;
85
- if (!wrapper) return;
86
-
87
- const trigger = wrapper.querySelector("[data-select-trigger]") as HTMLButtonElement;
88
- const dropdown = wrapper.querySelector("[data-select-dropdown]") as HTMLDivElement;
89
- const items = wrapper.querySelectorAll<HTMLElement>("[data-select-item]");
90
- const valueSpan = wrapper.querySelector("[data-select-value]");
91
- const hiddenInput = this.querySelector("[data-select-hidden-input]") as HTMLInputElement | null;
92
-
93
- if (!trigger || !dropdown) return;
94
-
95
- const onTriggerClick = (e: MouseEvent) => {
96
- e.stopPropagation();
97
- dropdown.classList.toggle("hidden");
98
- };
99
- trigger.addEventListener("click", onTriggerClick);
100
-
101
- const itemCleanups: (() => void)[] = [];
102
- items.forEach(item => {
103
- const onItemClick = (e: MouseEvent) => {
85
+ this.initTimeout = setTimeout(() => {
86
+ const wrapper = this.querySelector("[data-select-wrapper]") as HTMLElement | null;
87
+ if (!wrapper) return;
88
+
89
+ const trigger = wrapper.querySelector("[data-select-trigger]") as HTMLButtonElement;
90
+ const dropdown = wrapper.querySelector("[data-select-dropdown]") as HTMLDivElement;
91
+ const items = wrapper.querySelectorAll<HTMLElement>("[data-select-item]");
92
+ const valueSpan = wrapper.querySelector("[data-select-value]");
93
+ const hiddenInput = this.querySelector("[data-select-hidden-input]") as HTMLInputElement | null;
94
+
95
+ if (!trigger || !dropdown) return;
96
+
97
+ const onTriggerClick = (e: MouseEvent) => {
104
98
  e.stopPropagation();
105
- const val = (item as HTMLElement).dataset.value || "";
106
- const label = (item as HTMLElement).dataset.label || "";
107
-
108
- if (hiddenInput) {
109
- hiddenInput.value = val;
110
- hiddenInput.dispatchEvent(new Event("change", { bubbles: true }));
111
- }
112
- if (valueSpan) valueSpan.textContent = label;
113
-
114
- items.forEach(i => i.querySelector("[data-check-icon]")?.classList.add("hidden"));
115
- item.querySelector("[data-check-icon]")?.classList.remove("hidden");
116
-
99
+ dropdown.classList.toggle("hidden");
100
+ };
101
+ trigger.addEventListener("click", onTriggerClick);
102
+
103
+ const itemCleanups: (() => void)[] = [];
104
+ items.forEach(item => {
105
+ const onItemClick = (e: MouseEvent) => {
106
+ e.stopPropagation();
107
+ const val = (item as HTMLElement).dataset.value || "";
108
+ const label = (item as HTMLElement).dataset.label || "";
109
+ if (hiddenInput) {
110
+ hiddenInput.value = val;
111
+ hiddenInput.dispatchEvent(new Event("change", { bubbles: true }));
112
+ }
113
+ if (valueSpan) valueSpan.textContent = label;
114
+
115
+ items.forEach(i => i.querySelector("[data-check-icon]")?.classList.add("hidden"));
116
+ item.querySelector("[data-check-icon]")?.classList.remove("hidden");
117
+
118
+ dropdown.classList.add("hidden");
119
+ };
120
+ item.addEventListener("click", onItemClick);
121
+ itemCleanups.push(() => item.removeEventListener("click", onItemClick));
122
+ });
123
+
124
+ const onDocumentClick = () => {
117
125
  dropdown.classList.add("hidden");
118
126
  };
119
- item.addEventListener("click", onItemClick);
120
- itemCleanups.push(() => item.removeEventListener("click", onItemClick));
121
- });
122
-
123
- const onDocumentClick = () => {
124
- dropdown.classList.add("hidden");
125
- };
126
- document.addEventListener("click", onDocumentClick);
127
-
128
- this.selectCleanup = () => {
129
- trigger.removeEventListener("click", onTriggerClick);
130
- itemCleanups.forEach(fn => fn());
131
- document.removeEventListener("click", onDocumentClick);
132
- };
127
+ document.addEventListener("click", onDocumentClick);
128
+
129
+ this.selectCleanup = () => {
130
+ trigger.removeEventListener("click", onTriggerClick);
131
+ itemCleanups.forEach(fn => fn());
132
+ document.removeEventListener("click", onDocumentClick);
133
+ };
134
+ }, 0);
133
135
  }
134
136
 
135
137
  disconnectedCallback() {
138
+ if (this.initTimeout) {
139
+ clearTimeout(this.initTimeout);
140
+ }
136
141
  if (this.selectCleanup) {
137
142
  this.selectCleanup();
138
143
  this.selectCleanup = null;
@@ -41,7 +41,7 @@ function normalizeBadge(badge: any) {
41
41
  const v = badge.variant.toLowerCase()
42
42
  if (v === "tip" || v === "success") { color = "success"; variant = "soft" }
43
43
  else if (v === "caution" || v === "warning") { color = "warning"; variant = "soft" }
44
- else if (v === "danger") { color = "danger"; variant = "soft" }
44
+ else if (v === "error") { color = "error"; variant = "soft" }
45
45
  else if (v === "note" || v === "default") { color = "primary"; variant = "soft" }
46
46
  }
47
47
  return { text: badge.text || "", color, variant: variant as "solid" | "outline" | "soft", class: badge.class || "" }
@@ -15,6 +15,7 @@ const {
15
15
  tableOfContents: tableOfContentsConfig,
16
16
  ui: uiProp,
17
17
  class: className,
18
+ collapsible = false,
18
19
  containerSelector = ".markdown-content",
19
20
  ...rest
20
21
  } = Astro.props as TableOfContentsProps
@@ -36,32 +37,62 @@ const headingSelector = Array.from({ length: maxLevel - minLevel + 1 }, (_, i) =
36
37
  ---
37
38
 
38
39
  {tableOfContentsConfig !== false && filteredHeadings.length > 0 && (
39
- <div class={classes.root} data-slot="root" data-container-selector={containerSelector} data-heading-selector={headingSelector} {...rest}>
40
- <div class={classes.container} data-slot="container">
41
- <h4 class={classes.title} data-slot="title">On this page</h4>
42
- <ul class:list={[classes.list, "mt-2xl flex flex-col gap-3xs"]} data-slot="list">
43
- {filteredHeadings.map(heading => (
44
- <li
45
- class:list={[
46
- classes.item,
47
- heading.depth === 2 && "pl-3xs",
48
- heading.depth === 3 && "pl-6",
49
- heading.depth >= 4 && "pl-9"
50
- ]}
51
- data-slot="item"
52
- >
53
- <a
54
- href={`#${heading.slug}`}
55
- class:list={["toc-link", classes.link]}
56
- data-slot="link"
40
+ collapsible ? (
41
+ <details class:list={[className]} data-rla-toc-collapsible data-container-selector={containerSelector} data-heading-selector={headingSelector} {...rest}>
42
+ <summary class="flex items-center justify-between gap-3 px-4 h-10 text-sm font-medium text-foreground cursor-pointer select-none hover:bg-muted/40 transition-colors list-none">
43
+ <span class="flex items-center gap-2">
44
+ <span class="i-lucide-list size-4 text-primary shrink-0" />
45
+ <span>On this page</span>
46
+ </span>
47
+ <span class="rla-toc-chevron i-lucide-chevron-down size-4 text-muted-foreground shrink-0 transition-transform duration-200" />
48
+ </summary>
49
+ <div class="px-4 pt-3 max-h-[40vh] overflow-y-auto border-t border-border/50 bg-background">
50
+ <ul class="flex flex-col gap-0.5 pb-8">
51
+ {filteredHeadings.map(heading => (
52
+ <li class:list={[heading.depth === 3 && "pl-4"]}>
53
+ <a
54
+ href={`#${heading.slug}`}
55
+ class:list={[
56
+ "toc-link block py-1.5 text-sm transition-colors hover:text-primary no-underline",
57
+ heading.depth === 2 ? "text-foreground font-medium" : "text-muted-foreground"
58
+ ]}
59
+ data-rla-toc-link
60
+ >
61
+ {heading.text}
62
+ </a>
63
+ </li>
64
+ ))}
65
+ </ul>
66
+ </div>
67
+ </details>
68
+ ) : (
69
+ <div class={classes.root} data-slot="root" data-container-selector={containerSelector} data-heading-selector={headingSelector} {...rest}>
70
+ <div class={classes.container} data-slot="container">
71
+ <h4 class={classes.title} data-slot="title">On this page</h4>
72
+ <ul class:list={[classes.list, "mt-2xl flex flex-col gap-3xs"]} data-slot="list">
73
+ {filteredHeadings.map(heading => (
74
+ <li
75
+ class:list={[
76
+ classes.item,
77
+ heading.depth === 2 && "pl-3xs",
78
+ heading.depth === 3 && "pl-6",
79
+ heading.depth >= 4 && "pl-9"
80
+ ]}
81
+ data-slot="item"
57
82
  >
58
- <span class={classes.linkText} data-slot="link-text">{heading.text}</span>
59
- </a>
60
- </li>
61
- ))}
62
- </ul>
83
+ <a
84
+ href={`#${heading.slug}`}
85
+ class:list={["toc-link", classes.link]}
86
+ data-slot="link"
87
+ >
88
+ <span class={classes.linkText} data-slot="link-text">{heading.text}</span>
89
+ </a>
90
+ </li>
91
+ ))}
92
+ </ul>
93
+ </div>
63
94
  </div>
64
- </div>
95
+ )
65
96
  )}
66
97
 
67
98
  <style is:global>
@@ -73,10 +104,23 @@ const headingSelector = Array.from({ length: maxLevel - minLevel + 1 }, (_, i) =
73
104
 
74
105
  <script is:inline>
75
106
  (function() {
76
- var root = document.querySelector('[data-container-selector]');
77
- if (!root) return;
107
+ // ── Collapsible TOC behaviour (chevron + close-on-link-click) ──
108
+ function initCollapsibleTocs() {
109
+ document.querySelectorAll('[data-rla-toc-collapsible]').forEach(function(details) {
110
+ var chevron = details.querySelector('.rla-toc-chevron');
111
+ details.addEventListener('toggle', function() {
112
+ if (chevron) chevron.style.transform = details.open ? 'rotate(180deg)' : '';
113
+ });
114
+ details.querySelectorAll('[data-rla-toc-link]').forEach(function(link) {
115
+ link.addEventListener('click', function() { details.open = false; });
116
+ });
117
+ });
118
+ }
78
119
 
120
+ // ── Active-heading tracker (desktop sidebar) ──
79
121
  function updateActiveHeading() {
122
+ var root = document.querySelector('[data-slot="root"][data-container-selector]');
123
+ if (!root) return;
80
124
  var links = root.querySelectorAll(".toc-link");
81
125
  if (links.length === 0) return;
82
126
 
@@ -102,8 +146,13 @@ const headingSelector = Array.from({ length: maxLevel - minLevel + 1 }, (_, i) =
102
146
  }
103
147
  }
104
148
 
149
+ initCollapsibleTocs();
105
150
  updateActiveHeading();
151
+
106
152
  window.addEventListener("scroll", updateActiveHeading, { passive: true });
107
- document.addEventListener("astro:after-swap", updateActiveHeading);
153
+ document.addEventListener("astro:after-swap", function() {
154
+ initCollapsibleTocs();
155
+ updateActiveHeading();
156
+ });
108
157
  })();
109
158
  </script>
@@ -28,6 +28,7 @@ const classes = resolveClasses(themeSelector, {
28
28
  size={size}
29
29
  class:list={["theme-toggle-button", classes.root]}
30
30
  variant="ghost"
31
+ square
31
32
  aria-label="Toggle theme"
32
33
  >
33
34
  <span class:list={[classes.sun]} />
@@ -24,12 +24,12 @@ const {
24
24
  } = Astro.props as ToastProps
25
25
 
26
26
  const classes = resolveClasses(toast, {
27
- variant: (variant === "success" || variant === "destructive") ? variant : "default"
27
+ variant: ["success", "error", "warning", "info"].includes(variant ?? "") ? variant : "default"
28
28
  }, useUi("toast", uiProp), className)
29
29
 
30
30
  const variantIcons: Record<string, string> = {
31
31
  default: "i-lucide-bell",
32
- destructive: "i-lucide-alert-circle",
32
+ error: "i-lucide-alert-circle",
33
33
  success: "i-lucide-check-circle-2",
34
34
  warning: "i-lucide-alert-triangle",
35
35
  info: "i-lucide-info",
@@ -3,8 +3,8 @@ import { scv } from "css-variants"
3
3
  import { useUi, resolveClasses } from "../../utils"
4
4
  import type { ScrollToTopProps } from "../../types/components/scroll-to-top"
5
5
  import scrollToTopTheme from "../../themes/scroll-to-top.theme"
6
- import { useScrollToTop } from "../../composables/useScrollToTop"
7
- import { computed, onMounted, onUnmounted, watch, useAttrs } from "vue"
6
+ import { trackScroll, scrollToTop as scrollToTopFn } from "../../utils"
7
+ import { ref, computed, onMounted, onUnmounted, watch, useAttrs } from "vue"
8
8
 
9
9
  const attrs = useAttrs()
10
10
  const scrollToTop = scv(scrollToTopTheme)
@@ -21,13 +21,8 @@ const {
21
21
 
22
22
  const classes = resolveClasses(scrollToTop, { color }, useUi("scrollToTop", uiProp), className)
23
23
 
24
- const {
25
- isVisible,
26
- scrollPercentage,
27
- scrollToTop: scrollToTopFn
28
- } = useScrollToTop({
29
- threshold
30
- })
24
+ const isVisible = ref(false)
25
+ const scrollPercentage = ref(0)
31
26
 
32
27
  const instanceId = `rla-vue-${Math.random().toString(36).slice(2, 8)}`
33
28
  const gaugeBorderWidth = progressWidth * 0.5
@@ -49,16 +44,23 @@ function updateCssVars() {
49
44
 
50
45
  watch(scrollPercentage, updateCssVars)
51
46
 
47
+ let unsubscribe: (() => void) | null = null
48
+
52
49
  onMounted(() => {
53
50
  const nonce = document.querySelector('meta[name="csp-nonce"]')?.getAttribute("content") ?? ""
54
51
  styleEl = document.createElement("style")
55
52
  if (nonce) styleEl.setAttribute("nonce", nonce)
56
53
  document.head.appendChild(styleEl)
57
- updateCssVars()
54
+
55
+ unsubscribe = trackScroll({ threshold }, (state) => {
56
+ isVisible.value = state.isVisible
57
+ scrollPercentage.value = state.scrollPercentage
58
+ })
58
59
  })
59
60
 
60
61
  onUnmounted(() => {
61
62
  if (styleEl) styleEl.remove()
63
+ unsubscribe?.()
62
64
  })
63
65
  </script>
64
66
 
@@ -22,7 +22,7 @@ const resolvedClasses = computed(() =>
22
22
 
23
23
  const variantIcons: Record<string, string> = {
24
24
  default: "i-lucide-bell",
25
- destructive: "i-lucide-alert-circle",
25
+ error: "i-lucide-alert-circle",
26
26
  success: "i-lucide-check-circle-2",
27
27
  warning: "i-lucide-alert-triangle",
28
28
  info: "i-lucide-info"
@@ -1,8 +1,20 @@
1
1
  <script setup lang="ts">
2
- import { useToast } from "../../composables/useToast"
2
+ import { ref, onMounted, onUnmounted } from "vue"
3
+ import { toastsStore, removeToast } from "../../utils"
3
4
  import RLVToast from "./RLVToast.vue"
4
5
 
5
- const { toasts, remove } = useToast()
6
+ const toasts = ref(toastsStore.get())
7
+ let unsubscribe: (() => void) | null = null
8
+
9
+ onMounted(() => {
10
+ unsubscribe = toastsStore.subscribe((val) => {
11
+ toasts.value = val
12
+ })
13
+ })
14
+
15
+ onUnmounted(() => {
16
+ unsubscribe?.()
17
+ })
6
18
  </script>
7
19
 
8
20
  <template>
@@ -22,7 +34,7 @@ const { toasts, remove } = useToast()
22
34
  :key="toast.id"
23
35
  v-bind="toast"
24
36
  class="pointer-events-auto"
25
- @close="remove"
37
+ @close="removeToast"
26
38
  />
27
39
  </TransitionGroup>
28
40
  </div>
@@ -1,2 +1 @@
1
1
  export * from "./site.config"
2
- export * from "./security"
@@ -1,3 +1,2 @@
1
- export * from "./sri"
2
1
  export { ui } from "./ui"
3
2
  export type { UIOptions, UIConfigOverrides } from "./ui"
@@ -1,7 +1,7 @@
1
1
  import type { AstroIntegration } from "astro"
2
2
  import UnoCSS from "unocss/astro"
3
3
  import vue from "@astrojs/vue"
4
- import defu from "defu"
4
+ import { merge } from "../utils/merge"
5
5
  import { defaultUIConfig } from "../themes"
6
6
  import type { ComponentTheme } from "../utils/defineTheme"
7
7
  import rimelightUiPreset, {
@@ -11,7 +11,6 @@ import rimelightUiPreset, {
11
11
  type RimelightPresetOptions
12
12
  } from "../presets"
13
13
  import { extractAllMeta } from "../docs/extractAll"
14
- import virtual from "vite-plugin-virtual"
15
14
 
16
15
  export interface CategoryConfig {
17
16
  label: string
@@ -44,8 +43,8 @@ export interface UIOptions {
44
43
  }
45
44
  >
46
45
  /**
47
- * Global UI overrides applied to all components. Merged with defaults via `defu` (first arg
48
- * wins).
46
+ * Global UI overrides applied to all components. Merged with defaults via our custom recursive
47
+ * merge utility (first arg wins).
49
48
  */
50
49
  ui?: UIConfigOverrides
51
50
  /**
@@ -106,7 +105,7 @@ export interface UIOptions {
106
105
  * })
107
106
  */
108
107
  export function ui(options: UIOptions = {}): AstroIntegration[] {
109
- const resolvedUI = defu(options.ui ?? {}, defaultUIConfig)
108
+ const resolvedUI = merge(options.ui ?? {}, defaultUIConfig)
110
109
  const coreColors = [
111
110
  "primary",
112
111
  "secondary",
@@ -149,22 +148,24 @@ export function ui(options: UIOptions = {}): AstroIntegration[] {
149
148
  },
150
149
  plugins: [
151
150
  {
152
- name: "vite-plugin-rimelight-ui-config",
151
+ name: "vite-plugin-rimelight-ui-virtual",
153
152
  enforce: "pre",
154
153
  resolveId(id: string) {
155
- if (id === virtualModuleId) return resolvedVirtualModuleId
154
+ if (id === virtualModuleId || id === "virtual:rimelight-ui-docs-meta") {
155
+ return "\0" + id
156
+ }
156
157
  return null
157
158
  },
158
159
  load(id: string) {
159
160
  if (id === resolvedVirtualModuleId) {
160
161
  return `export const getUIConfig = () => (${JSON.stringify(resolvedConfig)});`
161
162
  }
163
+ if (id === "\0virtual:rimelight-ui-docs-meta") {
164
+ return `export default ${JSON.stringify(docsMeta)}`
165
+ }
162
166
  return null
163
167
  }
164
- },
165
- virtual({
166
- "virtual:rimelight-ui-docs-meta": `export default ${JSON.stringify(docsMeta)}`
167
- })
168
+ }
168
169
  ]
169
170
  }
170
171
  })
@@ -5,7 +5,7 @@ export const createLinkGroupTheme = () =>
5
5
  slots: ["root", "title", "list", "item", "link"],
6
6
  base: {
7
7
  root: "flex flex-col gap-4",
8
- title: "font-bold text-sm uppercase tracking-wider text-neutral-500 pl-2.5",
8
+ title: "font-bold text-sm uppercase tracking-wider text-neutral-500",
9
9
  list: "flex flex-col gap-2",
10
10
  item: "",
11
11
  link: "text-sm text-muted-foreground hover:text-accent-foreground transition-colors"
@@ -5,7 +5,7 @@ export const createLocaleSelectorTheme = () =>
5
5
  base: {
6
6
  root: "inline-flex flex-col gap-1.5",
7
7
  label: "text-xs font-semibold text-muted select-none",
8
- wrapper: "relative inline-block w-full",
8
+ wrapper: "relative inline-block",
9
9
  select:
10
10
  "w-full appearance-none font-medium rounded-lg transition-all focus:outline-none focus:ring-2 focus:ring-emerald-500/40 cursor-pointer pr-10",
11
11
  option: "bg-neutral-900 text-white selection:bg-neutral-800",
@@ -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