@rimelight/ui 0.0.47 → 0.0.48

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 (197) hide show
  1. package/dist/index.d.mts +4 -5
  2. package/dist/preset.mjs +73 -1
  3. package/dist/resolver.d.mts +3 -4
  4. package/dist/resolver.mjs +2 -4
  5. package/dist/shortcuts.d.mts +10 -11
  6. package/dist/stores.d.mts +3 -4
  7. package/package.json +8 -6
  8. package/src/components/3d-hover/RLS3dHover.tsx +21 -0
  9. package/src/components/accordion/RLSAccordion.tsx +112 -0
  10. package/src/components/app/RLSApp.tsx +21 -0
  11. package/src/components/aspect-ratio/RLSAspectRatio.tsx +30 -0
  12. package/src/components/audio/RLSAudio.tsx +27 -0
  13. package/src/components/avatar/RLSAvatar.tsx +142 -0
  14. package/src/components/avatar/avatar.ts +22 -21
  15. package/src/components/avatar-group/RLSAvatarGroup.tsx +163 -0
  16. package/src/components/badge/RLSBadge.tsx +1 -1
  17. package/src/components/badge/RLVBadge.vue +5 -3
  18. package/src/components/banner/RLSBanner.tsx +151 -0
  19. package/src/components/breadcrumb/RLSBreadcrumb.tsx +194 -0
  20. package/src/components/browser-mockup/RLSBrowserMockup.tsx +21 -0
  21. package/src/components/button/RLSButton.tsx +49 -40
  22. package/src/components/button/RLVButton.vue +44 -32
  23. package/src/components/calendar/RLSCalendar.tsx +26 -0
  24. package/src/components/callout/RLSCallout.tsx +151 -0
  25. package/src/components/card/RLSCard.tsx +4 -0
  26. package/src/components/card/RLVCard.vue +4 -0
  27. package/src/components/card/card.ts +10 -0
  28. package/src/components/carousel/RLACarousel.astro +14 -1
  29. package/src/components/carousel/RLSCarousel.tsx +260 -0
  30. package/src/components/chart/RLSChart.tsx +299 -0
  31. package/src/components/chat/RLSChat.tsx +27 -0
  32. package/src/components/chat-message/RLSChatMessage.tsx +27 -0
  33. package/src/components/chat-messages/RLSChatMessages.tsx +27 -0
  34. package/src/components/chat-palette/RLSChatPalette.tsx +27 -0
  35. package/src/components/chat-prompt/RLSChatPrompt.tsx +27 -0
  36. package/src/components/chat-prompt-submit/RLSChatPromptSubmit.tsx +27 -0
  37. package/src/components/chat-reasoning/RLSChatReasoning.tsx +27 -0
  38. package/src/components/chat-shimmer/RLSChatShimmer.tsx +27 -0
  39. package/src/components/chat-tool/RLSChatTool.tsx +27 -0
  40. package/src/components/checkbox/RLACheckbox.astro +1 -0
  41. package/src/components/checkbox/RLSCheckbox.tsx +1 -1
  42. package/src/components/checkbox/checkbox.theme.ts +2 -2
  43. package/src/components/chip/RLSChip.tsx +65 -0
  44. package/src/components/collapsible/RLSCollapsible.tsx +89 -0
  45. package/src/components/color-area/RLSColorArea.tsx +554 -0
  46. package/src/components/color-field/RLSColorField.tsx +236 -0
  47. package/src/components/color-picker/RLSColorPicker.tsx +26 -0
  48. package/src/components/color-slider/RLSColorSlider.tsx +373 -0
  49. package/src/components/command-palette/RLSCommandPalette.tsx +26 -0
  50. package/src/components/compare/RLSCompare.tsx +26 -0
  51. package/src/components/confirm/RLSConfirm.tsx +173 -0
  52. package/src/components/container/RLSContainer.tsx +24 -0
  53. package/src/components/context-menu/RLSContextMenu.tsx +26 -0
  54. package/src/components/copy-markdown/RLSCopyMarkdown.tsx +57 -0
  55. package/src/components/dashboard-group/RLSDashboardGroup.tsx +29 -0
  56. package/src/components/dashboard-navbar/RLSDashboardNavbar.tsx +116 -0
  57. package/src/components/dashboard-panel/RLSDashboardPanel.tsx +84 -0
  58. package/src/components/dashboard-resize-handle/RLSDashboardResizeHandle.tsx +35 -0
  59. package/src/components/dashboard-search/RLADashboardSearch.astro +1 -1
  60. package/src/components/dashboard-search/RLSDashboardSearch.tsx +125 -0
  61. package/src/components/dashboard-search-button/RLSDashboardSearchButton.tsx +82 -0
  62. package/src/components/dashboard-sidebar/RLSDashboardSidebar.tsx +184 -0
  63. package/src/components/dashboard-sidebar-collapse/RLSDashboardSidebarCollapse.tsx +46 -0
  64. package/src/components/dashboard-sidebar-toggle/RLSDashboardSidebarToggle.tsx +35 -0
  65. package/src/components/dashboard-toolbar/RLSDashboardToolbar.tsx +49 -0
  66. package/src/components/date/RLSDate.tsx +34 -0
  67. package/src/components/dock/RLSDock.tsx +26 -0
  68. package/src/components/drawer/RLSDrawer.tsx +177 -0
  69. package/src/components/dropdown-menu/RLADropdownMenu.astro +250 -241
  70. package/src/components/dropdown-menu/RLSDropdownMenu.tsx +183 -86
  71. package/src/components/editor/RLSEditor.tsx +26 -0
  72. package/src/components/empty/RLSEmpty.tsx +132 -0
  73. package/src/components/error/RLSError.tsx +26 -0
  74. package/src/components/field-group/RLSFieldGroup.tsx +46 -0
  75. package/src/components/fieldset/RLSFieldset.tsx +26 -0
  76. package/src/components/file-upload/RLSFileUpload.tsx +163 -0
  77. package/src/components/floating-action/RLSFloatingAction.tsx +26 -0
  78. package/src/components/footer/RLSFooter.tsx +56 -0
  79. package/src/components/form/RLSForm.tsx +34 -0
  80. package/src/components/gallery/RLSGallery.tsx +21 -0
  81. package/src/components/gamepad/RLSGamepad.tsx +269 -0
  82. package/src/components/grid/RLSGrid.tsx +29 -0
  83. package/src/components/head/RLAHead.astro +9 -0
  84. package/src/components/head/RLSHead.tsx +42 -0
  85. package/src/components/head/UIHead.astro +3 -0
  86. package/src/components/header/RLSHeader.tsx +87 -0
  87. package/src/components/hover-card/RLSHoverCard.tsx +72 -0
  88. package/src/components/icon/RLSIcon.tsx +3 -1
  89. package/src/components/image/RLAImage.astro +447 -138
  90. package/src/components/image/RLSImage.tsx +545 -0
  91. package/src/components/image/image.theme.ts +19 -3
  92. package/src/components/image/image.ts +31 -1
  93. package/src/components/input/RLAInput.astro +62 -51
  94. package/src/components/input/RLSInput.tsx +1 -1
  95. package/src/components/input-date/RLSInputDate.tsx +92 -0
  96. package/src/components/input-menu/RLSInputMenu.tsx +199 -0
  97. package/src/components/input-number/RLSInputNumber.tsx +184 -0
  98. package/src/components/input-pin/RLSInputPin.tsx +185 -0
  99. package/src/components/input-rating/RLSInputRating.tsx +198 -0
  100. package/src/components/input-rating/input-rating.ts +4 -0
  101. package/src/components/input-tags/RLSInputTags.tsx +94 -0
  102. package/src/components/input-time/RLSInputTime.tsx +92 -0
  103. package/src/components/kbd/RLSKbd.tsx +37 -0
  104. package/src/components/keyboard/RLSKeyboard.tsx +476 -0
  105. package/src/components/knob/RLSKnob.tsx +27 -0
  106. package/src/components/label/RLSLabel.tsx +27 -0
  107. package/src/components/layout-grid/RLSLayoutGrid.tsx +402 -0
  108. package/src/components/layout-grid/layout-grid.ts +25 -1
  109. package/src/components/link/RLSLink.tsx +2 -1
  110. package/src/components/link/link.ts +21 -12
  111. package/src/components/link-group/RLSLinkGroup.tsx +76 -0
  112. package/src/components/listbox/RLSListbox.tsx +27 -0
  113. package/src/components/locale-selector/RLALocaleSelector.astro +2 -1
  114. package/src/components/locale-selector/RLSLocaleSelector.tsx +178 -0
  115. package/src/components/logo/RLALogo.astro +69 -4
  116. package/src/components/logo/RLSLogo.tsx +129 -0
  117. package/src/components/main/RLSMain.tsx +40 -0
  118. package/src/components/marquee/RLSMarquee.tsx +80 -0
  119. package/src/components/meter/RLSMeter.tsx +27 -0
  120. package/src/components/modal/RLAModal.astro +1 -1
  121. package/src/components/modal/RLSModal.tsx +109 -72
  122. package/src/components/navigation-menu/RLSNavigationMenu.tsx +228 -0
  123. package/src/components/navigation-menu/navigation-menu.ts +2 -2
  124. package/src/components/package-managers/RLSPackageManagers.tsx +204 -0
  125. package/src/components/page/RLSPage.tsx +21 -0
  126. package/src/components/page-aside/RLSPageAside.tsx +21 -0
  127. package/src/components/page-body/RLSPageBody.tsx +21 -0
  128. package/src/components/page-header/RLSPageHeader.tsx +21 -0
  129. package/src/components/page-section/RLSPageSection.tsx +201 -0
  130. package/src/components/pagination/RLSPagination.tsx +114 -0
  131. package/src/components/password/RLSPassword.tsx +27 -0
  132. package/src/components/phone-mockup/RLSPhoneMockup.tsx +27 -0
  133. package/src/components/placeholder/RLSPlaceholder.tsx +47 -0
  134. package/src/components/popover/RLSPopover.tsx +124 -0
  135. package/src/components/post/RLSPost.tsx +134 -0
  136. package/src/components/pricing-plan/RLSPricingPlan.tsx +26 -0
  137. package/src/components/pricing-table/RLSPricingTable.tsx +26 -0
  138. package/src/components/progress/RLSProgress.tsx +53 -0
  139. package/src/components/prose/RLSProse.tsx +26 -0
  140. package/src/components/qr-code/RLSQrCode.tsx +26 -0
  141. package/src/components/quote/RLAQuote.astro +10 -6
  142. package/src/components/quote/RLSQuote.tsx +37 -0
  143. package/src/components/quote/quote.theme.ts +5 -2
  144. package/src/components/radio-group/RLSRadioGroup.tsx +37 -0
  145. package/src/components/rating/RLSRating.tsx +27 -0
  146. package/src/components/scroll-area/RLSScrollArea.tsx +210 -0
  147. package/src/components/scroll-to-top/RLAScrollToTop.astro +1 -0
  148. package/src/components/scroll-to-top/RLSScrollToTop.tsx +145 -0
  149. package/src/components/search/RLSSearch.tsx +237 -0
  150. package/src/components/select/RLASelect.astro +67 -4
  151. package/src/components/select/RLSSelect.tsx +107 -39
  152. package/src/components/select/RLVSelect.vue +50 -1
  153. package/src/components/select/select.theme.ts +30 -10
  154. package/src/components/select/select.ts +20 -1
  155. package/src/components/separator/RLSSeparator.tsx +92 -0
  156. package/src/components/share/RLSShare.tsx +66 -0
  157. package/src/components/shortcuts/RLSShortcuts.tsx +393 -0
  158. package/src/components/sidebar/RLSSidebar.tsx +271 -0
  159. package/src/components/skeleton/RLSSkeleton.tsx +21 -0
  160. package/src/components/slideover/RLASlideover.astro +1 -1
  161. package/src/components/slideover/RLSSlideover.tsx +159 -0
  162. package/src/components/slider/RLSSlider.tsx +112 -0
  163. package/src/components/speed-dial/RLSSpeedDial.tsx +27 -0
  164. package/src/components/spinner/RLSSpinner.tsx +22 -0
  165. package/src/components/splitter/RLSSplitter.tsx +178 -0
  166. package/src/components/spoiler/RLSSpoiler.tsx +26 -0
  167. package/src/components/stepper/RLSStepper.tsx +205 -0
  168. package/src/components/steps/RLSSteps.tsx +108 -0
  169. package/src/components/sticky-surface/RLSStickySurface.tsx +63 -0
  170. package/src/components/surround/RLSSurround.tsx +57 -0
  171. package/src/components/switch/RLSSwitch.tsx +56 -0
  172. package/src/components/table/RLATable.astro +1044 -579
  173. package/src/components/table/RLSTable.tsx +893 -0
  174. package/src/components/table/table.theme.ts +47 -16
  175. package/src/components/table/table.ts +104 -66
  176. package/src/components/table-of-contents/RLSTableOfContents.tsx +196 -0
  177. package/src/components/tabs/RLSTabs.tsx +298 -0
  178. package/src/components/textarea/RLATextarea.astro +1 -0
  179. package/src/components/textarea/RLSTextarea.tsx +3 -1
  180. package/src/components/theme-selector/RLAThemeSelector.astro +85 -56
  181. package/src/components/theme-selector/RLSThemeSelector.tsx +131 -0
  182. package/src/components/theme-selector/theme-selector.theme.ts +5 -7
  183. package/src/components/theme-selector/theme-selector.ts +11 -1
  184. package/src/components/timeline/RLSTimeline.tsx +171 -0
  185. package/src/components/toast/RLSToast.tsx +179 -0
  186. package/src/components/toaster/RLSToaster.tsx +82 -0
  187. package/src/components/tooltip/RLSTooltip.tsx +48 -0
  188. package/src/components/tour/RLSTour.tsx +26 -0
  189. package/src/components/tree/RLSTree.tsx +26 -0
  190. package/src/components/user/RLSUser.tsx +26 -0
  191. package/src/components/video/RLAVideo.astro +4 -1
  192. package/src/components/video/RLSVideo.tsx +158 -0
  193. package/src/components/watermark/RLSWatermark.tsx +27 -0
  194. package/src/components/window-mockup/RLSWindowMockup.tsx +27 -0
  195. package/src/preset.ts +73 -1
  196. package/src/resolver.ts +6 -1
  197. package/src/virtual.d.ts +6 -0
@@ -0,0 +1,298 @@
1
+ import {
2
+ createEffect,
3
+ createMemo,
4
+ createSignal,
5
+ For,
6
+ onCleanup,
7
+ onMount,
8
+ Show,
9
+ splitProps,
10
+ type Component,
11
+ type JSX
12
+ } from "solid-js"
13
+ import type { TabItem, TabsProps } from "./tabs"
14
+ import { tabsTheme } from "./tabs.theme"
15
+ import RLSBadge from "../badge/RLSBadge"
16
+ import RLSIcon from "../icon/RLSIcon"
17
+
18
+ export interface RLSTabsProps<T extends TabItem = TabItem> extends TabsProps<T> {
19
+ children?: JSX.Element
20
+ onChange?: (value: string) => void
21
+ }
22
+
23
+ const RLSTabs: Component<RLSTabsProps> = (allProps) => {
24
+ const [props, rest] = splitProps(allProps, [
25
+ "items",
26
+ "value",
27
+ "defaultValue",
28
+ "variant",
29
+ "orientation",
30
+ "size",
31
+ "color",
32
+ "content",
33
+ "unmountOnHide",
34
+ "valueKey",
35
+ "labelKey",
36
+ "maxTabs",
37
+ "estimatedTabWidth",
38
+ "onChange",
39
+ "class",
40
+ "ui",
41
+ "children"
42
+ ])
43
+
44
+ const items = () => props.items ?? []
45
+ const valueKey = () => props.valueKey ?? "value"
46
+ const labelKey = () => props.labelKey ?? "label"
47
+ const orientation = () => props.orientation ?? "horizontal"
48
+ const size = () => props.size ?? "md"
49
+ const showContent = () => props.content ?? true
50
+
51
+ const normalizedVariant = createMemo(() => {
52
+ const v = props.variant ?? "pill"
53
+ if (v === "solid" || v === "pills") return "pill"
54
+ if (v === "link") return "underline"
55
+ return v
56
+ })
57
+
58
+ const initialVal = createMemo(() => {
59
+ if (props.value !== undefined) return props.value
60
+ if (props.defaultValue !== undefined) return props.defaultValue
61
+ const list = items()
62
+ if (list.length > 0) {
63
+ return (list[0]?.[valueKey()] ?? list[0]?.value ?? "0") as string
64
+ }
65
+ return ""
66
+ })
67
+
68
+ const [internalValue, setInternalValue] = createSignal(initialVal())
69
+
70
+ createEffect(() => {
71
+ if (props.value !== undefined) {
72
+ setInternalValue(props.value)
73
+ }
74
+ })
75
+
76
+ const currentValue = () => (props.value !== undefined ? props.value : internalValue())
77
+
78
+ const handleSelect = (val: string) => {
79
+ if (props.value === undefined) {
80
+ setInternalValue(val)
81
+ }
82
+ props.onChange?.(val)
83
+ }
84
+
85
+ const resolvedClasses = createMemo(() =>
86
+ tabsTheme({
87
+ variant: normalizedVariant(),
88
+ orientation: orientation(),
89
+ size: size(),
90
+ color: props.color,
91
+ ui: props.ui,
92
+ class: props.class
93
+ })
94
+ )
95
+
96
+ let listRef: HTMLDivElement | null = null
97
+ let indicatorRef: HTMLDivElement | null = null
98
+ const [popoverOpen, setPopoverOpen] = createSignal(false)
99
+
100
+ const updateIndicator = () => {
101
+ const list = listRef
102
+ const indicator = indicatorRef
103
+ if (!list || !indicator) return
104
+ const activeEl = list.querySelector(`[role=tab][data-state=active]`) as HTMLElement | null
105
+ if (!activeEl || getComputedStyle(activeEl).display === "none") {
106
+ indicator.style.display = "none"
107
+ return
108
+ }
109
+
110
+ const listRect = list.getBoundingClientRect()
111
+ const triggerRect = activeEl.getBoundingClientRect()
112
+
113
+ if (triggerRect.width === 0 && triggerRect.height === 0) {
114
+ indicator.style.display = "none"
115
+ return
116
+ }
117
+
118
+ indicator.style.display = "block"
119
+ const isVert = orientation() === "vertical"
120
+
121
+ if (isVert) {
122
+ const top = triggerRect.top - listRect.top + list.scrollTop
123
+ const height = triggerRect.height
124
+ indicator.style.transform = `translateY(${top}px)`
125
+ indicator.style.height = `${height}px`
126
+ } else {
127
+ const left = triggerRect.left - listRect.left + list.scrollLeft
128
+ const width = triggerRect.width
129
+ indicator.style.transform = `translateX(${left}px)`
130
+ indicator.style.width = `${width}px`
131
+ }
132
+ }
133
+
134
+ onMount(() => {
135
+ updateIndicator()
136
+ if (typeof ResizeObserver !== "undefined" && listRef) {
137
+ const ro = new ResizeObserver(() => updateIndicator())
138
+ ro.observe(listRef)
139
+ onCleanup(() => ro.disconnect())
140
+ }
141
+ })
142
+
143
+ createEffect(() => {
144
+ currentValue()
145
+ requestAnimationFrame(() => updateIndicator())
146
+ })
147
+
148
+ const effectiveMax = () =>
149
+ props.maxTabs !== undefined ? Math.max(1, props.maxTabs) : items().length
150
+ const renderedListItems = () => items().slice(0, effectiveMax())
151
+ const overflowItems = () => items().slice(effectiveMax())
152
+ const needsOverflow = () => items().length > 1 && orientation() === "horizontal"
153
+
154
+ return (
155
+ <div
156
+ class={resolvedClasses().root}
157
+ data-slot="root"
158
+ data-orientation={orientation()}
159
+ data-variant={normalizedVariant()}
160
+ data-value={currentValue()}
161
+ {...rest}
162
+ >
163
+ <div
164
+ ref={(el) => (listRef = el)}
165
+ class={resolvedClasses().list}
166
+ data-slot="list"
167
+ role="tablist"
168
+ >
169
+ <div
170
+ ref={(el) => (indicatorRef = el)}
171
+ class={resolvedClasses().indicator}
172
+ data-slot="indicator"
173
+ style={{ display: "none" }}
174
+ />
175
+
176
+ <For each={renderedListItems()}>
177
+ {(item, index) => {
178
+ const itemVal = () => (item[valueKey()] ?? item.value ?? String(index())) as string
179
+ const itemLabel = () => item[labelKey()] ?? item.label
180
+ const active = () => itemVal() === currentValue()
181
+
182
+ return (
183
+ <button
184
+ type="button"
185
+ class={resolvedClasses().trigger}
186
+ data-slot="trigger"
187
+ role="tab"
188
+ data-value={itemVal()}
189
+ data-state={active() ? "active" : "inactive"}
190
+ disabled={item.disabled}
191
+ onClick={() => handleSelect(itemVal())}
192
+ >
193
+ {item.icon && (
194
+ <RLSIcon
195
+ name={item.icon}
196
+ class={resolvedClasses().leadingIcon}
197
+ data-slot="leadingIcon"
198
+ />
199
+ )}
200
+ {itemLabel() && (
201
+ <span class={resolvedClasses().label} data-slot="label">
202
+ {itemLabel()}
203
+ </span>
204
+ )}
205
+ {item.badge !== undefined && item.badge !== null && (
206
+ <RLSBadge
207
+ size="sm"
208
+ variant="soft"
209
+ color="neutral"
210
+ class={resolvedClasses().trailingBadge}
211
+ data-slot="trailingBadge"
212
+ >
213
+ {typeof item.badge === "object" ? item.badge.label : item.badge}
214
+ </RLSBadge>
215
+ )}
216
+ </button>
217
+ )
218
+ }}
219
+ </For>
220
+
221
+ <Show when={needsOverflow() && overflowItems().length > 0}>
222
+ <div class="relative inline-flex items-center ml-auto">
223
+ <button
224
+ type="button"
225
+ class={`${resolvedClasses().trigger} rla-tb-overflow-btn`}
226
+ data-slot="overflow-trigger"
227
+ aria-label="More tabs"
228
+ onClick={() => setPopoverOpen(!popoverOpen())}
229
+ >
230
+ <span class="rla-tb-label">+{overflowItems().length} more</span>
231
+ <RLSIcon name="chevronDown" class="ml-1 size-3.5 opacity-70" />
232
+ </button>
233
+
234
+ <Show when={popoverOpen()}>
235
+ <div
236
+ class="absolute right-0 top-full mt-1 z-50 rounded-lg border border-default bg-default p-1 shadow-lg backdrop-blur-sm min-w-[10rem] flex flex-col gap-0.5"
237
+ onClick={() => setPopoverOpen(false)}
238
+ >
239
+ <For each={overflowItems()}>
240
+ {(item, index) => {
241
+ const itemVal = () =>
242
+ (item[valueKey()] ?? item.value ?? String(effectiveMax() + index())) as string
243
+ const itemLabel = () => item[labelKey()] ?? item.label
244
+ const active = () => itemVal() === currentValue()
245
+
246
+ return (
247
+ <button
248
+ type="button"
249
+ class={`w-full text-left px-3 py-1.5 text-sm rounded-md transition-colors flex items-center justify-between hover:bg-muted focus-visible:outline-none cursor-pointer ${
250
+ active() ? "bg-muted/80 font-medium" : ""
251
+ }`}
252
+ onClick={() => handleSelect(itemVal())}
253
+ >
254
+ <span class="truncate">{itemLabel()}</span>
255
+ {active() && <RLSIcon name="check" class="size-4 opacity-100" />}
256
+ </button>
257
+ )
258
+ }}
259
+ </For>
260
+ </div>
261
+ </Show>
262
+ </div>
263
+ </Show>
264
+ </div>
265
+
266
+ <Show when={showContent()}>
267
+ <div class="flex-1">
268
+ {props.children ? (
269
+ props.children
270
+ ) : (
271
+ <For each={items()}>
272
+ {(item, index) => {
273
+ const itemVal = () => (item[valueKey()] ?? item.value ?? String(index())) as string
274
+ const active = () => itemVal() === currentValue()
275
+
276
+ return (
277
+ <Show when={props.unmountOnHide === false || active()}>
278
+ <div
279
+ class={resolvedClasses().content}
280
+ data-slot="content"
281
+ role="tabpanel"
282
+ data-value={itemVal()}
283
+ style={{ display: active() ? undefined : "none" }}
284
+ >
285
+ {item.content}
286
+ </div>
287
+ </Show>
288
+ )
289
+ }}
290
+ </For>
291
+ )}
292
+ </div>
293
+ </Show>
294
+ </div>
295
+ )
296
+ }
297
+
298
+ export default RLSTabs
@@ -34,6 +34,7 @@ const classes = textareaTheme({
34
34
  -->
35
35
  <textarea
36
36
  class={classes.textarea}
37
+ data-slot="textarea"
37
38
  placeholder={placeholder}
38
39
  disabled={disabled}
39
40
  readonly={readonly}
@@ -20,6 +20,7 @@ const RLSTextarea: Component<RLSTextareaProps> = (allProps) => {
20
20
  "name",
21
21
  "required",
22
22
  "rows",
23
+ "autoGrow",
23
24
  "error",
24
25
  "size",
25
26
  "color",
@@ -47,7 +48,8 @@ const RLSTextarea: Component<RLSTextareaProps> = (allProps) => {
47
48
  <textarea
48
49
  ref={props.ref}
49
50
  name={props.name}
50
- rows={props.rows ?? 3}
51
+ rows={props.autoGrow ? undefined : (props.rows ?? 4)}
52
+ style={props.autoGrow ? "field-sizing: content; min-height: unset;" : undefined}
51
53
  value={props.value !== undefined ? props.value : ""}
52
54
  placeholder={props.placeholder}
53
55
  disabled={props.disabled}
@@ -1,95 +1,124 @@
1
1
  ---
2
2
  import type { ThemeSelectorProps } from "./theme-selector"
3
3
  import { themeSelectorTheme } from "./theme-selector.theme"
4
- import RLAButton from "../button/RLAButton.astro"
4
+ import RLASelect from "../select/RLASelect.astro"
5
5
  import RLAIcon from "../icon/RLAIcon.astro"
6
6
 
7
-
8
-
9
7
  const {
8
+ mode = "select",
10
9
  size = "md",
10
+ theme,
11
+ class: classAttr,
11
12
  ...rest
12
13
  } = Astro.props as ThemeSelectorProps
13
14
 
14
15
  const classes = themeSelectorTheme(Astro.props);
16
+
17
+ const items = [
18
+ { label: "System", value: "system", icon: "i-lucide-laptop" },
19
+ { label: "Light", value: "light", icon: "i-rl-sun" },
20
+ { label: "Dark", value: "dark", icon: "i-rl-moon" }
21
+ ];
15
22
  ---
16
23
 
17
- <rla-theme-selector {...rest}>
18
- <RLAButton
19
- size={size}
20
- class:list={["theme-toggle-button", classes.root]}
21
- variant="ghost"
22
- square
23
- aria-label="Toggle theme"
24
- >
25
- <RLAIcon name="i-rl-sun" class={classes.sun} />
26
- <RLAIcon name="i-rl-moon" class={classes.moon} />
27
- </RLAButton>
24
+ <rla-theme-selector data-mode={mode} data-theme={theme} {...rest}>
25
+ <div class:list={[classes.root, classAttr]} data-theme-selector>
26
+ <div class:list={[classes.wrapper]}>
27
+ <RLASelect
28
+ items={items}
29
+ defaultValue="system"
30
+ size={size}
31
+ theme={theme}
32
+ variant={mode === "icon" ? "icon" : "default"}
33
+ aria-label="Toggle theme"
34
+ class={classes.select}
35
+ >
36
+ <span slot="leading" class="flex items-center">
37
+ <RLAIcon name="i-lucide-laptop" class="rla-theme-icon-system size-4" style="display: block;" />
38
+ <RLAIcon name="i-rl-sun" class="rla-theme-icon-light size-4" style="display: none;" />
39
+ <RLAIcon name="i-rl-moon" class="rla-theme-icon-dark size-4" style="display: none;" />
40
+ </span>
41
+ </RLASelect>
42
+ </div>
43
+ </div>
28
44
  </rla-theme-selector>
29
45
 
30
- <style is:global>
31
- .rla-theme-sun { display: none; }
32
- [data-theme="dark"] .rla-theme-sun { display: block; }
33
- .rla-theme-moon { display: block; }
34
- [data-theme="dark"] .rla-theme-moon { display: none; }
35
- </style>
36
-
37
46
  <script>
38
47
  import { defineShortcuts } from "../../shortcuts"
39
48
 
40
- function applyStoredTheme() {
41
- const stored = localStorage.getItem('theme');
42
- if (stored === 'dark' || stored === 'light') {
43
- document.documentElement.setAttribute('data-theme', stored);
44
- }
45
- }
49
+ class RLAThemeSelector extends HTMLElement {
50
+ private cleanup: (() => void) | null = null;
51
+ private select: HTMLSelectElement | null = null;
46
52
 
47
- applyStoredTheme();
53
+ private getPreference(): string {
54
+ const stored = localStorage.getItem('theme');
55
+ return stored === 'system' || stored === 'light' || stored === 'dark' ? stored : 'system';
56
+ }
48
57
 
49
- function onToggle() {
50
- const willBeDark = document.documentElement.getAttribute('data-theme') !== 'dark';
51
- document.documentElement.setAttribute('data-theme', willBeDark ? 'dark' : 'light');
52
- localStorage.setItem('theme', willBeDark ? 'dark' : 'light');
58
+ connectedCallback() {
59
+ this.style.display = 'contents';
60
+ const pref = this.getPreference();
53
61
 
54
- document.querySelectorAll('.theme-toggle-button').forEach(btn => {
55
- btn.setAttribute('data-state', willBeDark ? 'on' : 'off');
56
- btn.setAttribute('aria-pressed', willBeDark.toString());
57
- });
58
- }
62
+ const select = this.querySelector('select');
63
+ this.select = select;
64
+ if (select) {
65
+ select.value = pref;
66
+ select.dispatchEvent(new Event('change', { bubbles: true }));
67
+ }
59
68
 
60
- class RLAThemeSelector extends HTMLElement {
61
- private buttonCleanup: (() => void) | null = null;
69
+ const updateTheme = (next: string) => {
70
+ this.setAttribute('data-theme-preference', next);
71
+
72
+ const sysIcons = this.querySelectorAll('.rla-theme-icon-system');
73
+ const lightIcons = this.querySelectorAll('.rla-theme-icon-light');
74
+ const darkIcons = this.querySelectorAll('.rla-theme-icon-dark');
75
+ sysIcons.forEach(el => ((el as HTMLElement).style.display = next === 'system' ? 'block' : 'none'));
76
+ lightIcons.forEach(el => ((el as HTMLElement).style.display = next === 'light' ? 'block' : 'none'));
77
+ darkIcons.forEach(el => ((el as HTMLElement).style.display = next === 'dark' ? 'block' : 'none'));
78
+
79
+ const resolved = next === 'system'
80
+ ? window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
81
+ : next;
82
+ document.documentElement.setAttribute('data-theme', resolved);
83
+ localStorage.setItem('theme', next);
84
+ };
62
85
 
63
- connectedCallback() {
64
- this.style.display = 'contents';
65
- const button = this.querySelector('.theme-toggle-button');
66
- if (!button) return;
86
+ updateTheme(pref);
67
87
 
68
- const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
69
- button.setAttribute('data-state', isDark ? 'on' : 'off');
70
- button.setAttribute('aria-pressed', isDark.toString());
88
+ const onChange = (event: Event) => {
89
+ const target = event.target as HTMLSelectElement | null;
90
+ if (target && target.tagName === 'SELECT' && target.value) {
91
+ updateTheme(target.value);
92
+ }
93
+ };
94
+ this.addEventListener('change', onChange);
71
95
 
72
- button.addEventListener('click', onToggle);
96
+ const cycle = () => {
97
+ const current = localStorage.getItem('theme') || 'system';
98
+ const next = current === 'system' ? 'light' : current === 'light' ? 'dark' : 'system';
99
+ if (this.select) this.select.value = next;
100
+ updateTheme(next);
101
+ };
73
102
 
74
103
  const cleanupShortcut = defineShortcuts({
75
104
  meta_alt_t: {
76
- handler: onToggle,
77
- label: "Toggle Theme",
78
- description: "Switch between light and dark theme mode",
105
+ handler: cycle,
106
+ label: "Cycle Theme",
107
+ description: "Cycle between system, light, and dark theme mode",
79
108
  category: "system"
80
109
  }
81
110
  });
82
111
 
83
- this.buttonCleanup = () => {
84
- button.removeEventListener('click', onToggle);
112
+ this.cleanup = () => {
113
+ this.removeEventListener('change', onChange);
85
114
  cleanupShortcut();
86
115
  };
87
116
  }
88
117
 
89
118
  disconnectedCallback() {
90
- if (this.buttonCleanup) {
91
- this.buttonCleanup();
92
- this.buttonCleanup = null;
119
+ if (this.cleanup) {
120
+ this.cleanup();
121
+ this.cleanup = null;
93
122
  }
94
123
  }
95
124
  }
@@ -97,4 +126,4 @@ const classes = themeSelectorTheme(Astro.props);
97
126
  if (!customElements.get('rla-theme-selector')) {
98
127
  customElements.define('rla-theme-selector', RLAThemeSelector);
99
128
  }
100
- </script>
129
+ </script>
@@ -0,0 +1,131 @@
1
+ import {
2
+ createMemo,
3
+ createSignal,
4
+ onCleanup,
5
+ onMount,
6
+ splitProps,
7
+ type Component,
8
+ type JSX
9
+ } from "solid-js"
10
+ import type { ThemeSelectorProps } from "./theme-selector"
11
+ import { themeSelectorTheme } from "./theme-selector.theme"
12
+ import RLSSelect from "../select/RLSSelect"
13
+ import RLSIcon from "../icon/RLSIcon"
14
+ import { defineShortcuts } from "../../shortcuts"
15
+
16
+ export interface RLSThemeSelectorProps extends ThemeSelectorProps {
17
+ children?: JSX.Element
18
+ onChange?: (theme: string) => void
19
+ }
20
+
21
+ const themeItems = [
22
+ { label: "System", value: "system", icon: "i-lucide-laptop" },
23
+ { label: "Light", value: "light", icon: "i-rl-sun" },
24
+ { label: "Dark", value: "dark", icon: "i-rl-moon" }
25
+ ]
26
+
27
+ const RLSThemeSelector: Component<RLSThemeSelectorProps> = (allProps) => {
28
+ const [props, rest] = splitProps(allProps, [
29
+ "mode",
30
+ "size",
31
+ "theme",
32
+ "ui",
33
+ "class",
34
+ "children",
35
+ "onChange"
36
+ ])
37
+
38
+ const [currentPreference, setCurrentPreference] = createSignal<string>("system")
39
+
40
+ const getPreference = () => {
41
+ if (typeof localStorage === "undefined") return "system"
42
+ const stored = localStorage.getItem("theme")
43
+ return stored === "system" || stored === "light" || stored === "dark" ? stored : "system"
44
+ }
45
+
46
+ const applyTheme = (next: string) => {
47
+ setCurrentPreference(next)
48
+ if (typeof document !== "undefined") {
49
+ const resolved =
50
+ next === "system"
51
+ ? typeof window !== "undefined" &&
52
+ window.matchMedia("(prefers-color-scheme: dark)").matches
53
+ ? "dark"
54
+ : "light"
55
+ : next
56
+ document.documentElement.setAttribute("data-theme", resolved)
57
+ localStorage.setItem("theme", next)
58
+ }
59
+ props.onChange?.(next)
60
+ }
61
+
62
+ onMount(() => {
63
+ const pref = getPreference()
64
+ applyTheme(pref)
65
+
66
+ const cycle = () => {
67
+ const cur = currentPreference()
68
+ const next = cur === "system" ? "light" : cur === "light" ? "dark" : "system"
69
+ applyTheme(next)
70
+ }
71
+
72
+ const cleanupShortcut = defineShortcuts({
73
+ meta_alt_t: {
74
+ handler: cycle,
75
+ label: "Cycle Theme",
76
+ description: "Cycle between system, light, and dark theme mode",
77
+ category: "system"
78
+ }
79
+ })
80
+
81
+ onCleanup(() => cleanupShortcut())
82
+ })
83
+
84
+ const mode = () => props.mode || "select"
85
+ const size = () => props.size || "md"
86
+
87
+ const resolvedClasses = createMemo(() =>
88
+ themeSelectorTheme({
89
+ ui: props.ui,
90
+ class: props.class
91
+ })
92
+ )
93
+
94
+ const activeIcon = () => {
95
+ const pref = currentPreference()
96
+ if (pref === "light") return "i-rl-sun"
97
+ if (pref === "dark") return "i-rl-moon"
98
+ return "i-lucide-laptop"
99
+ }
100
+
101
+ return (
102
+ <div
103
+ class={resolvedClasses().root}
104
+ data-theme-selector
105
+ data-mode={mode()}
106
+ data-theme={props.theme}
107
+ data-theme-preference={currentPreference()}
108
+ {...rest}
109
+ >
110
+ <div class={resolvedClasses().wrapper}>
111
+ <RLSSelect
112
+ items={themeItems}
113
+ value={currentPreference()}
114
+ onChange={(e) => applyTheme(e.currentTarget.value)}
115
+ size={size() as any}
116
+ variant={mode() === "icon" ? "icon" : "default"}
117
+ aria-label="Toggle theme"
118
+ class={resolvedClasses().select}
119
+ leading={
120
+ <span class="flex items-center">
121
+ <RLSIcon name={activeIcon()} class="size-4" />
122
+ </span>
123
+ }
124
+ />
125
+ </div>
126
+ {props.children}
127
+ </div>
128
+ )
129
+ }
130
+
131
+ export default RLSThemeSelector
@@ -1,12 +1,10 @@
1
1
  import { defineTheme } from "../../resolver"
2
2
 
3
3
  export const themeSelectorTheme = defineTheme("theme-selector", {
4
- slots: ["root", "sun", "moon"],
4
+ slots: ["root", "wrapper", "select"],
5
5
  base: {
6
- root: "group rounded-full hover:bg-neutral-900/50 transition-colors inline-flex items-center justify-center text-neutral-400 hover:text-white cursor-pointer",
7
- sun: "rla-theme-sun w-5 h-5 text-current",
8
- moon: "rla-theme-moon w-5 h-5 text-current"
9
- },
10
- variants: {},
11
- defaultVariants: {}
6
+ root: "inline-flex flex-col gap-1.5",
7
+ wrapper: "relative inline-block",
8
+ select: ""
9
+ }
12
10
  })
@@ -1,8 +1,18 @@
1
1
  export interface ThemeSelectorProps {
2
2
  /**
3
- * The size of the button.
3
+ * The display mode of the theme selector.
4
+ *
5
+ * @default "select"
6
+ */
7
+ mode?: "select" | "icon"
8
+ /**
9
+ * The size of the select/button.
4
10
  */
5
11
  size?: "xs" | "sm" | "md" | "lg" | "xl"
12
+ /**
13
+ * Theme variant/mode.
14
+ */
15
+ theme?: "light" | "dark"
6
16
  /**
7
17
  * Slot-specific CSS class overrides.
8
18
  */