@rimelight/ui 0.0.46 → 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 (222) hide show
  1. package/dist/icons-DPnFQCts.d.mts +72 -0
  2. package/dist/icons.d.mts +1 -1
  3. package/dist/index.d.mts +5 -6
  4. package/dist/preset.d.mts +1 -1
  5. package/dist/preset.mjs +73 -1
  6. package/dist/resolver.d.mts +4 -5
  7. package/dist/resolver.mjs +2 -4
  8. package/dist/shortcuts.d.mts +10 -11
  9. package/dist/stores.d.mts +3 -4
  10. package/dist/{types-DqAgCNcm.d.mts → types-DWmKMcYC.d.mts} +1 -1
  11. package/dist/types.d.mts +1 -1
  12. package/package.json +8 -6
  13. package/src/components/3d-hover/RLS3dHover.tsx +21 -0
  14. package/src/components/accordion/RLSAccordion.tsx +112 -0
  15. package/src/components/app/RLSApp.tsx +21 -0
  16. package/src/components/aspect-ratio/RLSAspectRatio.tsx +30 -0
  17. package/src/components/audio/RLSAudio.tsx +27 -0
  18. package/src/components/avatar/RLSAvatar.tsx +142 -0
  19. package/src/components/avatar/avatar.ts +22 -21
  20. package/src/components/avatar-group/RLSAvatarGroup.tsx +163 -0
  21. package/src/components/badge/RLSBadge.tsx +1 -1
  22. package/src/components/badge/RLVBadge.vue +5 -3
  23. package/src/components/banner/RLABanner.astro +1 -1
  24. package/src/components/banner/RLSBanner.tsx +151 -0
  25. package/src/components/breadcrumb/RLSBreadcrumb.tsx +194 -0
  26. package/src/components/browser-mockup/RLSBrowserMockup.tsx +21 -0
  27. package/src/components/button/RLSButton.tsx +53 -38
  28. package/src/components/button/RLVButton.vue +44 -32
  29. package/src/components/calendar/RLSCalendar.tsx +26 -0
  30. package/src/components/callout/RLSCallout.tsx +151 -0
  31. package/src/components/card/RLSCard.tsx +4 -0
  32. package/src/components/card/RLVCard.vue +4 -0
  33. package/src/components/card/card.ts +10 -0
  34. package/src/components/carousel/RLACarousel.astro +14 -1
  35. package/src/components/carousel/RLSCarousel.tsx +260 -0
  36. package/src/components/chart/RLSChart.tsx +299 -0
  37. package/src/components/chat/RLSChat.tsx +27 -0
  38. package/src/components/chat-message/RLSChatMessage.tsx +27 -0
  39. package/src/components/chat-messages/RLSChatMessages.tsx +27 -0
  40. package/src/components/chat-palette/RLSChatPalette.tsx +27 -0
  41. package/src/components/chat-prompt/RLSChatPrompt.tsx +27 -0
  42. package/src/components/chat-prompt-submit/RLSChatPromptSubmit.tsx +27 -0
  43. package/src/components/chat-reasoning/RLSChatReasoning.tsx +27 -0
  44. package/src/components/chat-shimmer/RLSChatShimmer.tsx +27 -0
  45. package/src/components/chat-tool/RLSChatTool.tsx +27 -0
  46. package/src/components/checkbox/RLACheckbox.astro +1 -0
  47. package/src/components/checkbox/RLSCheckbox.tsx +1 -1
  48. package/src/components/checkbox/checkbox.theme.ts +2 -2
  49. package/src/components/chip/RLSChip.tsx +65 -0
  50. package/src/components/collapsible/RLSCollapsible.tsx +89 -0
  51. package/src/components/color-area/RLSColorArea.tsx +554 -0
  52. package/src/components/color-field/RLSColorField.tsx +236 -0
  53. package/src/components/color-picker/RLSColorPicker.tsx +26 -0
  54. package/src/components/color-slider/RLSColorSlider.tsx +373 -0
  55. package/src/components/command-palette/RLSCommandPalette.tsx +26 -0
  56. package/src/components/compare/RLSCompare.tsx +26 -0
  57. package/src/components/confirm/RLSConfirm.tsx +173 -0
  58. package/src/components/container/RLSContainer.tsx +24 -0
  59. package/src/components/context-menu/RLSContextMenu.tsx +26 -0
  60. package/src/components/copy-markdown/RLSCopyMarkdown.tsx +57 -0
  61. package/src/components/dashboard-group/RLSDashboardGroup.tsx +29 -0
  62. package/src/components/dashboard-navbar/RLSDashboardNavbar.tsx +116 -0
  63. package/src/components/dashboard-panel/RLSDashboardPanel.tsx +84 -0
  64. package/src/components/dashboard-resize-handle/RLSDashboardResizeHandle.tsx +35 -0
  65. package/src/components/dashboard-search/RLADashboardSearch.astro +1 -1
  66. package/src/components/dashboard-search/RLSDashboardSearch.tsx +125 -0
  67. package/src/components/dashboard-search-button/RLSDashboardSearchButton.tsx +82 -0
  68. package/src/components/dashboard-sidebar/RLADashboardSidebar.astro +4 -4
  69. package/src/components/dashboard-sidebar/RLSDashboardSidebar.tsx +184 -0
  70. package/src/components/dashboard-sidebar-collapse/RLSDashboardSidebarCollapse.tsx +46 -0
  71. package/src/components/dashboard-sidebar-toggle/RLSDashboardSidebarToggle.tsx +35 -0
  72. package/src/components/dashboard-toolbar/RLSDashboardToolbar.tsx +49 -0
  73. package/src/components/date/RLSDate.tsx +34 -0
  74. package/src/components/dock/RLSDock.tsx +26 -0
  75. package/src/components/drawer/RLSDrawer.tsx +177 -0
  76. package/src/components/dropdown-menu/RLADropdownMenu.astro +250 -241
  77. package/src/components/dropdown-menu/RLSDropdownMenu.tsx +183 -86
  78. package/src/components/editor/RLSEditor.tsx +26 -0
  79. package/src/components/empty/RLSEmpty.tsx +132 -0
  80. package/src/components/error/RLSError.tsx +26 -0
  81. package/src/components/field-group/RLSFieldGroup.tsx +46 -0
  82. package/src/components/fieldset/RLSFieldset.tsx +26 -0
  83. package/src/components/file-upload/RLAFileUpload.astro +1 -1
  84. package/src/components/file-upload/RLSFileUpload.tsx +163 -0
  85. package/src/components/floating-action/RLSFloatingAction.tsx +26 -0
  86. package/src/components/footer/RLSFooter.tsx +56 -0
  87. package/src/components/form/RLSForm.tsx +34 -0
  88. package/src/components/gallery/RLSGallery.tsx +21 -0
  89. package/src/components/gamepad/RLSGamepad.tsx +269 -0
  90. package/src/components/grid/RLSGrid.tsx +29 -0
  91. package/src/components/head/RLAHead.astro +9 -0
  92. package/src/components/head/RLSHead.tsx +42 -0
  93. package/src/components/head/UIHead.astro +3 -0
  94. package/src/components/header/HeaderLayer.astro +1 -1
  95. package/src/components/header/RLSHeader.tsx +87 -0
  96. package/src/components/hover-card/RLSHoverCard.tsx +72 -0
  97. package/src/components/icon/RLSIcon.tsx +3 -1
  98. package/src/components/image/RLAImage.astro +447 -138
  99. package/src/components/image/RLSImage.tsx +545 -0
  100. package/src/components/image/image.theme.ts +19 -3
  101. package/src/components/image/image.ts +31 -1
  102. package/src/components/input/RLAInput.astro +62 -51
  103. package/src/components/input/RLSInput.tsx +1 -1
  104. package/src/components/input-date/RLSInputDate.tsx +92 -0
  105. package/src/components/input-menu/RLAInputMenu.astro +3 -3
  106. package/src/components/input-menu/RLSInputMenu.tsx +199 -0
  107. package/src/components/input-number/RLSInputNumber.tsx +184 -0
  108. package/src/components/input-pin/RLSInputPin.tsx +185 -0
  109. package/src/components/input-rating/RLAInputRating.astro +8 -8
  110. package/src/components/input-rating/RLSInputRating.tsx +198 -0
  111. package/src/components/input-rating/input-rating.ts +4 -0
  112. package/src/components/input-tags/RLSInputTags.tsx +94 -0
  113. package/src/components/input-time/RLSInputTime.tsx +92 -0
  114. package/src/components/kbd/RLSKbd.tsx +37 -0
  115. package/src/components/keyboard/RLSKeyboard.tsx +476 -0
  116. package/src/components/knob/RLSKnob.tsx +27 -0
  117. package/src/components/label/RLSLabel.tsx +27 -0
  118. package/src/components/layout-grid/RLALayoutGrid.astro +29 -29
  119. package/src/components/layout-grid/RLSLayoutGrid.tsx +402 -0
  120. package/src/components/layout-grid/layout-grid.ts +25 -1
  121. package/src/components/link/RLALink.astro +5 -5
  122. package/src/components/link/RLSLink.tsx +6 -5
  123. package/src/components/link/link.ts +21 -12
  124. package/src/components/link-group/RLSLinkGroup.tsx +76 -0
  125. package/src/components/listbox/RLSListbox.tsx +27 -0
  126. package/src/components/locale-selector/RLALocaleSelector.astro +110 -102
  127. package/src/components/locale-selector/RLSLocaleSelector.tsx +178 -0
  128. package/src/components/locale-selector/locale-selector.ts +2 -7
  129. package/src/components/logo/RLALogo.astro +153 -102
  130. package/src/components/logo/RLSLogo.tsx +129 -0
  131. package/src/components/main/RLSMain.tsx +40 -0
  132. package/src/components/marquee/RLSMarquee.tsx +80 -0
  133. package/src/components/meter/RLSMeter.tsx +27 -0
  134. package/src/components/modal/RLAModal.astro +1 -1
  135. package/src/components/modal/RLSModal.tsx +109 -72
  136. package/src/components/navigation-menu/RLANavigationMenu.astro +6 -6
  137. package/src/components/navigation-menu/RLSNavigationMenu.tsx +228 -0
  138. package/src/components/navigation-menu/navigation-menu.ts +2 -2
  139. package/src/components/package-managers/RLSPackageManagers.tsx +204 -0
  140. package/src/components/page/RLSPage.tsx +21 -0
  141. package/src/components/page-aside/RLSPageAside.tsx +21 -0
  142. package/src/components/page-body/RLSPageBody.tsx +21 -0
  143. package/src/components/page-header/RLSPageHeader.tsx +21 -0
  144. package/src/components/page-section/RLSPageSection.tsx +201 -0
  145. package/src/components/pagination/RLSPagination.tsx +114 -0
  146. package/src/components/password/RLSPassword.tsx +27 -0
  147. package/src/components/phone-mockup/RLSPhoneMockup.tsx +27 -0
  148. package/src/components/placeholder/RLSPlaceholder.tsx +47 -0
  149. package/src/components/popover/RLAPopover.astro +2 -2
  150. package/src/components/popover/RLSPopover.tsx +124 -0
  151. package/src/components/post/RLSPost.tsx +134 -0
  152. package/src/components/pricing-plan/RLSPricingPlan.tsx +26 -0
  153. package/src/components/pricing-table/RLSPricingTable.tsx +26 -0
  154. package/src/components/progress/RLSProgress.tsx +53 -0
  155. package/src/components/prose/RLSProse.tsx +26 -0
  156. package/src/components/qr-code/RLSQrCode.tsx +26 -0
  157. package/src/components/quote/RLAQuote.astro +10 -6
  158. package/src/components/quote/RLSQuote.tsx +37 -0
  159. package/src/components/quote/quote.theme.ts +5 -2
  160. package/src/components/radio-group/RLSRadioGroup.tsx +37 -0
  161. package/src/components/rating/RLSRating.tsx +27 -0
  162. package/src/components/scroll-area/RLAScrollArea.astro +1 -1
  163. package/src/components/scroll-area/RLSScrollArea.tsx +210 -0
  164. package/src/components/scroll-to-top/RLAScrollToTop.astro +5 -4
  165. package/src/components/scroll-to-top/RLSScrollToTop.tsx +145 -0
  166. package/src/components/search/RLASearch.astro +6 -6
  167. package/src/components/search/RLSSearch.tsx +237 -0
  168. package/src/components/select/RLASelect.astro +67 -4
  169. package/src/components/select/RLSSelect.tsx +107 -39
  170. package/src/components/select/RLVSelect.vue +50 -1
  171. package/src/components/select/select.theme.ts +30 -10
  172. package/src/components/select/select.ts +20 -1
  173. package/src/components/separator/RLSSeparator.tsx +92 -0
  174. package/src/components/share/RLSShare.tsx +66 -0
  175. package/src/components/shortcuts/RLAShortcuts.astro +2 -2
  176. package/src/components/shortcuts/RLSShortcuts.tsx +393 -0
  177. package/src/components/sidebar/RLASidebar.astro +1 -1
  178. package/src/components/sidebar/RLSSidebar.tsx +271 -0
  179. package/src/components/skeleton/RLSSkeleton.tsx +21 -0
  180. package/src/components/slideover/RLASlideover.astro +1 -1
  181. package/src/components/slideover/RLSSlideover.tsx +159 -0
  182. package/src/components/slider/RLSSlider.tsx +112 -0
  183. package/src/components/speed-dial/RLSSpeedDial.tsx +27 -0
  184. package/src/components/spinner/RLSSpinner.tsx +22 -0
  185. package/src/components/splitter/RLASplitter.astro +1 -1
  186. package/src/components/splitter/RLSSplitter.tsx +178 -0
  187. package/src/components/spoiler/RLSSpoiler.tsx +26 -0
  188. package/src/components/stepper/RLAStepper.astro +9 -9
  189. package/src/components/stepper/RLSStepper.tsx +205 -0
  190. package/src/components/steps/RLSSteps.tsx +108 -0
  191. package/src/components/sticky-surface/RLSStickySurface.tsx +63 -0
  192. package/src/components/surround/RLSSurround.tsx +57 -0
  193. package/src/components/switch/RLSSwitch.tsx +56 -0
  194. package/src/components/table/RLATable.astro +1044 -579
  195. package/src/components/table/RLSTable.tsx +893 -0
  196. package/src/components/table/table.theme.ts +47 -16
  197. package/src/components/table/table.ts +104 -66
  198. package/src/components/table-of-contents/RLSTableOfContents.tsx +196 -0
  199. package/src/components/tabs/RLATabs.astro +8 -8
  200. package/src/components/tabs/RLSTabs.tsx +298 -0
  201. package/src/components/textarea/RLATextarea.astro +1 -0
  202. package/src/components/textarea/RLSTextarea.tsx +3 -1
  203. package/src/components/theme-selector/RLAThemeSelector.astro +85 -56
  204. package/src/components/theme-selector/RLSThemeSelector.tsx +131 -0
  205. package/src/components/theme-selector/theme-selector.theme.ts +5 -7
  206. package/src/components/theme-selector/theme-selector.ts +11 -1
  207. package/src/components/timeline/RLSTimeline.tsx +171 -0
  208. package/src/components/toast/RLAToast.astro +2 -2
  209. package/src/components/toast/RLSToast.tsx +179 -0
  210. package/src/components/toaster/RLSToaster.tsx +82 -0
  211. package/src/components/tooltip/RLSTooltip.tsx +48 -0
  212. package/src/components/tour/RLSTour.tsx +26 -0
  213. package/src/components/tree/RLSTree.tsx +26 -0
  214. package/src/components/user/RLSUser.tsx +26 -0
  215. package/src/components/video/RLAVideo.astro +4 -1
  216. package/src/components/video/RLSVideo.tsx +158 -0
  217. package/src/components/watermark/RLSWatermark.tsx +27 -0
  218. package/src/components/window-mockup/RLSWindowMockup.tsx +27 -0
  219. package/src/preset.ts +73 -1
  220. package/src/resolver.ts +6 -1
  221. package/src/virtual.d.ts +6 -0
  222. package/dist/icons--RzV1RTU.d.mts +0 -72
@@ -1,579 +1,1044 @@
1
- ---
2
- import type { TableProps, TableColumn, TableRow } from "./table"
3
- import { tableTheme } from "./table.theme"
4
- import RLAIcon from "../icon/RLAIcon.astro"
5
- import RLAInput from "../input/RLAInput.astro"
6
- import RLADropdownMenu from "../dropdown-menu/RLADropdownMenu.astro"
7
-
8
- const {
9
- data = [],
10
- columns = [],
11
- headers = [],
12
- rows = [],
13
- caption,
14
- meta,
15
- sticky = false,
16
- loading = false,
17
- empty = "No data available",
18
- virtualize = false,
19
- globalFilter = "",
20
- columnFilters = [],
21
- columnVisibility = {},
22
- columnPinning = {},
23
- sorting = [],
24
- rowPinning = {},
25
- rowSelection = {},
26
- expanded = {},
27
- pagination,
28
- searchable = false,
29
- searchPlaceholder = "Filter table...",
30
- showColumnsToggle = false,
31
- getSubRows,
32
- getRowId,
33
- class: className,
34
- ...rest
35
- } = Astro.props as TableProps
36
-
37
- const classes = tableTheme({
38
- ...Astro.props,
39
- sticky: typeof sticky === "boolean" ? sticky : sticky,
40
- externalScroll: Boolean(typeof virtualize === "object" && virtualize?.getScrollElement)
41
- })
42
-
43
- // Normalize columns from props.columns or props.headers or infer from data keys
44
- let resolvedColumns: TableColumn[] = []
45
-
46
- if (columns && columns.length > 0) {
47
- resolvedColumns = columns
48
- } else if (headers && headers.length > 0) {
49
- resolvedColumns = headers.map((h, i) => {
50
- if (typeof h === "string") {
51
- return { id: h, accessorKey: h, header: h, enableSorting: true }
52
- }
53
- return { id: h.key || String(i), accessorKey: h.key, header: h.label, enableSorting: true, meta: { class: { th: h.class || "", td: h.class || "" } } }
54
- }) as TableColumn[]
55
- } else if (data && data.length > 0 && typeof data[0] === "object" && data[0] !== null) {
56
- const firstRow = data[0] as Record<string, any>
57
- resolvedColumns = Object.keys(firstRow).map((key) => ({
58
- id: key,
59
- accessorKey: key,
60
- header: key.charAt(0).toUpperCase() + key.slice(1),
61
- enableSorting: true
62
- }))
63
- }
64
-
65
- // Filter visible columns based on columnVisibility map
66
- const visibleColumns = resolvedColumns.filter((col) => {
67
- const id = col.id || (col.accessorKey as string)
68
- if (!id) return true
69
- if (col.enableHiding === false) return true
70
- return columnVisibility[id] !== false
71
- })
72
-
73
- // Columns dropdown items for RLADropdownMenu
74
- const columnDropdownItems = resolvedColumns
75
- .filter((col) => {
76
- const colId = (col.id || col.accessorKey || "") as string
77
- return colId && colId !== "actions" && colId !== "select"
78
- })
79
- .map((col) => {
80
- const colId = (col.id || col.accessorKey || "") as string
81
- const label = typeof col.header === "string" ? col.header : colId
82
- return {
83
- id: colId,
84
- label: label.charAt(0).toUpperCase() + label.slice(1),
85
- type: "checkbox" as const,
86
- checked: columnVisibility[colId] !== false
87
- }
88
- })
89
-
90
- // Build row objects from data
91
- function buildRowTree(rawItems: any[], parentDepth = 0): TableRow[] {
92
- return rawItems.map((item, idx) => {
93
- const rId = getRowId ? getRowId(item, idx) : (item?.id !== undefined ? String(item.id) : String(idx))
94
- const isSelected = !!rowSelection[rId] || !!rowSelection[idx]
95
- const isExpanded = !!expanded[rId] || !!expanded[idx]
96
-
97
- let subRowsList: TableRow[] | undefined = undefined
98
- if (getSubRows) {
99
- const children = getSubRows(item, idx)
100
- if (children && children.length > 0) {
101
- subRowsList = buildRowTree(children, parentDepth + 1)
102
- }
103
- } else if (Array.isArray(item?.children)) {
104
- subRowsList = buildRowTree(item.children, parentDepth + 1)
105
- }
106
-
107
- const rowObj: TableRow = {
108
- id: rId,
109
- original: item,
110
- index: idx,
111
- depth: parentDepth,
112
- getValue: (key: string) => {
113
- if (!item || typeof item !== "object") return undefined
114
- if (key in item) return item[key]
115
- return undefined
116
- },
117
- getIsSelected: () => isSelected,
118
- toggleSelected: () => {},
119
- getIsExpanded: () => isExpanded,
120
- toggleExpanded: () => {},
121
- getIsPinned: () => {
122
- if (rowPinning.top?.includes(rId)) return "top"
123
- if (rowPinning.bottom?.includes(rId)) return "bottom"
124
- return false
125
- },
126
- pin: () => {},
127
- getIsGrouped: () => false,
128
- getCanExpand: () => !!(subRowsList && subRowsList.length > 0),
129
- ...(subRowsList ? { subRows: subRowsList } : {})
130
- }
131
-
132
- return rowObj
133
- })
134
- }
135
-
136
- let processedRows: TableRow[] = buildRowTree(data)
137
-
138
- // Global & column filtering
139
- if (globalFilter) {
140
- const query = globalFilter.toLowerCase()
141
- processedRows = processedRows.filter((r) => {
142
- if (typeof r.original !== "object" || !r.original) return String(r.original).toLowerCase().includes(query)
143
- return Object.values(r.original).some((val) => String(val ?? "").toLowerCase().includes(query))
144
- })
145
- }
146
-
147
- if (columnFilters && columnFilters.length > 0) {
148
- for (const filter of columnFilters) {
149
- if (!filter.id || filter.value === undefined || filter.value === null || filter.value === "") continue
150
- const fVal = String(filter.value).toLowerCase()
151
- processedRows = processedRows.filter((r) => {
152
- const val = String(r.getValue(filter.id) ?? "").toLowerCase()
153
- return val.includes(fVal)
154
- })
155
- }
156
- }
157
-
158
- // Sorting logic
159
- if (sorting && sorting.length > 0) {
160
- const activeSort = sorting[0]
161
- if (activeSort && activeSort.id) {
162
- const key = activeSort.id
163
- const isDesc = activeSort.desc
164
- processedRows = [...processedRows].toSorted((a, b) => {
165
- const valA = a.getValue(key)
166
- const valB = b.getValue(key)
167
- if (valA === valB) return 0
168
- if (valA === undefined || valA === null) return 1
169
- if (valB === undefined || valB === null) return -1
170
- if (typeof valA === "number" && typeof valB === "number") {
171
- return isDesc ? valB - valA : valA - valB
172
- }
173
- return isDesc
174
- ? String(valB).localeCompare(String(valA))
175
- : String(valA).localeCompare(String(valB))
176
- })
177
- }
178
- }
179
-
180
- // Row Pinning logic
181
- if (rowPinning.top && rowPinning.top.length > 0) {
182
- const pinnedTop = processedRows.filter((r) => rowPinning.top?.includes(r.id))
183
- const unpinned = processedRows.filter((r) => !rowPinning.top?.includes(r.id) && !rowPinning.bottom?.includes(r.id))
184
- const pinnedBottom = processedRows.filter((r) => rowPinning.bottom?.includes(r.id))
185
- processedRows = [...pinnedTop, ...unpinned, ...pinnedBottom]
186
- }
187
-
188
- // Pagination slicing
189
- let paginatedRows = processedRows
190
- let currentPage = 1
191
- let pageSize = processedRows.length
192
-
193
- if (pagination) {
194
- pageSize = pagination.pageSize || 10
195
- currentPage = (pagination.pageIndex || 0) + 1
196
- const start = (currentPage - 1) * pageSize
197
- paginatedRows = processedRows.slice(start, start + pageSize)
198
- }
199
-
200
- const hasFooters = visibleColumns.some((col) => col.footer || Astro.slots.has(`${String(col.id || col.accessorKey)}-footer`))
201
- const uid = Math.random().toString(36).substring(2, 9)
202
- const showToolbar = searchable || showColumnsToggle || Astro.slots.has("toolbar")
203
- ---
204
-
205
- <rla-table class:list={[classes.root, className]} data-slot="root" data-table-id={uid} {...rest}>
206
- {showToolbar && (
207
- <div class="flex items-center justify-between gap-3 p-3.5 border-b border-default bg-muted/20 overflow-x-auto" data-slot="toolbar">
208
- {searchable && (
209
- <div class="relative max-w-xs flex-1">
210
- <RLAInput
211
- type="search"
212
- placeholder={searchPlaceholder}
213
- leadingIcon="i-lucide-search"
214
- class="w-full"
215
- data-rla-table-search
216
- />
217
- </div>
218
- )}
219
-
220
- {Astro.slots.has("toolbar") && <slot name="toolbar" />}
221
-
222
- {showColumnsToggle && (
223
- <div class="ml-auto shrink-0">
224
- <RLADropdownMenu
225
- label="Columns"
226
- icon="i-lucide-columns"
227
- items={columnDropdownItems as any}
228
- content={{ align: "end" }}
229
- size="sm"
230
- color="neutral"
231
- />
232
- </div>
233
- )}
234
- </div>
235
- )}
236
-
237
- <div class="overflow-x-auto w-full">
238
- <table class={classes.base} data-slot="base">
239
- {(caption || Astro.slots.has("caption")) && (
240
- <caption class={classes.caption} data-slot="caption">
241
- <slot name="caption">{caption}</slot>
242
- </caption>
243
- )}
244
-
245
- {visibleColumns.length > 0 && (
246
- <thead class={classes.thead} data-slot="thead">
247
- <tr class={classes.tr} data-slot="tr">
248
- {visibleColumns.map((col) => {
249
- const colId = (col.id || col.accessorKey || "") as string
250
- const isPinned = columnPinning.left?.includes(colId) ? "left" : columnPinning.right?.includes(colId) ? "right" : false
251
- const headerSlotName = `${colId}-header`
252
- const metaClass = typeof col.meta?.class?.th === "function" ? col.meta.class.th(col) : (col.meta?.class?.th || "")
253
- const styleObj = typeof col.meta?.style?.th === "function" ? col.meta.style.th(col) : col.meta?.style?.th
254
- const canSort = col.enableSorting !== false && (!!col.accessorKey || !!col.id) && colId !== "actions" && colId !== "select"
255
- const activeSort = sorting.find((s) => s.id === colId)
256
- const sortDir = activeSort ? (activeSort.desc ? "desc" : "asc") : "none"
257
-
258
- return (
259
- <th
260
- scope="col"
261
- class:list={[
262
- classes.th,
263
- isPinned && (classes as any).pinned?.th,
264
- metaClass
265
- ]}
266
- style={col.size ? `width: ${typeof col.size === 'number' ? `${col.size}px` : col.size}; ${styleObj || ''}` : styleObj}
267
- data-col-id={colId}
268
- data-slot="th"
269
- >
270
- {Astro.slots.has(headerSlotName) ? (
271
- <slot name={headerSlotName} {...({ column: col } as any)} />
272
- ) : canSort ? (
273
- <button
274
- type="button"
275
- class="inline-flex items-center gap-1.5 hover:text-highlighted focus:outline-none transition-colors cursor-pointer group -mx-1 px-1 py-0.5 rounded"
276
- data-rla-sort-btn
277
- data-rla-col-id={colId}
278
- data-sort-dir={sortDir}
279
- >
280
- <span>
281
- {typeof col.header === "function" ? col.header({ column: col, table: {} }) : (col.header || colId)}
282
- </span>
283
- <span
284
- data-sort-icon
285
- class:list={[
286
- sortDir === "asc"
287
- ? "i-lucide-arrow-up-narrow-wide text-primary font-bold"
288
- : sortDir === "desc"
289
- ? "i-lucide-arrow-down-wide-narrow text-primary font-bold"
290
- : "i-lucide-arrow-up-down opacity-40 group-hover:opacity-100",
291
- "size-3.5 transition-all"
292
- ]}
293
- />
294
- </button>
295
- ) : typeof col.header === "function" ? (
296
- col.header({ column: col, table: {} })
297
- ) : (
298
- col.header || colId
299
- )}
300
- </th>
301
- )
302
- })}
303
- </tr>
304
- </thead>
305
- )}
306
-
307
- <tbody class={classes.tbody} data-slot="tbody">
308
- {Astro.slots.has("body-top") && <slot name="body-top" />}
309
-
310
- {loading ? (
311
- <tr>
312
- <td colspan={Math.max(1, visibleColumns.length)} class={classes.loading} data-slot="loading">
313
- {Astro.slots.has("loading") ? (
314
- <slot name="loading" />
315
- ) : (
316
- <div class="flex items-center justify-center gap-2 text-muted py-4">
317
- <RLAIcon name="i-lucide-loader-2" class="size-5 animate-spin text-primary" />
318
- <span>Loading table data...</span>
319
- </div>
320
- )}
321
- </td>
322
- </tr>
323
- ) : paginatedRows.length === 0 && (data.length > 0 || visibleColumns.length > 0) ? (
324
- <tr>
325
- <td colspan={Math.max(1, visibleColumns.length)} class={classes.empty} data-slot="empty">
326
- {Astro.slots.has("empty") ? (
327
- <slot name="empty" />
328
- ) : (
329
- <div class="flex flex-col items-center justify-center py-6 gap-2 text-muted">
330
- <RLAIcon name="i-lucide-inbox" class="size-8 opacity-50" />
331
- <p class="text-sm font-medium">{empty}</p>
332
- </div>
333
- )}
334
- </td>
335
- </tr>
336
- ) : (
337
- <slot>
338
- {/* Render data rows if data prop provided */}
339
- {data.length > 0 && paginatedRows.map((row) => {
340
- const trMetaClass = typeof meta?.class?.tr === "function" ? meta.class.tr(row) : (meta?.class?.tr || "")
341
- const isSelected = row.getIsSelected()
342
- const isExpanded = row.getIsExpanded()
343
-
344
- return (
345
- <>
346
- <tr
347
- class:list={[classes.tr, trMetaClass]}
348
- data-slot="tr"
349
- data-row-id={row.id}
350
- data-selected={isSelected ? "true" : undefined}
351
- data-expanded={isExpanded ? "true" : undefined}
352
- >
353
- {visibleColumns.map((col) => {
354
- const colId = (col.id || col.accessorKey || "") as string
355
- const cellValue = col.accessorKey ? row.getValue(col.accessorKey as string) : undefined
356
- const cellSlotName = `${colId}-cell`
357
- const metaTdClass = typeof col.meta?.class?.td === "function" ? col.meta.class.td(row) : (col.meta?.class?.td || "")
358
- const colSpanVal = typeof col.meta?.colspan?.td === "function" ? col.meta.colspan.td(row) : col.meta?.colspan?.td
359
- const rowSpanVal = typeof col.meta?.rowspan?.td === "function" ? col.meta.rowspan.td(row) : col.meta?.rowspan?.td
360
-
361
- return (
362
- <td
363
- class:list={[classes.td, metaTdClass]}
364
- colspan={colSpanVal}
365
- rowspan={rowSpanVal}
366
- data-slot="td"
367
- data-col-id={colId}
368
- >
369
- {Astro.slots.has(cellSlotName) ? (
370
- <slot
371
- name={cellSlotName}
372
- {...({
373
- row,
374
- column: col,
375
- cell: { value: cellValue },
376
- getValue: () => cellValue,
377
- renderValue: () => cellValue
378
- } as any)}
379
- />
380
- ) : typeof col.cell === "function" ? (
381
- col.cell({
382
- row,
383
- getValue: () => cellValue,
384
- renderValue: () => cellValue,
385
- column: col,
386
- table: {}
387
- })
388
- ) : cellValue !== undefined && cellValue !== null ? (
389
- String(cellValue)
390
- ) : (
391
- ""
392
- )}
393
- </td>
394
- )
395
- })}
396
- </tr>
397
-
398
- {isExpanded && Astro.slots.has("expanded") && (
399
- <tr class="bg-muted/20 border-b border-default" data-slot="expanded-tr">
400
- <td colspan={visibleColumns.length} class="p-4">
401
- <slot name="expanded" {...({ row } as any)} />
402
- </td>
403
- </tr>
404
- )}
405
- </>
406
- )
407
- })}
408
-
409
- {/* Render legacy rows array if rows prop provided */}
410
- {data.length === 0 && rows.length > 0 && rows.map((row) => {
411
- const cells = Array.isArray(row) ? row : Object.values(row)
412
- return (
413
- <tr class={classes.tr} data-slot="tr">
414
- {cells.map((cell) => (
415
- <td class={classes.td} data-slot="td">
416
- {cell}
417
- </td>
418
- ))}
419
- </tr>
420
- )
421
- })}
422
- </slot>
423
- )}
424
-
425
- {Astro.slots.has("body-bottom") && <slot name="body-bottom" />}
426
- </tbody>
427
-
428
- {hasFooters && (
429
- <tfoot class={classes.tfoot} data-slot="tfoot">
430
- <tr class={classes.tr} data-slot="tr">
431
- {visibleColumns.map((col) => {
432
- const colId = (col.id || col.accessorKey || "") as string
433
- const footerSlotName = `${colId}-footer`
434
- return (
435
- <td class={classes.td} data-slot="td">
436
- {Astro.slots.has(footerSlotName) ? (
437
- <slot name={footerSlotName} {...({ column: col } as any)} />
438
- ) : typeof col.footer === "function" ? (
439
- col.footer({ column: col, table: {} })
440
- ) : (
441
- col.footer || ""
442
- )}
443
- </td>
444
- )
445
- })}
446
- </tr>
447
- </tfoot>
448
- )}
449
- </table>
450
- </div>
451
- </rla-table>
452
-
453
- <script>
454
- if (typeof HTMLElement !== "undefined") {
455
- class RLATableElement extends HTMLElement {
456
- connectedCallback() {
457
- const table = this.querySelector("table");
458
- if (!table) return;
459
-
460
- const tbody = table.querySelector("tbody");
461
- if (!tbody) return;
462
-
463
- // Interactive Live Search filter
464
- const searchInput = this.querySelector("[data-rla-table-search]") as HTMLInputElement | null;
465
- if (searchInput) {
466
- searchInput.addEventListener("input", () => {
467
- const query = searchInput.value.toLowerCase().trim();
468
- const rows = tbody.querySelectorAll("tr[data-slot=tr]");
469
- rows.forEach((row) => {
470
- const text = row.textContent?.toLowerCase() || "";
471
- if (!query || text.includes(query)) {
472
- (row as HTMLElement).style.display = "";
473
- } else {
474
- (row as HTMLElement).style.display = "none";
475
- }
476
- });
477
- });
478
- }
479
-
480
- // Column Visibility Toggles via RLADropdownMenu
481
- this.addEventListener("rla-dropdown-checkbox-change", (e: any) => {
482
- const colId = e.detail?.id;
483
- const checked = e.detail?.checked;
484
- if (!colId) return;
485
-
486
- const th = table.querySelector(`th[data-col-id="${colId}"]`) as HTMLElement | null;
487
- if (th) {
488
- th.style.display = checked ? "" : "none";
489
- const colIndex = Array.from(th.parentElement?.children || []).indexOf(th);
490
- if (colIndex >= 0) {
491
- const rows = tbody.querySelectorAll("tr");
492
- rows.forEach((r) => {
493
- const cell = r.children[colIndex] as HTMLElement | undefined;
494
- if (cell) cell.style.display = checked ? "" : "none";
495
- });
496
- }
497
- }
498
-
499
- const tds = table.querySelectorAll(`td[data-col-id="${colId}"]`);
500
- tds.forEach((td) => ((td as HTMLElement).style.display = checked ? "" : "none"));
501
- });
502
-
503
-
504
-
505
- // Interactive Column Sorting
506
- const sortButtons = table.querySelectorAll("[data-rla-sort-btn]");
507
- sortButtons.forEach((btnNode) => {
508
- const btn = btnNode as HTMLButtonElement;
509
- btn.addEventListener("click", () => {
510
- const colId = btn.getAttribute("data-rla-col-id");
511
- if (!colId) return;
512
-
513
- const currentDir = btn.getAttribute("data-sort-dir") || "none";
514
- const nextDir = currentDir === "asc" ? "desc" : "asc";
515
-
516
- // Reset all other sort buttons
517
- sortButtons.forEach((otherBtn) => {
518
- if (otherBtn !== btn) {
519
- otherBtn.setAttribute("data-sort-dir", "none");
520
- const icon = otherBtn.querySelector("[data-sort-icon]");
521
- if (icon) icon.className = "i-lucide-arrow-up-down size-3.5 opacity-40 group-hover:opacity-100 transition-all";
522
- }
523
- });
524
-
525
- btn.setAttribute("data-sort-dir", nextDir);
526
- const icon = btn.querySelector("[data-sort-icon]");
527
- if (icon) {
528
- icon.className = nextDir === "asc"
529
- ? "i-lucide-arrow-up-narrow-wide size-3.5 text-primary font-bold transition-all"
530
- : "i-lucide-arrow-down-wide-narrow size-3.5 text-primary font-bold transition-all";
531
- }
532
-
533
- // Sort rows in DOM
534
- const rows = Array.from(tbody.querySelectorAll("tr[data-slot=tr]")) as HTMLTableRowElement[];
535
- const colIndex = Array.from(table.querySelectorAll("thead th")).findIndex(
536
- (th) => (th as HTMLElement).getAttribute("data-col-id") === colId
537
- );
538
-
539
- if (colIndex < 0) return;
540
-
541
- rows.sort((rowA, rowB) => {
542
- const cellA = rowA.children[colIndex]?.textContent?.trim() || "";
543
- const cellB = rowB.children[colIndex]?.textContent?.trim() || "";
544
- const numA = Number(cellA.replace(/[^0-9.-]+/g, ""));
545
- const numB = Number(cellB.replace(/[^0-9.-]+/g, ""));
546
-
547
- if (!isNaN(numA) && !isNaN(numB) && cellA !== "" && cellB !== "") {
548
- return nextDir === "asc" ? numA - numB : numB - numA;
549
- }
550
- return nextDir === "asc" ? cellA.localeCompare(cellB) : cellB.localeCompare(cellA);
551
- });
552
-
553
- rows.forEach((row) => tbody.appendChild(row));
554
- });
555
- });
556
-
557
- // Row Selection Event
558
- this.addEventListener("click", (e) => {
559
- const target = e.target as HTMLElement | null;
560
- if (!target) return;
561
- if (target.closest("a, button, input, select, textarea, [role=button]")) {
562
- return;
563
- }
564
- const tr = target.closest("tbody tr[data-row-id]");
565
- if (tr) {
566
- const rowId = tr.getAttribute("data-row-id");
567
- this.dispatchEvent(new CustomEvent("rla-row-select", { detail: { rowId, event: e }, bubbles: true }));
568
- }
569
- });
570
- }
571
- }
572
-
573
- if (!customElements.get("rla-table")) {
574
- customElements.define("rla-table", RLATableElement);
575
- }
576
- }
577
- </script>
578
-
579
-
1
+ ---
2
+ import type {
3
+ TableProps,
4
+ TableColumn,
5
+ TableRow,
6
+ TableGroup,
7
+ SortingState
8
+ } from "./table"
9
+ import { tableTheme } from "./table.theme"
10
+ import RLAIcon from "../icon/RLAIcon.astro"
11
+ import RLAInput from "../input/RLAInput.astro"
12
+ import RLADropdownMenu from "../dropdown-menu/RLADropdownMenu.astro"
13
+
14
+ type AnyData = Record<string, any>
15
+
16
+ const {
17
+ data = [],
18
+ columns = [],
19
+ caption,
20
+ meta,
21
+ sticky = false,
22
+ loading = false,
23
+ loadingColor = "primary",
24
+ loadingAnimation = "carousel",
25
+ empty = "No data available",
26
+ striped = false,
27
+ bordered = false,
28
+ hoverable = false,
29
+ globalFilter = "",
30
+ columnFilters = [],
31
+ columnVisibility = {},
32
+ columnPinning = {},
33
+ sorting = [],
34
+ grouping = [],
35
+ rowPinning = {},
36
+ rowSelection = {},
37
+ expanded = {},
38
+ pagination,
39
+ getSubRows,
40
+ getRowId,
41
+ searchable = false,
42
+ searchPlaceholder = "Filter table...",
43
+ showColumnsToggle = false,
44
+ class: className,
45
+ ui,
46
+ ...rest
47
+ } = Astro.props as TableProps
48
+
49
+ const classes = tableTheme({
50
+ sticky,
51
+ loading,
52
+ loadingColor,
53
+ loadingAnimation,
54
+ striped,
55
+ bordered,
56
+ hoverable,
57
+ ui,
58
+ class: className,
59
+ externalScroll: false
60
+ })
61
+
62
+ // ─── Column resolution ────────────────────────────────────────────────────────
63
+
64
+ let resolvedColumns: TableColumn[] = []
65
+
66
+ if (columns && columns.length > 0) {
67
+ resolvedColumns = columns
68
+ } else if (data && data.length > 0 && typeof data[0] === "object" && data[0] !== null) {
69
+ const firstRow = data[0] as AnyData
70
+ resolvedColumns = Object.keys(firstRow).map((key) => ({
71
+ id: key,
72
+ accessorKey: key,
73
+ header: key.charAt(0).toUpperCase() + key.slice(1),
74
+ enableSorting: true
75
+ }))
76
+ }
77
+
78
+ // Filter by column visibility
79
+ const visibleColumns = resolvedColumns.filter((col) => {
80
+ const id = (col.id || col.accessorKey || "") as string
81
+ if (!id) return true
82
+ if (col.enableHiding === false) return true
83
+ return columnVisibility[id] !== false
84
+ })
85
+
86
+ // Dropdown items for column visibility toggle
87
+ const columnDropdownItems = resolvedColumns
88
+ .filter((col) => col.enableHiding !== false)
89
+ .map((col) => {
90
+ const colId = (col.id || col.accessorKey || "") as string
91
+ const rawLabel = typeof col.header === "string" ? col.header : colId
92
+ return {
93
+ id: colId,
94
+ label: rawLabel.charAt(0).toUpperCase() + rawLabel.slice(1),
95
+ type: "checkbox" as const,
96
+ checked: columnVisibility[colId] !== false
97
+ }
98
+ })
99
+
100
+ // ─── Row building ─────────────────────────────────────────────────────────────
101
+
102
+ function getNestedValue(obj: AnyData, key: string): any {
103
+ if (key.includes(".")) {
104
+ return key.split(".").reduce((acc: any, k) => acc?.[k], obj)
105
+ }
106
+ return obj[key]
107
+ }
108
+
109
+ function buildRowTree(rawItems: any[], parentDepth = 0): TableRow[] {
110
+ return rawItems.map((item, idx) => {
111
+ const rId = getRowId
112
+ ? getRowId(item, idx)
113
+ : item?.id !== undefined
114
+ ? String(item.id)
115
+ : String(idx)
116
+
117
+ const isSelected = !!rowSelection[rId]
118
+ const isExpanded = !!expanded[rId]
119
+
120
+ let subRowsList: TableRow[] | undefined = undefined
121
+ if (getSubRows) {
122
+ const children = getSubRows(item, idx)
123
+ if (children && children.length > 0) {
124
+ subRowsList = buildRowTree(children, parentDepth + 1)
125
+ }
126
+ } else if (Array.isArray(item?.children) && item.children.length > 0) {
127
+ subRowsList = buildRowTree(item.children, parentDepth + 1)
128
+ }
129
+
130
+ const rowObj: TableRow = {
131
+ id: rId,
132
+ original: item,
133
+ index: idx,
134
+ depth: parentDepth,
135
+ getValue: (key: string) => {
136
+ if (!item || typeof item !== "object") return undefined
137
+ return getNestedValue(item as AnyData, key)
138
+ },
139
+ getIsSelected: () => isSelected,
140
+ toggleSelected: () => {},
141
+ getIsExpanded: () => isExpanded,
142
+ toggleExpanded: () => {},
143
+ getIsPinned: () => {
144
+ if (rowPinning.top?.includes(rId)) return "top"
145
+ if (rowPinning.bottom?.includes(rId)) return "bottom"
146
+ return false
147
+ },
148
+ pin: () => {},
149
+ getIsGrouped: () => false,
150
+ getCanExpand: () => !!(subRowsList && subRowsList.length > 0),
151
+ ...(subRowsList ? { subRows: subRowsList } : {})
152
+ }
153
+ return rowObj
154
+ })
155
+ }
156
+
157
+ let processedRows: TableRow[] = buildRowTree(data)
158
+
159
+ // ─── Filtering ────────────────────────────────────────────────────────────────
160
+
161
+ if (globalFilter) {
162
+ const query = globalFilter.toLowerCase()
163
+ processedRows = processedRows.filter((r) => {
164
+ if (typeof r.original !== "object" || !r.original)
165
+ return String(r.original).toLowerCase().includes(query)
166
+ return Object.values(r.original as AnyData).some((val) =>
167
+ String(val ?? "").toLowerCase().includes(query)
168
+ )
169
+ })
170
+ }
171
+
172
+ for (const filter of columnFilters) {
173
+ if (!filter.id || filter.value === undefined || filter.value === null || filter.value === "")
174
+ continue
175
+ const fVal = String(filter.value).toLowerCase()
176
+ processedRows = processedRows.filter((r) =>
177
+ String(r.getValue(filter.id) ?? "").toLowerCase().includes(fVal)
178
+ )
179
+ }
180
+
181
+ // ─── Sorting ──────────────────────────────────────────────────────────────────
182
+
183
+ if (sorting && sorting.length > 0) {
184
+ processedRows = [...processedRows].sort((a, b) => {
185
+ for (const { id, desc } of sorting as SortingState) {
186
+ const valA = a.getValue(id)
187
+ const valB = b.getValue(id)
188
+ if (valA === valB) continue
189
+ if (valA === undefined || valA === null) return 1
190
+ if (valB === undefined || valB === null) return -1
191
+ let cmp: number
192
+ if (typeof valA === "number" && typeof valB === "number") {
193
+ cmp = valA - valB
194
+ } else {
195
+ cmp = String(valA).localeCompare(String(valB))
196
+ }
197
+ return desc ? -cmp : cmp
198
+ }
199
+ return 0
200
+ })
201
+ }
202
+
203
+ // ─── Grouping ─────────────────────────────────────────────────────────────────
204
+
205
+ type GroupedItem =
206
+ | { type: "group"; group: TableGroup }
207
+ | { type: "row"; row: TableRow }
208
+
209
+ let groupedItems: GroupedItem[] = []
210
+ let groups: TableGroup[] = []
211
+
212
+ if (grouping && grouping.length > 0) {
213
+ const groupColId = grouping[0]!
214
+ const groupMap = new Map<string, TableRow[]>()
215
+ const groupOrder: string[] = []
216
+
217
+ for (const row of processedRows) {
218
+ const val = String(row.getValue(groupColId) ?? "")
219
+ if (!groupMap.has(val)) {
220
+ groupMap.set(val, [])
221
+ groupOrder.push(val)
222
+ }
223
+ groupMap.get(val)!.push(row)
224
+ }
225
+
226
+ for (const val of groupOrder) {
227
+ const rows = groupMap.get(val)!
228
+ const group: TableGroup = {
229
+ id: `group-${groupColId}-${val}`,
230
+ columnId: groupColId,
231
+ value: val,
232
+ rows
233
+ }
234
+ groups.push(group)
235
+ groupedItems.push({ type: "group", group })
236
+ for (const row of rows) {
237
+ groupedItems.push({ type: "row", row, groupId: group.id } as any)
238
+ }
239
+ }
240
+ } else {
241
+ groupedItems = processedRows.map((row) => ({ type: "row", row }))
242
+ }
243
+
244
+ // ─── Row pinning ──────────────────────────────────────────────────────────────
245
+
246
+ if ((rowPinning.top?.length ?? 0) > 0 || (rowPinning.bottom?.length ?? 0) > 0) {
247
+ const topItems: GroupedItem[] = []
248
+ const midItems: GroupedItem[] = []
249
+ const botItems: GroupedItem[] = []
250
+
251
+ for (const item of groupedItems) {
252
+ if (item.type === "row") {
253
+ if (rowPinning.top?.includes(item.row.id)) topItems.push(item)
254
+ else if (rowPinning.bottom?.includes(item.row.id)) botItems.push(item)
255
+ else midItems.push(item)
256
+ } else {
257
+ midItems.push(item)
258
+ }
259
+ }
260
+ groupedItems = [...topItems, ...midItems, ...botItems]
261
+ }
262
+
263
+ // ─── Pagination ───────────────────────────────────────────────────────────────
264
+
265
+ let paginatedItems = groupedItems
266
+ if (pagination) {
267
+ const { pageIndex = 0, pageSize = 10 } = pagination
268
+ const rowItems = groupedItems.filter((i) => i.type === "row")
269
+ const pageRows = rowItems.slice(pageIndex * pageSize, (pageIndex + 1) * pageSize)
270
+ const pageRowIds = new Set(pageRows.map((i) => (i as { type: "row"; row: TableRow }).row.id))
271
+
272
+ if (grouping && grouping.length > 0) {
273
+ const result: GroupedItem[] = []
274
+ for (const item of groupedItems) {
275
+ if (item.type === "group") {
276
+ if (item.group.rows.some((r) => pageRowIds.has(r.id))) result.push(item)
277
+ } else if (pageRowIds.has(item.row.id)) {
278
+ result.push(item)
279
+ }
280
+ }
281
+ paginatedItems = result
282
+ } else {
283
+ paginatedItems = pageRows
284
+ }
285
+ }
286
+
287
+ // ─── Column pinning offsets ───────────────────────────────────────────────────
288
+
289
+ const colOffsetLeft: Record<string, number> = {}
290
+ const colOffsetRight: Record<string, number> = {}
291
+
292
+ if (columnPinning.left && columnPinning.left.length > 0) {
293
+ let offset = 0
294
+ for (const colId of columnPinning.left) {
295
+ colOffsetLeft[colId] = offset
296
+ const col = resolvedColumns.find((c) => (c.id || c.accessorKey) === colId)
297
+ offset += typeof col?.size === "number" ? col.size : 150
298
+ }
299
+ }
300
+
301
+ if (columnPinning.right && columnPinning.right.length > 0) {
302
+ let offset = 0
303
+ for (const colId of [...columnPinning.right].reverse()) {
304
+ colOffsetRight[colId] = offset
305
+ const col = resolvedColumns.find((c) => (c.id || c.accessorKey) === colId)
306
+ offset += typeof col?.size === "number" ? col.size : 150
307
+ }
308
+ }
309
+
310
+ // ─── Helpers ──────────────────────────────────────────────────────────────────
311
+
312
+ const hasFooters = visibleColumns.some(
313
+ (col) =>
314
+ col.footer ||
315
+ Astro.slots.has(`${String(col.id || col.accessorKey)}-footer`)
316
+ )
317
+
318
+ const showToolbar = searchable || showColumnsToggle || Astro.slots.has("toolbar")
319
+
320
+ function getPinStyle(colId: string): string {
321
+ if (colOffsetLeft[colId] !== undefined)
322
+ return `left:${colOffsetLeft[colId]}px;`
323
+ if (colOffsetRight[colId] !== undefined)
324
+ return `right:${colOffsetRight[colId]}px;`
325
+ return ""
326
+ }
327
+
328
+ function isPinned(colId: string): boolean {
329
+ return colId in colOffsetLeft || colId in colOffsetRight
330
+ }
331
+ ---
332
+
333
+ <rla-table
334
+ class:list={[classes.root, className]}
335
+ data-slot="root"
336
+ {...rest}
337
+ >
338
+ {showToolbar && (
339
+ <div
340
+ class="flex items-center gap-3 px-3.5 py-3 border-b border-default bg-muted/20 overflow-x-auto"
341
+ data-slot="toolbar"
342
+ >
343
+ {searchable && (
344
+ <RLAInput
345
+ type="search"
346
+ size="sm"
347
+ placeholder={searchPlaceholder}
348
+ leadingIcon="i-lucide-search"
349
+ class="max-w-xs min-w-[10ch]"
350
+ data-rla-table-search
351
+ />
352
+ )}
353
+
354
+ <slot name="toolbar" />
355
+
356
+ {showColumnsToggle && (
357
+ <div class="ml-auto shrink-0">
358
+ <RLADropdownMenu
359
+ label="Columns"
360
+ icon="i-lucide-columns"
361
+ items={columnDropdownItems as any}
362
+ content={{ align: "end" }}
363
+ size="sm"
364
+ color="neutral"
365
+ />
366
+ </div>
367
+ )}
368
+ </div>
369
+ )}
370
+
371
+ <div class="overflow-x-auto w-full">
372
+ <table class={classes.base} data-slot="base">
373
+ {(caption || Astro.slots.has("caption")) && (
374
+ <caption class={classes.caption} data-slot="caption">
375
+ <slot name="caption">{caption}</slot>
376
+ </caption>
377
+ )}
378
+
379
+ {/* ── thead ── */}
380
+ {visibleColumns.length > 0 && (
381
+ <thead class={classes.thead} data-slot="thead">
382
+ <tr class={classes.tr} data-slot="tr">
383
+ {visibleColumns.map((col) => {
384
+ const colId = (col.id || col.accessorKey || "") as string
385
+ const pinned = isPinned(colId)
386
+ const metaThClass =
387
+ typeof col.meta?.class?.th === "function"
388
+ ? col.meta.class.th(col)
389
+ : col.meta?.class?.th || ""
390
+ const metaThStyle =
391
+ typeof col.meta?.style?.th === "function"
392
+ ? col.meta.style.th(col)
393
+ : col.meta?.style?.th || ""
394
+ const canSort =
395
+ col.enableSorting !== false &&
396
+ !!(col.accessorKey || col.id) &&
397
+ colId !== "actions" &&
398
+ colId !== "select"
399
+ const activeSort = (sorting as SortingState).find((s) => s.id === colId)
400
+ const sortDir = activeSort ? (activeSort.desc ? "desc" : "asc") : "none"
401
+ const headerSlotName = `${colId}-header`
402
+
403
+ const sizeStyle = col.size
404
+ ? `width:${typeof col.size === "number" ? `${col.size}px` : col.size};`
405
+ : ""
406
+ const pinStyle = getPinStyle(colId)
407
+ const combinedStyle = [sizeStyle, pinStyle, metaThStyle]
408
+ .filter(Boolean)
409
+ .join("")
410
+
411
+ return (
412
+ <th
413
+ scope="col"
414
+ class:list={[
415
+ classes.th,
416
+ pinned ? "sticky bg-default z-10" : "",
417
+ metaThClass
418
+ ]}
419
+ style={combinedStyle || undefined}
420
+ data-col-id={colId}
421
+ data-slot="th"
422
+ >
423
+ {Astro.slots.has(headerSlotName) ? (
424
+ <slot name={headerSlotName} {...({ column: col } as any)} />
425
+ ) : canSort ? (
426
+ <button
427
+ type="button"
428
+ class="inline-flex items-center gap-1.5 hover:text-highlighted focus:outline-none transition-colors cursor-pointer group -mx-1 px-1 py-0.5 rounded"
429
+ data-rla-sort-btn
430
+ data-rla-col-id={colId}
431
+ data-sort-dir={sortDir}
432
+ aria-label={`Sort by ${colId}`}
433
+ >
434
+ <span>
435
+ {typeof col.header === "function"
436
+ ? col.header({ column: col, table: {} })
437
+ : col.header || colId}
438
+ </span>
439
+ <span
440
+ data-sort-icon
441
+ class:list={[
442
+ sortDir === "asc"
443
+ ? "i-lucide-arrow-up-narrow-wide text-primary"
444
+ : sortDir === "desc"
445
+ ? "i-lucide-arrow-down-wide-narrow text-primary"
446
+ : "i-lucide-arrow-up-down opacity-40 group-hover:opacity-100",
447
+ "size-3.5 transition-all shrink-0"
448
+ ]}
449
+ />
450
+ </button>
451
+ ) : typeof col.header === "function" ? (
452
+ <Fragment set:html={col.header({ column: col, table: {} })} />
453
+ ) : (
454
+ col.header || colId
455
+ )}
456
+ </th>
457
+ )
458
+ })}
459
+ </tr>
460
+ </thead>
461
+ )}
462
+
463
+ {/* ── tbody ── */}
464
+ <tbody class={classes.tbody} data-slot="tbody">
465
+ <slot name="body-top" />
466
+
467
+ {loading ? (
468
+ <tr>
469
+ <td
470
+ colspan={Math.max(1, visibleColumns.length)}
471
+ class={classes.loading}
472
+ data-slot="loading"
473
+ >
474
+ {Astro.slots.has("loading") ? (
475
+ <slot name="loading" />
476
+ ) : (
477
+ <div class="flex items-center justify-center gap-2 text-muted py-4">
478
+ <RLAIcon name="i-lucide-loader-2" class="size-5 animate-spin text-primary" />
479
+ <span>Loading...</span>
480
+ </div>
481
+ )}
482
+ </td>
483
+ </tr>
484
+ ) : paginatedItems.length === 0 ? (
485
+ <tr>
486
+ <td
487
+ colspan={Math.max(1, visibleColumns.length)}
488
+ class={classes.empty}
489
+ data-slot="empty"
490
+ >
491
+ {Astro.slots.has("empty") ? (
492
+ <slot name="empty" />
493
+ ) : (
494
+ <div class="flex flex-col items-center justify-center py-6 gap-2 text-muted">
495
+ <RLAIcon name="i-lucide-inbox" class="size-8 opacity-40" />
496
+ <p class="text-sm font-medium">{empty}</p>
497
+ </div>
498
+ )}
499
+ </td>
500
+ </tr>
501
+ ) : (
502
+ paginatedItems.map((item) => {
503
+ // ── Group header row ──────────────────────────────────────────
504
+ if (item.type === "group") {
505
+ const { group } = item
506
+ return (
507
+ <tr class={classes.trGroup} data-slot="tr-group" data-group-id={group.id}>
508
+ <td
509
+ colspan={Math.max(1, visibleColumns.length)}
510
+ class={classes.tdGroup}
511
+ data-slot="td-group"
512
+ >
513
+ {Astro.slots.has("group-header") ? (
514
+ <slot
515
+ name="group-header"
516
+ {...({ group, colSpan: visibleColumns.length } as any)}
517
+ />
518
+ ) : (
519
+ <span class="flex items-center gap-2">
520
+ <span class="i-lucide-layers size-3.5 text-primary opacity-70" />
521
+ <span class="text-highlighted">{String(group.value)}</span>
522
+ <span class="ml-auto text-muted font-normal normal-case">
523
+ {group.rows.length} {group.rows.length === 1 ? "row" : "rows"}
524
+ </span>
525
+ </span>
526
+ )}
527
+ </td>
528
+ </tr>
529
+ )
530
+ }
531
+
532
+ // ── Data row ──────────────────────────────────────────────────
533
+ const { row } = item
534
+ const currentGroupId = item.type === "row" && (item as any).groupId ? (item as any).groupId : undefined
535
+ const trMetaClass =
536
+ typeof meta?.class?.tr === "function"
537
+ ? meta.class.tr(row)
538
+ : meta?.class?.tr || ""
539
+ const trMetaStyle =
540
+ typeof meta?.style?.tr === "function"
541
+ ? meta.style.tr(row)
542
+ : meta?.style?.tr || ""
543
+ const isSelected = row.getIsSelected()
544
+ const isExpanded = row.getIsExpanded()
545
+ const isPinned_ = row.getIsPinned()
546
+
547
+ return (
548
+ <>
549
+ <tr
550
+ class:list={[classes.tr, trMetaClass]}
551
+ style={trMetaStyle || undefined}
552
+ data-slot="tr"
553
+ data-row-id={row.id}
554
+ data-group-id={currentGroupId}
555
+ data-selected={isSelected ? "true" : undefined}
556
+ data-expanded={isExpanded ? "true" : undefined}
557
+ data-pinned={isPinned_ || undefined}
558
+ >
559
+ {visibleColumns.map((col) => {
560
+ const colId = (col.id || col.accessorKey || "") as string
561
+ const accessorKey = (col.accessorKey || col.id || "") as string
562
+ const cellValue = accessorKey ? row.getValue(accessorKey) : undefined
563
+ const cellSlotName = `${colId}-cell`
564
+ const pinned = isPinned(colId)
565
+
566
+ const metaTdClass =
567
+ typeof col.meta?.class?.td === "function"
568
+ ? col.meta.class.td(row)
569
+ : col.meta?.class?.td || ""
570
+ const metaTdStyle =
571
+ typeof col.meta?.style?.td === "function"
572
+ ? col.meta.style.td(row)
573
+ : col.meta?.style?.td || ""
574
+ const colSpanVal =
575
+ typeof col.meta?.colspan?.td === "function"
576
+ ? col.meta.colspan.td(row)
577
+ : col.meta?.colspan?.td
578
+ const rowSpanVal =
579
+ typeof col.meta?.rowspan?.td === "function"
580
+ ? col.meta.rowspan.td(row)
581
+ : col.meta?.rowspan?.td
582
+
583
+ const pinStyle_ = getPinStyle(colId)
584
+ const combinedTdStyle = [pinStyle_, metaTdStyle].filter(Boolean).join("")
585
+
586
+ return (
587
+ <td
588
+ class:list={[
589
+ classes.td,
590
+ pinned ? "sticky bg-default z-10" : "",
591
+ metaTdClass
592
+ ]}
593
+ style={combinedTdStyle || undefined}
594
+ colspan={colSpanVal}
595
+ rowspan={rowSpanVal}
596
+ data-slot="td"
597
+ data-col-id={colId}
598
+ >
599
+ {Astro.slots.has(cellSlotName) ? (
600
+ <slot
601
+ name={cellSlotName}
602
+ {...({
603
+ row,
604
+ column: col,
605
+ cell: { value: cellValue },
606
+ getValue: () => cellValue,
607
+ renderValue: () => cellValue
608
+ } as any)}
609
+ />
610
+ ) : typeof col.cell === "function" ? (
611
+ <Fragment set:html={col.cell({
612
+ row,
613
+ getValue: () => cellValue,
614
+ renderValue: () => cellValue,
615
+ column: col,
616
+ table: {}
617
+ })} />
618
+ ) : cellValue !== undefined && cellValue !== null ? (
619
+ String(cellValue)
620
+ ) : (
621
+ "—"
622
+ )}
623
+ </td>
624
+ )
625
+ })}
626
+ </tr>
627
+
628
+ {/* Expanded sub-row */}
629
+ {isExpanded && Astro.slots.has("expanded") && (
630
+ <tr class={classes.trExpanded} data-slot="tr-expanded" data-row-id={row.id}>
631
+ <td
632
+ colspan={Math.max(1, visibleColumns.length)}
633
+ class={classes.tdExpanded}
634
+ data-slot="td-expanded"
635
+ >
636
+ <slot name="expanded" {...({ row } as any)} />
637
+ </td>
638
+ </tr>
639
+ )}
640
+
641
+ {/* Sub-rows (tree data) */}
642
+ {row.subRows && row.subRows.length > 0 && isExpanded &&
643
+ row.subRows.map((subRow) => {
644
+ const subTrMetaClass =
645
+ typeof meta?.class?.tr === "function"
646
+ ? meta.class.tr(subRow)
647
+ : meta?.class?.tr || ""
648
+ return (
649
+ <tr
650
+ class:list={[classes.tr, subTrMetaClass]}
651
+ data-slot="tr"
652
+ data-row-id={subRow.id}
653
+ data-depth={subRow.depth}
654
+ >
655
+ {visibleColumns.map((col) => {
656
+ const colId = (col.id || col.accessorKey || "") as string
657
+ const accessorKey = (col.accessorKey || col.id || "") as string
658
+ const cellValue = accessorKey ? subRow.getValue(accessorKey) : undefined
659
+ const cellSlotName = `${colId}-cell`
660
+ const metaTdClass =
661
+ typeof col.meta?.class?.td === "function"
662
+ ? col.meta.class.td(subRow)
663
+ : col.meta?.class?.td || ""
664
+
665
+ return (
666
+ <td
667
+ class:list={[classes.td, metaTdClass]}
668
+ data-slot="td"
669
+ data-col-id={colId}
670
+ >
671
+ {Astro.slots.has(cellSlotName) ? (
672
+ <slot
673
+ name={cellSlotName}
674
+ {...({
675
+ row: subRow,
676
+ column: col,
677
+ cell: { value: cellValue },
678
+ getValue: () => cellValue,
679
+ renderValue: () => cellValue
680
+ } as any)}
681
+ />
682
+ ) : typeof col.cell === "function" ? (
683
+ col.cell({
684
+ row: subRow,
685
+ getValue: () => cellValue,
686
+ renderValue: () => cellValue,
687
+ column: col,
688
+ table: {}
689
+ })
690
+ ) : cellValue !== undefined && cellValue !== null ? (
691
+ String(cellValue)
692
+ ) : (
693
+ "—"
694
+ )}
695
+ </td>
696
+ )
697
+ })}
698
+ </tr>
699
+ )
700
+ })}
701
+ </>
702
+ )
703
+ })
704
+ )}
705
+
706
+ <slot name="body-bottom" />
707
+ </tbody>
708
+
709
+ {/* ── tfoot ── */}
710
+ {hasFooters && (
711
+ <tfoot class={classes.tfoot} data-slot="tfoot">
712
+ <tr class={classes.tr} data-slot="tr">
713
+ {visibleColumns.map((col) => {
714
+ const colId = (col.id || col.accessorKey || "") as string
715
+ const footerSlotName = `${colId}-footer`
716
+ return (
717
+ <td class={classes.td} data-slot="td">
718
+ {Astro.slots.has(footerSlotName) ? (
719
+ <slot name={footerSlotName} {...({ column: col } as any)} />
720
+ ) : typeof col.footer === "function" ? (
721
+ <Fragment set:html={col.footer({ column: col, table: {} })} />
722
+ ) : (
723
+ col.footer || ""
724
+ )}
725
+ </td>
726
+ )
727
+ })}
728
+ </tr>
729
+ </tfoot>
730
+ )}
731
+ </table>
732
+ </div>
733
+ </rla-table>
734
+
735
+ <script>
736
+ if (typeof HTMLElement !== "undefined") {
737
+ class RLATableElement extends HTMLElement {
738
+ private _allRows: HTMLTableRowElement[] = []
739
+
740
+ connectedCallback() {
741
+ const table = this.querySelector<HTMLTableElement>("table")
742
+ if (!table) return
743
+ const tbody = table.querySelector("tbody")
744
+ if (!tbody) return
745
+
746
+ // Cache original row order for sort resets
747
+ this._allRows = Array.from(tbody.querySelectorAll<HTMLTableRowElement>("tr[data-slot=tr]"))
748
+
749
+ this._initSearch(tbody)
750
+ this._initColumnVisibility(table, tbody)
751
+ this._initSort(table, tbody)
752
+ this._initRowEvents(tbody)
753
+ }
754
+
755
+ // ── Search ────────────────────────────────────────────────────────────
756
+ private _initSearch(tbody: Element) {
757
+ const input = this.querySelector<HTMLInputElement>("[data-rla-table-search]")
758
+ if (!input) return
759
+ input.addEventListener("input", () => {
760
+ const q = input.value.toLowerCase().trim()
761
+ tbody.querySelectorAll<HTMLTableRowElement>("tr[data-slot=tr]").forEach((row) => {
762
+ if (!q || (row.textContent?.toLowerCase() ?? "").includes(q)) {
763
+ row.style.display = ""
764
+ } else {
765
+ row.style.display = "none"
766
+ }
767
+ })
768
+ })
769
+ }
770
+
771
+ // ── Column visibility ─────────────────────────────────────────────────
772
+ private _initColumnVisibility(table: HTMLTableElement, tbody: Element) {
773
+ this.addEventListener("rla-dropdown-checkbox-change", (e: Event) => {
774
+ const ev = e as CustomEvent<{ id: string; checked: boolean }>
775
+ const { id: colId, checked } = ev.detail ?? {}
776
+ if (!colId) return
777
+
778
+ const display = checked ? "" : "none"
779
+
780
+ // Header th
781
+ const th = table.querySelector<HTMLElement>(`th[data-col-id="${colId}"]`)
782
+ if (th) th.style.display = display
783
+
784
+ // Footer td (tfoot)
785
+ const tfoot = table.querySelector<HTMLElement>("tfoot")
786
+ if (tfoot) {
787
+ tfoot
788
+ .querySelectorAll<HTMLElement>(`td[data-col-id="${colId}"]`)
789
+ .forEach((td) => (td.style.display = display))
790
+ }
791
+
792
+ // All body tds for this column
793
+ tbody
794
+ .querySelectorAll<HTMLElement>(`td[data-col-id="${colId}"]`)
795
+ .forEach((td) => (td.style.display = display))
796
+
797
+ this.dispatchEvent(
798
+ new CustomEvent("rla-column-visibility-change", {
799
+ detail: { id: colId, visible: checked },
800
+ bubbles: true
801
+ })
802
+ )
803
+ })
804
+ }
805
+
806
+ // ── Column sort ───────────────────────────────────────────────────────
807
+ private _initSort(table: HTMLTableElement, tbody: Element) {
808
+ const sortBtns = table.querySelectorAll<HTMLButtonElement>("[data-rla-sort-btn]")
809
+
810
+ sortBtns.forEach((btn) => {
811
+ btn.addEventListener("click", (e) => {
812
+ const colId = btn.getAttribute("data-rla-col-id")
813
+ if (!colId) return
814
+
815
+ const isMulti = (e as MouseEvent).shiftKey
816
+ const currentDir = btn.getAttribute("data-sort-dir") || "none"
817
+ const nextDir = currentDir === "asc" ? "desc" : currentDir === "desc" ? "none" : "asc"
818
+
819
+ if (!isMulti) {
820
+ // Reset all other buttons
821
+ sortBtns.forEach((other) => {
822
+ if (other !== btn) {
823
+ other.setAttribute("data-sort-dir", "none")
824
+ const icon = other.querySelector<HTMLElement>("[data-sort-icon]")
825
+ if (icon) {
826
+ icon.className =
827
+ "i-lucide-arrow-up-down size-3.5 opacity-40 group-hover:opacity-100 transition-all shrink-0"
828
+ }
829
+ }
830
+ })
831
+ }
832
+
833
+ btn.setAttribute("data-sort-dir", nextDir)
834
+ const icon = btn.querySelector<HTMLElement>("[data-sort-icon]")
835
+ if (icon) {
836
+ icon.className =
837
+ nextDir === "asc"
838
+ ? "i-lucide-arrow-up-narrow-wide size-3.5 text-primary transition-all shrink-0"
839
+ : nextDir === "desc"
840
+ ? "i-lucide-arrow-down-wide-narrow size-3.5 text-primary transition-all shrink-0"
841
+ : "i-lucide-arrow-up-down size-3.5 opacity-40 group-hover:opacity-100 transition-all shrink-0"
842
+ }
843
+
844
+ // Gather all active sorts
845
+ const activeSorts: Array<{ id: string; dir: string }> = []
846
+ sortBtns.forEach((b) => {
847
+ const dir = b.getAttribute("data-sort-dir") || "none"
848
+ if (dir !== "none") {
849
+ activeSorts.push({ id: b.getAttribute("data-rla-col-id") || "", dir })
850
+ }
851
+ })
852
+
853
+ if (nextDir === "none" && !isMulti) {
854
+ // Restore original DOM order
855
+ this._allRows.forEach((row) => tbody.appendChild(row))
856
+ } else {
857
+ const groupRows = Array.from(tbody.querySelectorAll<HTMLTableRowElement>("tr[data-slot=tr-group]"))
858
+
859
+ if (groupRows.length > 0) {
860
+ // Sort within each group
861
+ groupRows.forEach((groupHeader) => {
862
+ const groupId = groupHeader.getAttribute("data-group-id")
863
+ const rowsInGroup = Array.from(
864
+ tbody.querySelectorAll<HTMLTableRowElement>(`tr[data-slot=tr][data-group-id="${groupId}"]`)
865
+ )
866
+ rowsInGroup.sort((a, b) => {
867
+ for (const { id, dir } of activeSorts) {
868
+ const idx = Array.from(table.querySelectorAll("thead th")).findIndex(
869
+ (th) => (th as HTMLElement).getAttribute("data-col-id") === id
870
+ )
871
+ if (idx < 0) continue
872
+ const cellA = a.children[idx]?.textContent?.trim() ?? ""
873
+ const cellB = b.children[idx]?.textContent?.trim() ?? ""
874
+ const cleanA = cellA.replace(/[$€£,\s]/g, "")
875
+ const cleanB = cellB.replace(/[$€£,\s]/g, "")
876
+ const isNumA = cleanA !== "" && !isNaN(Number(cleanA))
877
+ const isNumB = cleanB !== "" && !isNaN(Number(cleanB))
878
+
879
+ let cmp: number
880
+ if (isNumA && isNumB) {
881
+ cmp = Number(cleanA) - Number(cleanB)
882
+ } else {
883
+ cmp = cellA.localeCompare(cellB)
884
+ }
885
+ if (dir === "desc") cmp = -cmp
886
+ if (cmp !== 0) return cmp
887
+ }
888
+ return 0
889
+ })
890
+ // Re-insert sorted rows directly following the group header
891
+ let lastElement: Element = groupHeader
892
+ rowsInGroup.forEach((r) => {
893
+ lastElement.insertAdjacentElement("afterend", r)
894
+ lastElement = r
895
+ })
896
+ })
897
+ } else {
898
+ const rows = Array.from(
899
+ tbody.querySelectorAll<HTMLTableRowElement>("tr[data-slot=tr]")
900
+ )
901
+ rows.sort((a, b) => {
902
+ for (const { id, dir } of activeSorts) {
903
+ const idx = Array.from(table.querySelectorAll("thead th")).findIndex(
904
+ (th) => (th as HTMLElement).getAttribute("data-col-id") === id
905
+ )
906
+ if (idx < 0) continue
907
+ const cellA = a.children[idx]?.textContent?.trim() ?? ""
908
+ const cellB = b.children[idx]?.textContent?.trim() ?? ""
909
+ const cleanA = cellA.replace(/[$€£,\s]/g, "")
910
+ const cleanB = cellB.replace(/[$€£,\s]/g, "")
911
+ const isNumA = cleanA !== "" && !isNaN(Number(cleanA))
912
+ const isNumB = cleanB !== "" && !isNaN(Number(cleanB))
913
+
914
+ let cmp: number
915
+ if (isNumA && isNumB) {
916
+ cmp = Number(cleanA) - Number(cleanB)
917
+ } else {
918
+ cmp = cellA.localeCompare(cellB)
919
+ }
920
+ if (dir === "desc") cmp = -cmp
921
+ if (cmp !== 0) return cmp
922
+ }
923
+ return 0
924
+ })
925
+ rows.forEach((row) => tbody.appendChild(row))
926
+ }
927
+ }
928
+
929
+ this.dispatchEvent(
930
+ new CustomEvent("rla-sorting-change", {
931
+ detail: { id: colId, dir: nextDir },
932
+ bubbles: true
933
+ })
934
+ )
935
+ })
936
+ })
937
+ }
938
+
939
+ // ── Row events ────────────────────────────────────────────────────────
940
+ private _initRowEvents(tbody: Element) {
941
+ // Select
942
+ tbody.addEventListener("click", (e) => {
943
+ const target = e.target as HTMLElement
944
+ if (target.closest("a, button, input, select, textarea, [role=button]")) return
945
+ const tr = target.closest<HTMLElement>("tr[data-row-id]")
946
+ if (!tr) return
947
+ this.dispatchEvent(
948
+ new CustomEvent("rla-row-select", {
949
+ detail: { rowId: tr.getAttribute("data-row-id"), event: e },
950
+ bubbles: true
951
+ })
952
+ )
953
+ })
954
+
955
+ // Hover
956
+ tbody.addEventListener("mouseover", (e) => {
957
+ const tr = (e.target as HTMLElement).closest<HTMLElement>("tr[data-row-id]")
958
+ if (!tr) return
959
+ this.dispatchEvent(
960
+ new CustomEvent("rla-row-hover", {
961
+ detail: { rowId: tr.getAttribute("data-row-id"), event: e },
962
+ bubbles: true
963
+ })
964
+ )
965
+ })
966
+
967
+ tbody.addEventListener("mouseleave", (e) => {
968
+ this.dispatchEvent(
969
+ new CustomEvent("rla-row-hover", {
970
+ detail: { rowId: null, event: e },
971
+ bubbles: true
972
+ })
973
+ )
974
+ })
975
+
976
+ // Context menu
977
+ tbody.addEventListener("contextmenu", (e) => {
978
+ const tr = (e.target as HTMLElement).closest<HTMLElement>("tr[data-row-id]")
979
+ if (!tr) return
980
+ this.dispatchEvent(
981
+ new CustomEvent("rla-row-contextmenu", {
982
+ detail: { rowId: tr.getAttribute("data-row-id"), event: e },
983
+ bubbles: true
984
+ })
985
+ )
986
+ })
987
+ }
988
+
989
+ // ── Public API (imperative) ───────────────────────────────────────────
990
+
991
+ /** Filter visible rows by a text query (case-insensitive). */
992
+ setGlobalFilter(query: string) {
993
+ const searchInput = this.querySelector<HTMLInputElement>("[data-rla-table-search]")
994
+ if (searchInput) {
995
+ searchInput.value = query
996
+ searchInput.dispatchEvent(new Event("input"))
997
+ } else {
998
+ // Manual DOM filter when no search input is rendered
999
+ const tbody = this.querySelector("tbody")
1000
+ if (!tbody) return
1001
+ const q = query.toLowerCase().trim()
1002
+ tbody
1003
+ .querySelectorAll<HTMLTableRowElement>("tr[data-slot=tr]")
1004
+ .forEach((row) => {
1005
+ row.style.display =
1006
+ !q || (row.textContent?.toLowerCase() ?? "").includes(q) ? "" : "none"
1007
+ })
1008
+ }
1009
+ }
1010
+
1011
+ /** Show or hide a column by its column ID. */
1012
+ setColumnVisibility(colId: string, visible: boolean) {
1013
+ this.dispatchEvent(
1014
+ new CustomEvent("rla-dropdown-checkbox-change", {
1015
+ detail: { id: colId, checked: visible },
1016
+ bubbles: false
1017
+ })
1018
+ )
1019
+ }
1020
+
1021
+ /** Return all currently-visible data rows. */
1022
+ getVisibleRows(): HTMLTableRowElement[] {
1023
+ const tbody = this.querySelector("tbody")
1024
+ if (!tbody) return []
1025
+ return Array.from(tbody.querySelectorAll<HTMLTableRowElement>("tr[data-slot=tr]")).filter(
1026
+ (r) => r.style.display !== "none"
1027
+ )
1028
+ }
1029
+
1030
+ /** Return all selected rows (those with data-selected=true). */
1031
+ getSelectedRows(): HTMLTableRowElement[] {
1032
+ const tbody = this.querySelector("tbody")
1033
+ if (!tbody) return []
1034
+ return Array.from(
1035
+ tbody.querySelectorAll<HTMLTableRowElement>("tr[data-selected=true]")
1036
+ )
1037
+ }
1038
+ }
1039
+
1040
+ if (!customElements.get("rla-table")) {
1041
+ customElements.define("rla-table", RLATableElement)
1042
+ }
1043
+ }
1044
+ </script>