@rimelight/ui 0.0.56 → 0.0.58

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 (43) hide show
  1. package/package.json +5 -5
  2. package/src/components/avatar/avatar.theme.ts +58 -1
  3. package/src/components/avatar/avatar.ts +21 -1
  4. package/src/components/avatar-group/RLAAvatarGroup.astro +37 -161
  5. package/src/components/avatar-group/RLSAvatarGroup.tsx +37 -121
  6. package/src/components/breadcrumb/RLABreadcrumb.astro +59 -196
  7. package/src/components/breadcrumb/RLSBreadcrumb.tsx +57 -120
  8. package/src/components/card/RLSCard.tsx +1 -2
  9. package/src/components/card/RLVCard.vue +1 -2
  10. package/src/components/carousel/RLACarousel.astro +2 -1
  11. package/src/components/carousel/RLSCarousel.tsx +3 -11
  12. package/src/components/chart/RLAChart.astro +211 -219
  13. package/src/components/dashboard-navbar/RLADashboardNavbar.astro +1 -1
  14. package/src/components/dashboard-navbar/RLSDashboardNavbar.tsx +1 -15
  15. package/src/components/dashboard-panel/RLADashboardPanel.astro +1 -1
  16. package/src/components/dashboard-panel/RLSDashboardPanel.tsx +1 -6
  17. package/src/components/dashboard-search/RLADashboardSearch.astro +1 -1
  18. package/src/components/head/UIHead.astro +0 -11
  19. package/src/components/image/RLAImage.astro +15 -20
  20. package/src/components/image/RLSImage.tsx +5 -12
  21. package/src/components/image/image.theme.ts +4 -4
  22. package/src/components/input-rating/RLAInputRating.astro +19 -14
  23. package/src/components/input-rating/RLSInputRating.tsx +39 -32
  24. package/src/components/input-tags/RLAInputTags.astro +27 -137
  25. package/src/components/layout-grid/RLALayoutGrid.astro +4 -4
  26. package/src/components/logo/RLALogo.astro +0 -16
  27. package/src/components/logo/RLSLogo.tsx +1 -8
  28. package/src/components/marquee/RLAMarquee.astro +55 -62
  29. package/src/components/page-section/RLAPageSection.astro +2 -2
  30. package/src/components/progress/RLAProgress.astro +39 -38
  31. package/src/components/progress/RLSProgress.tsx +16 -5
  32. package/src/components/progress/progress.theme.ts +1 -1
  33. package/src/components/scroll-area/RLAScrollArea.astro +4 -5
  34. package/src/components/scroll-area/RLSScrollArea.tsx +5 -6
  35. package/src/components/splitter/RLASplitter.astro +217 -230
  36. package/src/components/splitter/RLSSplitter.tsx +2 -6
  37. package/src/components/steps/RLASteps.astro +110 -110
  38. package/src/components/sticky-surface/RLAStickySurface.astro +49 -67
  39. package/src/components/table/RLATable.astro +16 -46
  40. package/src/components/tabs/RLATabs.astro +352 -385
  41. package/src/components/textarea/RLATextarea.astro +1 -2
  42. package/src/components/theme-selector/RLAThemeSelector.astro +6 -6
  43. package/src/components/tooltip/RLATooltip.astro +7 -11
@@ -1,385 +1,352 @@
1
- ---
2
- import type { TabsProps } from "./tabs"
3
- import { tabsTheme } from "./tabs.theme"
4
- import RLABadge from "../badge/RLABadge.astro"
5
- import RLAIcon from "../icon/RLAIcon.astro"
6
-
7
-
8
- const {
9
- items = [],
10
- value = "",
11
- defaultValue = "",
12
- variant = "pill",
13
- orientation = "horizontal",
14
- size = "md",
15
- color = "primary",
16
- content = true,
17
- valueKey = "value",
18
- labelKey = "label",
19
- maxTabs,
20
- estimatedTabWidth = 100,
21
- ...rest
22
- } = Astro.props as TabsProps
23
-
24
- // Normalize variant names
25
- const normalizedVariant = (variant === "solid" || variant === "pills") ? "pill" : (variant === "link" ? "underline" : variant);
26
-
27
- const classes = tabsTheme({
28
- ...Astro.props,
29
- variant: normalizedVariant
30
- });
31
-
32
- const initialValue = value || defaultValue || (items.length > 0 ? (items[0]?.[valueKey] ?? items[0]?.value ?? "0") : "")
33
-
34
- // ─── Responsive CSS Item Counter + Native Popover Overflow ───────────────────
35
- const uid = Math.random().toString(36).substring(2, 9)
36
- const cName = `rla-tb-${uid}`
37
- const popoverId = `rla-tb-pop-${uid}`
38
-
39
- const effectiveMaxTabs = maxTabs !== undefined ? Math.max(1, maxTabs) : items.length
40
- const renderedListItems = items.slice(0, effectiveMaxTabs)
41
- const overflowItems = items.slice(effectiveMaxTabs)
42
- const baseHiddenCount = overflowItems.length
43
- const needsOverflow = items.length > 1 && orientation === "horizontal"
44
-
45
- // Generate per-instance container-query CSS for horizontal layout
46
- let instanceCss = ""
47
- if (needsOverflow && renderedListItems.length > 1) {
48
- const maxBreaks = renderedListItems.length - 1
49
- for (let i = 0; i < maxBreaks; i++) {
50
- const hiddenItems = i + 1
51
- const visibleCount = renderedListItems.length - hiddenItems
52
- const maxWidth = visibleCount * estimatedTabWidth + 90
53
- const hideNthChild = renderedListItems.length - i
54
-
55
- instanceCss += `\n@container ${cName} (max-width: ${maxWidth}px) {`
56
- instanceCss += `\n [data-tb="${uid}"] { --hidden-items: ${hiddenItems}; }`
57
- instanceCss += `\n [data-tb="${uid}"] [data-slot=list] > button.hidable:nth-child(${hideNthChild + 1}) { display: none !important; }`
58
- if (baseHiddenCount === 0) {
59
- instanceCss += `\n [data-tb="${uid}"] .rla-tb-overflow-hidden { display: inline-flex !important; }`
60
- }
61
- instanceCss += `\n}`
62
- }
63
- }
64
- ---
65
- <rla-tabs {...rest} data-orientation={orientation} data-variant={normalizedVariant}>
66
- <div
67
- class:list={[classes.root, orientation === "horizontal" && "rla-tb-host"]}
68
- data-slot="root"
69
- data-tabs-container
70
- data-value={initialValue}
71
- data-tb={uid}
72
- style={orientation === "horizontal" ? `container-type: inline-size; container-name: ${cName}; --base-hidden: ${baseHiddenCount}; --hidden-items: 0;` : undefined}
73
- >
74
- <div class={classes.list} data-slot="list" role="tablist">
75
- <div class={classes.indicator} data-slot="indicator" style="display: none;"></div>
76
- {renderedListItems.map((item, index) => {
77
- const itemVal = item[valueKey] ?? item.value ?? String(index)
78
- const itemLabel = item[labelKey] ?? item.label
79
- const active = itemVal === initialValue
80
- const isHidable = needsOverflow && index > 0
81
- return (
82
- <button
83
- type="button"
84
- class:list={[classes.trigger, isHidable && "hidable"]}
85
- data-slot="trigger"
86
- role="tab"
87
- data-value={itemVal}
88
- data-state={active ? "active" : "inactive"}
89
- disabled={item.disabled}
90
- >
91
- {item.icon && <span class={item.icon + " " + classes.leadingIcon} data-slot="leadingIcon" />}
92
- {itemLabel && <span class={classes.label} data-slot="label">{itemLabel}</span>}
93
- {item.badge !== undefined && item.badge !== null && (
94
- <RLABadge
95
- size="sm"
96
- variant="subtle"
97
- color="neutral"
98
- class={classes.trailingBadge}
99
- data-slot="trailingBadge"
100
- >
101
- {typeof item.badge === "object" ? item.badge.label : item.badge}
102
- </RLABadge>
103
- )}
104
- </button>
105
- )
106
- })}
107
-
108
- {needsOverflow && (
109
- <div class="relative inline-flex items-center ml-auto">
110
- <button
111
- type="button"
112
- class:list={[
113
- classes.trigger,
114
- "rla-tb-overflow-btn",
115
- baseHiddenCount === 0 && "rla-tb-overflow-hidden"
116
- ]}
117
- popovertarget={popoverId}
118
- data-slot="overflow-trigger"
119
- aria-label="More tabs"
120
- >
121
- <span class="rla-tb-label">
122
- +<span class="rla-tb-count"></span> more
123
- </span>
124
- <RLAIcon name="chevronDown" class="ml-1 size-3.5 opacity-70" />
125
- </button>
126
-
127
- <div
128
- id={popoverId}
129
- popover="auto"
130
- class="rla-tb-popover rounded-lg border border-default bg-default p-1 shadow-lg backdrop-blur-sm"
131
- >
132
- <div class="flex flex-col gap-0.5 min-w-[10rem]">
133
- {items.map((item, index) => {
134
- const itemVal = item[valueKey] ?? item.value ?? String(index)
135
- const itemLabel = item[labelKey] ?? item.label
136
- const isExtra = index >= effectiveMaxTabs
137
- return (
138
- <button
139
- type="button"
140
- class:list={[
141
- "w-full text-left px-3 py-1.5 text-sm rounded-md transition-colors flex items-center justify-between",
142
- "hover:bg-muted focus-visible:outline-none cursor-pointer",
143
- !isExtra && "rla-tb-pop-rendered"
144
- ]}
145
- data-tab-overflow-value={itemVal}
146
- data-popover-item
147
- >
148
- <span class="truncate">{itemLabel}</span>
149
- <RLAIcon name="check" class="size-4 opacity-0 data-[active=true]:opacity-100" />
150
- </button>
151
- )
152
- })}
153
- </div>
154
- </div>
155
- </div>
156
- )}
157
- </div>
158
-
159
- {content && (
160
- <div class="flex-1">
161
- <slot>
162
- {items.map((item, index) => {
163
- const itemVal = item[valueKey] ?? item.value ?? String(index)
164
- const active = itemVal === initialValue
165
- return (
166
- <div
167
- class={classes.content}
168
- data-slot="content"
169
- role="tabpanel"
170
- data-value={itemVal}
171
- hidden={active ? undefined : "until-found"}
172
- >
173
- {item.content}
174
- </div>
175
- )
176
- })}
177
- </slot>
178
- </div>
179
- )}
180
- </div>
181
- </rla-tabs>
182
-
183
- <style is:inline set:html={instanceCss} />
184
-
185
- <style>
186
- .rla-tb-host {
187
- width: 100%;
188
- }
189
- .rla-tb-overflow-hidden {
190
- display: none !important;
191
- }
192
- .rla-tb-count::after {
193
- counter-reset: rla-tb-cnt calc(var(--base-hidden, 0) + var(--hidden-items, 0));
194
- content: counter(rla-tb-cnt);
195
- }
196
- .rla-tb-popover {
197
- margin: 0;
198
- inset: auto;
199
- position: absolute;
200
- }
201
- .rla-tb-popover:popover-open {
202
- display: block;
203
- }
204
- /* Show items in popover if they are hidden in list or extra */
205
- .rla-tb-pop-rendered {
206
- display: none;
207
- }
208
- @supports not (content-visibility: hidden) {
209
- [hidden="until-found"] {
210
- display: none !important;
211
- }
212
- }
213
- </style>
214
-
215
- <script>
216
- if (typeof HTMLElement !== 'undefined') {
217
- class RLATabs extends HTMLElement {
218
- private tabsCleanup: (() => void) | null = null;
219
- private resizeObserver: ResizeObserver | null = null;
220
-
221
- connectedCallback() {
222
- this.style.display = 'contents';
223
- const container = this.querySelector("[data-tabs-container]") as HTMLElement | null;
224
- if (!container) return;
225
-
226
- const list = container.querySelector("[data-slot=list]") as HTMLElement | null;
227
- const indicator = container.querySelector("[data-slot=indicator]") as HTMLElement | null;
228
- const orientation = this.dataset["orientation"] || "horizontal";
229
- const isVertical = orientation === "vertical";
230
-
231
- const updateIndicator = (activeTrigger: HTMLElement | null) => {
232
- if (!indicator || !list || !activeTrigger) {
233
- if (indicator) indicator.style.display = 'none';
234
- return;
235
- }
236
-
237
- // If active trigger is hidden due to container queries, position near overflow button
238
- if (getComputedStyle(activeTrigger).display === 'none') {
239
- const overflowBtn = list.querySelector('.rla-tb-overflow-btn') as HTMLElement | null;
240
- if (overflowBtn) activeTrigger = overflowBtn;
241
- }
242
-
243
- const listRect = list.getBoundingClientRect();
244
- const triggerRect = activeTrigger.getBoundingClientRect();
245
-
246
- if (triggerRect.width === 0 && triggerRect.height === 0) {
247
- indicator.style.display = 'none';
248
- return;
249
- }
250
-
251
- indicator.style.display = 'block';
252
-
253
- if (isVertical) {
254
- const top = triggerRect.top - listRect.top + list.scrollTop;
255
- const height = triggerRect.height;
256
- indicator.style.transform = `translateY(${top}px)`;
257
- indicator.style.height = `${height}px`;
258
- } else {
259
- const left = triggerRect.left - listRect.left + list.scrollLeft;
260
- const width = triggerRect.width;
261
- indicator.style.transform = `translateX(${left}px)`;
262
- indicator.style.width = `${width}px`;
263
- }
264
- };
265
-
266
- const containerValue = container.dataset["value"];
267
- const triggers = container.querySelectorAll("[role=tab]");
268
- const panels = container.querySelectorAll("[role=tabpanel]");
269
- const popoverItems = container.querySelectorAll("[data-popover-item]");
270
-
271
- let activeTrigger: HTMLElement | null = null;
272
-
273
- const syncState = (targetValue: string | undefined) => {
274
- triggers.forEach(trigger => {
275
- const el = trigger as HTMLElement;
276
- const val = el.dataset["value"];
277
- const isActive = val === targetValue;
278
- el.setAttribute("data-state", isActive ? "active" : "inactive");
279
- if (isActive) activeTrigger = el;
280
- });
281
-
282
- popoverItems.forEach(item => {
283
- const el = item as HTMLElement;
284
- const val = el.dataset["tabOverflowValue"];
285
- if (val === targetValue) {
286
- el.setAttribute("data-active", "true");
287
- el.classList.add("bg-muted/80", "font-medium");
288
- } else {
289
- el.removeAttribute("data-active");
290
- el.classList.remove("bg-muted/80", "font-medium");
291
- }
292
- });
293
-
294
- panels.forEach(panel => {
295
- const val = (panel as HTMLElement).dataset["value"];
296
- if (val === targetValue) {
297
- panel.removeAttribute("hidden");
298
- } else {
299
- panel.setAttribute("hidden", "until-found");
300
- }
301
- });
302
- };
303
-
304
- syncState(containerValue);
305
-
306
- requestAnimationFrame(() => updateIndicator(activeTrigger));
307
-
308
- if (window.ResizeObserver && list) {
309
- this.resizeObserver = new ResizeObserver(() => {
310
- const currentActive = list.querySelector("[role=tab][data-state=active]") as HTMLElement | null;
311
- updateIndicator(currentActive);
312
- });
313
- this.resizeObserver.observe(list);
314
- }
315
-
316
- const clickCleanups: (() => void)[] = [];
317
- const beforeMatchCleanups: (() => void)[] = [];
318
-
319
- // Watch for browser Find-in-Page auto-expands
320
- panels.forEach(panel => {
321
- const onBeforeMatch = () => {
322
- const targetValue = (panel as HTMLElement).dataset["value"];
323
- const trigger = container.querySelector(`[role=tab][data-value="${targetValue}"]`) as HTMLButtonElement | null;
324
- if (trigger) {
325
- trigger.click();
326
- } else {
327
- syncState(targetValue);
328
- }
329
- };
330
- panel.addEventListener("beforematch", onBeforeMatch);
331
- beforeMatchCleanups.push(() => panel.removeEventListener("beforematch", onBeforeMatch));
332
- });
333
-
334
- triggers.forEach(triggerNode => {
335
- const trigger = triggerNode as HTMLButtonElement;
336
- const onClick = () => {
337
- if (trigger.disabled) return;
338
- const targetValue = trigger.dataset["value"];
339
- syncState(targetValue);
340
- updateIndicator(trigger);
341
- };
342
- trigger.addEventListener("click", onClick);
343
- clickCleanups.push(() => trigger.removeEventListener("click", onClick));
344
- });
345
-
346
- popoverItems.forEach(itemNode => {
347
- const item = itemNode as HTMLButtonElement;
348
- const onClick = () => {
349
- const targetValue = item.dataset["tabOverflowValue"];
350
- syncState(targetValue);
351
- const activeTrig = container.querySelector(`[role=tab][data-value="${targetValue}"]`) as HTMLElement | null;
352
- updateIndicator(activeTrig);
353
- // Hide popover after selection
354
- const popover = item.closest('[popover]') as HTMLElement | null;
355
- if (popover && 'hidePopover' in popover) {
356
- (popover as any).hidePopover();
357
- }
358
- };
359
- item.addEventListener("click", onClick);
360
- clickCleanups.push(() => item.removeEventListener("click", onClick));
361
- });
362
-
363
- this.tabsCleanup = () => {
364
- clickCleanups.forEach(fn => fn());
365
- beforeMatchCleanups.forEach(fn => fn());
366
- if (this.resizeObserver) {
367
- this.resizeObserver.disconnect();
368
- this.resizeObserver = null;
369
- }
370
- };
371
- }
372
-
373
- disconnectedCallback() {
374
- if (this.tabsCleanup) {
375
- this.tabsCleanup();
376
- this.tabsCleanup = null;
377
- }
378
- }
379
- }
380
-
381
- if (!customElements.get('rla-tabs')) {
382
- customElements.define('rla-tabs', RLATabs);
383
- }
384
- }
385
- </script>
1
+ ---
2
+ import type { TabsProps } from "./tabs"
3
+ import { tabsTheme } from "./tabs.theme"
4
+ import RLABadge from "../badge/RLABadge.astro"
5
+ import RLAIcon from "../icon/RLAIcon.astro"
6
+
7
+
8
+ const {
9
+ items = [],
10
+ value = "",
11
+ defaultValue = "",
12
+ variant = "pill",
13
+ orientation = "horizontal",
14
+ size = "md",
15
+ color = "primary",
16
+ content = true,
17
+ valueKey = "value",
18
+ labelKey = "label",
19
+ maxTabs,
20
+ estimatedTabWidth = 100,
21
+ ...rest
22
+ } = Astro.props as TabsProps
23
+
24
+ // Normalize variant names
25
+ const normalizedVariant = (variant === "solid" || variant === "pills") ? "pill" : (variant === "link" ? "underline" : variant);
26
+
27
+ const classes = tabsTheme({
28
+ ...Astro.props,
29
+ variant: normalizedVariant
30
+ });
31
+
32
+ const initialValue = value || defaultValue || (items.length > 0 ? (items[0]?.[valueKey] ?? items[0]?.value ?? "0") : "")
33
+
34
+ // ─── Native Popover Overflow ───────────────────
35
+ const uid = Math.random().toString(36).substring(2, 9)
36
+ const popoverId = `rla-tb-pop-${uid}`
37
+
38
+ const effectiveMaxTabs = maxTabs !== undefined ? Math.max(1, maxTabs) : items.length
39
+ const renderedListItems = items.slice(0, effectiveMaxTabs)
40
+ const overflowItems = items.slice(effectiveMaxTabs)
41
+ const baseHiddenCount = overflowItems.length
42
+ const needsOverflow = items.length > 1 && orientation === "horizontal" && baseHiddenCount > 0
43
+ ---
44
+ <rla-tabs {...rest} data-orientation={orientation} data-variant={normalizedVariant}>
45
+ <div
46
+ class:list={[classes.root, orientation === "horizontal" && "rla-tb-host"]}
47
+ data-slot="root"
48
+ data-tabs-container
49
+ data-value={initialValue}
50
+ data-tb={uid}
51
+ >
52
+ <div class={classes.list} data-slot="list" role="tablist">
53
+ <div class:list={[classes.indicator, "hidden"]} data-slot="indicator"></div>
54
+ {renderedListItems.map((item, index) => {
55
+ const itemVal = item[valueKey] ?? item.value ?? String(index)
56
+ const itemLabel = item[labelKey] ?? item.label
57
+ const active = itemVal === initialValue
58
+ return (
59
+ <button
60
+ type="button"
61
+ class:list={[classes.trigger]}
62
+ data-slot="trigger"
63
+ role="tab"
64
+ data-value={itemVal}
65
+ data-state={active ? "active" : "inactive"}
66
+ disabled={item.disabled}
67
+ >
68
+ {item.icon && <span class={item.icon + " " + classes.leadingIcon} data-slot="leadingIcon" />}
69
+ {itemLabel && <span class={classes.label} data-slot="label">{itemLabel}</span>}
70
+ {item.badge !== undefined && item.badge !== null && (
71
+ <RLABadge
72
+ size="sm"
73
+ variant="subtle"
74
+ color="neutral"
75
+ class={classes.trailingBadge}
76
+ data-slot="trailingBadge"
77
+ >
78
+ {typeof item.badge === "object" ? item.badge.label : item.badge}
79
+ </RLABadge>
80
+ )}
81
+ </button>
82
+ )
83
+ })}
84
+
85
+ {needsOverflow && (
86
+ <div class="relative inline-flex items-center ml-auto">
87
+ <button
88
+ type="button"
89
+ class:list={[
90
+ classes.trigger,
91
+ "rla-tb-overflow-btn",
92
+ baseHiddenCount === 0 && "rla-tb-overflow-hidden"
93
+ ]}
94
+ popovertarget={popoverId}
95
+ data-slot="overflow-trigger"
96
+ aria-label="More tabs"
97
+ >
98
+ <span class="rla-tb-label">
99
+ +{baseHiddenCount} more
100
+ </span>
101
+ <RLAIcon name="chevronDown" class="ml-1 size-3.5 opacity-70" />
102
+ </button>
103
+
104
+ <div
105
+ id={popoverId}
106
+ popover="auto"
107
+ class="rla-tb-popover rounded-lg border border-default bg-default p-1 shadow-lg backdrop-blur-sm"
108
+ >
109
+ <div class="flex flex-col gap-0.5 min-w-[10rem]">
110
+ {items.map((item, index) => {
111
+ const itemVal = item[valueKey] ?? item.value ?? String(index)
112
+ const itemLabel = item[labelKey] ?? item.label
113
+ const isExtra = index >= effectiveMaxTabs
114
+ return (
115
+ <button
116
+ type="button"
117
+ class:list={[
118
+ "w-full text-left px-3 py-1.5 text-sm rounded-md transition-colors flex items-center justify-between",
119
+ "hover:bg-muted focus-visible:outline-none cursor-pointer",
120
+ !isExtra && "rla-tb-pop-rendered"
121
+ ]}
122
+ data-tab-overflow-value={itemVal}
123
+ data-popover-item
124
+ >
125
+ <span class="truncate">{itemLabel}</span>
126
+ <RLAIcon name="check" class="size-4 opacity-0 data-[active=true]:opacity-100" />
127
+ </button>
128
+ )
129
+ })}
130
+ </div>
131
+ </div>
132
+ </div>
133
+ )}
134
+ </div>
135
+
136
+ {content && (
137
+ <div class="flex-1">
138
+ <slot>
139
+ {items.map((item, index) => {
140
+ const itemVal = item[valueKey] ?? item.value ?? String(index)
141
+ const active = itemVal === initialValue
142
+ return (
143
+ <div
144
+ class={classes.content}
145
+ data-slot="content"
146
+ role="tabpanel"
147
+ data-value={itemVal}
148
+ hidden={active ? undefined : "until-found"}
149
+ >
150
+ {item.content}
151
+ </div>
152
+ )
153
+ })}
154
+ </slot>
155
+ </div>
156
+ )}
157
+ </div>
158
+ </rla-tabs>
159
+
160
+ <style>
161
+ .rla-tb-host {
162
+ width: 100%;
163
+ }
164
+ .rla-tb-popover {
165
+ margin: 0;
166
+ inset: auto;
167
+ position: absolute;
168
+ }
169
+ .rla-tb-popover:popover-open {
170
+ display: block;
171
+ }
172
+ .rla-tb-pop-rendered {
173
+ display: none;
174
+ }
175
+ @supports not (content-visibility: hidden) {
176
+ [hidden="until-found"] {
177
+ display: none !important;
178
+ }
179
+ }
180
+ </style>
181
+
182
+ <script>
183
+ if (typeof HTMLElement !== 'undefined') {
184
+ class RLATabs extends HTMLElement {
185
+ private tabsCleanup: (() => void) | null = null;
186
+ private resizeObserver: ResizeObserver | null = null;
187
+
188
+ connectedCallback() {
189
+ this.style.display = 'contents';
190
+ const container = this.querySelector("[data-tabs-container]") as HTMLElement | null;
191
+ if (!container) return;
192
+
193
+ const list = container.querySelector("[data-slot=list]") as HTMLElement | null;
194
+ const indicator = container.querySelector("[data-slot=indicator]") as HTMLElement | null;
195
+ const orientation = this.dataset["orientation"] || "horizontal";
196
+ const isVertical = orientation === "vertical";
197
+
198
+ const updateIndicator = (activeTrigger: HTMLElement | null) => {
199
+ if (!indicator || !list || !activeTrigger) {
200
+ if (indicator) indicator.style.display = 'none';
201
+ return;
202
+ }
203
+
204
+ // If active trigger is hidden due to container queries, position near overflow button
205
+ if (getComputedStyle(activeTrigger).display === 'none') {
206
+ const overflowBtn = list.querySelector('.rla-tb-overflow-btn') as HTMLElement | null;
207
+ if (overflowBtn) activeTrigger = overflowBtn;
208
+ }
209
+
210
+ const listRect = list.getBoundingClientRect();
211
+ const triggerRect = activeTrigger.getBoundingClientRect();
212
+
213
+ if (triggerRect.width === 0 && triggerRect.height === 0) {
214
+ indicator.style.display = 'none';
215
+ return;
216
+ }
217
+
218
+ indicator.style.display = 'block';
219
+
220
+ if (isVertical) {
221
+ const top = triggerRect.top - listRect.top + list.scrollTop;
222
+ const height = triggerRect.height;
223
+ indicator.style.transform = `translateY(${top}px)`;
224
+ indicator.style.height = `${height}px`;
225
+ } else {
226
+ const left = triggerRect.left - listRect.left + list.scrollLeft;
227
+ const width = triggerRect.width;
228
+ indicator.style.transform = `translateX(${left}px)`;
229
+ indicator.style.width = `${width}px`;
230
+ }
231
+ };
232
+
233
+ const containerValue = container.dataset["value"];
234
+ const triggers = container.querySelectorAll("[role=tab]");
235
+ const panels = container.querySelectorAll("[role=tabpanel]");
236
+ const popoverItems = container.querySelectorAll("[data-popover-item]");
237
+
238
+ let activeTrigger: HTMLElement | null = null;
239
+
240
+ const syncState = (targetValue: string | undefined) => {
241
+ triggers.forEach(trigger => {
242
+ const el = trigger as HTMLElement;
243
+ const val = el.dataset["value"];
244
+ const isActive = val === targetValue;
245
+ el.setAttribute("data-state", isActive ? "active" : "inactive");
246
+ if (isActive) activeTrigger = el;
247
+ });
248
+
249
+ popoverItems.forEach(item => {
250
+ const el = item as HTMLElement;
251
+ const val = el.dataset["tabOverflowValue"];
252
+ if (val === targetValue) {
253
+ el.setAttribute("data-active", "true");
254
+ el.classList.add("bg-muted/80", "font-medium");
255
+ } else {
256
+ el.removeAttribute("data-active");
257
+ el.classList.remove("bg-muted/80", "font-medium");
258
+ }
259
+ });
260
+
261
+ panels.forEach(panel => {
262
+ const val = (panel as HTMLElement).dataset["value"];
263
+ if (val === targetValue) {
264
+ panel.removeAttribute("hidden");
265
+ } else {
266
+ panel.setAttribute("hidden", "until-found");
267
+ }
268
+ });
269
+ };
270
+
271
+ syncState(containerValue);
272
+
273
+ requestAnimationFrame(() => updateIndicator(activeTrigger));
274
+
275
+ if (window.ResizeObserver && list) {
276
+ this.resizeObserver = new ResizeObserver(() => {
277
+ const currentActive = list.querySelector("[role=tab][data-state=active]") as HTMLElement | null;
278
+ updateIndicator(currentActive);
279
+ });
280
+ this.resizeObserver.observe(list);
281
+ }
282
+
283
+ const clickCleanups: (() => void)[] = [];
284
+ const beforeMatchCleanups: (() => void)[] = [];
285
+
286
+ // Watch for browser Find-in-Page auto-expands
287
+ panels.forEach(panel => {
288
+ const onBeforeMatch = () => {
289
+ const targetValue = (panel as HTMLElement).dataset["value"];
290
+ const trigger = container.querySelector(`[role=tab][data-value="${targetValue}"]`) as HTMLButtonElement | null;
291
+ if (trigger) {
292
+ trigger.click();
293
+ } else {
294
+ syncState(targetValue);
295
+ }
296
+ };
297
+ panel.addEventListener("beforematch", onBeforeMatch);
298
+ beforeMatchCleanups.push(() => panel.removeEventListener("beforematch", onBeforeMatch));
299
+ });
300
+
301
+ triggers.forEach(triggerNode => {
302
+ const trigger = triggerNode as HTMLButtonElement;
303
+ const onClick = () => {
304
+ if (trigger.disabled) return;
305
+ const targetValue = trigger.dataset["value"];
306
+ syncState(targetValue);
307
+ updateIndicator(trigger);
308
+ };
309
+ trigger.addEventListener("click", onClick);
310
+ clickCleanups.push(() => trigger.removeEventListener("click", onClick));
311
+ });
312
+
313
+ popoverItems.forEach(itemNode => {
314
+ const item = itemNode as HTMLButtonElement;
315
+ const onClick = () => {
316
+ const targetValue = item.dataset["tabOverflowValue"];
317
+ syncState(targetValue);
318
+ const activeTrig = container.querySelector(`[role=tab][data-value="${targetValue}"]`) as HTMLElement | null;
319
+ updateIndicator(activeTrig);
320
+ // Hide popover after selection
321
+ const popover = item.closest('[popover]') as HTMLElement | null;
322
+ if (popover && 'hidePopover' in popover) {
323
+ (popover as any).hidePopover();
324
+ }
325
+ };
326
+ item.addEventListener("click", onClick);
327
+ clickCleanups.push(() => item.removeEventListener("click", onClick));
328
+ });
329
+
330
+ this.tabsCleanup = () => {
331
+ clickCleanups.forEach(fn => fn());
332
+ beforeMatchCleanups.forEach(fn => fn());
333
+ if (this.resizeObserver) {
334
+ this.resizeObserver.disconnect();
335
+ this.resizeObserver = null;
336
+ }
337
+ };
338
+ }
339
+
340
+ disconnectedCallback() {
341
+ if (this.tabsCleanup) {
342
+ this.tabsCleanup();
343
+ this.tabsCleanup = null;
344
+ }
345
+ }
346
+ }
347
+
348
+ if (!customElements.get('rla-tabs')) {
349
+ customElements.define('rla-tabs', RLATabs);
350
+ }
351
+ }
352
+ </script>