@rimelight/ui 0.0.43 → 0.0.44

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 (153) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +14 -12
  3. package/package.json +11 -14
  4. package/src/components/avatar/RLAAvatar.astro +4 -12
  5. package/src/components/badge/RLABadge.astro +2 -1
  6. package/src/components/badge/badge.theme.ts +0 -4
  7. package/src/components/banner/RLABanner.astro +1 -1
  8. package/src/components/banner/banner.theme.ts +1 -1
  9. package/src/components/breadcrumb/RLABreadcrumb.astro +2 -2
  10. package/src/components/button/RLAButton.astro +4 -1
  11. package/src/components/button/button.theme.ts +1 -5
  12. package/src/components/callout/RLACallout.astro +7 -5
  13. package/src/components/callout/callout.theme.ts +1 -5
  14. package/src/components/card/card.theme.ts +3 -3
  15. package/src/components/carousel/RLACarousel.astro +2 -2
  16. package/src/components/carousel/carousel.theme.ts +2 -2
  17. package/src/components/chart/RLAChart.astro +1 -1
  18. package/src/components/checkbox/RLACheckbox.astro +2 -1
  19. package/src/components/checkbox/checkbox.theme.ts +0 -4
  20. package/src/components/chip/RLAChip.astro +2 -1
  21. package/src/components/chip/chip.theme.ts +0 -4
  22. package/src/components/collapsible/RLACollapsible.astro +1 -1
  23. package/src/components/color-area/RLAColorArea.astro +1 -0
  24. package/src/components/color-area/color-area.theme.ts +1 -5
  25. package/src/components/color-field/RLAColorField.astro +1 -0
  26. package/src/components/color-field/color-field.theme.ts +1 -5
  27. package/src/components/color-slider/RLAColorSlider.astro +1 -0
  28. package/src/components/color-slider/color-slider.theme.ts +1 -5
  29. package/src/components/dashboard-group/RLADashboardGroup.astro +5 -7
  30. package/src/components/dashboard-group/dashboard-group.theme.ts +1 -1
  31. package/src/components/dashboard-group/dashboard-group.ts +1 -1
  32. package/src/components/dashboard-navbar/RLADashboardNavbar.astro +4 -4
  33. package/src/components/dashboard-navbar/dashboard-navbar.theme.ts +3 -3
  34. package/src/components/dashboard-navbar/dashboard-navbar.ts +1 -1
  35. package/src/components/dashboard-panel/RLADashboardPanel.astro +9 -8
  36. package/src/components/dashboard-panel/dashboard-panel.theme.ts +1 -1
  37. package/src/components/dashboard-sidebar/RLADashboardSidebar.astro +7 -4
  38. package/src/components/dashboard-sidebar/dashboard-sidebar.theme.ts +6 -5
  39. package/src/components/dashboard-sidebar-collapse/RLADashboardSidebarCollapse.astro +1 -0
  40. package/src/components/drawer/RLADrawer.astro +1 -1
  41. package/src/components/dropdown-menu/dropdown-menu.theme.ts +2 -2
  42. package/src/components/footer/RLAFooter.astro +2 -0
  43. package/src/components/footer/footer.theme.ts +0 -8
  44. package/src/components/gamepad/RLAGamepad.astro +1 -1
  45. package/src/components/head/UIHead.astro +1 -2
  46. package/src/components/header/header.theme.ts +1 -1
  47. package/src/components/icon/RLAIcon.astro +2 -5
  48. package/src/components/icon/RLSIcon.tsx +2 -2
  49. package/src/components/icon/RLVIcon.vue +18 -6
  50. package/src/components/icon/icon.theme.ts +1 -1
  51. package/src/components/image/image.theme.ts +1 -1
  52. package/src/components/input/RLAInput.astro +2 -1
  53. package/src/components/input/input.theme.ts +0 -4
  54. package/src/components/input-menu/RLAInputMenu.astro +2 -2
  55. package/src/components/input-menu/input-menu.theme.ts +1 -1
  56. package/src/components/input-rating/input-rating.theme.ts +2 -2
  57. package/src/components/kbd/RLAKbd.astro +2 -1
  58. package/src/components/kbd/kbd.theme.ts +0 -4
  59. package/src/components/keyboard/RLAKeyboard.astro +1 -1
  60. package/src/components/link/RLALink.astro +1 -0
  61. package/src/components/link/link.theme.ts +0 -4
  62. package/src/components/link-group/RLALinkGroup.astro +1 -1
  63. package/src/components/link-group/link-group.theme.ts +1 -10
  64. package/src/components/locale-selector/RLALocaleSelector.astro +4 -36
  65. package/src/components/locale-selector/locale-selector.theme.ts +3 -11
  66. package/src/components/logo/RLALogo.astro +1 -2
  67. package/src/components/modal/RLAModal.astro +1 -1
  68. package/src/components/modal/RLSModal.tsx +2 -2
  69. package/src/components/navigation-menu/RLANavigationMenu.astro +17 -21
  70. package/src/components/navigation-menu/navigation-menu.theme.ts +3 -7
  71. package/src/components/pagination/RLAPagination.astro +2 -2
  72. package/src/components/pagination/pagination.theme.ts +1 -1
  73. package/src/components/popover/RLAPopover.astro +39 -0
  74. package/src/components/progress/RLAProgress.astro +2 -0
  75. package/src/components/progress/progress.theme.ts +0 -4
  76. package/src/components/scroll-area/scroll-area.theme.ts +1 -1
  77. package/src/components/scroll-to-top/RLAScrollToTop.astro +6 -5
  78. package/src/components/scroll-to-top/RLVScrollToTop.vue +7 -7
  79. package/src/components/scroll-to-top/scroll-to-top.theme.ts +3 -7
  80. package/src/components/search/RLASearch.astro +3 -3
  81. package/src/components/select/RLASelect.astro +96 -528
  82. package/src/components/select/RLSSelect.tsx +68 -14
  83. package/src/components/select/RLVSelect.vue +57 -0
  84. package/src/components/select/select.theme.ts +11 -96
  85. package/src/components/shortcuts/RLAShortcuts.astro +9 -9
  86. package/src/components/sidebar/RLASidebar.astro +3 -3
  87. package/src/components/slideover/RLASlideover.astro +1 -1
  88. package/src/components/slider/RLASlider.astro +2 -1
  89. package/src/components/slider/slider.theme.ts +1 -5
  90. package/src/components/spinner/RLASpinner.astro +2 -1
  91. package/src/components/spinner/spinner.theme.ts +1 -5
  92. package/src/components/splitter/splitter.theme.ts +1 -1
  93. package/src/components/stepper/RLAStepper.astro +8 -8
  94. package/src/components/stepper/stepper.theme.ts +6 -7
  95. package/src/components/steps/RLASteps.astro +3 -3
  96. package/src/components/switch/RLASwitch.astro +2 -1
  97. package/src/components/switch/switch.theme.ts +0 -4
  98. package/src/components/table/table.theme.ts +1 -1
  99. package/src/components/table-of-contents/RLATableOfContents.astro +5 -5
  100. package/src/components/theme-selector/RLAThemeSelector.astro +16 -14
  101. package/src/components/theme-selector/theme-selector.theme.ts +2 -2
  102. package/src/components/toast/RLAToast.astro +6 -6
  103. package/src/components/toast/store.ts +1 -1
  104. package/src/components/tooltip/tooltip.theme.ts +1 -1
  105. package/src/icons.raw.ts +67 -0
  106. package/src/icons.ts +31 -64
  107. package/src/index.ts +19 -18
  108. package/src/preset.ts +110 -108
  109. package/src/resolver.ts +127 -15
  110. package/src/shortcuts.ts +103 -84
  111. package/src/stores.ts +22 -0
  112. package/src/types.ts +27 -9
  113. package/src/virtual.d.ts +4 -0
  114. package/src/components/editor/PageEditor.vue +0 -459
  115. package/src/components/editor/PageMention.vue +0 -87
  116. package/src/components/editor/PagePropertiesEditor.vue +0 -358
  117. package/src/components/editor/PagePropertiesRenderer.vue +0 -285
  118. package/src/components/editor/PageRenderer.vue +0 -155
  119. package/src/components/editor/PageSurround.vue +0 -130
  120. package/src/components/editor/PageVersionSelector.vue +0 -371
  121. package/src/components/editor/blocks/AddBlockModal.vue +0 -196
  122. package/src/components/editor/blocks/Block.vue +0 -142
  123. package/src/components/editor/blocks/BlockEditRenderer.vue +0 -26
  124. package/src/components/editor/blocks/BlockEditor.vue +0 -141
  125. package/src/components/editor/blocks/BlockViewRenderer.vue +0 -71
  126. package/src/components/editor/blocks/TextRenderer.vue +0 -31
  127. package/src/components/editor/blocks/editor/CalloutBlockEditor.vue +0 -53
  128. package/src/components/editor/blocks/editor/ImageBlockEditor.vue +0 -55
  129. package/src/components/editor/blocks/editor/ParagraphBlockEditor.vue +0 -31
  130. package/src/components/editor/blocks/editor/SectionBlockEditor.vue +0 -65
  131. package/src/components/editor/blocks/nodes/LinkNode.vue +0 -28
  132. package/src/components/editor/blocks/nodes/TextNode.vue +0 -22
  133. package/src/components/editor/blocks/renderer/CalloutBlockRenderer.vue +0 -65
  134. package/src/components/editor/blocks/renderer/ImageBlockRenderer.vue +0 -20
  135. package/src/components/editor/blocks/renderer/ParagraphBlockRenderer.vue +0 -21
  136. package/src/components/editor/blocks/renderer/SectionBlockRenderer.vue +0 -55
  137. package/src/components/editor/composables/injectionKeys.ts +0 -3
  138. package/src/components/editor/composables/useBlockEditor.ts +0 -261
  139. package/src/components/editor/composables/usePageEditor.ts +0 -97
  140. package/src/components/editor/modals/CreatePageModal.vue +0 -160
  141. package/src/components/editor/modals/DeletePageModal.vue +0 -116
  142. package/src/components/editor/modals/PageTreeModal.vue +0 -275
  143. package/src/components/editor/types/blocks.ts +0 -100
  144. package/src/components/editor/types/pages.ts +0 -89
  145. package/src/components/editor/types/schemas.ts +0 -36
  146. package/src/components/editor/utils/blocks.ts +0 -42
  147. package/src/components/editor/utils/editorHelpers.ts +0 -24
  148. package/src/components/editor/utils/page.ts +0 -219
  149. package/src/components/editor/utils/richTextHelpers.ts +0 -35
  150. package/src/components/editor/views/PageEditView.vue +0 -195
  151. package/src/components/editor/views/PageLiveView.vue +0 -90
  152. package/src/components/editor/views/PageReviewView.vue +0 -308
  153. package/src/components/icon/resolver.ts +0 -23
@@ -2,550 +2,118 @@
2
2
  import type { SelectProps, SelectItem } from "./select"
3
3
  import { selectTheme } from "./select.theme"
4
4
  import RLAIcon from "../icon/RLAIcon.astro"
5
- import RLAAvatar from "../avatar/RLAAvatar.astro"
6
-
7
5
 
8
6
  const {
9
7
  items = [],
10
8
  value,
11
9
  defaultValue,
12
- modelValue,
13
- placeholder = "Select option...",
10
+ placeholder,
14
11
  disabled = false,
15
12
  required = false,
16
13
  name,
17
- form,
18
- label,
19
14
  size = "md",
20
- color = "neutral",
21
- createItem = false,
22
- searchInput = true,
23
- multiple = false,
24
- clear = false,
25
- clearIcon = "x",
26
- valueKey = "value",
27
- labelKey = "label",
28
- descriptionKey = "description",
29
- filterFields = [labelKey],
30
- ignoreFilter = false,
31
- icon,
32
- leadingIcon = icon,
33
- trailingIcon = "chevronDown",
34
- selectedIcon = "check",
35
- loading = false,
36
- loadingIcon = "loaderCircle",
37
- avatar,
15
+ multiple,
16
+ ui: _ui,
17
+ class: classAttr,
18
+ theme,
38
19
  ...rest
39
20
  } = Astro.props as SelectProps
40
21
 
41
- const activeValue = value ?? modelValue ?? defaultValue ?? (multiple ? [] : "")
42
-
43
- const classes = selectTheme(Astro.props)
22
+ const classes = selectTheme({ size, class: classAttr })
44
23
 
45
- // Helper to normalize items
46
- function normalizeItems(rawItems: SelectItem[]): any[] {
47
- const result: any[] = []
24
+ const currentValue = value !== undefined ? value : defaultValue !== undefined ? defaultValue : undefined
25
+ const currentValues = Array.isArray(currentValue) ? currentValue.map(String) : undefined
48
26
 
49
- function processItem(item: any) {
50
- if (Array.isArray(item)) {
51
- item.forEach(processItem)
52
- return
53
- }
54
- if (typeof item === "string" || typeof item === "number" || typeof item === "boolean") {
55
- result.push({ [labelKey]: String(item), [valueKey]: String(item) })
56
- return
57
- }
58
- if (item && typeof item === "object") {
59
- const val = item[valueKey] !== undefined ? item[valueKey] : item[labelKey]
60
- const lbl = item[labelKey] !== undefined ? item[labelKey] : item[valueKey]
61
- result.push({
62
- ...item,
63
- [labelKey]: String(lbl ?? ""),
64
- [valueKey]: String(val ?? "")
65
- })
66
- }
27
+ const normalizedItems = (items || []).map((item: SelectItem) => {
28
+ if (typeof item === "string" || typeof item === "number" || typeof item === "boolean") {
29
+ return { label: String(item), value: String(item), disabled: false }
67
30
  }
68
-
69
- rawItems.forEach(processItem)
70
- return result
71
- }
72
-
73
- const normalizedItems = normalizeItems(items)
74
-
75
- // Determine initial display text
76
- let initialDisplayText = placeholder
77
- if (multiple && Array.isArray(activeValue) && activeValue.length > 0) {
78
- initialDisplayText = activeValue
79
- .map((v) => normalizedItems.find((i) => i[valueKey] === v)?.[labelKey] || v)
80
- .join(", ")
81
- } else if (!multiple && activeValue !== undefined && activeValue !== null && activeValue !== "") {
82
- const found = normalizedItems.find((i) => i[valueKey] === String(activeValue))
83
- initialDisplayText = found ? found[labelKey] : String(activeValue)
84
- }
85
-
86
- const initialValueString = Array.isArray(activeValue) ? JSON.stringify(activeValue) : String(activeValue ?? "")
87
- const searchPlaceholder = typeof searchInput === "object" && searchInput.placeholder ? searchInput.placeholder : "Search..."
88
- const isSearchable = searchInput !== false
31
+ const i = item as {
32
+ label?: string
33
+ value?: any
34
+ disabled?: boolean
35
+ icon?: string
36
+ avatar?: { src?: string; alt?: string; loading?: "lazy" | "eager" }
37
+ }
38
+ return {
39
+ label: String(i.label ?? i.value ?? ""),
40
+ value: String(i.value ?? ""),
41
+ disabled: !!i.disabled,
42
+ icon: i.icon,
43
+ avatar: i.avatar
44
+ }
45
+ })
89
46
  ---
90
47
 
91
- {label && <label class={classes.label} data-slot="label">{label}</label>}
92
-
93
- <rla-select
94
- class="w-full block relative"
95
- data-create-item={typeof createItem === "object" ? JSON.stringify(createItem) : String(createItem)}
96
- data-value-key={valueKey}
97
- data-label-key={labelKey}
98
- data-description-key={descriptionKey}
99
- data-multiple={multiple ? "true" : "false"}
100
- data-filter-fields={JSON.stringify(filterFields)}
101
- data-ignore-filter={ignoreFilter ? "true" : "false"}
102
- data-placeholder={placeholder}
103
- data-items={JSON.stringify(normalizedItems)}
104
- data-selected-icon={selectedIcon}
105
- {...rest}
106
- >
107
- <div class={classes.root} data-slot="root" data-disabled={disabled ? "" : undefined}>
108
- <!-- Hidden input for standard HTML form submission -->
109
- <input
110
- type="hidden"
111
- name={name}
112
- form={form}
113
- value={initialValueString}
114
- required={required}
115
- data-select-hidden-input
116
- />
117
-
118
- <!-- Trigger Button -->
119
- <button
120
- type="button"
121
- class={classes.trigger}
122
- data-slot="trigger"
123
- disabled={disabled}
124
- data-select-trigger
125
- >
126
- <span class="inline-flex items-center gap-2 truncate flex-1 min-w-0 text-left">
127
- {leadingIcon && (
128
- <span class={classes.leading} data-slot="leading">
129
- <RLAIcon name={leadingIcon} class={classes.leadingIcon} />
130
- </span>
131
- )}
132
- {avatar && (
133
- <span class={classes.leading} data-slot="leading">
134
- <RLAAvatar {...(typeof avatar === "string" ? { src: avatar } : avatar)} class={classes.leadingAvatar} />
135
- </span>
136
- )}
137
- <span
138
- class={initialDisplayText === placeholder ? classes.placeholder : classes.value}
139
- data-slot="value"
140
- data-select-value-text
141
- >
142
- {initialDisplayText}
143
- </span>
144
- </span>
145
-
146
- <span class={classes.trailing} data-slot="trailing">
147
- {clear && (initialValueString !== "" && initialValueString !== "[]") && (
148
- <span class={classes.trailingClear} data-select-clear-btn title="Clear selection">
149
- <RLAIcon name={clearIcon} class="size-4" />
150
- </span>
151
- )}
152
- {loading ? (
153
- <RLAIcon name={loadingIcon} class={classes.trailingIcon + " animate-spin"} />
154
- ) : (
155
- <RLAIcon name={trailingIcon} class={classes.trailingIcon} data-select-chevron />
156
- )}
157
- </span>
158
- </button>
159
-
160
- <!-- Dropdown Content Menu -->
161
- <div class={classes.content + " hidden"} data-slot="content" data-select-dropdown>
162
- {isSearchable && (
163
- <div class="p-2 border-b border-default">
164
- <input
165
- type="text"
166
- class={classes.input}
167
- placeholder={searchPlaceholder}
168
- data-select-search-input
169
- autocomplete="off"
170
- />
171
- </div>
172
- )}
173
-
174
- <div class={classes.viewport} data-slot="viewport" data-select-viewport>
175
- {normalizedItems.map((item) => (
176
- <div
177
- class={classes.item}
178
- data-slot="item"
179
- data-value={item[valueKey]}
180
- data-label={item[labelKey]}
181
- data-description={item[descriptionKey] || ""}
182
- data-disabled={item.disabled ? "" : undefined}
183
- data-select-item
184
- >
185
- {item.icon && <RLAIcon name={item.icon} class={classes.itemLeadingIcon} />}
186
- {item.avatar && <RLAAvatar {...(typeof item.avatar === "string" ? { src: item.avatar } : item.avatar)} class={classes.itemLeadingAvatar} />}
187
- {item.chip && <span class={classes.itemLeadingChip} style={`background-color: ${item.chip.color || 'var(--color-primary)'}`} />}
188
-
189
- <div class={classes.itemWrapper}>
190
- <span class={classes.itemLabel}>{item[labelKey]}</span>
191
- {item[descriptionKey] && <span class={classes.itemDescription}>{item[descriptionKey]}</span>}
192
- </div>
193
-
194
- <RLAIcon
195
- name={selectedIcon}
196
- class={
197
- classes.itemTrailingIcon +
198
- (
199
- (multiple && Array.isArray(activeValue) && activeValue.includes(item[valueKey])) ||
200
- (!multiple && String(activeValue) === String(item[valueKey]))
201
- ? ""
202
- : " hidden"
203
- )
204
- }
205
- data-select-check-icon
206
- />
207
- </div>
208
- ))}
209
- </div>
210
-
211
- <div class={classes.empty + " hidden"} data-select-empty>
212
- No results found.
213
- </div>
214
-
215
- <div class={classes.createOption + " hidden"} data-select-create-option>
216
- <RLAIcon name="plus" class="size-4" />
217
- <span>Create "<span data-select-create-text></span>"</span>
218
- </div>
219
- </div>
220
- </div>
221
- </rla-select>
222
-
223
- <script>
224
- class RLASelect extends HTMLElement {
225
- private hiddenInput: HTMLInputElement | null = null;
226
- private triggerBtn: HTMLButtonElement | null = null;
227
- private valueTextSpan: HTMLElement | null = null;
228
- private clearBtn: HTMLElement | null = null;
229
- private dropdown: HTMLElement | null = null;
230
- private searchInputEl: HTMLInputElement | null = null;
231
- private viewport: HTMLElement | null = null;
232
- private emptyEl: HTMLElement | null = null;
233
- private createOptionEl: HTMLElement | null = null;
234
- private createTextEl: HTMLElement | null = null;
235
- private chevronEl: HTMLElement | null = null;
236
-
237
- valueKey = "value";
238
- labelKey = "label";
239
- descriptionKey = "description";
240
- isMultiple = false;
241
- createItemSetting: any = false;
242
- filterFields: string[] = ["label"];
243
- ignoreFilter = false;
244
- placeholder = "Select option...";
245
- selectedIcon = "check";
246
-
247
- private itemsList: any[] = [];
248
- private selectedValues: string[] = [];
249
-
250
- connectedCallback() {
251
- this.valueKey = this.dataset.valueKey || "value";
252
- this.labelKey = this.dataset.labelKey || "label";
253
- this.descriptionKey = this.dataset.descriptionKey || "description";
254
- this.isMultiple = this.dataset.multiple === "true";
255
- this.placeholder = this.dataset.placeholder || "Select option...";
256
- this.selectedIcon = this.dataset.selectedIcon || "check";
257
- this.ignoreFilter = this.dataset.ignoreFilter === "true";
258
-
259
- try {
260
- const createItemAttr = this.dataset.createItem;
261
- if (createItemAttr === "true") this.createItemSetting = true;
262
- else if (createItemAttr === "false") this.createItemSetting = false;
263
- else if (createItemAttr === "always") this.createItemSetting = "always";
264
- else if (createItemAttr) this.createItemSetting = JSON.parse(createItemAttr);
265
- } catch {
266
- this.createItemSetting = Boolean(this.dataset.createItem);
267
- }
268
-
269
- try {
270
- this.filterFields = JSON.parse(this.dataset.filterFields || `["${this.labelKey}"]`);
271
- } catch {
272
- this.filterFields = [this.labelKey];
273
- }
274
-
275
- try {
276
- this.itemsList = JSON.parse(this.dataset.items || "[]");
277
- } catch {
278
- this.itemsList = [];
279
- }
280
-
281
- this.hiddenInput = this.querySelector("[data-select-hidden-input]");
282
- this.triggerBtn = this.querySelector("[data-select-trigger]");
283
- this.valueTextSpan = this.querySelector("[data-select-value-text]");
284
- this.clearBtn = this.querySelector("[data-select-clear-btn]");
285
- this.dropdown = this.querySelector("[data-select-dropdown]");
286
- this.searchInputEl = this.querySelector("[data-select-search-input]");
287
- this.viewport = this.querySelector("[data-select-viewport]");
288
- this.emptyEl = this.querySelector("[data-select-empty]");
289
- this.createOptionEl = this.querySelector("[data-select-create-option]");
290
- this.createTextEl = this.querySelector("[data-select-create-text]");
291
- this.chevronEl = this.querySelector("[data-select-chevron]");
292
-
293
- // Sync initial selected value
294
- const initRaw = this.hiddenInput?.value || "";
295
- if (this.isMultiple) {
296
- try {
297
- this.selectedValues = JSON.parse(initRaw);
298
- if (!Array.isArray(this.selectedValues)) this.selectedValues = [];
299
- } catch {
300
- this.selectedValues = initRaw ? initRaw.split(",") : [];
301
- }
302
- } else {
303
- this.selectedValues = initRaw ? [initRaw] : [];
304
- }
305
-
306
- this.setupEventListeners();
307
- }
308
-
309
- private setupEventListeners() {
310
- // Toggle dropdown on trigger click
311
- this.triggerBtn?.addEventListener("click", (e) => {
312
- e.stopPropagation();
313
- this.toggleDropdown();
314
- });
315
-
316
- // Clear button
317
- this.clearBtn?.addEventListener("click", (e) => {
318
- e.stopPropagation();
319
- this.clearSelection();
320
- });
321
-
322
- // Item selection clicks inside viewport
323
- this.viewport?.addEventListener("click", (e) => {
324
- const itemEl = (e.target as HTMLElement).closest<HTMLElement>("[data-select-item]");
325
- if (itemEl && !itemEl.hasAttribute("data-disabled")) {
326
- const val = itemEl.dataset.value || "";
327
- const lbl = itemEl.dataset.label || val;
328
- this.selectItem(val, lbl);
329
- }
330
- });
331
-
332
- // Search input live filtering
333
- this.searchInputEl?.addEventListener("input", () => {
334
- this.filterItems(this.searchInputEl?.value || "");
335
- });
336
-
337
- // Prevent click inside search input from closing dropdown
338
- this.searchInputEl?.addEventListener("click", (e) => e.stopPropagation());
339
-
340
- // Create option click
341
- this.createOptionEl?.addEventListener("click", (e) => {
342
- e.stopPropagation();
343
- const newText = this.searchInputEl?.value.trim() || "";
344
- if (newText) {
345
- this.createAndSelectItem(newText);
346
- }
347
- });
348
-
349
- // Close dropdown when clicking outside
350
- document.addEventListener("click", (e) => {
351
- if (!this.contains(e.target as Node)) {
352
- this.closeDropdown();
353
- }
354
- });
355
-
356
- // Keyboard navigation (Escape to close)
357
- this.addEventListener("keydown", (e) => {
358
- if (e.key === "Escape") {
359
- this.closeDropdown();
360
- }
361
- });
362
- }
363
-
364
- private toggleDropdown() {
365
- if (this.dropdown?.classList.contains("hidden")) {
366
- this.openDropdown();
367
- } else {
368
- this.closeDropdown();
369
- }
370
- }
371
-
372
- private openDropdown() {
373
- // Close all other open rla-select dropdowns
374
- document.querySelectorAll("rla-select").forEach((el) => {
375
- if (el !== this) {
376
- el.querySelector("[data-select-dropdown]")?.classList.add("hidden");
377
- }
378
- });
379
-
380
- this.dropdown?.classList.remove("hidden");
381
- this.chevronEl?.classList.add("rotate-180");
382
- if (this.searchInputEl) {
383
- this.searchInputEl.value = "";
384
- this.filterItems("");
385
- setTimeout(() => this.searchInputEl?.focus(), 50);
386
- }
387
- }
388
-
389
- private closeDropdown() {
390
- this.dropdown?.classList.add("hidden");
391
- this.chevronEl?.classList.remove("rotate-180");
392
- }
393
-
394
- private selectItem(val: string, _lbl?: string) {
395
- if (this.isMultiple) {
396
- const index = this.selectedValues.indexOf(val);
397
- if (index > -1) {
398
- this.selectedValues.splice(index, 1);
399
- } else {
400
- this.selectedValues.push(val);
401
- }
402
- } else {
403
- this.selectedValues = [val];
404
- this.closeDropdown();
405
- }
406
-
407
- this.updateState();
408
- }
409
-
410
- private createAndSelectItem(newItemText: string) {
411
- const newItem = {
412
- [this.labelKey]: newItemText,
413
- [this.valueKey]: newItemText
414
- };
415
-
416
- // Add to items list if not already present
417
- if (!this.itemsList.some((i) => i[this.valueKey] === newItemText)) {
418
- this.itemsList.push(newItem);
419
-
420
- // Render new item in viewport DOM
421
- if (this.viewport) {
422
- const itemEl = document.createElement("div");
423
- itemEl.className = "group relative w-full flex items-center justify-between select-none outline-none rounded-md px-2 py-1.5 text-sm cursor-pointer transition-colors hover:bg-accent hover:text-accent-foreground";
424
- itemEl.setAttribute("data-slot", "item");
425
- itemEl.setAttribute("data-value", newItemText);
426
- itemEl.setAttribute("data-label", newItemText);
427
- itemEl.setAttribute("data-select-item", "");
428
-
429
- itemEl.innerHTML = `
430
- <div class="flex-1 flex flex-col min-w-0">
431
- <span class="truncate font-normal">${newItemText}</span>
432
- </div>
433
- <svg class="shrink-0 size-4 ms-auto text-primary hidden" data-select-check-icon viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
434
- <polyline points="20 6 9 17 4 12"></polyline>
435
- </svg>
436
- `;
437
- this.viewport.appendChild(itemEl);
438
- }
439
- }
440
-
441
- this.selectItem(newItemText, newItemText);
442
-
443
- // Dispatch custom 'create' event
444
- this.dispatchEvent(new CustomEvent("create", { detail: { item: newItemText }, bubbles: true }));
445
- }
446
-
447
- private clearSelection() {
448
- this.selectedValues = [];
449
- this.updateState();
450
- }
451
-
452
- private filterItems(query: string) {
453
- const q = query.toLowerCase().trim();
454
- let visibleCount = 0;
455
- let exactMatch = false;
456
-
457
- const itemEls = this.viewport?.querySelectorAll<HTMLElement>("[data-select-item]");
458
- itemEls?.forEach((itemEl) => {
459
- const val = itemEl.dataset.value || "";
460
- const lbl = itemEl.dataset.label || "";
461
- const desc = itemEl.dataset.description || "";
462
-
463
- let matches = true;
464
- if (q && !this.ignoreFilter) {
465
- matches =
466
- lbl.toLowerCase().includes(q) ||
467
- val.toLowerCase().includes(q) ||
468
- desc.toLowerCase().includes(q);
469
- }
470
-
471
- if (lbl.toLowerCase() === q || val.toLowerCase() === q) {
472
- exactMatch = true;
473
- }
474
-
475
- if (matches) {
476
- itemEl.classList.remove("hidden");
477
- visibleCount++;
478
- } else {
479
- itemEl.classList.add("hidden");
480
- }
481
- });
482
-
483
- if (this.emptyEl) {
484
- if (visibleCount === 0 && !this.createItemSetting) {
485
- this.emptyEl.classList.remove("hidden");
486
- } else {
487
- this.emptyEl.classList.add("hidden");
488
- }
489
- }
490
-
491
- // Handle create item option visibility
492
- if (this.createOptionEl && this.createTextEl) {
493
- const shouldShowCreate =
494
- Boolean(this.createItemSetting) &&
495
- q.length > 0 &&
496
- (!exactMatch || this.createItemSetting === "always");
497
-
498
- if (shouldShowCreate) {
499
- this.createTextEl.textContent = query.trim();
500
- this.createOptionEl.classList.remove("hidden");
501
- } else {
502
- this.createOptionEl.classList.add("hidden");
503
- }
504
- }
505
- }
506
-
507
- private updateState() {
508
- // 1. Update hidden input
509
- const newValue = this.isMultiple
510
- ? JSON.stringify(this.selectedValues)
511
- : this.selectedValues[0] || "";
512
-
513
- if (this.hiddenInput) {
514
- this.hiddenInput.value = newValue;
515
- this.hiddenInput.dispatchEvent(new Event("change", { bubbles: true }));
516
- this.hiddenInput.dispatchEvent(new Event("input", { bubbles: true }));
517
- }
518
-
519
- // 2. Update trigger display text
520
- if (this.valueTextSpan) {
521
- if (this.selectedValues.length === 0) {
522
- this.valueTextSpan.textContent = this.placeholder;
523
- this.valueTextSpan.className = "truncate text-muted";
524
- } else {
525
- const displayLabels = this.selectedValues.map((v) => {
526
- const found = this.itemsList.find((i) => i[this.valueKey] === v);
527
- return found ? found[this.labelKey] : v;
528
- });
529
- this.valueTextSpan.textContent = displayLabels.join(", ");
530
- this.valueTextSpan.className = "truncate text-highlighted";
531
- }
532
- }
533
-
534
- // 3. Update check icons in viewport
535
- const itemEls = this.viewport?.querySelectorAll<HTMLElement>("[data-select-item]");
536
- itemEls?.forEach((itemEl) => {
537
- const val = itemEl.dataset.value || "";
538
- const checkIcon = itemEl.querySelector("[data-select-check-icon]");
539
- if (this.selectedValues.includes(val)) {
540
- checkIcon?.classList.remove("hidden");
541
- } else {
542
- checkIcon?.classList.add("hidden");
543
- }
544
- });
545
- }
48
+ <div class={classes.root} data-slot="root" data-theme={theme}>
49
+ <select
50
+ class={classes.select}
51
+ name={name}
52
+ disabled={disabled}
53
+ required={required}
54
+ multiple={multiple}
55
+ data-slot="select"
56
+ {...rest}
57
+ >
58
+ {placeholder && !multiple && (
59
+ <option value="" disabled selected={currentValue === undefined}>{placeholder}</option>
60
+ )}
61
+ {normalizedItems.map((item) => (
62
+ <option
63
+ value={item.value}
64
+ disabled={item.disabled}
65
+ selected={currentValues ? currentValues.includes(item.value) : (currentValue !== undefined ? currentValue === item.value : undefined)}
66
+ >
67
+ {item.icon && <RLAIcon name={item.icon} class="rl-select-item-icon" />}
68
+ {item.avatar && <img src={item.avatar.src} alt={item.avatar.alt} class="rl-select-item-icon" />}
69
+ {item.label}
70
+ </option>
71
+ ))}
72
+ </select>
73
+
74
+ <span class={classes.icon} data-slot="icon">
75
+ <RLAIcon name="i-lucide-chevron-down" class="size-4" />
76
+ </span>
77
+ </div>
78
+
79
+ <style is:global>
80
+ select.rl-select,
81
+ select.rl-select::picker(select) { appearance: base-select; }
82
+ select.rl-select::picker-icon { display: none; }
83
+ select.rl-select::picker(select) {
84
+ border: 1px solid var(--rl-neutral-200);
85
+ border-radius: var(--radius-md, 0.375rem);
86
+ background-color: var(--rl-neutral-100);
87
+ color: var(--rl-neutral-950);
88
+ box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.25), 0 4px 6px -4px rgba(0, 0, 0, 0.15);
89
+ padding: 0.25rem;
90
+ margin-top: 0.25rem;
546
91
  }
547
-
548
- if (!customElements.get("rla-select")) {
549
- customElements.define("rla-select", RLASelect);
92
+ select.rl-select option {
93
+ display: flex;
94
+ align-items: center;
95
+ gap: 0.375rem;
96
+ padding: 0.375rem 0.625rem;
97
+ border-radius: calc(var(--radius-md, 0.375rem) - 0.125rem);
98
+ background-color: var(--rl-neutral-100);
99
+ color: var(--rl-neutral-950);
100
+ font-size: 0.875rem;
101
+ cursor: pointer;
102
+ transition: background-color 0.15s ease, color 0.15s ease;
103
+ }
104
+ select.rl-select .rl-select-item-icon {
105
+ width: 1rem;
106
+ height: 1rem;
107
+ flex-shrink: 0;
108
+ }
109
+ select.rl-select option:hover,
110
+ select.rl-select option:focus {
111
+ background-color: var(--rl-neutral-200);
112
+ color: var(--rl-neutral-950);
113
+ }
114
+ select.rl-select option:checked {
115
+ background-color: var(--rl-primary-500);
116
+ color: var(--rl-neutral-50);
117
+ font-weight: 500;
550
118
  }
551
- </script>
119
+ </style>