@rimelight/ui 0.0.34 → 0.0.35

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 (185) hide show
  1. package/README.md +1 -0
  2. package/package.json +19 -24
  3. package/src/components/astro/RLAAccordion.astro +94 -18
  4. package/src/components/astro/RLAAvatar.astro +135 -68
  5. package/src/components/astro/RLABadge.astro +4 -2
  6. package/src/components/astro/RLABanner.astro +138 -23
  7. package/src/components/astro/RLABreadcrumb.astro +13 -0
  8. package/src/components/astro/RLAButton.astro +8 -4
  9. package/src/components/astro/RLACallout.astro +112 -0
  10. package/src/components/astro/RLACard.astro +6 -5
  11. package/src/components/astro/RLACarousel.astro +104 -75
  12. package/src/components/astro/RLAChart.astro +4 -1
  13. package/src/components/astro/RLAChip.astro +11 -5
  14. package/src/components/astro/RLACollapsible.astro +4 -1
  15. package/src/components/astro/RLAColorArea.astro +432 -0
  16. package/src/components/astro/RLAColorField.astro +300 -0
  17. package/src/components/astro/RLAColorSlider.astro +413 -0
  18. package/src/components/astro/RLAConfirm.astro +13 -11
  19. package/src/components/astro/RLACopyMarkdown.astro +78 -0
  20. package/src/components/astro/RLADate.astro +30 -0
  21. package/src/components/astro/RLADrawer.astro +50 -3
  22. package/src/components/astro/RLADropdownMenu.astro +78 -51
  23. package/src/components/astro/RLAFieldGroup.astro +4 -1
  24. package/src/components/astro/RLAFileUpload.astro +114 -100
  25. package/src/components/astro/RLAFooter.astro +2 -1
  26. package/src/components/astro/RLAFormField.astro +10 -6
  27. package/src/components/astro/RLAGrid.astro +31 -39
  28. package/src/components/astro/RLAHead.astro +139 -39
  29. package/src/components/astro/RLAHeader.astro +2 -1
  30. package/src/components/astro/RLAImage.astro +274 -251
  31. package/src/components/astro/RLAInput.astro +14 -9
  32. package/src/components/astro/RLAInputMenu.astro +119 -83
  33. package/src/components/astro/RLAKbd.astro +5 -5
  34. package/src/components/astro/RLALayoutGrid.astro +395 -0
  35. package/src/components/astro/RLALink.astro +23 -1
  36. package/src/components/astro/RLALinkGroup.astro +99 -99
  37. package/src/components/astro/RLALocaleSelector.astro +29 -35
  38. package/src/components/astro/RLALogo.astro +117 -90
  39. package/src/components/astro/RLAModal.astro +48 -18
  40. package/src/components/astro/RLANavigationMenu.astro +381 -326
  41. package/src/components/astro/{RLADocsPackageManagers.astro → RLAPackageManagers.astro} +81 -87
  42. package/src/components/astro/RLAPageSection.astro +4 -2
  43. package/src/components/astro/RLAPopover.astro +4 -3
  44. package/src/components/astro/RLAPost.astro +11 -2
  45. package/src/components/astro/RLAScrollArea.astro +4 -1
  46. package/src/components/astro/RLAScrollToTop.astro +200 -172
  47. package/src/components/astro/RLASearch.astro +282 -0
  48. package/src/components/astro/RLASelect.astro +78 -55
  49. package/src/components/astro/RLASeparator.astro +11 -6
  50. package/src/components/astro/RLAShare.astro +60 -0
  51. package/src/components/astro/RLAShortcuts.astro +429 -0
  52. package/src/components/astro/RLASidebar.astro +158 -212
  53. package/src/components/astro/RLASlideover.astro +47 -17
  54. package/src/components/astro/RLASplitter.astro +194 -141
  55. package/src/components/astro/RLASteps.astro +118 -71
  56. package/src/components/astro/RLASurround.astro +39 -0
  57. package/src/components/astro/RLATable.astro +4 -1
  58. package/src/components/astro/RLATableOfContents.astro +109 -23
  59. package/src/components/astro/RLATabs.astro +147 -133
  60. package/src/components/astro/RLATextarea.astro +4 -1
  61. package/src/components/astro/RLAThemeSelector.astro +105 -76
  62. package/src/components/astro/RLAToast.astro +35 -21
  63. package/src/components/vue/RLVBanner.vue +230 -0
  64. package/src/components/vue/RLVButton.vue +3 -2
  65. package/src/components/vue/RLVCheckbox.vue +4 -2
  66. package/src/components/vue/RLVFooter.vue +3 -2
  67. package/src/components/vue/RLVHeader.vue +3 -2
  68. package/src/components/vue/RLVIcon.vue +4 -2
  69. package/src/components/vue/RLVInput.vue +5 -4
  70. package/src/components/vue/RLVLogo.vue +25 -3
  71. package/src/components/vue/RLVPlaceholder.vue +3 -2
  72. package/src/components/vue/RLVPost.vue +8 -2
  73. package/src/components/vue/RLVProgress.vue +60 -0
  74. package/src/components/vue/RLVScrollToTop.vue +65 -52
  75. package/src/components/vue/RLVSection.vue +14 -13
  76. package/src/components/vue/RLVToast.vue +17 -4
  77. package/src/composables/index.ts +1 -0
  78. package/src/composables/useHeaderStore.ts +1 -0
  79. package/src/composables/useShortcuts.ts +11 -0
  80. package/src/config/security.ts +43 -36
  81. package/src/config/site.config.ts +1 -0
  82. package/src/docs/components/ApiChangelog.astro +147 -0
  83. package/src/{plugins/starlightDocsApi → docs}/components/ApiEmits.astro +4 -4
  84. package/src/{plugins/starlightDocsApi → docs}/components/ApiProps.astro +4 -4
  85. package/src/{plugins/starlightDocsApi → docs}/components/ApiSlots.astro +4 -4
  86. package/src/docs/components/ApiTheme.astro +100 -0
  87. package/src/docs/extractAll.ts +94 -0
  88. package/src/docs/extractors/extractAstro.ts +69 -0
  89. package/src/docs/extractors/extractTheme.ts +56 -0
  90. package/src/{plugins/starlightDocsApi/extract-vue.ts → docs/extractors/extractVue.ts} +164 -15
  91. package/src/{plugins/starlightDocsApi → docs}/utils.ts +1 -2
  92. package/src/env.d.ts +0 -12
  93. package/src/integrations/ui.ts +35 -19
  94. package/src/middleware/security.ts +75 -19
  95. package/src/presets/index.ts +118 -293
  96. package/src/styles/index.css +0 -2
  97. package/src/themes/accordion.theme.ts +29 -3
  98. package/src/themes/aspect-ratio.theme.ts +1 -1
  99. package/src/themes/avatar-group.theme.ts +1 -1
  100. package/src/themes/avatar.theme.ts +2 -2
  101. package/src/themes/badge.theme.ts +2 -3
  102. package/src/themes/banner.theme.ts +40 -4
  103. package/src/themes/breadcrumb.theme.ts +1 -1
  104. package/src/themes/button.theme.ts +21 -22
  105. package/src/themes/{alert.theme.ts → callout.theme.ts} +12 -39
  106. package/src/themes/card.theme.ts +17 -9
  107. package/src/themes/carousel.theme.ts +1 -1
  108. package/src/themes/checkbox.theme.ts +1 -2
  109. package/src/themes/chip.theme.ts +2 -3
  110. package/src/themes/color-area.theme.ts +33 -0
  111. package/src/themes/color-field.theme.ts +32 -0
  112. package/src/themes/color-slider.theme.ts +34 -0
  113. package/src/themes/confirm.theme.ts +1 -1
  114. package/src/themes/container.theme.ts +1 -1
  115. package/src/themes/drawer.theme.ts +1 -1
  116. package/src/themes/footer.theme.ts +2 -2
  117. package/src/themes/grid.theme.ts +38 -0
  118. package/src/themes/header.theme.ts +4 -3
  119. package/src/themes/hover-card.theme.ts +1 -1
  120. package/src/themes/image.theme.ts +11 -1
  121. package/src/themes/input.theme.ts +7 -1
  122. package/src/themes/kbd.theme.ts +2 -3
  123. package/src/themes/label.theme.ts +1 -1
  124. package/src/themes/layout-grid.theme.ts +16 -0
  125. package/src/themes/link-group.theme.ts +0 -4
  126. package/src/themes/marquee.theme.ts +1 -1
  127. package/src/themes/modal.theme.ts +1 -1
  128. package/src/themes/navigation-menu.theme.ts +2 -2
  129. package/src/themes/page-section.theme.ts +4 -2
  130. package/src/themes/pagination.theme.ts +1 -1
  131. package/src/themes/pin-input.theme.ts +1 -1
  132. package/src/themes/popover.theme.ts +1 -1
  133. package/src/themes/post.theme.ts +23 -3
  134. package/src/themes/progress.theme.ts +1 -2
  135. package/src/themes/scroll-to-top.theme.ts +2 -3
  136. package/src/themes/search.theme.ts +64 -0
  137. package/src/themes/separator.theme.ts +1 -1
  138. package/src/themes/slideover.theme.ts +1 -1
  139. package/src/themes/slider.theme.ts +1 -2
  140. package/src/themes/spinner.theme.ts +1 -2
  141. package/src/themes/{megamenu.theme.ts → steps.theme.ts} +3 -3
  142. package/src/themes/sticky-surface.theme.ts +1 -1
  143. package/src/themes/switch.theme.ts +1 -2
  144. package/src/themes/table-of-contents.theme.ts +21 -2
  145. package/src/themes/timeline.theme.ts +7 -2
  146. package/src/themes/toast.theme.ts +1 -1
  147. package/src/types/components/accordion.ts +35 -2
  148. package/src/types/components/avatar.ts +16 -0
  149. package/src/types/components/banner.ts +42 -0
  150. package/src/types/components/button.ts +3 -4
  151. package/src/types/components/{alert.ts → callout.ts} +6 -6
  152. package/src/types/components/color-area.ts +19 -0
  153. package/src/types/components/color-field.ts +21 -0
  154. package/src/types/components/color-slider.ts +20 -0
  155. package/src/types/components/grid.ts +23 -0
  156. package/src/types/components/input.ts +4 -0
  157. package/src/types/components/layout-grid.ts +145 -0
  158. package/src/types/components/link-group.ts +4 -2
  159. package/src/types/components/link.ts +15 -0
  160. package/src/types/components/logo.ts +3 -3
  161. package/src/types/components/post.ts +12 -0
  162. package/src/types/components/search.ts +29 -0
  163. package/src/types/components/sidebar.ts +2 -6
  164. package/src/types/components/{megamenu.ts → steps.ts} +7 -1
  165. package/src/types/components/table-of-contents.ts +22 -0
  166. package/src/types/components/toast.ts +4 -0
  167. package/src/types/docs.ts +40 -0
  168. package/src/types/index.ts +151 -1
  169. package/src/types/theme.ts +6 -6
  170. package/src/utils/color.ts +165 -0
  171. package/src/utils/docs.ts +260 -110
  172. package/src/utils/headerStack.ts +24 -4
  173. package/src/utils/index.ts +2 -0
  174. package/src/utils/shortcuts.ts +322 -0
  175. package/src/components/TableOfContents.astro +0 -107
  176. package/src/components/astro/RLAAlert.astro +0 -93
  177. package/src/components/astro/RLAChangelog.astro +0 -23
  178. package/src/components/astro/RLAMegamenu.astro +0 -23
  179. package/src/plugins/index.ts +0 -2
  180. package/src/plugins/starlightDocsApi/components/ApiTheme.astro +0 -27
  181. package/src/plugins/starlightDocsApi/index.ts +0 -272
  182. package/src/styles/prism.css +0 -42
  183. package/src/themes/changelog.theme.ts +0 -13
  184. package/src/types/components/index.ts +0 -134
  185. /package/src/{plugins/starlightDocsApi → docs}/types.ts +0 -0
@@ -0,0 +1,282 @@
1
+ ---
2
+ import { scv } from "css-variants"
3
+ import { useUi, resolveClasses } from "../../utils"
4
+ import type { SearchProps } from "../../types"
5
+ import searchThemeDefault, { createSearchTheme } from "../../themes/search.theme"
6
+ import { getUIConfig } from "virtual:rimelight-ui"
7
+ import RLAInput from "./RLAInput.astro"
8
+ import RLAKbd from "./RLAKbd.astro"
9
+ import RLAButton from "./RLAButton.astro"
10
+
11
+ const uiConfig = getUIConfig()
12
+ const searchTheme = uiConfig.colors ? createSearchTheme() : searchThemeDefault
13
+
14
+ const search = scv(searchTheme)
15
+
16
+ const {
17
+ endpoint = "/api/search",
18
+ placeholder = "Search...",
19
+ messages = {},
20
+ ui: uiProp,
21
+ class: className,
22
+ ...rest
23
+ } = Astro.props as SearchProps
24
+
25
+ const classes = resolveClasses(search, {}, useUi("search", uiProp), className)
26
+
27
+ // Default messages
28
+ const searchPlaceholder = messages.searchPlaceholder || placeholder
29
+ const noResultsLabel = messages.noResultsLabel || "No results found"
30
+ const footerCloseHint = messages.footerCloseHint || "Escape to close"
31
+ const footerBackendHint = messages.footerBackendHint || "Postgres Full-Text Search"
32
+
33
+ // Generate unique IDs for this instance to avoid collisions if multiple search bars exist
34
+ const uniqueId = Math.random().toString(36).slice(2, 8)
35
+ const triggerId = `search-trigger-${uniqueId}`
36
+ const modalId = `search-modal-${uniqueId}`
37
+ const inputId = `search-input-${uniqueId}`
38
+ const resultsId = `search-results-${uniqueId}`
39
+ const emptyId = `search-empty-${uniqueId}`
40
+ ---
41
+
42
+ <rla-search
43
+ class:list={[classes.root, "rla-search-wrapper"]}
44
+ data-slot="root"
45
+ data-trigger-id={triggerId}
46
+ data-modal-id={modalId}
47
+ data-input-id={inputId}
48
+ data-results-id={resultsId}
49
+ data-empty-id={emptyId}
50
+ data-endpoint={endpoint}
51
+ {...rest}
52
+ >
53
+ {Astro.slots.has("trigger") ? (
54
+ <div id={triggerId} class="cursor-pointer">
55
+ <slot name="trigger" />
56
+ </div>
57
+ ) : (
58
+ <RLAButton
59
+ id={triggerId}
60
+ variant="ghost"
61
+ color="neutral"
62
+ square
63
+ aria-label="Open Search"
64
+ >
65
+ <span class="i-lucide-search w-5 h-5" />
66
+ </RLAButton>
67
+ )}
68
+
69
+ <!-- Search modal -->
70
+ <div id={modalId} class={classes.modal} data-slot="modal">
71
+ <div class={classes.dialog} data-slot="dialog">
72
+ <!-- Search header -->
73
+ <div class={classes.header} data-slot="header">
74
+ <RLAInput
75
+ variant="ghost"
76
+ type="text"
77
+ id={inputId}
78
+ placeholder={searchPlaceholder}
79
+ autocomplete="off"
80
+ ui={{
81
+ root: "relative flex items-center w-full",
82
+ input: "pl-8 pr-20 py-1.5 text-base outline-none text-white placeholder-neutral-500",
83
+ leading: "absolute left-0 text-neutral-400 flex items-center",
84
+ trailing: "absolute right-0 flex items-center gap-2"
85
+ }}
86
+ >
87
+ <span slot="leading" class={classes.headerIcon} />
88
+ <div slot="trailing" class="flex items-center gap-2">
89
+ <RLAKbd size="sm">Esc</RLAKbd>
90
+ <button id={`${modalId}-close`} class={classes.close} aria-label="Close search">
91
+ <span class={classes.closeIcon}></span>
92
+ </button>
93
+ </div>
94
+ </RLAInput>
95
+ </div>
96
+
97
+ <!-- Results list -->
98
+ <div id={resultsId} class={classes.results} data-slot="results"></div>
99
+
100
+ <!-- Empty state -->
101
+ <div id={emptyId} class={classes.empty} data-slot="empty">
102
+ {noResultsLabel}
103
+ </div>
104
+
105
+ <!-- Footer -->
106
+ <div class={classes.footer} data-slot="footer">
107
+ <span>{footerCloseHint}</span>
108
+ <span>{footerBackendHint}</span>
109
+ </div>
110
+ </div>
111
+ </div>
112
+ </rla-search>
113
+
114
+ <script>
115
+ import { defineShortcuts } from "../../utils/shortcuts"
116
+
117
+ class RLASearch extends HTMLElement {
118
+ private searchCleanup: (() => void) | null = null;
119
+
120
+ connectedCallback() {
121
+ this.style.display = 'contents';
122
+ const triggerId = this.dataset.triggerId;
123
+ const modalId = this.dataset.modalId;
124
+ const inputId = this.dataset.inputId;
125
+ const resultsId = this.dataset.resultsId;
126
+ const emptyId = this.dataset.emptyId;
127
+ const endpoint = this.dataset.endpoint;
128
+
129
+ const trigger = this.querySelector(`#${triggerId}`);
130
+ const modal = this.querySelector(`#${modalId}`) as HTMLElement | null;
131
+ const closeBtn = this.querySelector(`#${modalId}-close`);
132
+ const input = this.querySelector(`#${inputId}`) as HTMLInputElement | null;
133
+ const resultsContainer = this.querySelector(`#${resultsId}`) as HTMLElement | null;
134
+ const emptyState = this.querySelector(`#${emptyId}`) as HTMLElement | null;
135
+
136
+ // Teleport modal to body to avoid containing block/transform clipping from header containers
137
+ if (modal && modal.parentNode !== document.body) {
138
+ document.body.appendChild(modal);
139
+ }
140
+
141
+ let escapeCleanup: (() => void) | null = null;
142
+
143
+ const openModal = () => {
144
+ modal?.classList.remove("hidden");
145
+ modal?.classList.add("flex");
146
+ input?.focus();
147
+ document.body.style.overflow = "hidden";
148
+
149
+ escapeCleanup = defineShortcuts({
150
+ escape: {
151
+ handler: () => closeModal(),
152
+ usingInput: true
153
+ }
154
+ });
155
+ };
156
+
157
+ const closeModal = () => {
158
+ modal?.classList.add("hidden");
159
+ modal?.classList.remove("flex");
160
+ document.body.style.overflow = "";
161
+ if (input) input.value = "";
162
+ if (resultsContainer) resultsContainer.innerHTML = "";
163
+ emptyState?.classList.add("hidden");
164
+
165
+ if (escapeCleanup) {
166
+ escapeCleanup();
167
+ escapeCleanup = null;
168
+ }
169
+ };
170
+
171
+ const onTriggerClick = () => openModal();
172
+ const onCloseClick = () => closeModal();
173
+ const onModalClick = (e: MouseEvent) => {
174
+ const target = e.target as HTMLElement | null;
175
+ if (target && (target === modal || !target.closest('[data-slot="dialog"]'))) {
176
+ closeModal();
177
+ }
178
+ };
179
+
180
+ trigger?.addEventListener("click", onTriggerClick);
181
+ closeBtn?.addEventListener("click", onCloseClick);
182
+ modal?.addEventListener("click", onModalClick);
183
+
184
+ // Register global shortcut key Cmd+K / Ctrl+K
185
+ const metaKCleanup = defineShortcuts({
186
+ meta_k: {
187
+ handler: () => openModal(),
188
+ label: "Open Search",
189
+ description: "Open the global site search modal",
190
+ category: "navigation"
191
+ }
192
+ });
193
+
194
+ let debounceTimer: any;
195
+ const onInput = () => {
196
+ clearTimeout(debounceTimer);
197
+ const query = input?.value.trim();
198
+ if (!query) {
199
+ if (resultsContainer) resultsContainer.innerHTML = "";
200
+ emptyState?.classList.add("hidden");
201
+ return;
202
+ }
203
+
204
+ debounceTimer = setTimeout(async () => {
205
+ try {
206
+ console.log("[Search UI] Fetching search results for:", query);
207
+ const res = await fetch(`${endpoint}?q=${encodeURIComponent(query)}`);
208
+
209
+ if (!res.ok) {
210
+ throw new Error(`HTTP status: ${res.status}`);
211
+ }
212
+
213
+ const results = await res.json();
214
+ console.log("[Search UI] Received results:", results);
215
+
216
+ if (resultsContainer) resultsContainer.innerHTML = "";
217
+
218
+ if (!Array.isArray(results) || results.length === 0) {
219
+ emptyState?.classList.remove("hidden");
220
+ return;
221
+ }
222
+
223
+ emptyState?.classList.add("hidden");
224
+ results.forEach((item) => {
225
+ const div = document.createElement("a");
226
+ div.href = item.url || "#";
227
+ div.className = "flex flex-col gap-1 p-3 hover:bg-neutral-800 rounded-lg transition-colors border border-transparent hover:border-neutral-700 text-left decoration-none";
228
+
229
+ div.innerHTML = `
230
+ <div class="flex items-center justify-between">
231
+ <span class="text-sm font-semibold text-white">${item.title || "Untitled"}</span>
232
+ ${item.sourceType ? `<span class="text-[10px] uppercase tracking-wider px-1.5 py-0.5 rounded bg-neutral-800 text-neutral-400 font-mono">${item.sourceType}</span>` : ""}
233
+ </div>
234
+ ${item.snippet ? `<p class="text-xs text-neutral-400 line-clamp-2 m-0">${item.snippet}</p>` : ""}
235
+ `;
236
+ resultsContainer?.appendChild(div);
237
+ });
238
+ } catch (err) {
239
+ console.error("[Search UI] Search failed:", err);
240
+ if (resultsContainer) resultsContainer.innerHTML = "";
241
+ emptyState?.classList.remove("hidden");
242
+ }
243
+ }, 200);
244
+ };
245
+ input?.addEventListener("input", onInput);
246
+
247
+ this.searchCleanup = () => {
248
+ clearTimeout(debounceTimer);
249
+ trigger?.removeEventListener("click", onTriggerClick);
250
+ closeBtn?.removeEventListener("click", onCloseClick);
251
+ modal?.removeEventListener("click", onModalClick);
252
+ input?.removeEventListener("input", onInput);
253
+ if (escapeCleanup) escapeCleanup();
254
+ metaKCleanup();
255
+ if (modal && modal.parentNode === document.body) {
256
+ modal.remove();
257
+ }
258
+ };
259
+ }
260
+
261
+ disconnectedCallback() {
262
+ if (this.searchCleanup) {
263
+ this.searchCleanup();
264
+ this.searchCleanup = null;
265
+ }
266
+ }
267
+ }
268
+
269
+ if (!customElements.get('rla-search')) {
270
+ customElements.define('rla-search', RLASearch);
271
+ }
272
+ </script>
273
+
274
+ <style is:global>
275
+ #search-results mark {
276
+ background-color: rgba(168, 85, 247, 0.3) !important;
277
+ color: rgb(233, 213, 255) !important;
278
+ padding-left: 2px;
279
+ padding-right: 2px;
280
+ border-radius: 4px;
281
+ }
282
+ </style>
@@ -4,7 +4,10 @@ import { useUi, resolveClasses } from "../../utils"
4
4
  import type { SelectProps } from "../../types"
5
5
  import selectThemeDefault, { createSelectTheme } from "../../themes/select.theme"
6
6
 
7
- const selectTheme = selectThemeDefault
7
+ import { getUIConfig } from "virtual:rimelight-ui"
8
+
9
+ const uiConfig = getUIConfig()
10
+ const selectTheme = uiConfig.colors ? createSelectTheme() : selectThemeDefault
8
11
  const select = scv(selectTheme)
9
12
 
10
13
  const {
@@ -35,63 +38,69 @@ const normalizedItems = items.map(item => {
35
38
  const selectedItem = normalizedItems.find(item => item.value === value)
36
39
  const initialLabel = selectedItem ? selectedItem.label : ""
37
40
  ---
38
- <div class={classes.root} data-slot="root" data-disabled={disabled ? "" : undefined}>
39
- {label && <label class={classes.label} data-slot="label">{label}</label>}
40
-
41
- <div class="relative w-full" data-select-wrapper>
42
- <button
43
- type="button"
44
- class={classes.trigger}
45
- data-slot="trigger"
46
- disabled={disabled}
47
- data-select-trigger
48
- {...rest}
49
- >
50
- <span data-select-value>{initialLabel || placeholder}</span>
51
- <span class="i-lucide-chevron-down opacity-50" />
52
- </button>
53
-
54
- <div class={classes.content + " hidden"} data-select-dropdown data-slot="content">
55
- <div class="py-1" data-select-items>
56
- {normalizedItems.map(item => (
57
- <div
58
- class={classes.item}
59
- data-slot="item"
60
- data-value={item.value}
61
- data-label={item.label}
62
- data-select-item
63
- >
64
- <span class={classes.itemText}>{item.label}</span>
65
- <span class={classes.itemIcon + " hidden i-lucide-check"} data-check-icon />
66
- </div>
67
- ))}
41
+ <rla-select {...rest}>
42
+ <div class={classes.root} data-slot="root" data-disabled={disabled ? "" : undefined}>
43
+ {label && <label class={classes.label} data-slot="label">{label}</label>}
44
+
45
+ <div class="relative w-full" data-select-wrapper>
46
+ <button
47
+ type="button"
48
+ class={classes.trigger}
49
+ data-slot="trigger"
50
+ disabled={disabled}
51
+ data-select-trigger
52
+ >
53
+ <span data-select-value>{initialLabel || placeholder}</span>
54
+ <span class="i-lucide-chevron-down opacity-50" />
55
+ </button>
56
+
57
+ <div class={classes.content + " hidden"} data-select-dropdown data-slot="content">
58
+ <div class="py-1" data-select-items>
59
+ {normalizedItems.map(item => (
60
+ <div
61
+ class={classes.item}
62
+ data-slot="item"
63
+ data-value={item.value}
64
+ data-label={item.label}
65
+ data-select-item
66
+ >
67
+ <span class={classes.itemText}>{item.label}</span>
68
+ <span class={classes.itemIcon + " hidden i-lucide-check"} data-check-icon />
69
+ </div>
70
+ ))}
71
+ </div>
68
72
  </div>
69
73
  </div>
74
+ <input type="hidden" name={name} value={value} data-select-hidden-input />
70
75
  </div>
71
- <input type="hidden" name={name} value={value} data-select-hidden-input />
72
- </div>
76
+ </rla-select>
73
77
 
74
78
  <script>
75
- const initializeSelects = () => {
76
- document.querySelectorAll("[data-select-wrapper]").forEach(wrapper => {
77
- if (wrapper.hasAttribute("data-initialized")) return;
78
- wrapper.setAttribute("data-initialized", "true");
79
+ class RLASelect extends HTMLElement {
80
+ private selectCleanup: (() => void) | null = null;
81
+
82
+ connectedCallback() {
83
+ this.style.display = 'contents';
84
+ const wrapper = this.querySelector("[data-select-wrapper]") as HTMLElement | null;
85
+ if (!wrapper) return;
79
86
 
80
87
  const trigger = wrapper.querySelector("[data-select-trigger]") as HTMLButtonElement;
81
88
  const dropdown = wrapper.querySelector("[data-select-dropdown]") as HTMLDivElement;
82
- const items = wrapper.querySelectorAll("[data-select-item]");
89
+ const items = wrapper.querySelectorAll<HTMLElement>("[data-select-item]");
83
90
  const valueSpan = wrapper.querySelector("[data-select-value]");
84
- const hiddenInput = wrapper.closest("[data-slot=root]")?.querySelector("[data-select-hidden-input]") as HTMLInputElement;
91
+ const hiddenInput = this.querySelector("[data-select-hidden-input]") as HTMLInputElement | null;
85
92
 
86
93
  if (!trigger || !dropdown) return;
87
94
 
88
- trigger.addEventListener("click", (e) => {
95
+ const onTriggerClick = (e: MouseEvent) => {
89
96
  e.stopPropagation();
90
97
  dropdown.classList.toggle("hidden");
91
- });
98
+ };
99
+ trigger.addEventListener("click", onTriggerClick);
92
100
 
101
+ const itemCleanups: (() => void)[] = [];
93
102
  items.forEach(item => {
94
- item.addEventListener("click", (e) => {
103
+ const onItemClick = (e: MouseEvent) => {
95
104
  e.stopPropagation();
96
105
  const val = (item as HTMLElement).dataset.value || "";
97
106
  const label = (item as HTMLElement).dataset.label || "";
@@ -106,18 +115,32 @@ const initialLabel = selectedItem ? selectedItem.label : ""
106
115
  item.querySelector("[data-check-icon]")?.classList.remove("hidden");
107
116
 
108
117
  dropdown.classList.add("hidden");
109
- });
118
+ };
119
+ item.addEventListener("click", onItemClick);
120
+ itemCleanups.push(() => item.removeEventListener("click", onItemClick));
110
121
  });
111
- });
112
- };
113
-
114
- // Close all dropdowns when clicking outside
115
- document.addEventListener("click", () => {
116
- document.querySelectorAll("[data-select-dropdown]").forEach(dropdown => {
117
- dropdown.classList.add("hidden");
118
- });
119
- });
120
-
121
- document.addEventListener("DOMContentLoaded", initializeSelects);
122
- document.addEventListener("astro:page-load", initializeSelects);
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
+ };
133
+ }
134
+
135
+ disconnectedCallback() {
136
+ if (this.selectCleanup) {
137
+ this.selectCleanup();
138
+ this.selectCleanup = null;
139
+ }
140
+ }
141
+ }
142
+
143
+ if (!customElements.get('rla-select')) {
144
+ customElements.define('rla-select', RLASelect);
145
+ }
123
146
  </script>
@@ -8,7 +8,7 @@ import RLAAvatar from "./RLAAvatar.astro"
8
8
  import { cx } from "css-variants"
9
9
 
10
10
  const uiConfig = getUIConfig()
11
- const separatorTheme = uiConfig.colors ? createSeparatorTheme() : separatorThemeDefault
11
+ const separatorTheme = uiConfig.colors ? createSeparatorTheme(uiConfig.colors) : separatorThemeDefault
12
12
 
13
13
  const separator = scv(separatorTheme)
14
14
 
@@ -25,14 +25,19 @@ const {
25
25
  ui: uiProp,
26
26
  class: className,
27
27
  ...rest
28
- } = Astro.props as SeparatorProps
28
+ } = Astro.props as SeparatorProps & {
29
+ color?: string
30
+ size?: "xs" | "sm" | "md" | "lg" | "xl"
31
+ type?: "solid" | "dashed" | "dotted"
32
+ position?: "start" | "center" | "end"
33
+ }
29
34
 
30
35
  const classes = resolveClasses(separator, {
31
36
  orientation,
32
- color,
33
- size,
34
- type,
35
- position
37
+ color: color as "neutral",
38
+ size: size as "xs",
39
+ type: type as "solid",
40
+ position: position as "center"
36
41
  }, useUi("separator", uiProp), className)
37
42
  ---
38
43
  <div
@@ -0,0 +1,60 @@
1
+ ---
2
+ import RLAButton from "./RLAButton.astro"
3
+ import type { ButtonProps } from "../../types"
4
+
5
+ interface Props extends Omit<ButtonProps, "href" | "label" | "leadingIcon" | "trailingIcon" | "loading" | "loadingAuto" | "square" | "active" | "activeColor" | "activeVariant"> {}
6
+
7
+ const {
8
+ ...rest
9
+ } = Astro.props
10
+ ---
11
+
12
+ <RLAButton
13
+ id="rl-share"
14
+ color="neutral"
15
+ variant="outline"
16
+ size="sm"
17
+ leadingIcon="i-lucide-share-2"
18
+ label="Share"
19
+ {...rest}
20
+ />
21
+
22
+ <script>
23
+ function setupShare() {
24
+ const shareBtn = document.getElementById("rl-share") as (HTMLButtonElement & { rlShareRegistered?: boolean }) | null;
25
+ if (!shareBtn) return;
26
+
27
+ if (shareBtn.rlShareRegistered) return;
28
+ shareBtn.rlShareRegistered = true;
29
+
30
+ shareBtn.addEventListener("click", async () => {
31
+ if (navigator.share) {
32
+ try {
33
+ await navigator.share({ title: document.title, url: window.location.href });
34
+ } catch (err) {
35
+ const error = err as { name?: string };
36
+ if (error.name !== "AbortError") {
37
+ console.error("Share failed:", err);
38
+ }
39
+ }
40
+ } else {
41
+ try {
42
+ await navigator.clipboard.writeText(window.location.href);
43
+ const labelEl = shareBtn.querySelector('[data-slot="label"]');
44
+ if (labelEl) {
45
+ const origText = labelEl.textContent;
46
+ labelEl.textContent = "Link Copied!";
47
+ setTimeout(() => {
48
+ labelEl.textContent = origText;
49
+ }, 2000);
50
+ }
51
+ } catch (err) {
52
+ console.error("Failed to copy link:", err);
53
+ }
54
+ }
55
+ });
56
+ }
57
+
58
+ setupShare();
59
+ document.addEventListener("astro:after-swap", setupShare);
60
+ </script>