tyrell-components 1.0.0-RC13 → 1.0.0-RC15

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 (358) hide show
  1. package/README.md +10 -2
  2. package/css/tyrell-theme.css +192 -180
  3. package/css/tyrell.css +23 -0
  4. package/dist/tyrell-colors-static.css +1 -316
  5. package/dist/tyrell-theme.css +1 -1454
  6. package/dist/tyrell.css +1 -1371
  7. package/dist/tyrell.js +1 -1
  8. package/lib/base/ty-component.d.ts +20 -10
  9. package/lib/base/ty-component.js +196 -319
  10. package/lib/components/button.d.ts +3 -17
  11. package/lib/components/button.js +261 -295
  12. package/lib/components/calendar-month.d.ts +4 -50
  13. package/lib/components/calendar-month.js +443 -582
  14. package/lib/components/calendar-navigation.d.ts +3 -48
  15. package/lib/components/calendar-navigation.js +239 -362
  16. package/lib/components/calendar.d.ts +8 -83
  17. package/lib/components/calendar.js +616 -829
  18. package/lib/components/checkbox.d.ts +5 -72
  19. package/lib/components/checkbox.js +273 -416
  20. package/lib/components/copy.d.ts +4 -57
  21. package/lib/components/copy.js +280 -397
  22. package/lib/components/date-picker.d.ts +21 -163
  23. package/lib/components/date-picker.js +1143 -1674
  24. package/lib/components/file-upload.d.ts +9 -15
  25. package/lib/components/file-upload.js +404 -426
  26. package/lib/components/icon.d.ts +3 -24
  27. package/lib/components/icon.js +141 -230
  28. package/lib/components/input.d.ts +8 -108
  29. package/lib/components/input.js +593 -784
  30. package/lib/components/modal.d.ts +17 -26
  31. package/lib/components/modal.js +340 -484
  32. package/lib/components/option.d.ts +2 -9
  33. package/lib/components/option.js +168 -216
  34. package/lib/components/popup.d.ts +2 -17
  35. package/lib/components/popup.js +249 -380
  36. package/lib/components/radio.d.ts +5 -25
  37. package/lib/components/radio.js +407 -461
  38. package/lib/components/resize-observer.d.ts +3 -30
  39. package/lib/components/resize-observer.js +69 -103
  40. package/lib/components/scroll-container.d.ts +2 -12
  41. package/lib/components/scroll-container.js +293 -338
  42. package/lib/components/select.d.ts +87 -159
  43. package/lib/components/select.js +1609 -2090
  44. package/lib/components/selected-tags.d.ts +5 -48
  45. package/lib/components/selected-tags.js +188 -276
  46. package/lib/components/step.d.ts +1 -6
  47. package/lib/components/step.js +62 -68
  48. package/lib/components/switch.d.ts +1 -12
  49. package/lib/components/switch.js +214 -247
  50. package/lib/components/tab.d.ts +1 -6
  51. package/lib/components/tab.js +62 -69
  52. package/lib/components/tabs.d.ts +6 -63
  53. package/lib/components/tabs.js +545 -777
  54. package/lib/components/tag.d.ts +3 -37
  55. package/lib/components/tag.js +236 -335
  56. package/lib/components/textarea.d.ts +7 -87
  57. package/lib/components/textarea.js +457 -627
  58. package/lib/components/tooltip.d.ts +3 -11
  59. package/lib/components/tooltip.js +307 -443
  60. package/lib/components/wizard.d.ts +6 -54
  61. package/lib/components/wizard.js +405 -618
  62. package/lib/icons/fontawesome/brands.d.ts +0 -1
  63. package/lib/icons/fontawesome/brands.js +0 -8
  64. package/lib/icons/fontawesome/regular.d.ts +0 -1
  65. package/lib/icons/fontawesome/regular.js +0 -8
  66. package/lib/icons/fontawesome/solid.d.ts +0 -1
  67. package/lib/icons/fontawesome/solid.js +0 -8
  68. package/lib/icons/heroicons/micro.d.ts +0 -1
  69. package/lib/icons/heroicons/micro.js +392 -400
  70. package/lib/icons/heroicons/mini.d.ts +0 -1
  71. package/lib/icons/heroicons/mini.js +382 -390
  72. package/lib/icons/heroicons/outline.d.ts +0 -1
  73. package/lib/icons/heroicons/outline.js +337 -345
  74. package/lib/icons/heroicons/solid.d.ts +0 -1
  75. package/lib/icons/heroicons/solid.js +407 -415
  76. package/lib/icons/lucide.d.ts +0 -1
  77. package/lib/icons/lucide.js +24479 -26351
  78. package/lib/icons/material/filled.d.ts +0 -1
  79. package/lib/icons/material/filled.js +9653 -9663
  80. package/lib/icons/material/outlined.d.ts +0 -1
  81. package/lib/icons/material/outlined.js +10271 -10281
  82. package/lib/icons/material/round.d.ts +0 -1
  83. package/lib/icons/material/round.js +10495 -10505
  84. package/lib/icons/material/sharp.d.ts +0 -1
  85. package/lib/icons/material/sharp.js +9905 -9915
  86. package/lib/icons/material/two-tone.d.ts +0 -1
  87. package/lib/icons/material/two-tone.js +12792 -12802
  88. package/lib/index.d.ts +0 -1
  89. package/lib/index.js +21 -41
  90. package/lib/styles/button.d.ts +2 -20
  91. package/lib/styles/button.js +202 -404
  92. package/lib/styles/calendar-month.d.ts +1 -5
  93. package/lib/styles/calendar-month.js +127 -244
  94. package/lib/styles/calendar-navigation.d.ts +2 -6
  95. package/lib/styles/calendar-navigation.js +64 -104
  96. package/lib/styles/calendar.d.ts +1 -2
  97. package/lib/styles/calendar.js +9 -22
  98. package/lib/styles/checkbox.d.ts +0 -1
  99. package/lib/styles/checkbox.js +45 -94
  100. package/lib/styles/copy.d.ts +0 -1
  101. package/lib/styles/copy.js +67 -106
  102. package/lib/styles/custom-scrollbar.d.ts +1 -2
  103. package/lib/styles/custom-scrollbar.js +80 -115
  104. package/lib/styles/date-picker.d.ts +1 -5
  105. package/lib/styles/date-picker.js +261 -335
  106. package/lib/styles/field-size.d.ts +6 -0
  107. package/lib/styles/field-size.js +91 -0
  108. package/lib/styles/file-upload.d.ts +1 -2
  109. package/lib/styles/file-upload.js +143 -188
  110. package/lib/styles/icon.d.ts +2 -6
  111. package/lib/styles/icon.js +92 -170
  112. package/lib/styles/input.d.ts +1 -6
  113. package/lib/styles/input.js +125 -290
  114. package/lib/styles/modal.d.ts +1 -2
  115. package/lib/styles/modal.js +71 -108
  116. package/lib/styles/option.d.ts +2 -6
  117. package/lib/styles/option.js +49 -64
  118. package/lib/styles/popup.d.ts +1 -2
  119. package/lib/styles/popup.js +42 -77
  120. package/lib/styles/radio.d.ts +0 -1
  121. package/lib/styles/radio.js +69 -110
  122. package/lib/styles/resize-observer.d.ts +1 -2
  123. package/lib/styles/resize-observer.js +2 -12
  124. package/lib/styles/scroll-container.d.ts +1 -2
  125. package/lib/styles/scroll-container.js +100 -149
  126. package/lib/styles/select-base.d.ts +0 -1
  127. package/lib/styles/select-base.js +597 -785
  128. package/lib/styles/select.d.ts +1 -2
  129. package/lib/styles/select.js +140 -236
  130. package/lib/styles/step.d.ts +1 -2
  131. package/lib/styles/step.js +22 -33
  132. package/lib/styles/switch.d.ts +0 -1
  133. package/lib/styles/switch.js +34 -73
  134. package/lib/styles/tab.d.ts +1 -2
  135. package/lib/styles/tab.js +22 -34
  136. package/lib/styles/tabs.d.ts +3 -11
  137. package/lib/styles/tabs.js +190 -222
  138. package/lib/styles/tag.d.ts +1 -5
  139. package/lib/styles/tag.js +136 -218
  140. package/lib/styles/textarea.d.ts +1 -2
  141. package/lib/styles/textarea.js +210 -319
  142. package/lib/styles/tooltip.d.ts +0 -1
  143. package/lib/styles/tooltip.js +3 -13
  144. package/lib/styles/wizard.d.ts +5 -21
  145. package/lib/styles/wizard.js +170 -306
  146. package/lib/types/common.d.ts +2 -2
  147. package/lib/types/common.js +6 -17
  148. package/lib/utils/calendar-utils.d.ts +4 -21
  149. package/lib/utils/calendar-utils.js +100 -273
  150. package/lib/utils/custom-scrollbar.d.ts +6 -20
  151. package/lib/utils/custom-scrollbar.js +273 -317
  152. package/lib/utils/flavor-sheet.d.ts +0 -1
  153. package/lib/utils/flavor-sheet.js +14 -37
  154. package/lib/utils/icon-registry.d.ts +0 -2
  155. package/lib/utils/icon-registry.js +137 -243
  156. package/lib/utils/icons.d.ts +0 -1
  157. package/lib/utils/icons.js +0 -9
  158. package/lib/utils/loader-registry.d.ts +0 -1
  159. package/lib/utils/loader-registry.js +3 -35
  160. package/lib/utils/locale.d.ts +2 -23
  161. package/lib/utils/locale.js +34 -120
  162. package/lib/utils/mobile.d.ts +0 -1
  163. package/lib/utils/mobile.js +1 -18
  164. package/lib/utils/number-format.d.ts +6 -3
  165. package/lib/utils/number-format.js +54 -110
  166. package/lib/utils/positioning.d.ts +50 -7
  167. package/lib/utils/positioning.js +224 -234
  168. package/lib/utils/property-capture.d.ts +5 -27
  169. package/lib/utils/property-capture.js +36 -144
  170. package/lib/utils/property-manager.d.ts +0 -1
  171. package/lib/utils/property-manager.js +126 -196
  172. package/lib/utils/resize-observer.d.ts +0 -1
  173. package/lib/utils/resize-observer.js +23 -59
  174. package/lib/utils/scroll-lock.d.ts +4 -11
  175. package/lib/utils/scroll-lock.js +65 -172
  176. package/lib/utils/styles.d.ts +0 -1
  177. package/lib/utils/styles.js +17 -41
  178. package/lib/version.d.ts +1 -2
  179. package/lib/version.js +1 -11
  180. package/package.json +4 -4
  181. package/lib/base/ty-component.d.ts.map +0 -1
  182. package/lib/base/ty-component.js.map +0 -1
  183. package/lib/components/button.d.ts.map +0 -1
  184. package/lib/components/button.js.map +0 -1
  185. package/lib/components/calendar-month.d.ts.map +0 -1
  186. package/lib/components/calendar-month.js.map +0 -1
  187. package/lib/components/calendar-navigation.d.ts.map +0 -1
  188. package/lib/components/calendar-navigation.js.map +0 -1
  189. package/lib/components/calendar.d.ts.map +0 -1
  190. package/lib/components/calendar.js.map +0 -1
  191. package/lib/components/checkbox.d.ts.map +0 -1
  192. package/lib/components/checkbox.js.map +0 -1
  193. package/lib/components/copy.d.ts.map +0 -1
  194. package/lib/components/copy.js.map +0 -1
  195. package/lib/components/date-picker.d.ts.map +0 -1
  196. package/lib/components/date-picker.js.map +0 -1
  197. package/lib/components/file-upload.d.ts.map +0 -1
  198. package/lib/components/file-upload.js.map +0 -1
  199. package/lib/components/icon.d.ts.map +0 -1
  200. package/lib/components/icon.js.map +0 -1
  201. package/lib/components/input.d.ts.map +0 -1
  202. package/lib/components/input.js.map +0 -1
  203. package/lib/components/modal.d.ts.map +0 -1
  204. package/lib/components/modal.js.map +0 -1
  205. package/lib/components/option.d.ts.map +0 -1
  206. package/lib/components/option.js.map +0 -1
  207. package/lib/components/popup.d.ts.map +0 -1
  208. package/lib/components/popup.js.map +0 -1
  209. package/lib/components/radio.d.ts.map +0 -1
  210. package/lib/components/radio.js.map +0 -1
  211. package/lib/components/resize-observer.d.ts.map +0 -1
  212. package/lib/components/resize-observer.js.map +0 -1
  213. package/lib/components/scroll-container.d.ts.map +0 -1
  214. package/lib/components/scroll-container.js.map +0 -1
  215. package/lib/components/select.d.ts.map +0 -1
  216. package/lib/components/select.js.map +0 -1
  217. package/lib/components/selected-tags.d.ts.map +0 -1
  218. package/lib/components/selected-tags.js.map +0 -1
  219. package/lib/components/step.d.ts.map +0 -1
  220. package/lib/components/step.js.map +0 -1
  221. package/lib/components/switch.d.ts.map +0 -1
  222. package/lib/components/switch.js.map +0 -1
  223. package/lib/components/tab.d.ts.map +0 -1
  224. package/lib/components/tab.js.map +0 -1
  225. package/lib/components/tabs.d.ts.map +0 -1
  226. package/lib/components/tabs.js.map +0 -1
  227. package/lib/components/tag.d.ts.map +0 -1
  228. package/lib/components/tag.js.map +0 -1
  229. package/lib/components/textarea.d.ts.map +0 -1
  230. package/lib/components/textarea.js.map +0 -1
  231. package/lib/components/tooltip.d.ts.map +0 -1
  232. package/lib/components/tooltip.js.map +0 -1
  233. package/lib/components/wizard.d.ts.map +0 -1
  234. package/lib/components/wizard.js.map +0 -1
  235. package/lib/icons/fontawesome/brands.d.ts.map +0 -1
  236. package/lib/icons/fontawesome/brands.js.map +0 -1
  237. package/lib/icons/fontawesome/regular.d.ts.map +0 -1
  238. package/lib/icons/fontawesome/regular.js.map +0 -1
  239. package/lib/icons/fontawesome/solid.d.ts.map +0 -1
  240. package/lib/icons/fontawesome/solid.js.map +0 -1
  241. package/lib/icons/heroicons/micro.d.ts.map +0 -1
  242. package/lib/icons/heroicons/micro.js.map +0 -1
  243. package/lib/icons/heroicons/mini.d.ts.map +0 -1
  244. package/lib/icons/heroicons/mini.js.map +0 -1
  245. package/lib/icons/heroicons/outline.d.ts.map +0 -1
  246. package/lib/icons/heroicons/outline.js.map +0 -1
  247. package/lib/icons/heroicons/solid.d.ts.map +0 -1
  248. package/lib/icons/heroicons/solid.js.map +0 -1
  249. package/lib/icons/lucide.d.ts.map +0 -1
  250. package/lib/icons/lucide.js.map +0 -1
  251. package/lib/icons/material/filled.d.ts.map +0 -1
  252. package/lib/icons/material/filled.js.map +0 -1
  253. package/lib/icons/material/outlined.d.ts.map +0 -1
  254. package/lib/icons/material/outlined.js.map +0 -1
  255. package/lib/icons/material/round.d.ts.map +0 -1
  256. package/lib/icons/material/round.js.map +0 -1
  257. package/lib/icons/material/sharp.d.ts.map +0 -1
  258. package/lib/icons/material/sharp.js.map +0 -1
  259. package/lib/icons/material/two-tone.d.ts.map +0 -1
  260. package/lib/icons/material/two-tone.js.map +0 -1
  261. package/lib/index.d.ts.map +0 -1
  262. package/lib/index.js.map +0 -1
  263. package/lib/styles/button.d.ts.map +0 -1
  264. package/lib/styles/button.js.map +0 -1
  265. package/lib/styles/calendar-month.d.ts.map +0 -1
  266. package/lib/styles/calendar-month.js.map +0 -1
  267. package/lib/styles/calendar-navigation.d.ts.map +0 -1
  268. package/lib/styles/calendar-navigation.js.map +0 -1
  269. package/lib/styles/calendar.d.ts.map +0 -1
  270. package/lib/styles/calendar.js.map +0 -1
  271. package/lib/styles/checkbox.d.ts.map +0 -1
  272. package/lib/styles/checkbox.js.map +0 -1
  273. package/lib/styles/copy.d.ts.map +0 -1
  274. package/lib/styles/copy.js.map +0 -1
  275. package/lib/styles/custom-scrollbar.d.ts.map +0 -1
  276. package/lib/styles/custom-scrollbar.js.map +0 -1
  277. package/lib/styles/date-picker.d.ts.map +0 -1
  278. package/lib/styles/date-picker.js.map +0 -1
  279. package/lib/styles/file-upload.d.ts.map +0 -1
  280. package/lib/styles/file-upload.js.map +0 -1
  281. package/lib/styles/icon.d.ts.map +0 -1
  282. package/lib/styles/icon.js.map +0 -1
  283. package/lib/styles/input.d.ts.map +0 -1
  284. package/lib/styles/input.js.map +0 -1
  285. package/lib/styles/modal.d.ts.map +0 -1
  286. package/lib/styles/modal.js.map +0 -1
  287. package/lib/styles/multiselect.d.ts +0 -8
  288. package/lib/styles/multiselect.d.ts.map +0 -1
  289. package/lib/styles/multiselect.js +0 -849
  290. package/lib/styles/multiselect.js.map +0 -1
  291. package/lib/styles/option.d.ts.map +0 -1
  292. package/lib/styles/option.js.map +0 -1
  293. package/lib/styles/popup.d.ts.map +0 -1
  294. package/lib/styles/popup.js.map +0 -1
  295. package/lib/styles/radio.d.ts.map +0 -1
  296. package/lib/styles/radio.js.map +0 -1
  297. package/lib/styles/resize-observer.d.ts.map +0 -1
  298. package/lib/styles/resize-observer.js.map +0 -1
  299. package/lib/styles/scroll-container.d.ts.map +0 -1
  300. package/lib/styles/scroll-container.js.map +0 -1
  301. package/lib/styles/select-base.d.ts.map +0 -1
  302. package/lib/styles/select-base.js.map +0 -1
  303. package/lib/styles/select.d.ts.map +0 -1
  304. package/lib/styles/select.js.map +0 -1
  305. package/lib/styles/step.d.ts.map +0 -1
  306. package/lib/styles/step.js.map +0 -1
  307. package/lib/styles/switch.d.ts.map +0 -1
  308. package/lib/styles/switch.js.map +0 -1
  309. package/lib/styles/tab.d.ts.map +0 -1
  310. package/lib/styles/tab.js.map +0 -1
  311. package/lib/styles/tabs.d.ts.map +0 -1
  312. package/lib/styles/tabs.js.map +0 -1
  313. package/lib/styles/tag.d.ts.map +0 -1
  314. package/lib/styles/tag.js.map +0 -1
  315. package/lib/styles/textarea.d.ts.map +0 -1
  316. package/lib/styles/textarea.js.map +0 -1
  317. package/lib/styles/tooltip.d.ts.map +0 -1
  318. package/lib/styles/tooltip.js.map +0 -1
  319. package/lib/styles/wizard.d.ts.map +0 -1
  320. package/lib/styles/wizard.js.map +0 -1
  321. package/lib/types/common.d.ts.map +0 -1
  322. package/lib/types/common.js.map +0 -1
  323. package/lib/utils/calendar-utils.d.ts.map +0 -1
  324. package/lib/utils/calendar-utils.js.map +0 -1
  325. package/lib/utils/custom-scrollbar.d.ts.map +0 -1
  326. package/lib/utils/custom-scrollbar.js.map +0 -1
  327. package/lib/utils/flavor-sheet.d.ts.map +0 -1
  328. package/lib/utils/flavor-sheet.js.map +0 -1
  329. package/lib/utils/icon-registry.d.ts.map +0 -1
  330. package/lib/utils/icon-registry.js.map +0 -1
  331. package/lib/utils/icons.d.ts.map +0 -1
  332. package/lib/utils/icons.js.map +0 -1
  333. package/lib/utils/loader-registry.d.ts.map +0 -1
  334. package/lib/utils/loader-registry.js.map +0 -1
  335. package/lib/utils/locale.d.ts.map +0 -1
  336. package/lib/utils/locale.js.map +0 -1
  337. package/lib/utils/mobile.d.ts.map +0 -1
  338. package/lib/utils/mobile.js.map +0 -1
  339. package/lib/utils/number-format.d.ts.map +0 -1
  340. package/lib/utils/number-format.js.map +0 -1
  341. package/lib/utils/parse-boolean.d.ts +0 -39
  342. package/lib/utils/parse-boolean.d.ts.map +0 -1
  343. package/lib/utils/parse-boolean.js +0 -58
  344. package/lib/utils/parse-boolean.js.map +0 -1
  345. package/lib/utils/positioning.d.ts.map +0 -1
  346. package/lib/utils/positioning.js.map +0 -1
  347. package/lib/utils/property-capture.d.ts.map +0 -1
  348. package/lib/utils/property-capture.js.map +0 -1
  349. package/lib/utils/property-manager.d.ts.map +0 -1
  350. package/lib/utils/property-manager.js.map +0 -1
  351. package/lib/utils/resize-observer.d.ts.map +0 -1
  352. package/lib/utils/resize-observer.js.map +0 -1
  353. package/lib/utils/scroll-lock.d.ts.map +0 -1
  354. package/lib/utils/scroll-lock.js.map +0 -1
  355. package/lib/utils/styles.d.ts.map +0 -1
  356. package/lib/utils/styles.js.map +0 -1
  357. package/lib/version.d.ts.map +0 -1
  358. package/lib/version.js.map +0 -1
@@ -1,818 +1,586 @@
1
- /**
2
- * Tabs Component
3
- *
4
- * A carousel-based tabs component with smooth animations and fixed container dimensions.
5
- * Features horizontal sliding transitions, smart positioning marker, and independent panel scrolling.
6
- *
7
- * Features:
8
- * - Carousel animation with smooth sliding transitions
9
- * - Fixed dimensions prevent layout shift between tabs
10
- * - Animated marker that follows active tab button
11
- * - Rich label support via slots (icons, badges, custom content)
12
- * - Custom marker slot for complete active tab styling control
13
- * - Independent panel scrolling with scroll position reset
14
- * - ResizeObserver for responsive percentage widths
15
- * - Smart rendering - only updates DOM when necessary
16
- * - Accessibility with ARIA roles and attributes
17
- * - Top/bottom placement for tab buttons
18
- *
19
- * @example
20
- * <!-- Basic tabs with text labels -->
21
- * <ty-tabs width="800px" height="600px" active="general">
22
- * <ty-tab id="general" label="General Settings">
23
- * <div class="p-6">General content...</div>
24
- * </ty-tab>
25
- * <ty-tab id="advanced" label="Advanced Settings">
26
- * <div class="p-6">Advanced content...</div>
27
- * </ty-tab>
28
- * </ty-tabs>
29
- *
30
- * @example
31
- * <!-- Rich labels with icons and badges -->
32
- * <ty-tabs width="800px" height="600px">
33
- * <!-- Rich label as direct child -->
34
- * <span slot="label-profile" class="flex items-center gap-2">
35
- * <ty-icon name="user" size="sm"></ty-icon>
36
- * Profile
37
- * </span>
38
- *
39
- * <span slot="label-notifications" class="flex items-center gap-2">
40
- * <ty-icon name="bell" size="sm"></ty-icon>
41
- * Notifications
42
- * <span class="ty-bg-danger ty-text-danger++ px-2 py-0.5 rounded-full text-xs">5</span>
43
- * </span>
44
- *
45
- * <!-- Tab panels -->
46
- * <ty-tab id="profile">...</ty-tab>
47
- * <ty-tab id="notifications">...</ty-tab>
48
- * </ty-tabs>
49
- *
50
- * @example
51
- * <!-- Custom marker styling -->
52
- * <ty-tabs width="100%" height="400px">
53
- * <!-- Gradient marker slot -->
54
- * <div slot="marker" style="
55
- * background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
56
- * border-radius: 0.5rem;
57
- * box-shadow: 0 2px 8px rgba(0,0,0,0.15);
58
- * "></div>
59
- *
60
- * <ty-tab id="tab1" label="Dashboard">...</ty-tab>
61
- * <ty-tab id="tab2" label="Analytics">...</ty-tab>
62
- * </ty-tabs>
63
- */
64
1
  import { ensureStyles } from '../utils/styles.js';
65
2
  import { tabsStyles } from '../styles/tabs.js';
66
- // ============================================================================
67
- // WeakMaps for State Management
68
- // ============================================================================
69
3
  const eventHandlers = new WeakMap();
70
4
  const resizeObservers = new WeakMap();
71
- // ============================================================================
72
- // Helper Functions - Attribute Parsing
73
- // ============================================================================
74
- /**
75
- * Extract tabs configuration from element attributes
76
- */
5
+ const pendingInitialSnap = new WeakSet();
77
6
  function getTabsAttributes(el) {
78
- return {
79
- width: el.getAttribute('width') || '100%',
80
- height: el.getAttribute('height') || '400px',
81
- active: el.getAttribute('active'),
82
- placement: (el.getAttribute('placement') || 'top'),
83
- };
84
- }
85
- /**
86
- * Get all ty-tab child elements
87
- */
7
+ return {
8
+ width: el.getAttribute('width') || '100%',
9
+ height: el.getAttribute('height') || '400px',
10
+ active: el.getAttribute('active'),
11
+ placement: (el.getAttribute('placement') || 'top'),
12
+ fixed: el.hasAttribute('fixed'),
13
+ };
14
+ }
88
15
  function getChildTabs(el) {
89
- return Array.from(el.querySelectorAll('ty-tab'));
16
+ return Array.from(el.querySelectorAll('ty-tab'));
90
17
  }
91
- /**
92
- * Get ID from a ty-tab element
93
- */
94
18
  function getTabId(tab) {
95
- return tab.getAttribute('id');
19
+ return tab.getAttribute('id');
20
+ }
21
+ function getSlotLabel(tabsEl, tabId) {
22
+ return tabsEl.querySelector(`:scope > [slot='label-${tabId}']`);
96
23
  }
97
- /**
98
- * Check if ty-tabs has a direct child label slot for this tab-id.
99
- * Looks in ty-tabs' light DOM for slot='label-{tab-id}' elements.
100
- */
101
24
  function hasSlotLabel(tabsEl, tabId) {
102
- return tabsEl.querySelector(`[slot='label-${tabId}']`) !== null;
25
+ return getSlotLabel(tabsEl, tabId) !== null;
103
26
  }
104
- /**
105
- * Determine label type: 'slot' (rich content) or 'text' (simple attribute).
106
- * Checks ty-tabs element for slot='label-{tab-id}' direct children.
107
- */
108
27
  function getTabLabelType(tabsEl, tab) {
109
- const tabId = getTabId(tab);
110
- if (tabId && hasSlotLabel(tabsEl, tabId)) {
111
- return 'slot';
112
- }
113
- return 'text';
114
- }
115
- /**
116
- * Check if tab is disabled
117
- */
28
+ const tabId = getTabId(tab);
29
+ if (tabId && hasSlotLabel(tabsEl, tabId)) {
30
+ return 'slot';
31
+ }
32
+ return 'text';
33
+ }
118
34
  function isTabDisabled(tab) {
119
- return tab.hasAttribute('disabled');
35
+ return tab.hasAttribute('disabled');
120
36
  }
121
- /**
122
- * Check if user provided custom marker content via slot
123
- */
124
37
  function hasCustomMarker(el) {
125
- return el.querySelector('[slot="marker"]') !== null;
126
- }
127
- // ============================================================================
128
- // Helper Functions - Active Tab Management
129
- // ============================================================================
130
- /**
131
- * Find index of tab with given ID
132
- */
38
+ return el.querySelector('[slot="marker"]') !== null;
39
+ }
133
40
  function findTabIndex(tabs, tabId) {
134
- const index = tabs.findIndex(tab => getTabId(tab) === tabId);
135
- return index >= 0 ? index : undefined;
41
+ const index = tabs.findIndex(tab => getTabId(tab) === tabId);
42
+ return index >= 0 ? index : undefined;
43
+ }
44
+ function currentActive(el) {
45
+ const tabs = getChildTabs(el);
46
+ const id = getActiveTabId(el, tabs);
47
+ return { id, index: (id ? findTabIndex(tabs, id) : undefined) ?? 0 };
136
48
  }
137
- /**
138
- * Get the active tab ID, defaulting to first tab if not specified
139
- */
140
49
  function getActiveTabId(el, tabs) {
141
- const activeAttr = el.getAttribute('active');
142
- if (activeAttr && findTabIndex(tabs, activeAttr) !== undefined) {
143
- return activeAttr;
144
- }
145
- // Default to first tab
146
- if (tabs.length > 0) {
147
- return getTabId(tabs[0]);
148
- }
149
- return null;
150
- }
151
- /**
152
- * Set the active tab by ID
153
- */
50
+ const activeAttr = el.getAttribute('active');
51
+ if (activeAttr && findTabIndex(tabs, activeAttr) !== undefined) {
52
+ return activeAttr;
53
+ }
54
+ if (tabs.length > 0) {
55
+ return getTabId(tabs[0]);
56
+ }
57
+ return null;
58
+ }
154
59
  function setActiveTab(el, tabId) {
155
- el.setAttribute('active', tabId);
60
+ el.setAttribute('active', tabId);
156
61
  }
157
- /**
158
- * Dispatch ty-tab-change event
159
- */
160
62
  function dispatchTabChangeEvent(el, activeId, activeIndex, previousId, previousIndex) {
161
- const event = new CustomEvent('ty-tab-change', {
162
- detail: {
163
- activeId,
164
- activeIndex,
165
- previousId,
166
- previousIndex,
167
- },
168
- bubbles: true,
169
- cancelable: false,
170
- });
171
- el.dispatchEvent(event);
172
- }
173
- // ============================================================================
174
- // Event Handlers - Tab Button Click
175
- // ============================================================================
176
- /**
177
- * Handle tab button click
178
- */
63
+ const event = new CustomEvent('ty-tab-change', {
64
+ detail: {
65
+ activeId,
66
+ activeIndex,
67
+ previousId,
68
+ previousIndex,
69
+ },
70
+ bubbles: true,
71
+ cancelable: false,
72
+ });
73
+ el.dispatchEvent(event);
74
+ }
179
75
  function handleTabClick(el, tabId, event) {
180
- event.preventDefault();
181
- event.stopPropagation();
182
- // Set the active attribute (which will trigger state update)
183
- setActiveTab(el, tabId);
184
- }
185
- /**
186
- * Cleanup existing event listeners
187
- */
76
+ event.preventDefault();
77
+ event.stopPropagation();
78
+ setActiveTab(el, tabId);
79
+ }
188
80
  function cleanupEventListeners(el) {
189
- const handlers = eventHandlers.get(el);
190
- if (!handlers)
191
- return;
192
- const shadowRoot = el.shadowRoot;
193
- if (!shadowRoot)
194
- return;
195
- // Remove all tab click handlers
196
- handlers.tabClickHandlers.forEach((handler, tabId) => {
197
- const button = shadowRoot.querySelector(`[data-tab-id='${tabId}']`);
198
- if (button) {
199
- button.removeEventListener('pointerdown', handler);
200
- }
201
- });
202
- handlers.tabKeyClickHandlers.forEach((handler, tabId) => {
203
- const button = shadowRoot.querySelector(`[data-tab-id='${tabId}']`);
204
- if (button) {
205
- button.removeEventListener('click', handler);
206
- }
207
- });
208
- handlers.tabClickHandlers.clear();
209
- handlers.tabKeyClickHandlers.clear();
210
- }
211
- /**
212
- * Setup event listeners for tab button clicks
213
- */
81
+ const handlers = eventHandlers.get(el);
82
+ if (!handlers)
83
+ return;
84
+ const shadowRoot = el.shadowRoot;
85
+ if (!shadowRoot)
86
+ return;
87
+ handlers.tabClickHandlers.forEach((handler, tabId) => {
88
+ const button = shadowRoot.querySelector(`[data-tab-id='${tabId}']`);
89
+ if (button) {
90
+ button.removeEventListener('pointerdown', handler);
91
+ }
92
+ });
93
+ handlers.tabKeyClickHandlers.forEach((handler, tabId) => {
94
+ const button = shadowRoot.querySelector(`[data-tab-id='${tabId}']`);
95
+ if (button) {
96
+ button.removeEventListener('click', handler);
97
+ }
98
+ });
99
+ handlers.tabClickHandlers.clear();
100
+ handlers.tabKeyClickHandlers.clear();
101
+ }
214
102
  function setupEventListeners(el, shadowRoot, tabs) {
215
- // Clean up any existing listeners first
216
- cleanupEventListeners(el);
217
- // Initialize handlers storage
218
- const handlers = {
219
- tabClickHandlers: new Map(),
220
- tabKeyClickHandlers: new Map(),
221
- };
222
- // Add click listener for each tab button
223
- tabs.forEach((tab) => {
224
- const tabId = getTabId(tab);
225
- if (!tabId)
226
- return;
227
- const button = shadowRoot.querySelector(`[data-tab-id='${tabId}']`);
228
- if (button) {
229
- const handler = (e) => handleTabClick(el, tabId, e);
230
- // pointerdown: snappy mouse/touch activation (no native click delay).
231
- // click, gated to event.detail === 0: native <button> keyboard
232
- // activation (Enter/Space) fires 'click' with detail 0 — real mouse
233
- // clicks report detail >= 1 — so this covers keyboard users without
234
- // double-firing handleTabClick on an actual pointer click (which
235
- // already fired via pointerdown above). Without this, keyboard-only
236
- // users could Tab to a tab button but had no way to activate it.
237
- const keyClickHandler = (e) => {
238
- if (e.detail === 0)
239
- handleTabClick(el, tabId, e);
240
- };
241
- button.addEventListener('pointerdown', handler);
242
- button.addEventListener('click', keyClickHandler);
243
- handlers.tabClickHandlers.set(tabId, handler);
244
- handlers.tabKeyClickHandlers.set(tabId, keyClickHandler);
245
- }
246
- });
247
- // Store handlers for cleanup
248
- eventHandlers.set(el, handlers);
249
- }
250
- // ============================================================================
251
- // Transform & Positioning Updates
252
- // ============================================================================
253
- /**
254
- * Update the transform on panels-wrapper based on active index and measured width
255
- */
103
+ cleanupEventListeners(el);
104
+ const handlers = {
105
+ tabClickHandlers: new Map(),
106
+ tabKeyClickHandlers: new Map(),
107
+ };
108
+ tabs.forEach((tab) => {
109
+ const tabId = getTabId(tab);
110
+ if (!tabId)
111
+ return;
112
+ const button = shadowRoot.querySelector(`[data-tab-id='${tabId}']`);
113
+ if (button) {
114
+ const handler = (e) => handleTabClick(el, tabId, e);
115
+ const keyClickHandler = (e) => {
116
+ if (e.detail === 0)
117
+ handleTabClick(el, tabId, e);
118
+ };
119
+ button.addEventListener('pointerdown', handler);
120
+ button.addEventListener('click', keyClickHandler);
121
+ handlers.tabClickHandlers.set(tabId, handler);
122
+ handlers.tabKeyClickHandlers.set(tabId, keyClickHandler);
123
+ }
124
+ });
125
+ eventHandlers.set(el, handlers);
126
+ }
256
127
  function updateTransform(el, activeIndex) {
257
- const shadowRoot = el.shadowRoot;
258
- if (!shadowRoot)
259
- return;
260
- const panelsWrapper = shadowRoot.querySelector('.panels-wrapper');
261
- if (!panelsWrapper)
262
- return;
263
- // Measure the actual width of the container
264
- const containerWidth = el.offsetWidth;
265
- const offsetPx = activeIndex * containerWidth;
266
- // Apply transform directly in pixels
267
- panelsWrapper.style.transform = `translateX(-${offsetPx}px)`;
268
- }
269
- /**
270
- * Update ARIA attributes on tab buttons without re-rendering
271
- */
128
+ const shadowRoot = el.shadowRoot;
129
+ if (!shadowRoot)
130
+ return;
131
+ const panelsWrapper = shadowRoot.querySelector('.panels-wrapper');
132
+ if (!panelsWrapper)
133
+ return;
134
+ const containerWidth = el.offsetWidth;
135
+ const offsetPx = activeIndex * containerWidth;
136
+ panelsWrapper.style.transform = `translateX(-${offsetPx}px)`;
137
+ }
272
138
  function updateAriaAttributes(el, shadowRoot, activeId) {
273
- const tabs = getChildTabs(el);
274
- tabs.forEach((tab, idx) => {
275
- const tabId = getTabId(tab);
276
- if (!tabId)
277
- return;
278
- const button = shadowRoot.querySelector(`[data-tab-id='${tabId}']`);
279
- const isActive = tabId === activeId;
280
- if (button) {
281
- button.setAttribute('aria-selected', String(isActive));
282
- button.setAttribute('tabindex', isActive ? '0' : '-1');
283
- // Add data-active attribute for styling slotted content
284
- button.setAttribute('data-active', String(isActive));
285
- // Also set data-active on the slotted label element in light DOM
286
- const slottedLabel = el.querySelector(`[slot='label-${tabId}']`);
287
- if (slottedLabel) {
288
- slottedLabel.setAttribute('data-active', String(isActive));
289
- }
290
- }
291
- });
292
- }
293
- /**
294
- * Update pointer-events, opacity, and data-active on tab panels without re-rendering
295
- */
139
+ const tabs = getChildTabs(el);
140
+ tabs.forEach((tab, idx) => {
141
+ const tabId = getTabId(tab);
142
+ if (!tabId)
143
+ return;
144
+ const button = shadowRoot.querySelector(`[data-tab-id='${tabId}']`);
145
+ const isActive = tabId === activeId;
146
+ if (button) {
147
+ button.setAttribute('aria-selected', String(isActive));
148
+ button.setAttribute('tabindex', isActive ? '0' : '-1');
149
+ button.setAttribute('data-active', String(isActive));
150
+ const slottedLabel = getSlotLabel(el, tabId);
151
+ if (slottedLabel) {
152
+ slottedLabel.setAttribute('data-active', String(isActive));
153
+ }
154
+ }
155
+ });
156
+ }
296
157
  function updatePanelInteraction(el, activeId) {
297
- const tabs = getChildTabs(el);
298
- tabs.forEach((tab) => {
299
- const tabId = getTabId(tab);
300
- if (!tabId)
301
- return;
302
- const isActive = tabId === activeId;
303
- // Set data-active attribute for framework conditional rendering
304
- tab.setAttribute('data-active', String(isActive));
305
- if (isActive) {
306
- tab.style.pointerEvents = 'auto';
307
- tab.style.opacity = '1';
308
- }
309
- else {
310
- tab.style.pointerEvents = 'none';
311
- tab.style.opacity = '0';
312
- }
313
- });
314
- }
315
- /**
316
- * Calculate marker position and dimensions based on active tab button.
317
- * Returns position object with left, top, width, height in pixels, or null if button not found.
318
- */
158
+ const tabs = getChildTabs(el);
159
+ tabs.forEach((tab) => {
160
+ const tabId = getTabId(tab);
161
+ if (!tabId)
162
+ return;
163
+ const isActive = tabId === activeId;
164
+ tab.setAttribute('data-active', String(isActive));
165
+ if (isActive) {
166
+ tab.style.pointerEvents = 'auto';
167
+ tab.style.opacity = '1';
168
+ }
169
+ else {
170
+ tab.style.pointerEvents = 'none';
171
+ tab.style.opacity = '0';
172
+ }
173
+ });
174
+ }
319
175
  function calculateMarkerPosition(el, shadowRoot, activeId) {
320
- const button = shadowRoot.querySelector(`[data-tab-id='${activeId}']`);
321
- const buttonsContainer = shadowRoot.querySelector('.tab-buttons');
322
- if (!button || !buttonsContainer)
323
- return null;
324
- const buttonRect = button.getBoundingClientRect();
325
- // Use offset properties for position relative to container (accounts for padding)
326
- const left = button.offsetLeft;
327
- const top = button.offsetTop;
328
- return {
329
- left,
330
- top,
331
- width: buttonRect.width,
332
- height: buttonRect.height,
333
- };
334
- }
335
- /**
336
- * Update marker wrapper position and dimensions to match active tab button
337
- */
338
- function updateMarker(el, activeId) {
339
- const shadowRoot = el.shadowRoot;
340
- if (!shadowRoot)
341
- return;
342
- const marker = shadowRoot.querySelector('.marker-wrapper');
343
- if (!marker)
344
- return;
345
- const position = calculateMarkerPosition(el, shadowRoot, activeId);
346
- if (!position)
347
- return;
348
- // Snap without animating when the marker was never positioned, or was
349
- // positioned while the tabs were hidden (zero-size rects) — otherwise it
350
- // visibly glides in from 0,0 on first display.
351
- const snap = !marker.style.left || marker.offsetWidth === 0;
352
- if (snap)
353
- marker.style.transition = 'none';
354
- marker.style.left = `${position.left}px`;
355
- marker.style.top = `${position.top}px`;
356
- marker.style.width = `${position.width}px`;
357
- marker.style.height = `${position.height}px`;
358
- if (snap) {
359
- marker.offsetWidth; // flush styles so the snap isn't animated
360
- marker.style.transition = '';
361
- }
362
- }
363
- /**
364
- * Update only the active tab state without re-rendering DOM.
365
- * Called from `attributeChangedCallback` when the `active` attribute changes.
366
- *
367
- * `previousId` MUST be the value the attribute held *before* it was changed —
368
- * never call `getActiveTabId(el, ...)` here, because the attribute has already
369
- * been updated to `tabId` by the time this runs. Reading it back would always
370
- * yield the new value, making the change-detection guard below trip on every
371
- * click and silently suppress `ty-tab-change` dispatch.
372
- */
176
+ const button = shadowRoot.querySelector(`[data-tab-id='${activeId}']`);
177
+ const buttonsContainer = shadowRoot.querySelector('.tab-buttons');
178
+ if (!button || !buttonsContainer)
179
+ return null;
180
+ const left = button.offsetLeft;
181
+ const top = button.offsetTop;
182
+ const width = button.offsetWidth;
183
+ const height = button.offsetHeight;
184
+ return { left, top, width, height };
185
+ }
186
+ function updateMarker(el, activeId, forceSnap = false) {
187
+ const shadowRoot = el.shadowRoot;
188
+ if (!shadowRoot)
189
+ return;
190
+ const marker = shadowRoot.querySelector('.marker-wrapper');
191
+ if (!marker)
192
+ return;
193
+ const position = calculateMarkerPosition(el, shadowRoot, activeId);
194
+ if (!position)
195
+ return;
196
+ const snap = forceSnap || !marker.style.left || marker.offsetWidth === 0;
197
+ if (snap)
198
+ marker.style.transition = 'none';
199
+ marker.style.left = `${position.left}px`;
200
+ marker.style.top = `${position.top}px`;
201
+ marker.style.width = `${position.width}px`;
202
+ marker.style.height = `${position.height}px`;
203
+ if (snap) {
204
+ marker.offsetWidth;
205
+ marker.style.transition = '';
206
+ }
207
+ }
373
208
  function updateActiveTabState(el, tabId, previousId) {
374
- const tabs = getChildTabs(el);
375
- const shadowRoot = el.shadowRoot;
376
- if (!shadowRoot)
377
- return;
378
- const currentIndex = previousId ? findTabIndex(tabs, previousId) : undefined;
379
- const newIndex = findTabIndex(tabs, tabId);
380
- // Only update if different tab and valid
381
- if (previousId === tabId || newIndex === undefined)
382
- return;
383
- // Update CSS variable for transform
384
- el.style.setProperty('--active-index', String(newIndex));
385
- // Update transform directly
386
- updateTransform(el, newIndex);
387
- // Update ARIA attributes on buttons
388
- updateAriaAttributes(el, shadowRoot, tabId);
389
- // Update pointer-events on panels
390
- updatePanelInteraction(el, tabId);
391
- // Update marker position to match new active tab
392
- updateMarker(el, tabId);
393
- // Reset scroll position of new active panel
394
- const newPanel = tabs[newIndex];
395
- if (newPanel?.resetScroll) {
396
- newPanel.resetScroll();
397
- }
398
- // Dispatch change event
399
- dispatchTabChangeEvent(el, tabId, newIndex, previousId, currentIndex ?? null);
400
- }
401
- // ============================================================================
402
- // ResizeObserver for Responsive Width
403
- // ============================================================================
404
- /**
405
- * Setup ResizeObserver for percentage widths and marker updates
406
- */
209
+ const tabs = getChildTabs(el);
210
+ const shadowRoot = el.shadowRoot;
211
+ if (!shadowRoot)
212
+ return;
213
+ const currentIndex = previousId ? findTabIndex(tabs, previousId) : undefined;
214
+ const newIndex = findTabIndex(tabs, tabId);
215
+ if (previousId === tabId || newIndex === undefined)
216
+ return;
217
+ el.style.setProperty('--active-index', String(newIndex));
218
+ updateTransform(el, newIndex);
219
+ updateAriaAttributes(el, shadowRoot, tabId);
220
+ updatePanelInteraction(el, tabId);
221
+ updateMarker(el, tabId, pendingInitialSnap.delete(el));
222
+ shadowRoot.querySelectorAll('.tab-overflow-item').forEach((item) => {
223
+ item.setAttribute('data-active', String(item.dataset.tabId === tabId));
224
+ });
225
+ const newPanel = tabs[newIndex];
226
+ if (newPanel?.resetScroll) {
227
+ newPanel.resetScroll();
228
+ }
229
+ dispatchTabChangeEvent(el, tabId, newIndex, previousId, currentIndex ?? null);
230
+ }
407
231
  function setupResizeObserver(el) {
408
- // Clean up old observer
409
- const oldObserver = resizeObservers.get(el);
410
- if (oldObserver) {
411
- oldObserver.disconnect();
412
- }
413
- // Observe unconditionally (not just for '%' widths) — overflow can happen
414
- // at any fixed width too, and needs to react to container resizes.
415
- const observer = new ResizeObserver((entries) => {
416
- const entry = entries[0];
417
- const { width } = getTabsAttributes(el);
418
- const tabs = getChildTabs(el);
419
- const activeId = getActiveTabId(el, tabs);
420
- const activeIndex = activeId ? findTabIndex(tabs, activeId) : 0;
421
- // Update CSS variable with measured width (percentage widths only)
422
- if (width.includes('%')) {
423
- el.style.setProperty('--tabs-width', `${entry.contentRect.width}px`);
424
- }
425
- // Update transform with new width
426
- if (activeIndex !== undefined) {
427
- updateTransform(el, activeIndex);
428
- }
429
- // Update marker position (tab button positions may have changed)
430
- if (activeId) {
431
- updateMarker(el, activeId);
432
- }
433
- updateOverflow(el);
434
- });
435
- observer.observe(el);
436
- resizeObservers.set(el, observer);
437
- }
438
- /**
439
- * Cleanup ResizeObserver
440
- */
232
+ const oldObserver = resizeObservers.get(el);
233
+ if (oldObserver) {
234
+ oldObserver.disconnect();
235
+ }
236
+ const observer = new ResizeObserver((entries) => {
237
+ const entry = entries[0];
238
+ const { width } = getTabsAttributes(el);
239
+ const tabs = getChildTabs(el);
240
+ const activeId = getActiveTabId(el, tabs);
241
+ const activeIndex = activeId ? findTabIndex(tabs, activeId) : 0;
242
+ if (width.includes('%')) {
243
+ el.style.setProperty('--tabs-page-width', `${entry.contentRect.width}px`);
244
+ }
245
+ if (activeIndex !== undefined) {
246
+ updateTransform(el, activeIndex);
247
+ }
248
+ updateOverflow(el);
249
+ if (activeId) {
250
+ updateMarker(el, activeId);
251
+ scrollActiveIntoView(el, activeId, false);
252
+ }
253
+ });
254
+ observer.observe(el);
255
+ resizeObservers.set(el, observer);
256
+ }
441
257
  function cleanupResizeObserver(el) {
442
- const observer = resizeObservers.get(el);
443
- if (observer) {
444
- observer.disconnect();
445
- resizeObservers.delete(el);
446
- }
447
- }
448
- // ============================================================================
449
- // Rendering Functions
450
- // ============================================================================
451
- /**
452
- * Generate HTML for tab buttons using slots for rich labels.
453
- * Slots are looked up as direct children of tabs-el with slot='label-{tab-id}'.
454
- * Also renders marker wrapper with optional default underline (only if no custom marker).
455
- */
258
+ const observer = resizeObservers.get(el);
259
+ if (observer) {
260
+ observer.disconnect();
261
+ resizeObservers.delete(el);
262
+ }
263
+ }
456
264
  function renderTabButtons(tabsEl, tabs, activeId) {
457
- const buttons = tabs.map((tab) => {
458
- const tabId = getTabId(tab);
459
- if (!tabId)
460
- return '';
461
- const labelType = getTabLabelType(tabsEl, tab);
462
- const textLabel = labelType === 'text' ? (tab.getAttribute('label') || 'Tab') : '';
463
- const disabled = isTabDisabled(tab);
464
- const active = tabId === activeId;
465
- return `<button
466
- class="tab-button"
467
- role="tab"
468
- data-tab-id="${tabId}"
469
- id="tab-${tabId}"
470
- aria-selected="${active}"
471
- tabindex="${active ? '0' : '-1'}"
472
- data-active="${active}"
473
- ${disabled ? 'disabled aria-disabled="true"' : ''}
474
- >
475
- ${labelType === 'slot' ? `<slot name="label-${tabId}"></slot>` : textLabel}
476
- </button>`;
477
- }).join('');
478
- return `
479
- <div class="tab-buttons" role="tablist" part="buttons-container">
480
- <div class="marker-wrapper" part="marker-wrapper">
481
- ${!hasCustomMarker(tabsEl) ? '<div class="default-marker"></div>' : ''}
482
- <slot name="marker"></slot>
483
- </div>
484
- ${buttons}
485
- </div>
486
- `;
487
- }
488
- /**
489
- * Generate HTML for ONLY the button elements (no marker wrapper).
490
- * Used during smart updates to preserve the marker wrapper.
491
- */
265
+ const buttons = tabs.map((tab) => {
266
+ const tabId = getTabId(tab);
267
+ if (!tabId)
268
+ return '';
269
+ const labelType = getTabLabelType(tabsEl, tab);
270
+ const textLabel = labelType === 'text' ? (tab.getAttribute('label') || 'Tab') : '';
271
+ const disabled = isTabDisabled(tab);
272
+ const active = tabId === activeId;
273
+ return `<button
274
+ class="tab-button"
275
+ role="tab"
276
+ data-tab-id="${tabId}"
277
+ id="tab-${tabId}"
278
+ aria-selected="${active}"
279
+ tabindex="${active ? '0' : '-1'}"
280
+ data-active="${active}"
281
+ ${disabled ? 'disabled aria-disabled="true"' : ''}
282
+ >
283
+ ${labelType === 'slot' ? `<slot name="label-${tabId}"></slot>` : `<span class="tab-label">${textLabel}</span>`}
284
+ </button>`;
285
+ }).join('');
286
+ return `
287
+ <div class="tab-buttons" role="tablist" part="buttons-container">
288
+ <div class="tab-strip" part="strip">
289
+ <div class="marker-wrapper" part="marker-wrapper">
290
+ ${!hasCustomMarker(tabsEl) ? '<div class="default-marker"></div>' : ''}
291
+ <slot name="marker"></slot>
292
+ </div>
293
+ ${buttons}
294
+ </div>
295
+ </div>
296
+ `;
297
+ }
492
298
  function renderButtonsOnly(tabsEl, tabs, activeId) {
493
- return tabs.map((tab) => {
494
- const tabId = getTabId(tab);
495
- if (!tabId)
496
- return '';
497
- const labelType = getTabLabelType(tabsEl, tab);
498
- const textLabel = labelType === 'text' ? (tab.getAttribute('label') || 'Tab') : '';
499
- const disabled = isTabDisabled(tab);
500
- const active = tabId === activeId;
501
- return `<button
502
- class="tab-button"
503
- role="tab"
504
- data-tab-id="${tabId}"
505
- id="tab-${tabId}"
506
- aria-selected="${active}"
507
- tabindex="${active ? '0' : '-1'}"
508
- data-active="${active}"
509
- ${disabled ? 'disabled aria-disabled="true"' : ''}
510
- >
511
- ${labelType === 'slot' ? `<slot name="label-${tabId}"></slot>` : textLabel}
512
- </button>`;
513
- }).join('');
514
- }
515
- // ============================================================================
516
- // Overflow Handling (collapse tabs that don't fit into a "more" menu)
517
- // ============================================================================
518
- const OVERFLOW_TRIGGER_WIDTH = 40;
519
- /** Build a display label for a hidden tab's overflow-menu entry. */
299
+ return tabs.map((tab) => {
300
+ const tabId = getTabId(tab);
301
+ if (!tabId)
302
+ return '';
303
+ const labelType = getTabLabelType(tabsEl, tab);
304
+ const textLabel = labelType === 'text' ? (tab.getAttribute('label') || 'Tab') : '';
305
+ const disabled = isTabDisabled(tab);
306
+ const active = tabId === activeId;
307
+ return `<button
308
+ class="tab-button"
309
+ role="tab"
310
+ data-tab-id="${tabId}"
311
+ id="tab-${tabId}"
312
+ aria-selected="${active}"
313
+ tabindex="${active ? '0' : '-1'}"
314
+ data-active="${active}"
315
+ ${disabled ? 'disabled aria-disabled="true"' : ''}
316
+ >
317
+ ${labelType === 'slot' ? `<slot name="label-${tabId}"></slot>` : `<span class="tab-label">${textLabel}</span>`}
318
+ </button>`;
319
+ }).join('');
320
+ }
520
321
  function getTabMenuLabel(tabsEl, tab) {
521
- const tabId = getTabId(tab);
522
- if (tabId && getTabLabelType(tabsEl, tab) === 'slot') {
523
- const slotted = tabsEl.querySelector(`[slot='label-${tabId}']`);
524
- return slotted?.textContent?.trim() || tabId;
525
- }
526
- return tab.getAttribute('label') || 'Tab';
527
- }
528
- /** Remove the "more" trigger + its popup, if present. */
322
+ const tabId = getTabId(tab);
323
+ if (tabId && getTabLabelType(tabsEl, tab) === 'slot') {
324
+ const slotted = getSlotLabel(tabsEl, tabId);
325
+ return slotted?.textContent?.trim() || tabId;
326
+ }
327
+ return tab.getAttribute('label') || 'Tab';
328
+ }
529
329
  function removeOverflowMenu(shadowRoot) {
530
- shadowRoot.querySelector('.tab-overflow-trigger')?.remove();
531
- }
532
- /**
533
- * Build the "more" trigger button + its ty-popup menu for the given
534
- * (already-hidden) tab buttons, and append it to the buttons container.
535
- * Reuses ty-popup instead of a second floating-menu implementation.
536
- */
537
- function renderOverflowMenu(el, container, hiddenButtons, allTabs) {
538
- if (hiddenButtons.length === 0)
539
- return;
540
- const tabsById = new Map(allTabs.map((tab) => [getTabId(tab), tab]));
541
- const trigger = document.createElement('button');
542
- trigger.className = 'tab-overflow-trigger';
543
- trigger.type = 'button';
544
- trigger.setAttribute('aria-label', `${hiddenButtons.length} more tabs`);
545
- trigger.textContent = '⋯';
546
- const popup = document.createElement('ty-popup');
547
- popup.setAttribute('placement', 'bottom');
548
- const menu = document.createElement('div');
549
- menu.className = 'tab-overflow-menu';
550
- menu.setAttribute('role', 'menu');
551
- hiddenButtons.forEach((btn) => {
552
- const tabId = btn.dataset.tabId;
553
- const tab = tabId ? tabsById.get(tabId) : undefined;
554
- if (!tabId || !tab)
555
- return;
556
- const item = document.createElement('button');
557
- item.type = 'button';
558
- item.className = 'tab-overflow-item';
559
- item.setAttribute('role', 'menuitem');
560
- item.textContent = getTabMenuLabel(el, tab);
561
- if (isTabDisabled(tab)) {
562
- item.disabled = true;
563
- }
564
- else {
565
- item.addEventListener('click', () => {
566
- setActiveTab(el, tabId);
567
- popup.closePopup?.();
568
- });
569
- }
570
- menu.appendChild(item);
571
- });
572
- popup.appendChild(menu);
573
- trigger.appendChild(popup);
574
- container.appendChild(trigger);
575
- }
576
- /**
577
- * Measure the tab buttons against the available width and collapse whatever
578
- * doesn't fit behind a "more" trigger + popup menu. The active tab is always
579
- * kept visible — if it would be the one to overflow, the last visible button
580
- * is hidden in its place instead.
581
- */
330
+ shadowRoot.querySelector('.tab-overflow-trigger')?.remove();
331
+ }
332
+ function renderOverflowMenu(el, container, allTabs) {
333
+ const activeId = getActiveTabId(el, allTabs);
334
+ const trigger = document.createElement('button');
335
+ trigger.className = 'tab-overflow-trigger';
336
+ trigger.type = 'button';
337
+ trigger.setAttribute('aria-label', `${allTabs.length} tabs`);
338
+ trigger.textContent = '⋯';
339
+ const popup = document.createElement('ty-popup');
340
+ popup.setAttribute('placement', 'bottom');
341
+ const menu = document.createElement('div');
342
+ menu.className = 'tab-overflow-menu';
343
+ menu.setAttribute('role', 'menu');
344
+ allTabs.forEach((tab) => {
345
+ const tabId = getTabId(tab);
346
+ if (!tabId)
347
+ return;
348
+ const item = document.createElement('button');
349
+ item.type = 'button';
350
+ item.className = 'tab-overflow-item';
351
+ item.setAttribute('role', 'menuitem');
352
+ item.setAttribute('data-tab-id', tabId);
353
+ item.setAttribute('data-active', String(tabId === activeId));
354
+ item.textContent = getTabMenuLabel(el, tab);
355
+ if (isTabDisabled(tab)) {
356
+ item.disabled = true;
357
+ }
358
+ else {
359
+ item.addEventListener('click', () => {
360
+ setActiveTab(el, tabId);
361
+ popup.closePopup?.();
362
+ });
363
+ }
364
+ menu.appendChild(item);
365
+ });
366
+ popup.appendChild(menu);
367
+ trigger.appendChild(popup);
368
+ container.appendChild(trigger);
369
+ }
582
370
  function updateOverflow(el) {
583
- const shadowRoot = el.shadowRoot;
584
- if (!shadowRoot)
585
- return;
586
- const container = shadowRoot.querySelector('.tab-buttons');
587
- if (!container)
588
- return;
589
- removeOverflowMenu(shadowRoot);
590
- const buttons = Array.from(container.querySelectorAll('.tab-button'));
591
- buttons.forEach((b) => b.classList.remove('overflow-hidden'));
592
- if (buttons.length === 0)
593
- return;
594
- const available = container.clientWidth;
595
- let total = 0;
596
- let cutoff = buttons.length;
597
- for (let i = 0; i < buttons.length; i++) {
598
- total += buttons[i].offsetWidth;
599
- if (total > available) {
600
- cutoff = i;
601
- break;
602
- }
603
- }
604
- if (cutoff >= buttons.length)
605
- return; // everything fits
606
- // Recompute the cutoff leaving room for the "more" trigger itself.
607
- total = 0;
608
- cutoff = buttons.length;
609
- for (let i = 0; i < buttons.length; i++) {
610
- total += buttons[i].offsetWidth;
611
- if (total > available - OVERFLOW_TRIGGER_WIDTH) {
612
- cutoff = i;
613
- break;
614
- }
615
- }
616
- cutoff = Math.max(cutoff, 1);
617
- const tabs = getChildTabs(el);
618
- const activeId = getActiveTabId(el, tabs);
619
- const hidden = new Set(buttons.slice(cutoff));
620
- const activeButton = activeId ? buttons.find((b) => b.dataset.tabId === activeId) : undefined;
621
- if (activeButton && hidden.has(activeButton)) {
622
- const lastVisible = buttons[cutoff - 1];
623
- hidden.delete(activeButton);
624
- hidden.add(lastVisible);
625
- }
626
- buttons.forEach((b) => b.classList.toggle('overflow-hidden', hidden.has(b)));
627
- renderOverflowMenu(el, container, buttons.filter((b) => hidden.has(b)), tabs);
628
- }
629
- /**
630
- * Render the tabs container with buttons and panel viewport.
631
- * Smart rendering: checks if structure exists and only updates when needed.
632
- */
371
+ const shadowRoot = el.shadowRoot;
372
+ if (!shadowRoot)
373
+ return;
374
+ const container = shadowRoot.querySelector('.tab-buttons');
375
+ const strip = shadowRoot.querySelector('.tab-strip');
376
+ if (!container || !strip)
377
+ return;
378
+ if (getTabsAttributes(el).fixed) {
379
+ removeOverflowMenu(shadowRoot);
380
+ strip.classList.add('unclipped');
381
+ strip.style.removeProperty('--fade-left');
382
+ strip.style.removeProperty('--fade-right');
383
+ return;
384
+ }
385
+ const savedScrollLeft = strip.scrollLeft;
386
+ removeOverflowMenu(shadowRoot);
387
+ if (strip.clientWidth <= 0)
388
+ return;
389
+ const buttons = strip.querySelectorAll('.tab-button');
390
+ const last = buttons[buttons.length - 1];
391
+ const contentWidth = last ? last.offsetLeft + last.offsetWidth : 0;
392
+ const stripWidth = strip.clientWidth;
393
+ const hidden = contentWidth - stripWidth;
394
+ strip.classList.toggle('unclipped', hidden <= 1);
395
+ if (hidden <= 1) {
396
+ strip.scrollLeft = savedScrollLeft;
397
+ updateEdgeFades(strip);
398
+ return;
399
+ }
400
+ renderOverflowMenu(el, container, getChildTabs(el));
401
+ const triggerCost = container.clientWidth - strip.clientWidth;
402
+ if (hidden <= triggerCost) {
403
+ removeOverflowMenu(shadowRoot);
404
+ }
405
+ strip.scrollLeft = savedScrollLeft;
406
+ updateEdgeFades(strip);
407
+ }
408
+ function updateEdgeFades(strip) {
409
+ const maxScroll = strip.scrollWidth - strip.clientWidth;
410
+ const fade = (distance) => `${distance > 1 ? Math.min(28, distance) : 0}px`;
411
+ strip.style.setProperty('--fade-left', fade(strip.scrollLeft));
412
+ strip.style.setProperty('--fade-right', fade(maxScroll - strip.scrollLeft));
413
+ }
414
+ function scrollActiveIntoView(el, activeId, smooth) {
415
+ const shadowRoot = el.shadowRoot;
416
+ if (!shadowRoot)
417
+ return;
418
+ const strip = shadowRoot.querySelector('.tab-strip');
419
+ const button = shadowRoot.querySelector(`[data-tab-id='${activeId}']`);
420
+ if (!strip || !button || strip.clientWidth <= 0)
421
+ return;
422
+ const centered = button.offsetLeft + button.offsetWidth / 2 - strip.clientWidth / 2;
423
+ const target = Math.max(0, Math.min(centered, strip.scrollWidth - strip.clientWidth));
424
+ if (Math.round(target) !== Math.round(strip.scrollLeft)) {
425
+ const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
426
+ strip.scrollTo({ left: target, behavior: smooth && !reduce ? 'smooth' : 'auto' });
427
+ }
428
+ }
633
429
  function render(el) {
634
- const shadowRoot = el.shadowRoot;
635
- if (!shadowRoot)
636
- return;
637
- const { width, height, placement } = getTabsAttributes(el);
638
- const tabs = getChildTabs(el);
639
- const activeId = getActiveTabId(el, tabs);
640
- const activeIndex = activeId ? (findTabIndex(tabs, activeId) ?? 0) : 0;
641
- // Each <ty-tab> IS its tabpanel (no separate wrapper — there's just one
642
- // catch-all <slot> in the carousel viewport below). role/aria-labelledby/
643
- // tabindex go directly on the light-DOM host; its `id` attribute is ALSO
644
- // its tabId (getTabId reads it directly), so it's never touched here.
645
- // No aria-controls on the tab BUTTON pointing at this: it lives in the
646
- // shadow root while the panel is in light DOM, and ARIA id-references
647
- // don't resolve across a shadow-tree boundary — axe's aria-valid-attr-value
648
- // correctly flags that as invalid even though getElementById can find the
649
- // target. role=tab + aria-selected + aria-labelledby (this direction) is
650
- // what's actually load-bearing; caught by axe once ty-tabs got coverage.
651
- tabs.forEach((tab) => {
652
- const tabId = getTabId(tab);
653
- if (!tabId)
654
- return;
655
- tab.setAttribute('role', 'tabpanel');
656
- tab.setAttribute('aria-labelledby', `tab-${tabId}`);
657
- tab.setAttribute('tabindex', '0');
658
- });
659
- // Check if structure already exists
660
- const existingContainer = shadowRoot.querySelector('.tabs-container');
661
- const existingButtons = shadowRoot.querySelector('.tab-buttons');
662
- const existingViewport = shadowRoot.querySelector('.panels-viewport');
663
- // Ensure styles are loaded
664
- ensureStyles(shadowRoot, { css: tabsStyles, id: 'ty-tabs' });
665
- // Dev warning for too many tabs
666
- if (tabs.length > 7) {
667
- console.warn(`[ty-tabs] More than 7 tabs detected (${tabs.length} tabs). ` +
668
- 'This may cause overflow and poor UX. ' +
669
- 'Consider using sidebar navigation, accordion menu, or other patterns.');
670
- }
671
- // Set CSS variables for dimensions
672
- el.style.setProperty('--tabs-width', width.includes('%') ? '100%' : width);
673
- el.style.setProperty('--tabs-height', height);
674
- el.style.setProperty('--active-index', String(activeIndex));
675
- if (existingContainer && existingButtons && existingViewport) {
676
- // === SMART UPDATE: Structure exists, only update what changed ===
677
- // Update placement if changed
678
- existingContainer.setAttribute('data-placement', placement);
679
- // SMART UPDATE: Preserve marker wrapper, only update buttons
680
- existingButtons.querySelector('.tab-overflow-trigger')?.remove();
681
- const allButtons = existingButtons.querySelectorAll('.tab-button');
682
- allButtons.forEach(button => button.remove());
683
- // Generate and append new buttons after the marker wrapper
684
- const buttonsHtml = renderButtonsOnly(el, tabs, activeId);
685
- const tempDiv = document.createElement('div');
686
- tempDiv.innerHTML = buttonsHtml;
687
- Array.from(tempDiv.children).forEach(button => {
688
- existingButtons.appendChild(button);
689
- });
690
- // Re-setup event listeners (buttons were recreated)
691
- setupEventListeners(el, shadowRoot, tabs);
692
- // Update ARIA and data-active attributes on initial render
693
- updateAriaAttributes(el, shadowRoot, activeId || '');
694
- // Measure button height and update marker after update
695
- requestAnimationFrame(() => {
696
- const buttons = shadowRoot.querySelector('.tab-buttons');
697
- if (buttons) {
698
- const buttonsHeight = buttons.offsetHeight;
699
- el.style.setProperty('--buttons-height', `${buttonsHeight}px`);
700
- }
701
- // Update transform with current active index
702
- updateTransform(el, activeIndex);
703
- // Update marker position to match active tab
704
- if (activeId) {
705
- updateMarker(el, activeId);
706
- }
707
- updateOverflow(el);
708
- });
709
- // Update panel interaction states
710
- if (activeId) {
711
- updatePanelInteraction(el, activeId);
712
- }
713
- }
714
- else {
715
- // === FULL RENDER: First time or structure missing ===
716
- shadowRoot.innerHTML = `
717
- <div class="tabs-container" data-placement="${placement}">
718
- ${renderTabButtons(el, tabs, activeId)}
719
- <div class="panels-viewport" part="panels-container">
720
- <div class="panels-wrapper">
721
- <slot></slot>
722
- </div>
723
- </div>
724
- </div>
725
- `;
726
- // Measure button height, update transform and marker after render
727
- requestAnimationFrame(() => {
728
- const buttons = shadowRoot.querySelector('.tab-buttons');
729
- if (buttons) {
730
- const buttonsHeight = buttons.offsetHeight;
731
- el.style.setProperty('--buttons-height', `${buttonsHeight}px`);
732
- }
733
- // Update transform with measured width
734
- updateTransform(el, activeIndex);
735
- // Update marker position to match active tab
736
- if (activeId) {
737
- updateMarker(el, activeId);
738
- }
739
- updateOverflow(el);
740
- });
741
- // Setup event listeners
742
- setupEventListeners(el, shadowRoot, tabs);
743
- // Update ARIA and data-active attributes on initial render
744
- updateAriaAttributes(el, shadowRoot, activeId || '');
745
- // Setup ResizeObserver for responsive width
746
- setupResizeObserver(el);
747
- // Update tab panel states
748
- if (activeId) {
749
- updatePanelInteraction(el, activeId);
750
- }
751
- // Listen for marker slot changes to hide default marker
752
- const markerSlot = shadowRoot.querySelector('slot[name="marker"]');
753
- if (markerSlot) {
754
- const updateDefaultMarkerVisibility = () => {
755
- const defaultMarker = shadowRoot.querySelector('.default-marker');
756
- if (defaultMarker) {
757
- const hasCustom = markerSlot.assignedElements().length > 0;
758
- defaultMarker.style.display = hasCustom ? 'none' : '';
759
- }
760
- };
761
- markerSlot.addEventListener('slotchange', updateDefaultMarkerVisibility);
762
- // Check immediately in case content is already slotted
763
- updateDefaultMarkerVisibility();
764
- }
765
- }
766
- }
767
- /**
768
- * Cleanup when tabs component is disconnected
769
- */
430
+ const shadowRoot = el.shadowRoot;
431
+ if (!shadowRoot)
432
+ return;
433
+ const { width, height, placement, fixed } = getTabsAttributes(el);
434
+ const tabs = getChildTabs(el);
435
+ const activeId = getActiveTabId(el, tabs);
436
+ const activeIndex = activeId ? (findTabIndex(tabs, activeId) ?? 0) : 0;
437
+ tabs.forEach((tab) => {
438
+ const tabId = getTabId(tab);
439
+ if (!tabId)
440
+ return;
441
+ tab.setAttribute('role', 'tabpanel');
442
+ tab.setAttribute('aria-labelledby', `tab-${tabId}`);
443
+ tab.setAttribute('tabindex', '0');
444
+ });
445
+ const existingContainer = shadowRoot.querySelector('.tabs-container');
446
+ const existingButtons = shadowRoot.querySelector('.tab-buttons');
447
+ const existingStrip = shadowRoot.querySelector('.tab-strip');
448
+ const existingViewport = shadowRoot.querySelector('.panels-viewport');
449
+ ensureStyles(shadowRoot, { css: tabsStyles, id: 'ty-tabs' });
450
+ el.style.setProperty('--tabs-width', width.includes('%') ? '100%' : width);
451
+ el.style.setProperty('--tabs-height', height);
452
+ el.style.setProperty('--active-index', String(activeIndex));
453
+ if (existingContainer && existingButtons && existingStrip && existingViewport) {
454
+ existingContainer.setAttribute('data-placement', placement);
455
+ existingContainer.toggleAttribute('data-fixed', fixed);
456
+ const savedScrollLeft = existingStrip.scrollLeft;
457
+ existingButtons.querySelector('.tab-overflow-trigger')?.remove();
458
+ const allButtons = existingStrip.querySelectorAll('.tab-button');
459
+ allButtons.forEach(button => button.remove());
460
+ const buttonsHtml = renderButtonsOnly(el, tabs, activeId);
461
+ const tempDiv = document.createElement('div');
462
+ tempDiv.innerHTML = buttonsHtml;
463
+ Array.from(tempDiv.children).forEach(button => {
464
+ existingStrip.appendChild(button);
465
+ });
466
+ existingStrip.scrollLeft = savedScrollLeft;
467
+ setupEventListeners(el, shadowRoot, tabs);
468
+ updateAriaAttributes(el, shadowRoot, activeId || '');
469
+ requestAnimationFrame(() => {
470
+ const buttons = shadowRoot.querySelector('.tab-buttons');
471
+ if (buttons) {
472
+ const buttonsHeight = buttons.offsetHeight;
473
+ el.style.setProperty('--buttons-height', `${buttonsHeight}px`);
474
+ }
475
+ const current = currentActive(el);
476
+ updateTransform(el, current.index);
477
+ updateOverflow(el);
478
+ if (current.id) {
479
+ updateMarker(el, current.id);
480
+ scrollActiveIntoView(el, current.id, true);
481
+ }
482
+ });
483
+ if (activeId) {
484
+ updatePanelInteraction(el, activeId);
485
+ }
486
+ }
487
+ else {
488
+ pendingInitialSnap.add(el);
489
+ shadowRoot.innerHTML = `
490
+ <div class="tabs-container" data-placement="${placement}"${fixed ? ' data-fixed' : ''}>
491
+ ${renderTabButtons(el, tabs, activeId)}
492
+ <div class="panels-viewport" part="panels-container">
493
+ <div class="panels-wrapper">
494
+ <slot></slot>
495
+ </div>
496
+ </div>
497
+ </div>
498
+ `;
499
+ requestAnimationFrame(() => {
500
+ const buttons = shadowRoot.querySelector('.tab-buttons');
501
+ if (buttons) {
502
+ const buttonsHeight = buttons.offsetHeight;
503
+ el.style.setProperty('--buttons-height', `${buttonsHeight}px`);
504
+ }
505
+ const current = currentActive(el);
506
+ updateTransform(el, current.index);
507
+ updateOverflow(el);
508
+ if (current.id) {
509
+ updateMarker(el, current.id);
510
+ scrollActiveIntoView(el, current.id, false);
511
+ }
512
+ });
513
+ setupEventListeners(el, shadowRoot, tabs);
514
+ updateAriaAttributes(el, shadowRoot, activeId || '');
515
+ setupResizeObserver(el);
516
+ const strip = shadowRoot.querySelector('.tab-strip');
517
+ strip?.addEventListener('scroll', () => updateEdgeFades(strip), { passive: true });
518
+ if (activeId) {
519
+ updatePanelInteraction(el, activeId);
520
+ }
521
+ const markerSlot = shadowRoot.querySelector('slot[name="marker"]');
522
+ if (markerSlot) {
523
+ const updateDefaultMarkerVisibility = () => {
524
+ const defaultMarker = shadowRoot.querySelector('.default-marker');
525
+ if (defaultMarker) {
526
+ const hasCustom = markerSlot.assignedElements().length > 0;
527
+ defaultMarker.style.display = hasCustom ? 'none' : '';
528
+ }
529
+ };
530
+ markerSlot.addEventListener('slotchange', updateDefaultMarkerVisibility);
531
+ updateDefaultMarkerVisibility();
532
+ }
533
+ }
534
+ }
770
535
  function cleanup(el) {
771
- cleanupEventListeners(el);
772
- cleanupResizeObserver(el);
773
- }
774
- // ============================================================================
775
- // Component Definition
776
- // ============================================================================
777
- /**
778
- * TyTabs Web Component
779
- */
536
+ cleanupEventListeners(el);
537
+ cleanupResizeObserver(el);
538
+ }
780
539
  export class TyTabs extends HTMLElement {
781
- /** Observed attributes */
782
- static get observedAttributes() {
783
- return ['width', 'height', 'active', 'placement'];
784
- }
785
- constructor() {
786
- super();
787
- this.attachShadow({ mode: 'open' });
788
- }
789
- connectedCallback() {
790
- render(this);
791
- }
792
- disconnectedCallback() {
793
- cleanup(this);
794
- }
795
- attributeChangedCallback(name, oldValue, newValue) {
796
- // Smart rendering: only full render when structural attributes change
797
- if (name === 'active') {
798
- // Active tab changed - update state, then do smart render
799
- if (newValue) {
800
- // Pass `oldValue` explicitly so the change-detection inside
801
- // `updateActiveTabState` compares against the previous value
802
- // rather than re-reading the (already updated) attribute.
803
- updateActiveTabState(this, newValue, oldValue);
804
- }
805
- // Always call render after active change to update button states
806
- render(this);
807
- }
808
- else {
809
- // Other attributes changed (width, height, placement) - full render
810
- render(this);
811
- }
812
- }
813
- }
814
- // Register the custom element
540
+ static get observedAttributes() {
541
+ return ['width', 'height', 'active', 'placement', 'fixed'];
542
+ }
543
+ constructor() {
544
+ super();
545
+ this._childObserver = null;
546
+ this._childRenderQueued = false;
547
+ this.attachShadow({ mode: 'open' });
548
+ }
549
+ connectedCallback() {
550
+ render(this);
551
+ this._childObserver = new MutationObserver(() => {
552
+ if (this._childRenderQueued)
553
+ return;
554
+ this._childRenderQueued = true;
555
+ queueMicrotask(() => {
556
+ this._childRenderQueued = false;
557
+ if (this.isConnected)
558
+ render(this);
559
+ });
560
+ });
561
+ this._childObserver.observe(this, { childList: true });
562
+ }
563
+ disconnectedCallback() {
564
+ this._childObserver?.disconnect();
565
+ this._childObserver = null;
566
+ cleanup(this);
567
+ }
568
+ attributeChangedCallback(name, oldValue, newValue) {
569
+ if (name === 'active') {
570
+ if (!newValue)
571
+ return;
572
+ if (!this.shadowRoot?.querySelector(`[data-tab-id='${newValue}']`)) {
573
+ render(this);
574
+ return;
575
+ }
576
+ updateActiveTabState(this, newValue, oldValue);
577
+ scrollActiveIntoView(this, newValue, true);
578
+ }
579
+ else {
580
+ render(this);
581
+ }
582
+ }
583
+ }
815
584
  if (!customElements.get('ty-tabs')) {
816
- customElements.define('ty-tabs', TyTabs);
585
+ customElements.define('ty-tabs', TyTabs);
817
586
  }
818
- //# sourceMappingURL=tabs.js.map