plass-ui 1.3.0 → 1.5.0

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 (454) hide show
  1. package/CHANGELOG.md +962 -0
  2. package/README.md +19 -17
  3. package/dist/components/accordion/PlAccordion.d.ts +1 -1
  4. package/dist/components/alert/PlAlert.js +1 -1
  5. package/dist/components/anchor/PlAnchor.d.ts +85 -0
  6. package/dist/components/anchor/PlAnchor.js +1 -0
  7. package/dist/components/anchor/index.d.ts +2 -0
  8. package/dist/components/anchor/index.js +1 -0
  9. package/dist/components/animate-appear/PlAnimateAppear.d.ts +10 -0
  10. package/dist/components/animate-appear/PlAnimateAppear.js +1 -1
  11. package/dist/components/animate-blink/PlAnimateBlink.d.ts +2 -2
  12. package/dist/components/animate-blink/PlAnimateBlink.js +1 -1
  13. package/dist/components/animate-counter/PlAnimateCounter.d.ts +83 -0
  14. package/dist/components/animate-counter/PlAnimateCounter.js +1 -0
  15. package/dist/components/animate-counter/index.d.ts +2 -0
  16. package/dist/components/animate-counter/index.js +1 -0
  17. package/dist/components/animate-fade/PlAnimateFade.d.ts +2 -2
  18. package/dist/components/animate-fade/PlAnimateFade.js +1 -1
  19. package/dist/components/animate-float/PlAnimateFloat.d.ts +51 -0
  20. package/dist/components/animate-float/PlAnimateFloat.js +1 -0
  21. package/dist/components/animate-float/index.d.ts +2 -0
  22. package/dist/components/animate-float/index.js +1 -0
  23. package/dist/components/animate-grow/PlAnimateGrow.d.ts +2 -2
  24. package/dist/components/animate-grow/PlAnimateGrow.js +1 -1
  25. package/dist/components/animate-headline/PlAnimateHeadline.js +1 -1
  26. package/dist/components/animate-lighting/PlAnimateLighting.js +1 -1
  27. package/dist/components/animate-marquee/PlAnimateMarquee.d.ts +13 -4
  28. package/dist/components/animate-marquee/PlAnimateMarquee.js +1 -1
  29. package/dist/components/animate-reveal/PlAnimateReveal.d.ts +51 -0
  30. package/dist/components/animate-reveal/PlAnimateReveal.js +1 -0
  31. package/dist/components/animate-reveal/index.d.ts +2 -0
  32. package/dist/components/animate-reveal/index.js +1 -0
  33. package/dist/components/animate-rotate/PlAnimateRotate.d.ts +2 -2
  34. package/dist/components/animate-rotate/PlAnimateRotate.js +1 -1
  35. package/dist/components/animate-scramble/PlAnimateScramble.d.ts +75 -0
  36. package/dist/components/animate-scramble/PlAnimateScramble.js +1 -0
  37. package/dist/components/animate-scramble/index.d.ts +2 -0
  38. package/dist/components/animate-scramble/index.js +1 -0
  39. package/dist/components/animate-shake/PlAnimateShake.d.ts +57 -0
  40. package/dist/components/animate-shake/PlAnimateShake.js +1 -0
  41. package/dist/components/animate-shake/index.d.ts +2 -0
  42. package/dist/components/animate-shake/index.js +1 -0
  43. package/dist/components/animate-slide/PlAnimateSlide.d.ts +2 -2
  44. package/dist/components/animate-slide/PlAnimateSlide.js +1 -1
  45. package/dist/components/animate-split/PlAnimateSplit.d.ts +57 -0
  46. package/dist/components/animate-split/PlAnimateSplit.js +1 -0
  47. package/dist/components/animate-split/index.d.ts +2 -0
  48. package/dist/components/animate-split/index.js +1 -0
  49. package/dist/components/animate-typing/PlAnimateTyping.d.ts +3 -2
  50. package/dist/components/animate-typing/PlAnimateTyping.js +1 -1
  51. package/dist/components/animate-zoom/PlAnimateZoom.d.ts +2 -2
  52. package/dist/components/animate-zoom/PlAnimateZoom.js +1 -1
  53. package/dist/components/app-logo/PlAppLogo.d.ts +78 -0
  54. package/dist/components/app-logo/PlAppLogo.js +1 -0
  55. package/dist/components/app-logo/index.d.ts +2 -0
  56. package/dist/components/app-logo/index.js +1 -0
  57. package/dist/components/area-chart/PlAreaChart.d.ts +54 -0
  58. package/dist/components/area-chart/PlAreaChart.js +1 -0
  59. package/dist/components/area-chart/index.d.ts +2 -0
  60. package/dist/components/area-chart/index.js +1 -0
  61. package/dist/components/aspect-ratio/PlAspectRatio.d.ts +2 -1
  62. package/dist/components/aspect-ratio/PlAspectRatio.js +1 -1
  63. package/dist/components/avatar/PlAvatar.js +1 -1
  64. package/dist/components/back-top/PlBackTop.d.ts +2 -1
  65. package/dist/components/back-top/PlBackTop.js +1 -1
  66. package/dist/components/badge/PlBadge.js +1 -1
  67. package/dist/components/bar-chart/PlBarChart.d.ts +63 -0
  68. package/dist/components/bar-chart/PlBarChart.js +1 -0
  69. package/dist/components/bar-chart/index.d.ts +2 -0
  70. package/dist/components/bar-chart/index.js +1 -0
  71. package/dist/components/bottom-navigation/PlBottomNavigation.d.ts +3 -0
  72. package/dist/components/breadcrumb/PlBreadcrumb.js +1 -1
  73. package/dist/components/button/PlButton.d.ts +8 -0
  74. package/dist/components/button/PlButton.js +1 -1
  75. package/dist/components/button-group/PlButtonGroup.js +1 -1
  76. package/dist/components/calendar/PlCalendar.js +1 -1
  77. package/dist/components/card/PlCard.js +1 -1
  78. package/dist/components/carousel/PlCarousel.d.ts +1 -1
  79. package/dist/components/carousel/PlCarousel.js +1 -1
  80. package/dist/components/chat-bubble/PlChatBubble.d.ts +4 -1
  81. package/dist/components/chat-bubble/PlChatBubble.js +1 -1
  82. package/dist/components/checkbox/PlCheckbox.js +1 -1
  83. package/dist/components/chip/PlChip.d.ts +6 -2
  84. package/dist/components/chip/PlChip.js +1 -1
  85. package/dist/components/code-block/PlCodeBlock.d.ts +192 -0
  86. package/dist/components/code-block/PlCodeBlock.js +1 -0
  87. package/dist/components/code-block/index.d.ts +2 -0
  88. package/dist/components/code-block/index.js +1 -0
  89. package/dist/components/color-picker/PlColorPicker.js +1 -1
  90. package/dist/components/combobox/PlCombobox.d.ts +6 -5
  91. package/dist/components/combobox/PlCombobox.js +1 -1
  92. package/dist/components/command-palette/PlCommandPalette.d.ts +6 -2
  93. package/dist/components/command-palette/PlCommandPalette.js +1 -1
  94. package/dist/components/confirm/PlConfirmProvider.js +1 -1
  95. package/dist/components/container/PlContainer.d.ts +22 -6
  96. package/dist/components/container/PlContainer.js +1 -1
  97. package/dist/components/data-list/PlDataList.d.ts +74 -0
  98. package/dist/components/data-list/PlDataList.js +1 -0
  99. package/dist/components/data-list/index.d.ts +2 -0
  100. package/dist/components/data-list/index.js +1 -0
  101. package/dist/components/data-table/PlDataTable.d.ts +216 -0
  102. package/dist/components/data-table/PlDataTable.js +1 -0
  103. package/dist/components/data-table/index.d.ts +2 -0
  104. package/dist/components/data-table/index.js +1 -0
  105. package/dist/components/date-picker/PlDatePicker.js +1 -1
  106. package/dist/components/date-range-picker/PlDateRangePicker.js +1 -1
  107. package/dist/components/date-time-picker/PlDateTimePicker.js +1 -1
  108. package/dist/components/drawer/PlDrawer.d.ts +1 -1
  109. package/dist/components/drawer/PlDrawer.js +1 -1
  110. package/dist/components/file-picker/PlFilePicker.d.ts +4 -1
  111. package/dist/components/file-picker/PlFilePicker.js +1 -1
  112. package/dist/components/flex/PlFlex.d.ts +83 -0
  113. package/dist/components/flex/PlFlex.js +1 -0
  114. package/dist/components/flex/index.d.ts +2 -0
  115. package/dist/components/flex/index.js +1 -0
  116. package/dist/components/floating-action-button/PlFloatingActionButton.d.ts +81 -0
  117. package/dist/components/floating-action-button/PlFloatingActionButton.js +1 -0
  118. package/dist/components/floating-action-button/index.d.ts +2 -0
  119. package/dist/components/floating-action-button/index.js +1 -0
  120. package/dist/components/floating-bottom-navigation/PlFloatingBottomNavigation.d.ts +3 -0
  121. package/dist/components/floating-bottom-navigation/PlFloatingBottomNavigation.js +1 -1
  122. package/dist/components/footer/PlFooter.d.ts +4 -3
  123. package/dist/components/footer/PlFooter.js +1 -1
  124. package/dist/components/form/PlForm.js +1 -1
  125. package/dist/components/gallery/PlGallery.d.ts +192 -0
  126. package/dist/components/gallery/PlGallery.js +1 -0
  127. package/dist/components/gallery/PlGalleryViewer.d.ts +31 -0
  128. package/dist/components/gallery/PlGalleryViewer.js +1 -0
  129. package/dist/components/gallery/index.d.ts +2 -0
  130. package/dist/components/gallery/index.js +1 -0
  131. package/dist/components/gauge-chart/PlGaugeChart.d.ts +64 -0
  132. package/dist/components/gauge-chart/PlGaugeChart.js +1 -0
  133. package/dist/components/gauge-chart/index.d.ts +2 -0
  134. package/dist/components/gauge-chart/index.js +1 -0
  135. package/dist/components/grid/PlGrid.js +1 -1
  136. package/dist/components/grid/PlGridItem.js +1 -1
  137. package/dist/components/header/PlHeader.d.ts +4 -3
  138. package/dist/components/header/PlHeader.js +1 -1
  139. package/dist/components/heatmap-chart/PlHeatmapChart.d.ts +69 -0
  140. package/dist/components/heatmap-chart/PlHeatmapChart.js +1 -0
  141. package/dist/components/heatmap-chart/index.d.ts +2 -0
  142. package/dist/components/heatmap-chart/index.js +1 -0
  143. package/dist/components/highlight/PlHighlight.js +1 -1
  144. package/dist/components/hot-keys/PlHotKeys.js +1 -1
  145. package/dist/components/hover-card/PlHoverCard.d.ts +96 -0
  146. package/dist/components/hover-card/PlHoverCard.js +1 -0
  147. package/dist/components/hover-card/index.d.ts +2 -0
  148. package/dist/components/hover-card/index.js +1 -0
  149. package/dist/components/how-to-steps/PlHowToSteps.d.ts +87 -0
  150. package/dist/components/how-to-steps/PlHowToSteps.js +1 -0
  151. package/dist/components/how-to-steps/index.d.ts +2 -0
  152. package/dist/components/how-to-steps/index.js +1 -0
  153. package/dist/components/image/PlImage.d.ts +195 -8
  154. package/dist/components/image/PlImage.js +1 -1
  155. package/dist/components/image/PlImagePreview.d.ts +58 -0
  156. package/dist/components/image/PlImagePreview.js +1 -0
  157. package/dist/components/image/index.d.ts +1 -1
  158. package/dist/components/line-chart/PlLineChart.d.ts +74 -0
  159. package/dist/components/line-chart/PlLineChart.js +1 -0
  160. package/dist/components/line-chart/index.d.ts +2 -0
  161. package/dist/components/line-chart/index.js +1 -0
  162. package/dist/components/menu/PlMenu.d.ts +14 -5
  163. package/dist/components/menu/PlMenu.js +1 -1
  164. package/dist/components/meter/PlMeter.d.ts +83 -0
  165. package/dist/components/meter/PlMeter.js +1 -0
  166. package/dist/components/meter/index.d.ts +2 -0
  167. package/dist/components/meter/index.js +1 -0
  168. package/dist/components/mockup/PlMockup.d.ts +146 -0
  169. package/dist/components/mockup/PlMockup.js +1 -0
  170. package/dist/components/mockup/index.d.ts +2 -0
  171. package/dist/components/mockup/index.js +1 -0
  172. package/dist/components/modal/PlModal.d.ts +1 -1
  173. package/dist/components/modal/PlModal.js +1 -1
  174. package/dist/components/navigation-menu/PlNavigationMenu.d.ts +7 -1
  175. package/dist/components/navigation-menu/PlNavigationMenu.js +1 -1
  176. package/dist/components/number-field/PlNumberField.d.ts +1 -1
  177. package/dist/components/number-field/PlNumberField.js +1 -1
  178. package/dist/components/otp-field/PlOtpField.d.ts +6 -0
  179. package/dist/components/otp-field/PlOtpField.js +1 -1
  180. package/dist/components/overlay/PlOverlay.d.ts +1 -1
  181. package/dist/components/overlay/PlOverlay.js +1 -1
  182. package/dist/components/page-layout/PlPageLayout.js +1 -1
  183. package/dist/components/pagination/PlPagination.d.ts +12 -9
  184. package/dist/components/pagination/PlPagination.js +1 -1
  185. package/dist/components/panes/PlPanes.d.ts +7 -2
  186. package/dist/components/panes/PlPanes.js +1 -1
  187. package/dist/components/pie-chart/PlPieChart.d.ts +58 -0
  188. package/dist/components/pie-chart/PlPieChart.js +1 -0
  189. package/dist/components/pie-chart/index.d.ts +2 -0
  190. package/dist/components/pie-chart/index.js +1 -0
  191. package/dist/components/pill/PlPill.js +1 -1
  192. package/dist/components/popconfirm/PlPopconfirm.d.ts +1 -1
  193. package/dist/components/popconfirm/PlPopconfirm.js +1 -1
  194. package/dist/components/popover/PlPopover.d.ts +6 -6
  195. package/dist/components/popover/PlPopover.js +1 -1
  196. package/dist/components/portal/PlPortal.d.ts +78 -0
  197. package/dist/components/portal/PlPortal.js +1 -0
  198. package/dist/components/portal/index.d.ts +2 -0
  199. package/dist/components/portal/index.js +1 -0
  200. package/dist/components/progress-box/PlProgressBox.js +1 -1
  201. package/dist/components/progress-circular/PlProgressCircular.js +1 -1
  202. package/dist/components/progress-linear/PlProgressLinear.js +1 -1
  203. package/dist/components/radio-group/PlRadioGroup.js +1 -1
  204. package/dist/components/rating/PlRating.d.ts +1 -1
  205. package/dist/components/rating/PlRating.js +1 -1
  206. package/dist/components/scatter-chart/PlScatterChart.d.ts +46 -0
  207. package/dist/components/scatter-chart/PlScatterChart.js +1 -0
  208. package/dist/components/scatter-chart/index.d.ts +2 -0
  209. package/dist/components/scatter-chart/index.js +1 -0
  210. package/dist/components/scroll-area/PlScrollArea.d.ts +98 -0
  211. package/dist/components/scroll-area/PlScrollArea.js +1 -0
  212. package/dist/components/scroll-area/index.d.ts +2 -0
  213. package/dist/components/scroll-area/index.js +1 -0
  214. package/dist/components/scroll-zone/PlScrollZone.d.ts +27 -10
  215. package/dist/components/scroll-zone/PlScrollZone.js +1 -1
  216. package/dist/components/segmented-button/PlSegmentedButton.js +1 -1
  217. package/dist/components/select/PlSelect.js +1 -1
  218. package/dist/components/show/PlShow.d.ts +76 -0
  219. package/dist/components/show/PlShow.js +1 -0
  220. package/dist/components/show/index.d.ts +2 -0
  221. package/dist/components/show/index.js +1 -0
  222. package/dist/components/sidebar/PlSidebar.js +1 -1
  223. package/dist/components/sidebar/PlSidebarTrigger.js +1 -1
  224. package/dist/components/skeleton/PlSkeleton.js +1 -1
  225. package/dist/components/slider/PlSlider.d.ts +13 -1
  226. package/dist/components/slider/PlSlider.js +1 -1
  227. package/dist/components/sparkline/PlSparkline.d.ts +68 -0
  228. package/dist/components/sparkline/PlSparkline.js +1 -0
  229. package/dist/components/sparkline/index.d.ts +2 -0
  230. package/dist/components/sparkline/index.js +1 -0
  231. package/dist/components/spoiler/PlSpoiler.d.ts +16 -1
  232. package/dist/components/spoiler/PlSpoiler.js +1 -1
  233. package/dist/components/stack/PlStack.d.ts +114 -0
  234. package/dist/components/stack/PlStack.js +1 -0
  235. package/dist/components/stack/index.d.ts +2 -0
  236. package/dist/components/stack/index.js +1 -0
  237. package/dist/components/stat/PlStat.d.ts +4 -0
  238. package/dist/components/stat/PlStat.js +1 -1
  239. package/dist/components/stepper/PlStepper.d.ts +3 -3
  240. package/dist/components/stepper/PlStepper.js +1 -1
  241. package/dist/components/switch/PlSwitch.js +1 -1
  242. package/dist/components/table/PlTable.d.ts +5 -5
  243. package/dist/components/table/PlTable.js +1 -1
  244. package/dist/components/tabs/PlTabs.d.ts +27 -2
  245. package/dist/components/tabs/PlTabs.js +1 -1
  246. package/dist/components/text-link/PlTextLink.js +1 -1
  247. package/dist/components/time-picker/PlTimePicker.js +1 -1
  248. package/dist/components/timeline/PlTimeline.d.ts +2 -2
  249. package/dist/components/timeline/PlTimeline.js +1 -1
  250. package/dist/components/timeline-chart/PlTimelineChart.d.ts +44 -0
  251. package/dist/components/timeline-chart/PlTimelineChart.js +1 -0
  252. package/dist/components/timeline-chart/index.d.ts +2 -0
  253. package/dist/components/timeline-chart/index.js +1 -0
  254. package/dist/components/toast/PlToast.d.ts +1 -1
  255. package/dist/components/toast/PlToast.js +1 -1
  256. package/dist/components/toggle/PlToggle.js +1 -1
  257. package/dist/components/toggle/PlToggleGroup.js +1 -1
  258. package/dist/components/tooltip/PlTooltip.js +1 -1
  259. package/dist/components/tour/PlTour.d.ts +134 -0
  260. package/dist/components/tour/PlTour.js +1 -0
  261. package/dist/components/tour/index.d.ts +2 -0
  262. package/dist/components/tour/index.js +1 -0
  263. package/dist/components/transfer/PlTransfer.d.ts +10 -1
  264. package/dist/components/transfer/PlTransfer.js +1 -1
  265. package/dist/components/tree/PlTree.js +1 -1
  266. package/dist/components/tree-select/PlTreeSelect.d.ts +99 -0
  267. package/dist/components/tree-select/PlTreeSelect.js +1 -0
  268. package/dist/components/tree-select/index.d.ts +2 -0
  269. package/dist/components/tree-select/index.js +1 -0
  270. package/dist/components/typography/PlTypography.js +1 -1
  271. package/dist/components/window-pane/PlWindowPane.d.ts +179 -0
  272. package/dist/components/window-pane/PlWindowPane.js +1 -0
  273. package/dist/components/window-pane/index.d.ts +2 -0
  274. package/dist/components/window-pane/index.js +1 -0
  275. package/dist/css/anchor.css +2 -0
  276. package/dist/css/animate-counter.css +2 -0
  277. package/dist/css/animate-float.css +2 -0
  278. package/dist/css/animate-reveal.css +2 -0
  279. package/dist/css/animate-scramble.css +2 -0
  280. package/dist/css/animate-shake.css +2 -0
  281. package/dist/css/animate-split.css +2 -0
  282. package/dist/css/app-logo.css +2 -0
  283. package/dist/css/area-chart.css +3 -0
  284. package/dist/css/back-top.css +2 -0
  285. package/dist/css/bar-chart.css +3 -0
  286. package/dist/css/calendar.css +1 -0
  287. package/dist/css/carousel.css +2 -0
  288. package/dist/css/code-block.css +2 -0
  289. package/dist/css/color-picker.css +1 -0
  290. package/dist/css/combobox.css +1 -0
  291. package/dist/css/command-palette.css +1 -0
  292. package/dist/css/confirm.css +2 -0
  293. package/dist/css/data-list.css +2 -0
  294. package/dist/css/data-table.css +7 -0
  295. package/dist/css/date-picker.css +1 -0
  296. package/dist/css/date-range-picker.css +1 -0
  297. package/dist/css/date-time-picker.css +1 -0
  298. package/dist/css/flex.css +2 -0
  299. package/dist/css/floating-action-button.css +4 -0
  300. package/dist/css/gallery.css +7 -0
  301. package/dist/css/gauge-chart.css +3 -0
  302. package/dist/css/heatmap-chart.css +3 -0
  303. package/dist/css/hover-card.css +2 -0
  304. package/dist/css/how-to-steps.css +2 -0
  305. package/dist/css/icon-button.css +1 -0
  306. package/dist/css/image.css +2 -0
  307. package/dist/css/line-chart.css +3 -0
  308. package/dist/css/menubar.css +1 -0
  309. package/dist/css/meter.css +2 -0
  310. package/dist/css/mockup.css +2 -0
  311. package/dist/css/pagination.css +1 -0
  312. package/dist/css/pie-chart.css +3 -0
  313. package/dist/css/popconfirm.css +2 -0
  314. package/dist/css/portal.css +2 -0
  315. package/dist/css/scatter-chart.css +3 -0
  316. package/dist/css/scroll-area.css +2 -0
  317. package/dist/css/scroll-zone.css +2 -0
  318. package/dist/css/show.css +2 -0
  319. package/dist/css/sidebar.css +3 -0
  320. package/dist/css/sparkline.css +3 -0
  321. package/dist/css/spoiler.css +1 -0
  322. package/dist/css/stack.css +2 -0
  323. package/dist/css/stat.css +1 -0
  324. package/dist/css/time-picker.css +1 -0
  325. package/dist/css/timeline-chart.css +3 -0
  326. package/dist/css/tour.css +3 -0
  327. package/dist/css/transfer.css +4 -0
  328. package/dist/css/tree-select.css +4 -0
  329. package/dist/css/window-pane.css +2 -0
  330. package/dist/hooks/index.d.ts +6 -0
  331. package/dist/hooks/index.js +1 -1
  332. package/dist/hooks/usePlBreakpoint.d.ts +3 -0
  333. package/dist/hooks/usePlBreakpoint.js +1 -1
  334. package/dist/hooks/usePlDisclosure.d.ts +44 -0
  335. package/dist/hooks/usePlDisclosure.js +1 -0
  336. package/dist/hooks/usePlElementSize.d.ts +34 -0
  337. package/dist/hooks/usePlElementSize.js +1 -0
  338. package/dist/hooks/usePlHotKeys.d.ts +5 -1
  339. package/dist/hooks/usePlHotKeys.js +1 -1
  340. package/dist/hooks/usePlOnScreen.d.ts +54 -0
  341. package/dist/hooks/usePlOnScreen.js +1 -0
  342. package/dist/index.d.ts +34 -1
  343. package/dist/index.js +1 -1
  344. package/dist/internal/animate.d.ts +121 -7
  345. package/dist/internal/animate.js +1 -1
  346. package/dist/internal/breakpoints.d.ts +31 -0
  347. package/dist/internal/breakpoints.js +1 -0
  348. package/dist/internal/button-group.d.ts +29 -2
  349. package/dist/internal/button-group.js +2 -2
  350. package/dist/internal/calendar.d.ts +11 -36
  351. package/dist/internal/calendar.js +1 -1
  352. package/dist/internal/chart-frame.d.ts +462 -0
  353. package/dist/internal/chart-frame.js +1 -0
  354. package/dist/internal/chart-line.d.ts +44 -0
  355. package/dist/internal/chart-line.js +1 -0
  356. package/dist/internal/chart.d.ts +551 -0
  357. package/dist/internal/chart.js +1 -0
  358. package/dist/internal/color-scheme.js +1 -1
  359. package/dist/internal/data-table.d.ts +70 -0
  360. package/dist/internal/data-table.js +1 -0
  361. package/dist/internal/date.d.ts +10 -0
  362. package/dist/internal/date.js +1 -1
  363. package/dist/internal/defaults.d.ts +8 -3
  364. package/dist/internal/drag.d.ts +65 -0
  365. package/dist/internal/drag.js +1 -0
  366. package/dist/internal/focusable.d.ts +8 -0
  367. package/dist/internal/focusable.js +1 -0
  368. package/dist/internal/form.d.ts +61 -0
  369. package/dist/internal/form.js +1 -0
  370. package/dist/internal/format.d.ts +2 -0
  371. package/dist/internal/format.js +1 -1
  372. package/dist/internal/gallery.d.ts +80 -0
  373. package/dist/internal/gallery.js +1 -0
  374. package/dist/internal/glow.d.ts +17 -0
  375. package/dist/internal/glow.js +1 -0
  376. package/dist/internal/grid.d.ts +6 -27
  377. package/dist/internal/grid.js +1 -1
  378. package/dist/internal/highlight.d.ts +98 -0
  379. package/dist/internal/highlight.js +1 -0
  380. package/dist/internal/icons.d.ts +10 -0
  381. package/dist/internal/icons.js +1 -1
  382. package/dist/internal/image.d.ts +94 -0
  383. package/dist/internal/image.js +1 -0
  384. package/dist/internal/inert.d.ts +19 -0
  385. package/dist/internal/inert.js +1 -0
  386. package/dist/internal/keys.d.ts +19 -0
  387. package/dist/internal/keys.js +1 -1
  388. package/dist/internal/labels.d.ts +182 -0
  389. package/dist/internal/labels.js +1 -0
  390. package/dist/internal/link.d.ts +17 -0
  391. package/dist/internal/link.js +1 -1
  392. package/dist/internal/mockup.d.ts +193 -0
  393. package/dist/internal/mockup.js +1 -0
  394. package/dist/internal/page-layout.d.ts +14 -7
  395. package/dist/internal/page-layout.js +1 -1
  396. package/dist/internal/picker.d.ts +43 -6
  397. package/dist/internal/picker.js +1 -1
  398. package/dist/internal/progress.d.ts +22 -8
  399. package/dist/internal/progress.js +1 -1
  400. package/dist/internal/responsive.d.ts +108 -0
  401. package/dist/internal/responsive.js +1 -0
  402. package/dist/internal/scramble.d.ts +35 -0
  403. package/dist/internal/scramble.js +1 -0
  404. package/dist/internal/scroll-target.d.ts +5 -0
  405. package/dist/internal/scroll-target.js +1 -0
  406. package/dist/internal/steps.js +1 -1
  407. package/dist/internal/styles.d.ts +81 -0
  408. package/dist/internal/styles.js +1 -1
  409. package/dist/internal/table.d.ts +86 -0
  410. package/dist/internal/table.js +1 -0
  411. package/dist/internal/text.d.ts +39 -0
  412. package/dist/internal/text.js +1 -0
  413. package/dist/internal/threshold.d.ts +19 -0
  414. package/dist/internal/threshold.js +1 -0
  415. package/dist/internal/tour.d.ts +42 -0
  416. package/dist/internal/tour.js +1 -0
  417. package/dist/internal/watermark.d.ts +44 -0
  418. package/dist/internal/watermark.js +1 -0
  419. package/dist/internal/wheel.d.ts +52 -0
  420. package/dist/internal/wheel.js +1 -0
  421. package/dist/internal/window.d.ts +201 -0
  422. package/dist/internal/window.js +1 -0
  423. package/dist/locales/de.d.ts +3 -0
  424. package/dist/locales/de.js +1 -0
  425. package/dist/locales/en.d.ts +2 -0
  426. package/dist/locales/en.js +1 -0
  427. package/dist/locales/es.d.ts +3 -0
  428. package/dist/locales/es.js +1 -0
  429. package/dist/locales/fr.d.ts +3 -0
  430. package/dist/locales/fr.js +1 -0
  431. package/dist/locales/index.d.ts +33 -0
  432. package/dist/locales/index.js +1 -0
  433. package/dist/locales/ja.d.ts +3 -0
  434. package/dist/locales/ja.js +1 -0
  435. package/dist/locales/ko.d.ts +3 -0
  436. package/dist/locales/ko.js +1 -0
  437. package/dist/locales/zh-Hans.d.ts +3 -0
  438. package/dist/locales/zh-Hans.js +1 -0
  439. package/dist/provider/PlColorSchemeScript.js +1 -1
  440. package/dist/provider/PlassProvider.js +1 -1
  441. package/dist/provider/index.d.ts +1 -0
  442. package/dist/provider/index.js +1 -1
  443. package/dist/styles.css +1 -1
  444. package/dist/tailwind.css +1357 -25
  445. package/dist/tokens.css +1357 -25
  446. package/dist/types.d.ts +368 -2
  447. package/package.json +26 -16
  448. package/dist/components/avatar-group/PlAvatarGroup.d.ts +0 -47
  449. package/dist/components/avatar-group/PlAvatarGroup.js +0 -1
  450. package/dist/components/avatar-group/index.d.ts +0 -2
  451. package/dist/components/avatar-group/index.js +0 -1
  452. package/dist/css/avatar-group.css +0 -2
  453. package/dist/internal/avatar-group.d.ts +0 -25
  454. package/dist/internal/avatar-group.js +0 -2
package/CHANGELOG.md CHANGED
@@ -2,6 +2,968 @@
2
2
 
3
3
  > This package's history. The Flutter package keeps its own at [`packages/flutter/CHANGELOG.md`](https://github.com/jooy2/plass-ui/blob/main/packages/flutter/CHANGELOG.md), because the two version independently.
4
4
 
5
+ ## vNext (2026--)
6
+
7
+ ## 1.5.0 (2026-09-15)
8
+
9
+ ### Breaking changes
10
+
11
+ - **A label pack now holds functions, so a Server Component cannot pass one to `PlassProvider`.** React does not send a function across the server boundary. Render the `PlassProvider` that takes a pack from a file with `'use client'` at the top.
12
+
13
+ - **Without `getRowKey`, a `PlDataTable` row is keyed by its position in `rows`, and every callback's `index` is that position.** The default key and the `index` handed to `getRowKey`, `render`, `onRowClick` and `isRowSelectable` used to be the row's place in what was drawn, while `onSelectedChange` looked the rows up by their place in `rows`. Ticking the first row on page two also showed the first row on page one as ticked and handed back `rows[0]`, and a sort or a search did the same. Both now count from the top of `rows`, so a row keeps its key and its `index` wherever a sort, a search or a page puts it. Code that used `index` as a place on the screen, such as numbering the rows as drawn, has to count them itself. A selection kept by default keys now names different rows, and `getRowKey` is still the way to key rows that survive `rows` changing.
14
+
15
+ - **Picking a day in `PlDateTimePicker` keeps the moment inside `minDate` and `maxDate`.** The day that holds a bound stays selectable, and picking it used to keep the clock as it was, or put midnight on it, without checking the bound again. With `minDate={new Date()}`, picking today committed today at 00:00. The clock is now moved into the bounds, up to the next whole minute (or second, with `showSeconds`) at or after `minDate` and down to the last one at or before `maxDate`. A clock that is already inside the bounds is kept as before.
16
+
17
+ - **A lone `width` or `height` on `PlImage` now sizes its box.** Passed alone, either one used to reach the `<img>` as an attribute and change nothing on the page. Now `height={200}` draws a box 200 pixels tall, and `width={320}` one 320 pixels wide. If you passed one of them only as a hint about the file, pass both dimensions of the file, or remove the one.
18
+
19
+ ### Added
20
+
21
+ - **`PlShow` takes `render`, so a gate inside a paragraph can be a `<span>`.** It always rendered a `<div>`, which a `<p>` cannot hold, so the HTML parser closed the paragraph in front of it and a server-rendered page failed to hydrate. `render={<span />}` renders another element, and it gates the same way.
22
+
23
+ - **`PlButton` takes `focusableWhenDisabled`.** A `disabled` button leaves the tab order, which drops the focus when a control becomes unavailable under it. With `focusableWhenDisabled` it stays a tab stop and is announced as unavailable, the way a stepper at the end of a `PlPagination` row now is.
24
+
25
+ - **`PlAnchor` takes a `target`, for headings that scroll inside an element rather than the window.** In an app shell whose `<main>` scrolls on its own the window never moves, so no row was ever lit. `target` takes an element, a ref or a function returning one, as the `target` of `PlBackTop` does, and the reading line, `offset` and the last-row rule are measured against that element. Left out, the list follows the window as before.
26
+
27
+ - **Eleven words join the label packs**, for the strings that were written into a component in English with no way to translate them but a prop: `acknowledge` (the OK on `PlConfirmProvider`'s alert), `optional` (`PlStep`), `notifications` (the region `PlToastProvider` announces toasts in), `sidebarOpen` (`PlSidebarTrigger`), `spoilerWarning` (`PlSpoiler`), `filePickerTitle` (`PlFilePicker`), and the five words a `PlChatBubble`'s mark says, `messageSending`, `messageSent`, `messageDelivered`, `messageRead` and `messageFailed`. All seven packs translate them. A pack of your own built against `PlassLabels` needs the new keys.
28
+
29
+ - **`PlImage` and `PlAspectRatio` take `fit="scale-down"`.** It is `contain` that never enlarges a file smaller than the box.
30
+
31
+ - **A lone `width` or `height` sizes a `PlImage`'s box.** A lone `height` is a box that tall across its container, and takes its width from a `ratio` when there is one. A lone `width` is a box that wide, never wider than its container. A number or a string of digits is pixels, and any other string is a CSS length. Both dimensions together keep describing the file. See Breaking changes for what this changes in existing code.
32
+
33
+ - **`PlImage` takes a `position`.** `center`, a side, a corner such as `'top left'`, or two percentages such as `'30% 20%'`. It decides which part of the picture a `cover` crop keeps, and where `contain`, `none` and `scale-down` leave their empty space. It is read on the picture as it is shown, so `position="top"` keeps the top of what the reader sees through `rotate` and `flip`, and it is physical, so it does not move on a right-to-left page. Any other `object-position` value passes through as written.
34
+
35
+ - **`PlImage` takes a `letterbox`.** It fills the part of the box that `contain`, `none` and `scale-down` leave empty. `blur` draws the picture itself behind it, covering the box and blurred, turned, mirrored, placed and tinted like the picture. The copy loads from the picture's own `src`, `srcSet`, `sizes`, `loading`, `decoding`, `crossOrigin` and `referrerPolicy`, so it costs no second request, and it is hidden from assistive technology and takes no pointer. Any other string is a CSS `background` painted on the box.
36
+
37
+ - **A `PlImage` placeholder can be a picture.** <code v-pre>placeholder={{ src, blur }}</code> stands a small copy of the picture in while the file arrives, from a URL, a data URI or a `Blob`. It is drawn under the picture with the picture's `fit`, `position`, `rotate`, `flip` and `filter`, stays until the picture has finished fading in over it, and is then removed in one step, so the page never shows through a cross-fade. `blur: true` blurs it by 20 pixels and a number by that many. A `Blob` gets an object URL that is revoked when the stand-in goes, and the skeleton is not drawn while a picture stands in. The stand-in needs a reserved box, as the skeleton does.
38
+
39
+ - **`PlImage` takes a `priority`.** For the picture a page is judged by, usually its Largest Contentful Paint image: it sets `loading="eager"` and a high fetch priority, and an attribute written out still wins. The attribute is written as `fetchPriority` under React 19 and as `fetchpriority` under React 18, so neither warns. A blurred `letterbox` copy is asked for the same way.
40
+
41
+ - **`PlImage` takes a `rotate`.** `0`, `90`, `180` or `270` degrees clockwise, and any other number goes to the nearest quarter. A picture on its side is laid out on its side: `width` and `height` still describe the file, so `width={1200} height={800} rotate={90}` reserves a box two wide by three tall, a `ratio` is kept as the shape of the layout, and without either the box takes the turned shape once the file has loaded. The turn is the CSS `rotate` property rather than `transform`, so a `transform` of the caller's own still applies on top, and `preview` opens the picture turned the same way.
42
+
43
+ - **`PlImage` takes a `flip`.** `horizontal`, `vertical` or `both`, along the axes the picture is shown on, so `flip="horizontal"` swaps left and right on the screen whether or not `rotate` has turned it. It is drawn with the CSS `scale` property, which leaves `transform` free, and `preview` opens the picture mirrored the same way.
44
+
45
+ - **`PlGallery` carries the new `PlImage` props to its pictures.** An item takes `rotate`, `flip`, `position` and a picture `placeholder`, and the gallery takes `fit` (still `cover` by default), `letterbox` and `loading` for every tile. An item's `ratio` stays the stored file's proportion: a picture on its side is dealt into a masonry lane, grown in a justified row and opened in the viewer by its turned proportion, while a `grid` tile keeps the gallery's shape. Ratios of unturned items still reach the picture as written. `hover="zoom"` is now a `transform`, so it still zooms a mirrored picture.
46
+
47
+ - **`PlImage` takes a `filter`.** Six named treatments — `grayscale`, `sepia`, `saturate`, `desaturate`, `contrast` and `dim` — and anything else you pass is used as a CSS `filter` chain exactly as written. It is set through a `--p-filter` slot and rides the house transition, which `filter` was already on, so a treatment swapped on hover travels at the same pace as the picture's own fade instead of snapping while the fade is still moving. The skeleton and the fallback are left alone.
48
+
49
+ - **`PlImage` takes a `watermark`.** A bare string sits in the bottom corner; an object says where it goes, how visible it is and at what angle. `placement: 'tile'` covers the whole picture, which is what a proof or a preview wants — a corner mark is cropped off in a second. A tiled mark is one repeating SVG background rather than forty or fifty elements, and the layer is turned as one layer so the repeat has no seam. It is drawn only once the picture has arrived, is `aria-hidden`, takes no pointer, and follows the picture into `preview`.
50
+
51
+ - **`PlImage` takes a `protect`.** Refuses the context menu, a drag out of the page, a text selection over the picture and — the one that is easy to forget — the iOS long-press callout, which on that platform _is_ the context menu. It is a deterrent and not a lock, and the documentation says so: the file is still one request away. A caller's own `onContextMenu` still runs and cannot turn the refusal off, and the refusal follows the picture into `preview`. There is no Flutter equivalent, because a Flutter app paints its pictures onto a canvas and there is no per-picture menu to refuse.
52
+
53
+ - **`PlTabs` answers the wheel.** A bar with more tabs than room is a scroll container like any other, and a mouse has one wheel that points down the page — the one direction the bar does not run in — so the reader who could see there were more tabs had nothing to reach them with but the arrow keys, which also change the selection. A vertical wheel over an overflowing bar now moves it along, exactly as it does over a `PlScrollZone`. `wheel={false}` turns it off, a bar whose tabs all fit never takes the gesture, and a bar that runs down the side is left alone.
54
+
55
+ - **`PlScrollZone` and `PlTabs` take an `overscroll`.** `'contain'`, the default, or `'auto'`, spelled after CSS's own `overscroll-behavior` and shared as `PlassOverscroll`. See below for what the default changes.
56
+
57
+ ### Fixed
58
+
59
+ - **Adding a slide at the front of a `PlCarousel` keeps the slides already there.** Each slide was keyed by its position, so a new first slide remounted every slide after it and threw away their state, such as a playing video or a half-filled form. A slide is now keyed by its own `key`.
60
+
61
+ - **A vertical `PlStepper` shows only the panel of the step `active` is on.** The panel opened on any step whose `status` was `current`, so marking a step behind the reader `current` again, as for one that failed validation, opened a second panel, and giving the active step another `status` closed its own. The panel now follows `active` whatever `status` says, as the horizontal panel and the Flutter build already did.
62
+
63
+ - **A screen reader names the panel of a horizontal `PlStepper` after its step.** The panel carried `aria-labelledby` on a `div` with no role, and a name on such an element is never read, so moving into the panel said nothing about which step it belonged to. The panel is now a `role="group"` named by the step's label.
64
+
65
+ - **An `interactive` `PlCard` no longer glides up under reduced motion.** The lift under the pointer and its shadow still eased over `--plass-duration` for a reader who had asked for less motion. Both now change at once, as in the Flutter build.
66
+
67
+ - **A picker trigger measured with a `format` that writes the weekday leaves room for Friday.** The dates it measures to hold its width fell on every weekday but Friday, so the trigger could be a little narrower than a Friday needed. A Friday is now among them.
68
+
69
+ - **A held `PlScrollZone` button, and the wheel over a `PlScrollZone` or `PlTabs` strip, keep up on a strip whose CSS sets `scroll-behavior: smooth`.** Each frame of a hold and each wheel step scrolled with `behavior: 'auto'`, which follows `scroll-behavior`, so every step started a slide that the next one cut short. The strip fell behind the button and the wheel, to under half the distance in Chromium, and all but stopped in Firefox. Those steps now scroll with `'instant'`. The buttons in `item` and `page` mode still slide.
70
+
71
+ - **A `PlPill` does less work when its `details` re-renders.** A pill with `details` written inline disconnected its size observer, made a new one and measured its panel again on every render, so a pill showing a live counter did all three every second. It now keeps one observer for as long as it has `details`.
72
+
73
+ - **A `PlChatBubble` link preview fetches its picture only when the card comes near the screen.** Every preview picture was requested as soon as its bubble rendered, so a long thread with many link cards downloaded the pictures of cards scrolled far out of view. The picture now loads lazily and decodes off the main thread.
74
+
75
+ - **An `inline` `PlColorPicker` inside a disabled `<fieldset>` is disabled.** Its square and rails are not form controls, so the fieldset did not reach them: they stayed tab stops and still took keys and the pointer. They now behave as with `disabled`, the label is muted, and the picker no longer reports its value to a `PlForm`. A `PlFieldset` with `disabled` does the same, and turning the fieldset back on brings the picker back.
76
+
77
+ - **A `PlColorPicker` swatch the picker cannot read is left out.** A swatch such as `'red'` was drawn as a button that did nothing when pressed, since named colours are not read, and every swatch was painted from its string, so a bare `'ff0000'`, which the picker reads as red, was an empty circle. Such a swatch is now not drawn, and the rest are painted with the colour a press chooses.
78
+
79
+ - **The two select-all ticks of a `PlTransfer` have different names.** Both were read as "Select all", so a screen reader could not tell which list a tick belonged to. Each is now named by `selectAllLabel` and then by its list's heading, "Select all Available", read from the heading itself, so a heading given as an element is read as well.
80
+
81
+ - **A `vertical` `PlScrollArea` no longer scrolls sideways.** The viewport scrolled both ways, so a `<pre>` or a long URL wider than the box could be scrolled horizontally with no scrollbar to show it. An area now scrolls only along the axes `orientation` names, as in the Flutter build, and content wider than a `vertical` area is cut off at the edge. Let such content wrap, or use `orientation="both"`.
82
+
83
+ - **A collapsed `PlSidebar` opens from the side its `PlassProvider` `direction` says.** The drawer read only the document's direction, so a `start` sidebar in a right-to-left subtree of a left-to-right page slid in from the left. It now follows the `direction` of the nearest provider, and the document's direction when no provider names one, as the Flutter build follows `Directionality`.
84
+
85
+ - **A `PlCarousel` with its own `onPointerEnter` or `onFocus` still pauses `autoPlay` under the pointer and with the focus inside.** The caller's `onPointerEnter`, `onPointerLeave`, `onFocus` and `onBlur` replaced the carousel's own instead of running beside them. Passing one of them let the slides keep turning while the reader pointed at them or had tabbed into them, or kept a paused carousel from starting again. The carousel's handlers and the caller's now both run.
86
+
87
+ - **A pressed `PlToggle` keeps its fill in forced-colours mode under the pointer.** In a Windows contrast theme, a pressed `glass` or `ghost` toggle is filled with the system's highlight colour, but pointing at it or holding it down repainted it in the page's colour, so it looked released for as long as the pointer was on it, with its label still in the colour meant for the highlight. It now stays filled. A `solid` toggle was not affected.
88
+
89
+ - **`PlImage` in Firefox no longer reports `loaded` for a picture that has not arrived after its `src` changes.** When the previous picture was already in the browser cache, Firefox still fired that picture's `load` after the `src` had changed, and `onStatusChange` took it for the new one. A caller heard `loaded` before the new picture arrived, or after its `error`, and a picture that failed showed no fallback. That late `load` is now ignored.
90
+
91
+ - **→ on an open `PlTree` branch with nothing to step into keeps the focus where it is.** A branch with `children: []`, or whose children are all `disabled`, handed the focus to the next row, which is a sibling. The focus now moves only into a child, as the ARIA tree pattern says.
92
+
93
+ - **`PlProgressLinear`, `PlProgressCircular` and `PlProgressBox` write their value in the provider's `locale`.** Unlike `PlMeter`, they did not pass on the `locale` of the nearest `PlassProvider`, so a `format` followed the browser's language and the percentage shown without one was always written `75%`. Both now follow the provider, so `locale="de-DE"` writes `75 %` on screen and in `aria-valuetext`. Without a provider, the percentage follows the browser's language, as a `format` already did.
94
+
95
+ - **A `PlSkeleton` with a `label` says it to a screen reader.** The label was only the name of its `role="status"`, and a screen reader reads out the text inside a live region, not its name. The status was also `aria-busy`, which tells a screen reader to wait until the region is no longer busy, and a skeleton is removed instead. The label is now visually hidden text inside the status, and the status is no longer `aria-busy`. Put `aria-busy` on the region being loaded if it needs one.
96
+
97
+ - **Adding a step at the start of a `PlTimeline` keeps the steps already there.** Each step was keyed by its position, so a new first event in an activity feed remounted every step after it and threw away their state. A step is now keyed by its own `key`.
98
+
99
+ - **`PlStat` writes `change` to one decimal at most.** The number was printed as it came, so `change={0.1 + 0.2}` read `+0.30000000000000004%`. It is now rounded to one decimal, half away from zero, as the Flutter build writes it. A change that rounds to 0 is drawn as `0%` in the muted colour with no arrow.
100
+
101
+ - **Pressing the × on an uncontrolled `inline` `PlDrawer` closes it.** An inline drawer read only `open` and `defaultOpen`, so without `open` the × called `onOpenChange` and the panel stayed. It now keeps its own open state, and a controlled `open` still decides.
102
+
103
+ - **`PlTour` cuts its light round an SVG target.** Only an HTML element was measured, so a step pointed at a chart bar or any other SVG element dimmed the whole page, the target included, and the target could not be pressed. Any element is measured now.
104
+
105
+ - **`PlTour`, `PlBackTop` and `PlScrollZone` jump to where they are going when the reader has asked for less motion, whatever the page's CSS says.** `PlTour` always scrolled each target into view smoothly, so under `prefers-reduced-motion` the page still slid past on every step, and `PlBackTop` and `PlScrollZone` scrolled with `behavior: 'auto'`, which follows `scroll-behavior`, so on a page with `html { scroll-behavior: smooth }` they still slid. All three now scroll with `'instant'`.
106
+
107
+ - **The lowest bar of a `PlSparkline` bar strip above zero draws inside its box.** A value at the foot of the scale is drawn as a bar one pixel tall, and when every value is above zero the foot is the bottom edge, so that pixel sat just below the box. It now grows up from the bottom edge, and strips with values below zero draw as before.
108
+
109
+ - **A `PlTimelineChart` span whose start or end is not a time has no row in the table.** Such a span is not drawn, but the hidden table gave it a row with an empty start and end, which a screen reader read out, and a label only that span carried added a label column. It is now left out of the table, as it is left out of the marks.
110
+
111
+ - **A `PlTransfer` row that leaves `items` and comes back is no longer still ticked.** The ticks were kept for the life of the component, so a value that left and came back returned ticked, with its arrow pressable. The ticks of values that leave `items` are now dropped, as in the Flutter build.
112
+
113
+ - **`registerLanguage` also replaces a name the library reads as an alias.** A name such as `vue`, `html` or `svelte` was turned into the language it stands for before the registered grammars were checked, so after `registerLanguage('vue', vue)` a `language="vue"` block was still coloured as XML. A registered name is now looked up first, as the documentation already said, and the bar shows that name.
114
+
115
+ - **A `PlCodeBlock` whose `title` is an element names its code region after that title.** Only a string could become the region's `aria-label`, so `title={<code>src/index.ts</code>}` left a focus stop with no name. The region now takes its name from the title the bar draws, through `aria-labelledby`, and a string title keeps the name it had. With `toolbar={false}`, a string title still names the region and any other title falls back to the language, then `codeLabel`.
116
+
117
+ - **A `PlCodeBlock` `highlightLines` range stops at the last line of the block.** A range was walked from end to end whatever the block held, so a typo such as `'1-100000000'` built a hundred million line numbers and froze the tab. Only the lines the block has are walked now, so a range that runs past the end marks up to the last line.
118
+
119
+ - **A bar `PlSparkline` whose values are all below zero draws inside its box.** The bars grew from zero, which for such a strip lies above its top, so they covered the content above it. They now hang from the top of the strip, and strips with positive or mixed values draw as before.
120
+
121
+ - **A `PlTimelineChart` span that lies wholly outside a fixed `min` and `max` can no longer be reached.** It was not drawn but stayed a mark, so the arrow keys stopped on nothing and pointing beside the plot showed its tooltip outside it. It is now left out of the marks and the table, and a span that crosses an edge answers only on the part that is drawn. The tooltip and the table still give that span's real start and end.
122
+
123
+ - **A bar chart with `valueLabels="last"` labels the last value it has.** The label went on the last slot, so a series that ends in a gap, such as `[10, 20, null]`, had no label at all. It now goes on the 20, as it already did on a line chart, and `extremes` no longer walks the whole series again for every bar on each render.
124
+
125
+ - **The × on a `PlChip`, on a `PlCombobox` chip and on a picker trigger can be pressed anywhere in a 24px square around it.** It could be pressed only on the glyph, about 15px across and two pixels from the label or the trigger, which is under the minimum target size of WCAG 2.5.8 and easy to mistake for the trigger on a touch screen. The × is drawn at the same size and the chip and the trigger keep theirs; where the square reaches over the label or the trigger, a press inside the square goes to the × and the rest still goes to its neighbour. This covers `PlDatePicker`, `PlDateRangePicker`, `PlDateTimePicker`, `PlTimePicker`, `PlColorPicker` and `PlTreeSelect`.
126
+
127
+ - **A `PlHeatmapChart` thins its column names by one stride.** Each name set its own stride from its own width, so the short names either side of a long one were written over it. The stride is now taken once, from the widest name, so every name that is written clears its neighbours.
128
+
129
+ - **↑ and ↓ move between rows in a `PlHeatmapChart` grid.** Both did what → and ← do, so reaching the cell directly below in a 7×24 grid took 24 presses. On a grid they now move to the same column in the row below or above, stepping over a gap. ← and → and every key on a treemap are unchanged.
130
+
131
+ - **A tiled `PlImage` watermark draws in a token colour or `currentColor`.** The colour was written into the SVG image the tile repeats, and an image cannot read the page's custom properties or its `color`, so `var(--plass-…)` and `currentColor` came out black. The tile is now a mask over the layer's own background colour, so any CSS colour works, as it already did for a mark in a corner.
132
+
133
+ - **A tiled `PlImage` watermark covers every corner of a wide or tall picture.** The turned layer was a fixed share larger than each side, so from about 16:9 two opposite corners were left as empty triangles. It is now sized from the diagonal of the picture, which no turn can uncover.
134
+
135
+ - **A pressable `PlGallery` tile is described by the caption it draws.** The button's name is the picture's `alt` and its place in the set, and that name kept the `title` and `description` drawn inside the button from being read. The lines a tile draws now describe it through `aria-describedby`, a `hover` caption included before the pointer has shown it. The name is unchanged.
136
+
137
+ - **A `PlChatBubble` link card is a link only when `preview.url` is an address the bubble will follow.** The URL was written straight into the `href`, and a message's link preview is usually built from a URL somebody else wrote. `http`, `https`, `mailto` and relative addresses are unchanged; given anything else the same card is drawn with nothing to click, so the title, the site and the picture are still read and nothing announces a destination that is not one.
138
+
139
+ - **A chart legend asked for on the left or the right stays on that side under RTL.** `PlassSide` is a physical direction, but the legend was placed by the order of a flex row, which follows the writing direction, so `side: 'left'` put the legend on the right of an Arabic or Hebrew page. The row is now laid out physically, and the words inside the legend still read in the page's own direction.
140
+
141
+ - **Pointing at the legend entry of a series that is switched off no longer fades the whole chart.** The bar, scatter and pie marks checked only whether the hovered entry was some other series, so hovering a hidden one dimmed every series that was actually drawn, to highlight one that was not there. All of them now ask the one condition the line chart already used.
142
+
143
+ - **A `PlTooltip` wedge is the same sheet as the plate it points from.** It was drawn as two stacked triangles, a hairline one with the fill over it a pixel down, and both of those colours are translucent — so the wedge came out lighter than the plate, most visibly in the dark theme. It is now filled once with the sheet's own colour and the hairline is stroked along its two slanted sides, which is what `PlPopover` and `PlHoverCard` already drew. All three now draw the one arrow.
144
+
145
+ - **`PlCodeBlock` with `theme="auto"` follows a page that forces light with the `.light` class.** The block's own rule named only `[data-theme='light']`, so on a page whose system prefers dark and which had forced itself light with the class, the code was the one dark rectangle on a white page. Both ways of forcing a theme now count, as they already did for the tokens.
146
+
147
+ - **A macOS traffic light in `PlWindowPane` shows its mark while it holds the keyboard focus.** The mark appeared under the pointer only, so a reader who tabbed to one got a focus ring around a blank coloured dot with nothing to say which of the three it was on. The pointer still lights the whole set, and the keyboard lights the one it has reached, as the Flutter build does.
148
+
149
+ - **A `PlAnimateSplit` part wraps inside its own box.** Each part was `white-space: pre`, so a line with no space in it — a Chinese or Japanese sentence, which `by="word"` has nothing to cut at — was one part that ran out of its box instead of wrapping. A part now starts on a new line when it does not fit and wraps between its characters, as the `Text` of a part does in the Flutter build, and the line still enters as one part.
150
+
151
+ - **`PlAnimateSplit` and `PlAnimateScramble` cut text by grapheme.** Both cut by code point, so a flag or a family emoji came apart into pieces that draw as broken glyphs, a letter and its combining accent became two parts, and the scramble drew half of a surrogate pair as noise. Both now cut with the `Intl.Segmenter` `PlAnimateTyping` already used, so the two packages produce the same parts. A browser without `Intl.Segmenter`, which is Firefox below 125, still cuts by code point.
152
+
153
+ - **`PlAnimateCounter` and `PlAnimateScramble` hold their `delay` as well when `paused`.** A count held while it was still waiting started the whole `delay` again when it was let go, so a counter paused 100ms into a one-second wait took another full second to begin. What is left of the wait is now what is waited out.
154
+
155
+ - **A `trigger="hover"` effect plays once for a visit from the keyboard.** Focus events bubble, so tabbing between two links inside the wrapper started the effect again at each one and an entrance replayed itself under the reader's hands. A focus arriving from, or leaving for, something the wrapper already holds is now ignored, so the effect runs for as long as the focus is inside it, as the Flutter build does. The pointer is unchanged, and a focus arriving from outside still starts it.
156
+
157
+ - **A `trigger="visible"` effect starts on where the element will land rather than on where its first frame is holding it.** The observer watched the element after its start state had already moved it, so a `PlAnimateSlide` inside the `overflow: hidden` mask its own page recommends, and a turned `PlAnimateRotate` in a box of its own size, reported as off the screen for ever and never played. How much of the element is on screen is now measured on its resting box, with a second observer on the parent saying when the view has moved, so `threshold` is a share of the element wherever it sits, which is what it already means in the Flutter build. A slide with nothing clipping it starts a little earlier than before.
158
+
159
+ - **`PlAnimateTyping` holds the box of the whole string from the first frame, so nothing around it moves as it types.** The drawn copy held only the characters that had arrived, so a phrase that wrapped pushed the content below it down when its second line began, and the server HTML held an empty box. The characters still to come are now laid out after the caret without being drawn, as generated content that is not selected or copied, and the caret takes no room of its own while it moves. Every frame is laid out as the finished line, in the server HTML too.
160
+
161
+ - **`PlAnimateTyping` types the text inside elements among its children.** `Ship <strong>faster</strong>` was typed and read out as `Ship `, because the text of any element was dropped, while the docs said an element contributes its text. That text is now typed and read, and the markup is still left out, so the line is `Ship faster` without the bold.
162
+
163
+ - **`PlAnimateCounter` and `PlAnimateScramble` go on from where they were held when `paused` is released.** Resuming took the start time again, so a count held at 400 dropped back to `from` and a half-settled line scrambled again from its first character. A count also started over whenever a parent rendered it with an inline `easing`, which is a new function each time. Both now work the start time back from how far they had got and read `easing` from the latest render without restarting, as the Flutter build does. A hover, a new `play` or a new `value` still starts from the beginning.
164
+
165
+ - **`PlAnimateSplit` with `by="character"` wraps a line between words.** Every character was its own inline-block, and a line may break before and after each one, so in a narrow box a word wrapped partway through, such as "Internationali / zation". The characters of each word now sit together in one inline-block, which moves to the next line whole and wraps inside only when the word is wider than the line. Chinese, Japanese, Thai and the other scripts written without spaces still wrap between their characters.
166
+
167
+ - **`PlAnimateHeadline` keeps turning inside a parent that renders often.** The timer was restarted whenever the function that turns the reel changed, and that function changed with an inline `onIndexChange`, so inside a parent that rendered every second the 2600ms interval was reset before it ever fired and the line never changed. The timer now restarts only when the line, the count, `loop`, `interval`, `delay` or the running state changes.
168
+
169
+ - **`PlAnimateSplit` parts move with `effect="slide"`, and `effect="zoom"` is no longer the same as `grow`.** The parts were given no start position or scale, so they ran on the keyframe fallbacks: a slide only faded in place, and a zoom started from 0.8, the `grow` scale. Each part now starts where the component of the same name starts by default, so a slide rises from its own height below and a zoom grows from 0.4.
170
+
171
+ - **Under `prefers-reduced-motion`, the `PlAnimateMarquee` items past the edge of the box can be reached.** The strip stopped, but the box still clipped it, so with ten headlines in a 400px box only the first one or two could ever be seen. The box now draws only the first copy and scrolls along it, and it is a tab stop while there is anything to scroll, so the arrow keys move it. A vertical marquee scrolls down its own axis. Where the platform draws a permanent scrollbar, the box is taller by the bar's thickness.
172
+
173
+ - **Tab passes over the copies of a `PlAnimateMarquee` after the first.** The copies carried `aria-hidden`, which takes them off the accessibility tree but not off the tab order, so a strip of ten links was thirty Tab stops, twenty of them on a link with no name. The copies after the first are now `inert` as well, and only the first copy's links take the focus.
174
+
175
+ - **`PlRating` writes a fractional score the way the pack's language writes a number.** The packs put the raw number into the sentence, so a half star read `2.5 von 5` in German, French and Spanish, which write a decimal with a comma. Each pack now formats the score with `Intl` in its own language, `2,5 von 5`, and a whole score still reads `3`. A `ratingValue` you write yourself still receives the plain number.
176
+
177
+ - **An empty `PlTransfer` heading falls back to the label pack.** A `sourceLabel` or `targetLabel` of `''` drew the English `Available` or `Selected` whatever pack the page used. It now draws the pack's `transferAvailable` or `transferSelected`, as a heading left out already did.
178
+
179
+ - **A `PlChip` delete button is named in the word order of the label pack.** The name was the pack's `remove` followed by the chip's text, English's order in every language, so a Korean page read `삭제 Design`. It now comes from the pack's `removeItem`, `Design 삭제` in Korean, and `deleteLabel` still replaces the whole name. Words that a component of their own draws cannot be read from `children`, and still follow `remove`.
180
+
181
+ - **`PlGallery` says where a picture sits in the language of the label pack.** A tile's name and the viewer's counter were built from an English template inside the component, so a Korean page read `A harbour — 2 of 4` inside a list named `갤러리`. They now come from the new `galleryItem` label key, translated in all seven packs in each language's own word order, and `itemLabel` still wins.
182
+
183
+ - **Pressing a `PlPagination` page or stepper keeps the focus on it.** With `getPageHref`, the page that became current turned from a link into a button, a new element, and the focus fell to the top of the page; on the last page, Next became `disabled` and dropped the focus the same way, with or without `getPageHref`. The current page now stays a link marked `aria-current`, and a stepper at the end of the row stays in the tab order, announced as unavailable: an `<a>` with no address and `aria-disabled` in a link row, a button with `aria-disabled` otherwise. A `disabled` row still takes every control out of the tab order.
184
+
185
+ - **Each `PlCarousel` dot is a 24px press target.** A dot was a button the size of the dot it drew, 4 to 8 pixels across, which falls short of WCAG 2.5.8 and made jumping to a slide on a phone hard, above all with `arrows={false}`. The button is now 24px on each side with the dot drawn in its middle, and the targets sit side by side, so the dots are spaced further apart.
186
+
187
+ - **`PlCarousel` `autoPlay` keeps advancing inside a parent that renders often.** The interval was restarted whenever the function the component moves with changed, and that function changed with an inline `onValueChange`, so inside a parent that rendered every second a five-second interval was reset before it ever fired and the carousel stood still. The interval now restarts only when the slide, the count, the pause or the interval itself changes.
188
+
189
+ - **Pressing a `PlTransfer` arrow keeps the focus and says what moved.** The pressed arrow was disabled by the move, so the focus fell to the page and nothing said how many rows had gone across. The focus now goes to the first row that arrived, or to the target list when a controlled owner refused the rows, and a polite live region announces the count from the new `transferMoved` label key. The new `movedLabel` prop changes the sentence for one pair.
190
+
191
+ - **`PlOtpField` names every slot by its place in the row.** Every slot was named by the field label alone, so a screen reader read "Verification code" six times and never said which box the caret was in. Each slot is now named by the label and then its position, "Verification code Character 2 of 6", from the new `otpSlot` label key, and the new `slotLabel` prop changes the wording for one field.
192
+
193
+ - **Accessibility sentences with a value in them are read in the language of the label pack.** `PlPagination`'s page names and live line, `PlRating`'s scores, `PlCarousel`'s slide names and role descriptions, and the remove and add rows of `PlFilePicker` and `PlCombobox` were English templates inside each component, so a Korean page read `Page 5` inside a landmark called `페이지 이동`. They now come from new `PlassLabels` function keys (`paginationPage`, `paginationStatus`, `ratingValue`, `carouselSlide`, `removeItem`, `addCustom`) and the strings `ratingNone` and `slide`, translated in all seven packs in each language's own word order. A component's own prop still wins.
194
+
195
+ - **Moving the focus in `PlTree` redraws only the rows it moves between.** Every step of the focus rendered every row the tree had loaded again, and the rows inside closed branches were built on every render and thrown away, so holding an arrow key down in a tree of 10,000 nodes stuttered. Rows are now memoised: a step redraws the row the focus left, the row it reached and the branches they sit in, and a closed branch builds its rows only while it is open or closing.
196
+
197
+ - **`PlGallery` masonry tiles are read and tabbed in the order they were given, and keep their pictures when the lane count changes.** Each lane was a list of its own, so with three columns and twelve pictures Tab went 1, 4, 7, 10, 2, a screen reader read one lane after another, and the "4 of 12" in each tile's name came out of order. A tile that moved to another lane when the column count changed, at a breakpoint or right after server rendering, was mounted again and its `PlImage` lost its state. The tiles are now one list in the order they were given, placed into their lanes with CSS grid rows, so a tile that changes lane stays the same element. A picture that runs past the end of a tile in another lane is drawn taller by the gap there, and by that tile's caption with `caption="below"`.
198
+
199
+ - **Pressing Reveal or Hide on a `PlSpoiler`, or close on a `PlWindowPane`, from the keyboard no longer drops the focus to the top of the page.** The pressed button went `inert` while it still held the focus, so the browser moved the focus to the document body and the next Tab started the page over. Revealing now moves the focus to the uncovered content, and hiding moves it back to the Reveal button (or the first control of a custom `action`). Closing a window hands the focus back to the element it came in from, or, when that is gone, to the next focusable element after the window or the last one before it. The focus is left alone when it was elsewhere on the page.
200
+
201
+ - **`PlAnimateCounter`, `PlAnimateScramble` and `PlAnimateTyping` play again on every hover.** With `trigger="hover"` they ran on the first hover only, because a later hover changed nothing their loops watched, while every keyframe effect restarted each time. Each hover now starts the count, the scramble or the typing over, as the Flutter build does.
202
+
203
+ - **Restarting a `PlAnimate*` no longer replays another one nested inside it.** A restart rewound every animated element under its root, so an error message in a `PlAnimateFade` inside <code v-pre>&lt;PlAnimateShake replay={attempts}&gt;</code> faded in again on every shake. A restart now rewinds only the root and the parts that animation drew itself: its staggered children, the parts of a `PlAnimateSplit` and the tracks of a `PlAnimateMarquee`.
204
+
205
+ - **A caller's `onPointerEnter` and `onFocus` reach every `PlAnimate*` with `trigger="hover"`, and no longer stop the effect from starting.** Twelve of the components (Appear, Blink, Fade, Float, Grow, Lighting, Reveal, Rotate, Shake, Slide, Split and Zoom) laid their hover handlers over the caller's, so `onPointerEnter={prefetch}` was never called. The other five (Counter, Headline, Marquee, Scramble and Typing) did the opposite, so a caller's `onFocus` replaced the handler that starts the effect on focus, and a keyboard reader never saw it play. The two sets of handlers are now merged, and both run.
206
+
207
+ - **A `fullWidth` `PlDatePicker`, `PlTimePicker`, `PlDateTimePicker`, `PlColorPicker` or `PlTreeSelect` no longer renders its width samples.** The picker trigger held itself open at the widest value it could show by rendering every sample, hidden, even when `fullWidth` made its width its container's. A `fullWidth` trigger now renders only what it shows, as `PlSelect` and `PlDateRangePicker` do. A trigger without `fullWidth` keeps its samples.
208
+
209
+ - **A `fullWidth` `PlDateRangePicker` no longer renders every date sample in its trigger.** Each half held itself open at the widest date it could show by rendering the samples, hidden, even when `fullWidth` made the trigger's width its container's. A `fullWidth` trigger now renders only what it shows. A trigger without `fullWidth` keeps its samples, which stop it changing width as the range is filled in.
210
+
211
+ - **The `PlPill` button says whether its `details` are open.** With `details` and an `onClick`, the middle button had no `aria-expanded` and pointed at no panel, and since the other props land on the shell, a caller could not add either. The button now takes `aria-expanded` from `expanded` and points at the panel with `aria-controls`. A pill without `details` claims neither.
212
+
213
+ - **A `PlTabPanel` inside a Fragment is sorted out of the tab bar.** Children were sorted by looking only at the top level, so `items.map((item) => <><PlTab /><PlTabPanel /></>)` put every panel inside the `role="tablist"` strip, clipped under the tabs. Fragments are now opened on the way. A panel wrapped in a component of the caller's own still cannot be told apart from a tab, which the tabs page now says.
214
+
215
+ - **`PlHowToSteps` stays a list for Safari and VoiceOver.** The `<ol>` has its markers removed, which makes WebKit drop its list semantics, and the number discs are hidden from screen readers, so the order of the steps was not announced there. The list now carries `role="list"`, as `PlStepper` already did.
216
+
217
+ - **A `PlNavigationMenuItem` link can mark the page the reader is on.** There was no `active` and no way to pass `aria-current` through, so a top-level link could not say it was the current page. `active` now marks a link item `aria-current="page"` and draws its word in the accent.
218
+
219
+ - **A `PlMenuSubmenu` opens towards the end of the line under RTL, and its chevron turns with it.** The submenu opened against the physical right edge whatever the direction, while under RTL the arrow key that opens it is the left one, and the chevron kept pointing right. Left out, `side` is now the inline end, the left under RTL, and the chevron follows. A `side` given explicitly is still a physical edge.
220
+
221
+ - **An unavailable `PlMenuItem` with an `href` no longer navigates.** The link row took no `disabled`, so `<PlMenuItem href="/admin" disabled>` was drawn as available and followed its link when pressed. A disabled row with an `href` is now the plain row it has become: dimmed, announced as unavailable, and going nowhere.
222
+
223
+ - **The `PlFloatingBottomNavigation` key disappears when `value` names no destination.** The key stayed under the last destination, which no longer carried `aria-current`, so the bar drew a destination as current that nothing said was. It is now hidden, and appears in place when a destination is current again, as in the Flutter build.
224
+
225
+ - **Pressing `PlBackTop` moves the focus to the top of what it scrolled.** The button hides itself as the page scrolls up, and it kept the focus while hidden, so the next Tab went to the end of the page and scrolled it back down. When the button holds the focus, a press now hands it to the first focusable element at the top of the target, or lets go of it when there is none.
226
+
227
+ - **An unfinished mouse drag on a `PlScrollZone` ends when the button comes up outside the strip.** Until a press had moved far enough to become a drag, nothing was captured, so a button let go outside the strip left the drag listening: the strip then followed the pointer on hover alone, text could not be selected and the next click was swallowed. The drag now ends on the first move with no button held.
228
+
229
+ - **`PlCommandPalette` opens with an empty field however it was closed.** The search text was cleared only when Base UI closed the sheet, on Escape or a press outside, so running a command or setting a controlled `open` to `false` left the last search in the field for the next time it opened. It is now cleared whenever the palette goes from open to closed.
230
+
231
+ - **A `PlScrollZone` strip is a tab stop only while it overflows.** The scroller always took `tabIndex={0}`, so a strip that fits, or one whose chips are already focusable, added a stop with nothing to scroll, and without a `label` that stop had no name. It is now focusable only while there is somewhere to scroll, as `PlScrollArea` already was.
232
+
233
+ - **`PlPanes` handles can be named, and say which pane they resize.** A split with two handles read as "separator, 50" twice, with no way to tell them apart, while the Flutter build already took a `label`. `PlPanes` takes a `label` for its handles, and each handle points at the pane before it with `aria-controls`, using the pane's own `id` when it has one.
234
+
235
+ - **`PlContainer` keeps its `maxWidth` when it is given a `style`.** The caller's `style` replaced the one that carries the measure, so a `maxWidth="md"` container given <code v-pre>style={{ paddingBlock: 24 }}</code> had no width limit at all. The two are now merged, with the caller's declarations laid over the measure.
236
+
237
+ - **A `PlHeader` or `PlFooter` inside the page no longer takes over the `PlPageLayout` bar.** Every `PlHeader` and `PlFooter` under a layout registered itself as the layout's bar, and the last one won, so an article's own header inside `<main>` was measured instead of the site's and the sticky sidebars started below the wrong height. When that header went away, the layout measured nothing and the offset fell to 0. Only the bars in the `header` and `footer` slots are measured now.
238
+
239
+ - **The `PlSidebar` resize handle says the width it holds.** The handle is a focusable separator, and one of those is a value between two bounds, but it had no `aria-valuenow`, `aria-valuemin` or `aria-valuemax`, so moving it with the arrow keys told a screen reader nothing. It now carries the width in pixels between `minWidth` and `maxWidth`, and a drag or a key press updates it as the edge moves.
240
+
241
+ - **Each `PlSlider` thumb can have its own name and spoken value, and is described by `description`.** The two thumbs of a range were both named by `label`, their values were read in Base UI's own English wording such as "start range" whatever the page's language, and `description` was drawn under the track without being connected to either. `getAriaLabel` and `getAriaValueText` are passed to every thumb, and `description` now describes each one.
242
+
243
+ - **The `PlTransfer` arrows point at their own lists under RTL.** The selected list is at the inline end, which is the left under a right-to-left direction, but the arrow to it was always drawn pointing right and the arrow back pointing left. Both now turn over with the direction.
244
+
245
+ - **A `fullWidth` `PlSelect` no longer renders every option label in its trigger.** The trigger held itself open at the width of its longest label by rendering all of them, hidden, even when `fullWidth` made its width the container's. A list of 250 countries with a flag in each label requested 250 images before the list was ever opened. A `fullWidth` trigger now renders only what it shows. A trigger without `fullWidth` still renders every label, because that is what keeps it from changing width with its value.
246
+
247
+ - **The options of a disabled `PlRadioGroup` look disabled.** A group's `disabled` stopped its options answering, but each option chose its look from its own `disabled` alone, so the options of a disabled group were drawn at full strength with a pointer cursor and a hover. They now dim, take the not-allowed cursor and lose the hover, as an option disabled on its own does.
248
+
249
+ - **An `inline` `PlColorPicker` is grouped under its `label`.** The square and the rails are named "Saturation and brightness", "Hue" and "Opacity" whatever field they belong to, and the label, the description and the error were not connected to them, so two inline pickers on one page were two sets of the same sliders and an error was never announced. The panel is now a `role="group"` named by `label` and described by `description` and `error`, and an error marks the square and the rails `aria-invalid`.
250
+
251
+ - **The hue and opacity rails of `PlColorPicker` answer ↑, ↓, Home and End.** A rail took only ← and →, so a keyboard reader who pressed the other keys every slider answers to changed nothing. ↑ and ↓ now move a rail as → and ← do, and Home and End take it to its two ends.
252
+
253
+ - **A `PlFilePicker` button says which field it belongs to.** Its name was only the words inside the box, so a résumé picker and a cover letter picker on one screen were both read out as the same line. The name now starts with `label` and goes on with the box's own words.
254
+
255
+ - **The month and year buttons of a calendar say the month and the year.** Their `aria-label` was "Choose a month" and "Choose a year", which replaced the words on the buttons, so a screen reader never said which month was on screen and a voice command saying "July" found nothing. The buttons in `PlCalendar` and every picker are now named by what they show and described by what they do, and the grid under them, which had no name, is named by the header: `July 2026`.
256
+
257
+ - **The next question in a `PlConfirmProvider` queue places the focus again.** The sheet stays open between two queued questions and reused its buttons, so `autoFocus` did not run for the second one and the focus stayed on the button that had just been pressed. After a first question answered with its confirm button, a second, destructive question opened with the focus on its confirm button too, and pressing Enter twice approved both. Each question now gets new buttons, and its own `initialFocus` decides where the focus lands.
258
+
259
+ - **`PlTour` says the next step when the focus stays on Next.** Pressing Next left the focus on the button and changed the card around it, so a screen reader said nothing about the new step. The title and the content are now a polite live region that stays in place from step to step.
260
+
261
+ - **`PlTypography` clamps to the number of `lines` it is given, and cuts a `caption` or an `overline` short.** `lines` above 6 clamped to 6, because there was a class for each count up to six and nothing after. And `lines={1}` on a `caption` or an `overline` did nothing, since those levels are `<span>`s and an ellipsis needs a box. Any count now clamps to that many lines, and a one-line clamp is drawn as a block.
262
+
263
+ - **`PlImage` reports a new `src` that was already in the cache.** The status last reported was kept across a change of `src`, so a second picture that had already arrived settled as `loaded` without calling `onStatusChange`, and a caller that showed a spinner until it heard `loaded` kept showing it. A new `src` now starts over and reports its own arrival.
264
+
265
+ - **Every link that opens a new tab takes `noopener noreferrer`.** `PlTextLink` added them only for `newTab`, so a caller's own `target="_blank"` got neither them nor the "(opens in a new tab)" line a screen reader hears. `PlNavigationMenuLink` passed its `target` through with the `rel` untouched, and `PlMenuItem` took no `rel` at all. All three now merge the two tokens into the `rel` of any link whose `target` is not this tab, `PlTextLink` announces a `target="_blank"` link as it does a `newTab` one, and `PlMenuItem` takes a `rel`.
266
+
267
+ - **Each `PlChip` delete button is named after its chip.** Every delete button was named "Remove", so tabbing along a row of tags read the same word for each one and never said which tag it would remove. The name is now the label pack's word followed by the chip's text, such as "Remove design". A `deleteLabel` is still the whole name.
268
+
269
+ - **The `PlGallery` viewer keeps the focus when an arrow reaches the end of the set.** Pressing Next onto the last picture, or Previous onto the first, disabled the button that had the focus, which dropped the focus to the page, and the arrow keys stopped moving between pictures. The focus now crosses to the other arrow.
270
+
271
+ - **A `PlDataTable` sorted descending keeps its blank cells last.** The built-in order put a blank value after every other value, and the direction was then applied to that answer as well, so turning a column round put the blanks first, against what the page says. The direction now turns only the values that are there. A column's own `compare` is still turned round as a whole.
272
+
273
+ - **Opening a folded `PlBreadcrumb` keeps the keyboard focus in the trail.** The `…` button is removed when the hidden steps come back, so pressing it with the keyboard dropped the focus to the page and the next Tab started from the top. The focus now moves to the first step that came back and can take it.
274
+
275
+ - **A `PlCodeBlock` with a long run of spaces in its code no longer stalls a server render.** The trailing whitespace was cut with a regular expression that retries from every space in a run followed by more text, so 40,000 characters of a pasted log took almost three seconds in Node. It is now cut in one pass.
276
+
277
+ - **A `PlCodeBlock` handed new `code` or a new `language` stops drawing the last code.** The colours were kept without the code they were made from, so switching a block from a TypeScript file to a Python one drew the TypeScript lines under the new title until the Python grammar had loaded, while the copy button already copied the Python. The new code is now drawn plain until its own colours are ready.
278
+
279
+ - **A `PlTimelineChart` whose hours run over more than one day writes the date with each time.** An axis stepping in hours, minutes or seconds wrote only the time, so on a range of two days `09:00` could be either day, and the tooltip and the table wrote `09:00 – 17:00` with no day either. When the axis crosses midnight, every tick, the tooltip and the table now write the date in front of the time. A range inside one day is unchanged.
280
+
281
+ - **A treemap's hidden table names each tile under its own group.** `PlHeatmapChart` with `shape="treemap"` wrote its data table as a grid, with the first group's tile names as the column headings, so a screen reader heard the second group's tiles under the first group's names: "Tooling, Servers, 400". The table is now a group of rows per series, headed by the group's name, with a row per tile giving its own name and value.
282
+
283
+ - **A `PlScatterChart` whose `x` values are dates ticks its x axis like a calendar.** The axis ran on the dates' milliseconds with the same 1-2-5 steps a count gets, so it was labelled `1.7T`. It now takes the steps and the labels of a time axis, as `PlTimelineChart` does, and `xAxis.tickFormat` is handed each tick as a `Date` rather than as a number.
284
+
285
+ - **A chart stacked to `full` writes the caller's numbers in the caller's `format`.** `PlBarChart` and `PlAreaChart` keep the number each point was given for the tooltip and the table, and wrote it with `String()`, so a revenue chart with a currency `format` showed `4000` where every other chart showed `$4,000`. The numbers are now written the way the chart writes every other value.
286
+
287
+ - **`PlPieChart` and `PlGaugeChart` no longer call themselves empty on a server.** Both decided they had nothing to show from the radius they could draw, which comes from the measured width, and on a server or before the first measurement that width is zero. The server-rendered page said "Nothing here" until hydration, and the pie left its data table out, so a crawler saw neither the numbers nor the chart. The empty state now comes from the data alone, and the drawing waits for a width.
288
+
289
+ - **An axis pinned at one end stays there when every value is the same.** A flat series opens a band around itself so there is something to divide by, and the band was opened on both sides even past an end the caller had pinned, so <code v-pre>yAxis={{ min: 0 }}</code> over a row of zeros drew an axis from −1. Only the free end moves now: that chart runs from 0 to 1.
290
+
291
+ - **A chart of many categories does less work when its active column moves.** Every move of the pointer or the arrow keys to another column re-rendered `PlLineChart`, `PlBarChart` and `PlAreaChart` whole: every category label was written and measured again, the hidden data table compared a row for each category, and a group was created for each column even where it drew nothing. The labels are now written once per change to the data or to the room they have, the table is skipped when its data has not changed, and a column with nothing to draw adds nothing.
292
+
293
+ - **A finger can drag a `PlPanes` handle, a `PlSidebar` edge and a `PlWindowPane` title bar.** None of the three set `touch-action`, so on a touch screen the browser took the drag for a pan after a few pixels and ended it with `pointercancel`. They now take `touch-action: none` while they can be dragged, as `PlSlider` and the window's resize handles already did.
294
+
295
+ - **`usePlElementSize` and `usePlOnScreen` follow an element attached after the first render.** Both started watching in an effect keyed on the ref object, which never changes, so an element rendered later, as in `loading ? <Spinner /> : <div ref={box} />`, was never measured and never seen, and one swapped for another kept the old one watched. Both now read the ref after every render and start over on a different element, and `usePlOnScreen` does the same for `root`. `usePlElementSize` goes back to `null` when its element is removed.
296
+
297
+ - **`usePlColorScheme` no longer throws in a sandboxed frame.** The check for storage, `typeof localStorage`, sat outside the `try` that caught storage errors, and in a frame sandboxed without `allow-same-origin` that check is what throws, so the component using the hook took its whole tree down on first render. The check is now caught with the rest, and the choice simply does not survive a reload there, as the hook's page describes.
298
+
299
+ - **`PlColorSchemeScript` keeps its `storageKey` inside the script.** The key was written into the inline script with `JSON.stringify`, which leaves `<` as it is, so a key built from a value the page does not control, such as a tenant's slug, could close the `<script>` with `</script>` and put markup after it into a server-rendered page. `<`, `>`, `&`, U+2028 and U+2029 are now written as escapes, and the script reads the same key as before.
300
+
301
+ - **A pressable `PlPill`'s light stays on the pill and follows the pointer.** A pill in the flow was not positioned, so the two layers of its interaction light spread across the nearest positioned box around it, a card or the whole viewport, and the light sat in the middle because nothing told it where the pointer was. The pill now contains the light and moves it with the pointer, through the same handler `PlButton` and `PlToggle` use.
302
+
303
+ - **Controls keep their shape and their state in forced-colours mode.** Windows' contrast themes drop background images and box shadows and repaint backgrounds in the page's colour, so a `solid` button was left as its label with no edge, a `solid` field as a caret with no box, and every state the library draws as a fill disappeared: a checked `PlCheckbox`, `PlRadio` or `PlSwitch` looked unchecked, a pressed `PlToggle` unpressed, and the chosen `PlSegmentedButton` segment, the active `PlTabs` tab, the filled part of `PlSlider`, `PlProgressLinear`, `PlProgressBox` and `PlMeter`, the finished steps of `PlStepper`, `PlTimeline` and `PlHowToSteps`, the chosen day in `PlCalendar` and the pickers, every `PlHighlight` match, and the row the pointer or the arrow keys had reached in `PlSelect`, `PlCombobox`, `PlCommandPalette` and `PlMenu` were gone. In that mode those states are now painted in the system's own colours, `Highlight` behind `HighlightText` and `Mark` for a match, a `solid` button, field, chip, pill, badge, avatar, alert, toast, message bubble, app logo plate and key cap draws a border in a system colour, `GrayText` when disabled, and the thumbs of `PlSlider` and `PlScrollArea` are drawn in the text colour. Nothing changes outside forced-colours mode.
304
+
305
+ - **A shortcut answers the key it names, not only the character that key typed.** `usePlHotKeys` and a field's `hotKeys` compared `event.key` alone, so `Alt+K` never fired on a Mac, where Option+K types `˚`, `Mod+Shift+1` never fired because Shift turns the 1 into `!`, and `?` failed the Shift check it needs to be typed at all, while `PlHotKeys` drew those caps as if they worked. A letter or a digit is now also matched by its physical key when a modifier turned it into something else, and a symbol that takes Shift ignores Shift. On a layout that moves the letters, the printed letter still wins.
306
+
307
+ - **Shortcuts leave a key an input method is composing alone.** The Enter that commits a Korean syllable ran an `Enter` binding and, being consumed, broke the syllable, and the Escape that cancels a Japanese conversion closed a panel. Both hooks now skip a key while `isComposing` is set or the browser reports key code 229.
308
+
309
+ - **A nested `PlassProvider` changes only the words its `labels` name.** Its `labels` replaced the whole set from the provider around it, so a provider that renamed one button inside a Korean application put every other word back into English. The two sets are now merged per word, as the locales guide describes.
310
+
311
+ - **`PlConfirmProvider` and `PlTimelineChart` say the label pack's words.** The confirm dialog's Cancel and Confirm, and the start and end headings of a timeline's table, were written in English whatever `PlassProvider` said, while `PlPopconfirm` beside them already read the pack. Both now read `cancel`, `confirm`, `start` and `end`, and a component's own prop still wins.
312
+
313
+ - **The pickers, `PlCalendar` and `PlFilePicker` take part in a `PlForm`.** Base UI's form sees only the controls registered with a field, and these carried plain hidden inputs, so a `<PlDatePicker name="departure" required />` inside a `PlForm` was missing from the values `onSubmit` received, did not stop an empty submit, and never showed the form's `errors` entry for its name. They now register like any other field: an empty `required` one stops the submit and takes the focus, an `errors` entry is shown on it, and its value is in `onSubmit`. A `PlDateRangePicker`, a `multiple` `PlTreeSelect` and a `PlFilePicker` report arrays.
314
+
315
+ - **A `required` or `disabled` picker is respected by a plain `<form>`.** The hidden input under `PlDatePicker`, `PlDateRangePicker`, `PlDateTimePicker`, `PlTimePicker`, `PlColorPicker` and `PlTreeSelect` carried neither attribute, so an empty `required` picker was submitted and a `disabled` one still sent its value. An empty `required` picker now stops the submit, and a `disabled` picker, `PlCalendar` or inline `PlColorPicker` sends nothing. An empty single-choice `PlTreeSelect` now submits an empty entry under its name, as the other pickers do.
316
+
317
+ - **`PlCarousel` no longer scrolls the page when it changes slide.** It brought the slide into view with `scrollIntoView`, which moves every scrollable ancestor as well as the strip, so a carousel partly off screen pulled the page to itself on every slide, and on every tick while `autoPlay` ran. Only the strip scrolls now.
318
+
319
+ - **A responsive `PlFlex`, `PlGrid` or `PlGridItem` nested in another keeps its own values.** The per-breakpoint values are custom properties, and custom properties inherit, so a child took every breakpoint its parent named and it did not: a vertical `PlFlex` inside one that turns horizontal at `md` turned with it, and a `PlGridItem` with no `span` inside a cell of eight was eight columns wide. The slots are now registered as not inheriting. Browsers without `@property` support, Firefox before 128, keep the old behaviour.
320
+
321
+ - **A `PlFilePicker` in a form submits the files it lists.** The file input held only the last pick from the dialog, so a dropped file was never sent, a file removed from the list or turned away by `accept` or `maxSize` still was, and in `multiple` a second pick replaced the first. The input now holds the list as it is shown.
322
+
323
+ - **The time columns of `PlTimePicker` and `PlDateTimePicker` work from the keyboard.** Every row was its own tab stop and the arrow keys did nothing, so reaching Done took more than a hundred presses of Tab on a clock with seconds. Each column is now one tab stop, and <kbd>↑</kbd>, <kbd>↓</kbd>, <kbd>Home</kbd> and <kbd>End</kbd> choose a row and move the focus with it, stepping over a blocked row.
324
+
325
+ - **A labelled picker's trigger reads the value as well as the label.** `PlDatePicker`, `PlDateRangePicker`, `PlDateTimePicker`, `PlTimePicker`, `PlColorPicker` and `PlTreeSelect` named their trigger with the label alone, so a screen reader heard "Departure, button" and had to open the popup to find out which day was chosen. The trigger is now named by the label and then by the value it shows.
326
+
327
+ - **`PlTour`'s card follows the tour.** The card was given its target as a getter, which Base UI asks only when the popup opens, so from the second step on the cut-out moved and the card stayed beside the first target. A step with no target left the card invisible. The card now moves to each step's target, and a step with no target centres it on the viewport, as the documentation says.
328
+
329
+ - **`PlScatterChart` and `PlTimelineChart` no longer redraw on every pixel the pointer moves.** Both stored the pointer's offset for a tooltip mode that only a chart of columns reads, so each move re-laid the whole chart out and re-rendered every mark. They now re-render only when the nearest mark changes.
330
+
331
+ - **`plass-ui/css/<component>.css` also scans the components a component renders.** Each manifest named only the component's own folder, but Tailwind scans files, so a project that registered `icon-button.css` alone got none of the utilities spelled in `PlButton`, and the date pickers and charts lost the ones in `PlButton` and `PlBox`. A manifest now lists every component folder its modules reach, directly or through the library's internals.
332
+
333
+ - **Hidden content is inert under React 18 as well.** A covered `PlSpoiler`, a collapsed `PlPill`'s details, a closing or minimized `PlWindowPane` and a `disabled` `PlCalendar` set `inert`, which React 18 does not know and dropped with a warning. The content stayed in the tab order and on the accessibility tree, and a disabled calendar still took a pick. The attribute is now written in the form each supported React version keeps.
334
+
335
+ - **A `PlScrollZone` or `PlTabs` with `overscroll="auto"` gives the wheel back from the first moment.** The latch that keeps a gesture on a strip it was just scrolling counted from the page's own start rather than from a real move, so for the first quarter-second after a page loaded, a strip at its end held the wheel it should have handed on. The Flutter build already counted from nothing.
336
+
337
+ - **A `PlImage` with `preview` reserves its box again.** The preview's trigger is a `<button>`, and a button sizes itself to its content even when it is displayed as a block. The content is a picture sized off the box, so before the file arrived the box had no width and its `ratio` reserved nothing, and a file smaller than the container shrank the box to the file. The trigger now takes the full width, as a picture without `preview` always did.
338
+
339
+ - **`PlChip` keeps the tails of its letters.** The label is truncated, and truncation clips at the line box, which the chip's one-em leading made shorter than the glyphs inside it — so every g, j, p, q and y lost its descender, at every size. The label's line box is now the font's own height. The chip is the same height and the words sit where they did.
340
+
341
+ - **`PlSpoiler` no longer changes height when it is uncovered.** The cover is a line of explanation and a button, so it is routinely taller than the text it covers — and it was taken out of the layout on reveal, which collapsed the sheet to its content and pushed the whole page below it up. Covering it again pushed everything back down. The cover now keeps its place and is held hidden and `inert`, exactly as the `reversible` hide row already was, so the sheet measures the same in both states. A `maxHeight` clamp is still released on reveal, which is the one thing that may resize it.
342
+
343
+ - **A justified `PlGallery` no longer blows up its last row.** Every tile is grown in proportion to its own width, and the last row has fewer of them sharing the same width — so one leftover photograph stretched to fill it and stood two or three times as tall as the gallery above it. The list now carries a `::after` that eats the slack, which keeps the last row at the height it was aiming for. It is a pseudo-element rather than a filler item on purpose: an extra `<li>` would be an extra entry in a `role="list"`, counted by every screen reader that announces how many there are. The Flutter build already left its last row unstretched.
344
+
345
+ - **`PlImage` no longer hides a picture that had already arrived.** The component moved out of its loading state on the `<img>`'s `load` event alone, and an event is only heard by something already listening: a file served from the cache — or one a server rendered, so the browser began fetching it while parsing the HTML — can finish decoding before React attaches the handler. The picture then sat at `opacity: 0` behind its own placeholder for good. It now asks the element where it got to on mount and on every `src` change, so a picture that is already `complete` is shown rather than waited for.
346
+
347
+ ### Changed
348
+
349
+ - **`PlCombobox` and `PlCommandPalette` say the label pack's `empty` when nothing matched.** Their `emptyMessage` defaults, "No matches" and "No commands found", were written in English, so a translated application still said them in English. Both now fall back to `empty`, as `PlTreeSelect` and `PlTransfer` do, which makes the English default "Nothing here" for both. Pass `emptyMessage` to keep the old words.
350
+
351
+ - **Focus rings are drawn in the family's `accent`, opaque.** The ring was the `solid` colour at 55% opacity, which came to about 2.2:1 against a white surface, and 1.4:1 for `warning`, under the 3:1 a focus indicator needs. `--plass-{c}-ring` now resolves to `--plass-{c}-accent`, which clears 3:1 against the surface and the page in both themes. Every focused control looks different: darker in the light theme and lighter in the dark one. A project that overrides an `accent` now moves the ring with it.
352
+
353
+ - **A `PlScrollZone` no longer hands the wheel back to the page at its ends.** The pointer being on the shelf is the reader saying which of the two things under it they meant to move, and reaching the last card is not them saying something else — so the page used to start moving at a pixel nobody chose, in the middle of a flick. The new `overscroll` default is `'contain'`, and `overscroll="auto"` is the old behaviour. Even `auto` now keeps a gesture that was scrolling the strip a moment ago and hands the page the wheel only once the reader has paused. Two things keep this from being a trap: a strip everything fits in is not a scroller and holds nothing back either way, and only the axis the strip runs on is contained, so a finger sliding down a horizontal shelf still scrolls the page.
354
+
355
+ - **`PlImage`'s preview overlay is now a separate chunk.** `preview` is off by default and the overlay is several times the weight of the picture component that opens it, so it is reached through `React.lazy` — as `PlGallery`'s viewer already was. That takes 21 kB gzipped off the first paint of any page importing `PlImage` — it measured 26.8 kB and the overlay was 25.4 kB of it — and the same 21 kB off `PlGallery`, which draws its tiles with a `PlImage` and whose own lazy viewer had been undone by this one import. Nothing to configure, and no API change; on a cold cache the overlay now appears a moment after the first press.
356
+
357
+ - **The size budget covers `PlImage` and `PlGallery`**, the two components whose heaviest feature is off by default, so a static import added back to either is caught rather than reviewed past.
358
+
359
+ - The two runtime dependencies move their floor up: `@base-ui/react` to `^1.8.0` and `highlight.js` to `^11.12.0`. Neither release changes an API this package uses.
360
+
361
+ ### Documentation
362
+
363
+ - **The `PlShow` page says that a layer which portals out is not hidden with its half.** It said both halves are hidden with `display: none`, but a `PlModal`, `PlDrawer` or popover inside the closed half renders at the end of `<body>` and still shows, and two halves reading the same `open` open two windows. The page and the JSDoc now say so and point to `usePlBreakpointValue` for layers.
364
+
365
+ - **The line chart page lists the fields of `PlassChartAxis`, `PlassChartLegend` and `PlassChartTooltip`.** The props tables named the three types and nothing more, so `min`, `tickCount`, `tickFormat`, `showValue`, `crosshair` and `mode` could only be found in the type declarations. The bar, area and scatter chart pages link to them.
366
+
367
+ - **The documentation was audited and rewritten for accuracy and plainness.** Three claims were wrong and are corrected: `PlImage` said there is no gallery component, the component index said every component has a Flutter half, and the component counts had gone stale (130 previews on the index, 130 components registered by `plass-ui/tailwind.css`, 127 shared between the two frameworks).
368
+
369
+ - **The section headings name the section.** 135 of them were essay titles (`What it answers before it knows`, `Which floating surface`, `Why it is not a PlCard`) and now name what the section holds. Every anchor link moved with them, and three links that were already broken are fixed.
370
+
371
+ - **The prose reads as reference rather than as an essay.** Most of the em dashes in both locales were replaced with plain punctuation or a sentence of their own. Around thirty of the longest paragraphs are split into sentences with the rationale taken out and the facts kept, the `is not X, it is Y` construction is gone where the negated half added nothing, and a 661-character explanation that had been copied into six pages is one clause per framework and a link.
372
+
373
+ - **The Korean pages read as Korean.** 해요체 no longer drifts into 합쇼체 pages, 구어 축약 is written out, `말합니다`/`주장합니다`/`손을 뻗다` as literal renderings of say, assert and reach for are replaced by the verb the sentence actually needs, `가지고 있다` becomes `~이 있다`, and 31 ledes that were noun-phrase fragments are complete sentences.
374
+
375
+ - **The home page and the READMEs read as an introduction.** The headline says what the library is, the feature cards are one or two plain sentences each, and the internals that belonged on a reference page are gone from them.
376
+
377
+ ## 1.4.0 (2026-09-05)
378
+
379
+ ### Added
380
+
381
+ - **`PlWindowPane`.** A window, drawn the way one of eight systems draws it, with anything at all inside it.
382
+
383
+ It is not a real window and does not pretend to be one: there is no desktop, no z-order and no dock. What it is is a _frame that behaves_ — the title bar drags, the corners resize, the three buttons are real buttons with real names — so a screenshot of an app, a demo of a feature or a piece of a landing page can be shown as the thing it will be rather than as a picture of it.
384
+
385
+ **Nothing here is transformed.** A dragged window moves on `left`/`top` and a resized one changes `width`/`height`, which keeps the text inside it at whole pixels through both gestures: a `translate()` would resample every glyph in the window for the length of the drag, which is exactly what the house rule against transforming a surface exists to prevent.
386
+
387
+ `os` covers `macos`, `macosx` (Aqua), the five Windows the title bar actually changed on, and `linux`. Which buttons a window has is the caller's decision; what order they sit in is the system's. `minimize` rolls the window up to its title bar rather than sending it anywhere, because a page has nowhere to send it — the content stays in the tree, marked inert.
388
+
389
+ `size` scales the chrome and nothing else, which makes this the third component after `PlBox` and `PlMockup` where the ladder is not a control height. Left out, `active` works itself out: a window is in front until another one on the page is pressed or takes the focus, and a click on the page _around_ them changes nothing.
390
+
391
+ Nothing drawn is a copy of any of those systems: a bar, a border and three buttons at the proportions they used, and no mark, wordmark or icon belonging to anyone else.
392
+
393
+ - **`internal/drag`**, the scaffold every pointer drag needs and none of the arithmetic: three listeners, a `data-dragging`, the document's text selection taken for the length of the gesture, and a teardown an unmount can call.
394
+
395
+ - **Four words in `PlassLabels`** — `minimize`, `maximize`, `restore` and `resizeWindow` — translated in all six locale bundles.
396
+
397
+ - **`PlMockup`.** A device with a screen you can put anything on: a phone, a tablet, a monitor or a laptop, with the system's own bars drawn on it.
398
+
399
+ **The screen is a real viewport at the device's own resolution** — an `md` phone is 390 by 844 — and the whole device is then scaled once to whatever room it has been given. So the content inside is laid out against a _screen_ rather than against the page: a 390-pixel column wraps where it would wrap on a phone, and the mockup can be 200 pixels wide without the content knowing.
400
+
401
+ That scale is the one `transform` in the library. The rule it excepts is about controls, where a scale resamples the label under the pointer pressing it; nothing here is pressed, and the scale is set once from the space available rather than changing on an interaction.
402
+
403
+ `size` sets the **resolution** rather than a height or a type scale — the second component after `PlBox` where the ladder means something other than a control height. The system's bars each take their own space rather than covering the content, because a caller putting a screenshot in a mockup wants all of the screenshot; the cut-out is the exception, being a real hole in the glass, and is drawn whether or not `systemUi` is on.
404
+
405
+ `bezel="none"` is no hardware at all rather than a thinner frame, and `finish` is fixed colours rather than theme tokens: a graphite phone is graphite on a page switched to dark. Landscape turns the screen, the bezel and the cut-out together; a desktop ignores it, because its stand does not turn.
406
+
407
+ The reference data — five resolutions per device, three shells, six systems' worth of chrome — lives in `internal/mockup`, for the reason `internal/chart` does.
408
+
409
+ - **`PlTimelineChart`.** Work against time — a row per thing, a bar per stretch of it.
410
+
411
+ The two axes are a set of rows and a calendar, which makes this a `PlBarChart` turned on its side with the baseline taken away: every bar starts where its own data says rather than at zero, so what the chart is about is _when_ rather than _how much_. It is not `PlTimeline`, which is a list of steps and draws no axis at all.
412
+
413
+ A row takes `PlassTimelineSeries`, whose data are spans rather than values. There is **no legend and no `hidden`**: the rows are the category axis, already named down the side, and a twenty-entry legend restating them is not a filter anyone wants.
414
+
415
+ Overlapping spans on one row are moved onto lanes of their own, by the greedy interval packing every scheduler uses. A row with no overlaps stays in a single lane, so the common row is exactly as thick as it was. Lanes are assigned in _start_ order but stored against the span's original index, because that is the order the arrow keys walk and a layout decision must not reshuffle it.
416
+
417
+ A span is cut to the plot rather than to the data: a bar that runs past a pinned `min` stops at the edge and says there is more of it off the side, where one drawn past the edge would say the axis is wrong. A zero-width span keeps a hairline, so a milestone is still something on the row. Both ends round, unlike a bar chart — neither end of a span is a zero.
418
+
419
+ The table under it is a row per span rather than the usual grid, because two rows of a Gantt have no columns in common.
420
+
421
+ - **`PlassTimelinePoint`** and **`PlassTimelineSeries`** in `src/types.ts`.
422
+
423
+ - **`PlHeatmapChart`.** A magnitude per cell, coloured rather than measured.
424
+
425
+ Two shapes of one idea. A `grid` is for two categorical axes and one magnitude — which hour of which day, which cohort in which week — where a bar chart of the same data would be forty bars nobody can scan. A `treemap` is for parts of a whole with more parts than a `PlPieChart` can hold, and it is the same component because the data is the same shape: a row of a heatmap and a group of a treemap are both a named series of named magnitudes.
426
+
427
+ **Colour here encodes size and not identity**, so it comes off a one-hue ramp rather than the categorical palette. One ladder covers the whole chart rather than one per row, because the colour of a cell has to mean the same number wherever it is — which is the entire promise a heatmap makes.
428
+
429
+ `scale="diverging"` is for a value with a _middle_ that means something. It is read from that middle rather than from the bottom, and both arms reach as far as the further one, so a set running from −2 to +40 does not paint every negative the deepest blue there is.
430
+
431
+ The treemap packing is squarified rather than sliced: a slice-and-dice treemap of twenty values ends in slivers a pixel wide, and a sliver's area is unreadable however exact it is. A tile's area is its share, so a negative value stays in the table and off the picture.
432
+
433
+ A label that does not fit its cell is dropped rather than clipped — a missing label sends the reader to the tooltip and a clipped one sends them nowhere.
434
+
435
+ - **`PlGaugeChart`.** One number on a scale that is known in advance, drawn as a dial.
436
+
437
+ It is a `PlMeter` bent into an arc, and the two are deliberately the same idea in two shapes: `value`, `min`, `max` and `thresholds` mean exactly what they mean there, so a page can move a reading from a bar to a dial without changing what it says. It is **not** a `PlPieChart` with `shape="semi"` — a pie is parts of a whole and every slice is a category, while this is one value against a scale and the unfilled arc is the rest of the dial rather than a second category.
438
+
439
+ `sweep` is degrees opened symmetrically about twelve o'clock: `180` for a dashboard tile, `270` for the instrument shape, `360` for a ring. The dial is sized against the box rather than assuming a circle, because how far it reaches _below_ its centre depends on the sweep — which is what keeps a wide, short card from drawing a thin band with an empty half above it.
440
+
441
+ The reading is **real text rather than a label painted into the drawing**, so it can be selected and found, and its size is solved against the room the hole actually leaves rather than fixed: `38` and `10,000%` are the same prop. The arc travels to a new value as a _length_ — a stroke's drawn fraction — because an outline is not a property CSS can transition, and a dial that scaled would resample the numbers written across it.
442
+
443
+ `ticks` is off by default. A gauge on a dashboard is read as a proportion, and rim marks are for an instrument somebody takes a number off. `showRange` is dropped past 330° whatever it says, because by then the two ends have come within a label's width of each other.
444
+
445
+ - **`PlassThreshold`**, in `src/types.ts`, and the band rule behind it in `internal/threshold`. `PlMeter` and `PlGaugeChart` now read the same list the same way, which is what stops a quota turning `danger` on a bar but not on the dial beside it. `PlMeterThreshold` is unchanged as a name — it is an alias of the new one, so nothing a caller imports has moved.
446
+
447
+ - **`PlSparkline`.** A chart with everything taken away except the shape.
448
+
449
+ No axes, no grid, no legend, no tooltip. It is not a small chart, it is a different thing: a word-sized picture that goes inside a sentence, beside a `PlStat`, or in a table cell, and says which way something has been going. Every number it could label is one the surrounding text already has, which is why it labels none of them.
450
+
451
+ `shape` is `line` for a trend, `area` for a quantity or `bar` for a count of discrete things — the same three sentences the full charts say, at a size where nothing else is being said at all.
452
+
453
+ **It scales itself to its own range, so the strip is always full.** That is what makes it readable at twenty pixels tall, and it is also the trap: two of them side by side are on two different scales, so the one that climbs more steeply may be the smaller number. `min` and `max` are how a row of them becomes small multiples instead.
454
+
455
+ `baseline` draws a rule across the strip and is pulled into the range if it sits outside the data. `endDot` marks the last point that is _a point_, not the last slot; bars do not take it, because a bar already ends where it ends. The colour is given directly rather than off the palette — one series and no legend leaves nothing to hand out.
456
+
457
+ Without a `label` the strip is taken off the accessibility tree entirely, because an unlabelled image announced as an image is noise beside text that already has the numbers. With one it is a named `role="img"` and the values are written out beside it.
458
+
459
+ - **`PlScatterChart`.** Two numbers per point, and whether they move together.
460
+
461
+ Both axes measure, which makes this the only chart in the library with no categories: there is no column a mark belongs to and no order the points could be shuffled out of. A point with a `z` is drawn as a bubble and one without it as a dot, so a scatter and a bubble chart are the same component reading the same data.
462
+
463
+ A `z` is an **area** and not a radius. Encoded as a radius, a value twice as large draws a mark four times the size; the square root keeps the ink proportional to the number. One scale covers the whole chart and is taken over every series including the hidden ones, so switching a legend entry off does not resize the rest. Bubbles paint largest first, because a small one inside a big one is otherwise invisible and the usual fix — half alpha everywhere — would undo the contrast the palette was solved for.
464
+
465
+ `shape="auto"` draws circles while colour alone can carry identity and switches to a shape per series from the fourth on. **That threshold is measured against this library's own palette, not inherited**: taking the first three slots, the closest pair under deuteranopia is ΔE 64 on the light sheet and 51 on the dark one; add the fourth and those fall to 4.9 and 2.8. A series carrying its own `color` does not count against it.
466
+
467
+ The table under the chart is a row per point rather than the usual grid — two points that are both the fifth of their series have nothing to do with each other, and a shared row would invent a relationship. Its columns take the axis labels, falling back to `x`, `y` and `z`.
468
+
469
+ - **`PlPieChart`.** Parts of a whole, at a glance.
470
+
471
+ The narrowest chart in the library and the easiest one to misuse. An angle is a poor thing to compare — two slices within a few percent of each other are indistinguishable, and a reader cannot rank six of them — so the pie is right for exactly one question: _is one of these most of it?_ Anything finer, and anything past six slices, is a `PlBarChart`.
472
+
473
+ It takes **one list of slices rather than a list of series**, because that is what a pie is: the slices are the entities here, so each takes a palette slot of its own and the legend lists them. The colour follows the slice and not its size, so a chart that is refiltered or resorted keeps every category the colour it had.
474
+
475
+ A `null` and a zero are both left undrawn. Neither has an angle, and a slice of no width is a slice a reader cannot point at.
476
+
477
+ `shape` is `pie`, `donut` or `semi`. A `semi` takes the **whole** height as its radius rather than half of it, because it only draws the top half — reserving room for the bottom would leave a blank band under the chart — and its centre then sits half a radius below the middle, which is what puts the arc in the middle of the tile.
478
+
479
+ `valueLabels="all"` writes each slice's **share** and not its value: a share is what a pie is a picture of, and the value is one hover away. A label wider than the slice it belongs to is dropped rather than clipped, so it can never end up sitting over the neighbour it would then be labelling. The gap between two slices is a constant on screen rather than in the data, so it subtends a wider angle on a small pie than on a large one.
480
+
481
+ The plot is a tab stop and the arrow keys walk the slices, with the value announced in a live region as the focus moves. Everything else — the legend, the tooltip panel and the hidden table — is the same frame every other chart uses.
482
+
483
+ - **`PlBarChart`.** Lengths, compared.
484
+
485
+ A bar says _how much_, and it says it by being longer — which is the whole reason its axis starts at zero and cannot be talked out of it. Crop the scale and a bar twice as long stops meaning twice as much, and the reader has no way to know it happened.
486
+
487
+ A `null` is a gap and the bar is simply **not drawn**, which matters more here than anywhere: a zero-length bar and a missing bar are the same picture, and only one of them is honest.
488
+
489
+ `orientation="horizontal"` is the right answer whenever the category names are words — it has a whole column for them where a vertical chart has the width of one bar — and everything swaps with it: which band each axis reserves, which way the grid and the crosshair run, and which end of a bar is rounded.
490
+
491
+ Grouped bars answer "which series is bigger here"; stacked bars answer "what is this total made of". They are different questions and the chart should be asked only one of them at a time. **The gap between two stacked segments is taken off the far end of each**, so the stack still totals the right length and the seam is the sheet showing through rather than a line drawn on it — a border around a bar is ink that is not data. The two arms accumulate separately, so a series that dips does not shorten the one above it, and the baseline is redrawn _over_ the bars because every one of them starts there.
492
+
493
+ `rounded` cuts the corners off the **data** end only: a rounded foot makes the axis look scalloped.
494
+
495
+ - **`PlAreaChart`.** A line with the space under it filled — which changes what the chart is about.
496
+
497
+ A line says where a value went. An area says how much of something there _was_, and stacked it says how that amount was made up. That is the whole test for reaching for it instead of a `PlLineChart`: if the quantity does not add up to anything — a temperature, a rate, a score — the fill under it is decoration, and a chart with two of them is two washes fighting.
498
+
499
+ Unstacked bands are a **wash fading downward**, so two of them overlapping stay readable and the line along the top is what carries the value. Stacked bands take a flatter, opaquer tint, because there the fill _is_ the mark and a band that faded out would have no bottom edge — and a stacked band is deliberately **not** also given a line along its top, since a stroke between two marks is ink that is not data.
500
+
501
+ `stacked="full"` normalises every category to a hundred, so the chart stops being about size and starts being about **share**. The normalising is a change to the _data_ rather than to the drawing, which is what lets the axis, the tooltip and the table all agree that the number is a share — and the tooltip still carries the number the caller passed, because a chart that can only tell you percentages has thrown the data away.
502
+
503
+ **The baseline is always zero**, which is the one axis rule it does not share with the line chart: an area's fill is its magnitude, so a cropped scale makes the band's thickness meaningless.
504
+
505
+ Measured with `npm run size`: **−0.1 kB on the whole library** and nothing anywhere else. The component is forty lines on top of the frame and the line painter that shipped with `PlLineChart`; the difference is compression noise, and it is what the foundation was for.
506
+
507
+ - **`PlLineChart`, and the foundation every chart after it stands on.** A value against time, or against anything else with an order to it.
508
+
509
+ The line is the mark that says _change_: it claims the space between two points is a journey rather than two separate facts, which is true of a temperature and false of four product categories.
510
+
511
+ **A `null` is a gap and never a zero** — a sensor that was offline, a month that has not closed yet. The line breaks across it, and a point with a gap either side is drawn as a dot rather than dropped. A chart that renders missing data as zero reports an outage as a collapse; `connectNulls` bridges it and should stay off unless the gap is an artefact of collection.
512
+
513
+ `curve="smooth"` is a **monotone cubic and not a spline**. A plain spline overshoots between two close points, so a series that never goes below zero draws a curve that does: a chart is allowed to be curved and it is not allowed to show a value that is not in the data.
514
+
515
+ **The value axis leaves zero out, and a bar chart's will not.** A line encodes a _position_, so cropping the scale moves every point by the same amount and the shape survives; a bar encodes a _length_, which stops meaning anything the moment it starts from 98.
516
+
517
+ The palette is **eight hues in a fixed order** — `--plass-chart-1` through `-8` — and it is the one place in the library where a colour is not a semantic role: a series is an entity, not a severity. Every slot clears 4:1 on the light surface and 4.9:1 on the dark one, and adjacent pairs are at least 10.4 apart in OKLab under simulated protanopia and deuteranopia, which is the pair that touches in a stack or a legend. Slot one is the page's own `primary`, so a one-series chart looks like it belongs. **Slots follow a series' index in the array it was passed**, never its position among the visible ones: a reader who learned that Europe is blue has learned something a filter is not allowed to take back. There are also a five-step sequential ramp and a five-step diverging one, for a mark whose colour is a magnitude rather than an identity.
518
+
519
+ Two internals arrive with it and are the reason the next eight charts are small. `internal/chart.ts` is the arithmetic — scales, nice numbers, band scales, path building, colour — and knows nothing about React or SVG. `internal/chart-frame.tsx` is everything a chart draws that is _not_ its marks: the axes, the grid, the crosshair, the legend, the tooltip, the empty state, the hidden table a screen reader reads instead of the picture, and the measurement that turns a percentage width into the pixels an SVG needs.
520
+
521
+ One new label, `chart`, in all seven packs.
522
+
523
+ Measured with `npm run size`: **+8.4 kB on the whole library and +0.0 kB on all four other scenarios** — the foundation is most of it, and the eight charts after this one pay for it once.
524
+
525
+ - **`PlGallery`.** A set of pictures, arranged — four layouts, captions, a pointer treatment and an optional lightbox, with everything but the arrangement identical across all four.
526
+
527
+ `grid` is a contact sheet: every tile the gallery's own ratio, whatever shape the file is. `masonry` keeps each picture's proportion and stacks the columns. `justified` keeps the proportions **and** fills every row to the edge — the only arrangement where nothing is cropped and no space is left over. `quilted` is a grid whose tiles span more than one cell, packed densely so a tile too wide for the space left drops to the next row that fits it and a later, narrower one fills the hole.
528
+
529
+ **Nothing is measured.** Every layout runs off the item's own `ratio`, so a wall of forty photographs is right in the frame the browser first paints and never reflows as the files arrive — the bargain `PlImage`'s `ratio` makes, one level up.
530
+
531
+ A masonry **deals across before it deals down**. CSS `columns` fills the first column top to bottom before it starts the second, so a set numbered 1 to 12 reads down the left edge and the first three pictures a reader meets are stacked on top of each other. The dealing is in `internal/gallery.ts` because the Dart build needs the same answers: a masonry with two orders is a masonry whose order means nothing.
532
+
533
+ `caption` is `below`, `overlay` or `hover`, and a tile with no words draws none whatever it says — a row with one caption and three gaps is worse than a row with none. `hover` is `lift`, `dim` or `zoom`, and `zoom` is the design language's named exception to the no-transform rule: what moves is a photograph inside a frame that stays exactly where it was, with no text on it to resample.
534
+
535
+ `preview` opens a lightbox with the rest of the set an arrow key away, behind a `React.lazy` so a wall of thumbnails costs nothing for a viewer nobody opened. It is deliberately not a `PlCarousel` — no autoplay, no wrap, and the arrows stop at the ends rather than looping back to a photograph the reader has already seen.
536
+
537
+ A tile is a button only when pressing it does something, and its name is the picture's own words plus where it sits — "A harbour — 1 of 6". `itemLabel` is how that sentence is written in another language, and it is a callback rather than a string with slots because the word order differs. One new label, `gallery`, in all seven packs.
538
+
539
+ Measured with `npm run size`: **+4.0 kB on the whole library**. The four narrower scenarios moved too, by 0.2 to 1.9 kB, and none of them contains a line of gallery code — checked against esbuild's own metafile. A second lazily-loaded module changes how the bundler splits the chunk the entry pulls in, which is also why `scripts/size.mjs` changed in this commit: it now builds with splitting on and counts the entry chunk plus every chunk statically reachable from it, rather than inlining every `import()` whether or not the module that made it survived tree-shaking.
540
+
541
+ - **`PlCodeBlock`.** A viewer for one line of code or a thousand — a bar over it, numbers down the side, a prompt in front of every line, and twelve palettes to read it in.
542
+
543
+ Everything it draws above the code is optional and off one prop each, because the same component has to be a snippet inside a sentence — no bar, no numbers, no chrome — and the full transcript at the top of a README.
544
+
545
+ **It is the one surface in the library that is not made of glass.** Every other sheet here is translucent and takes the page's colour family; this one paints its own opaque ground and refuses the family entirely, because the palette is the subject rather than the setting. A Dracula block tinted `primary` would be a Dracula block nobody chose.
546
+
547
+ Twelve themes: four the house's — `dark`, which is the default and the one that is not a preference, since code has been read on a dark ground since terminals; `light`; `auto`, the opt-in for a block that would rather follow the page; and `mono`, which has no hue in it at all and carries the structure by weight — and eight ports kept at the hex they were published in. `theme` takes **any string**, so a `[data-code-theme='ours']` block of custom properties is a theme with nothing to register and nothing to import. Sixteen slots, five of them derived from the other two.
548
+
549
+ **The line numbers and the prompt are generated content**, and that is the whole point of drawing them that way: a `$` a reader drags across and pastes into their shell is a `$` their shell chokes on. Neither can be selected, found by find-in-page, or copied.
550
+
551
+ **`highlight.js` is the package's second runtime dependency**, and it is reached only through a dynamic import — the core plus one module per language, thirty-five of them, never the package root. A page that imports `PlCodeBlock` and never highlights anything downloads none of it, and a page that highlights TypeScript downloads the core and TypeScript and not the other thirty-four. It is a real `dependencies` entry rather than an optional peer because a specifier a bundler cannot resolve fails the _whole_ build, not just the part that would have used it. Dart is in the built-in set, which highlight.js's own "common" list leaves out and half of this project's pages are written in.
552
+
553
+ Five new labels — `copy`, `copied`, `copyFailed`, `raw` and `code` — in all seven packs.
554
+
555
+ Measured with `npm run size`: **+3.2 kB on the whole library and +0.0 kB on all four other scenarios**. The grammars are not in that number and are not meant to be: they are chunks an `import()` fetches, one language at a time, and `scripts/size.mjs` now bundles with splitting on and counts only the chunk a page loads to render.
556
+
557
+ - **`PlTreeSelect`.** A value chosen out of a hierarchy rather than out of a list — a `PlTree` behind the same field every other picker wears.
558
+
559
+ It is the two of them composed and almost nothing else. The trigger is `PickerShell`, the popup is a `PlTree` with a `PlTextField` over it, and what the component adds is the arithmetic between them: which nodes a query keeps, which branches that opens, and which of the ids coming back out of the tree are answers rather than roads.
560
+
561
+ **`selectableBranches` is off by default**, which is the shape most of these trees actually have: the branches are the taxonomy and the leaves are the answers, and a "Europe" held alongside "France" is usually a data model nobody meant. A node's own `selectable` overrides it either way, and a branch that cannot be chosen still opens and closes — pressing it is how you get at what is under it.
562
+
563
+ Which is why **turning a branch press down is not the same as clearing**. A single-value tree hands back exactly one id, so treating an unusable one as an empty answer would empty the field every time somebody opened a folder. The press is dropped and what is held stays held.
564
+
565
+ `searchable` keeps every match **and every ancestor of one**. A tree filtered to bare matches is a list, and a list of leaves is exactly what a tree was chosen over — a "Seoul" with nothing above it does not say which taxonomy it came out of. Every branch the filter kept is opened, since a match folded inside a shut parent is a match nobody was shown, and emptying the field hands the folds back to the reader untouched. The fold is `internal/search`'s, so `jose` finds `José` here exactly as it does in a `PlCommandPalette`.
566
+
567
+ `multiple` keeps every node a press adds and leaves the popup up, because a picker that shut after the first of several answers would have to be reopened for each of the rest. `format` writes the trigger however the caller likes, and `name` puts one `<input type="hidden">` on the page per held id.
568
+
569
+ No new label: `search`, `empty` and `clear` were already words this library says.
570
+
571
+ Measured with `npm run size`: **+0.9 kB on the whole library and +0.0 kB on all four other scenarios**.
572
+
573
+ - **`PlTour`.** A guided walk over a page that already exists — the three things a new reader has to be shown once, pointed at where they actually are.
574
+
575
+ It is `PlHowToSteps` turned inside out. That component puts the instructions _in_ the page and the reader follows them; this one leaves the page as it is and stands over it. So a step **says what it is about** rather than describing it: what a tour points at is already on screen, and a second copy inside the card is a second copy to keep in step.
576
+
577
+ **The dimming takes the pointer and the light does not**, and the whole component rests on it. The scrim is one layer over the viewport with the target cut out of it as a **clip** rather than as a painted hole, and a clipped-away region is not hit-tested — so the reader can use the control being pointed at and nothing else. That is the difference between a tour and a dialog with a picture of a control in it, and it falls out of the geometry rather than being a second mechanism that has to agree with it.
578
+
579
+ The same clip is why the dimming can **blur**. A hole drawn as a box shadow or as four rectangles around the target can only paint a colour; a clipped layer carries a `backdrop-filter`, so the page around the light is out of focus as well as dark — this library's own material rather than a grey wash over it.
580
+
581
+ `target` takes a **ref, a selector or a getter**, and the ref is the one to reach for: a selector is a string that stops matching the moment somebody renames a class, and the tour would go on running with the hole over an empty piece of background. The selector is offered because it is the only form that works for an element the page did not render.
582
+
583
+ Escape, the ×, Skip and Done are the only ways out. A press outside the card is ignored and so is the focus leaving it, because using the page is exactly what a tour is for. `open` and `step` are each controllable on their own, `onFinish` runs before the tour closes — which is where "this reader has seen it" goes — and the component remembers nothing itself.
584
+
585
+ The counter is **two numbers rather than a sentence**: "3 of 7" is a string that has to be translated and a word order that differs by language, and the count itself does not. One new label, `skip`, in all seven packs.
586
+
587
+ Measured with `npm run size`: **+1.5 kB on the whole library and +0.0 kB on all four other scenarios**.
588
+
589
+ - **`PlDataTable`.** A table that owns its rows: it sorts them, narrows them to what was typed, hands them out a page at a time and remembers which of them are ticked.
590
+
591
+ `PlTable` stays exactly what it was, and the split is not a size decision. A table whose every column is a `render` callback is the one component in this library a React Server Component has to be able to render, and reading a context would take that away — so the component that has to remember four things between renders is a second component rather than a prop on the first.
592
+
593
+ **Below the columns they are the same grid.** The measured widths, the hover band, the rule between rows, the pinned header and every inline style that keeps a host stylesheet's `td { border: 1px solid }` off them now live in `internal/table.ts`, and both components draw out of it. Two copies of that is how the rows under a sorted table end up a shade off the rows under a plain one.
594
+
595
+ **Sort, search, selection and page are each uncontrolled by default and controllable one at a time.** That is what lets one component cover both of the tables people build: the ordinary one is `columns` and `rows`, and a table backed by a server is the same markup with `manual` and four handlers. Nothing in between changes shape.
596
+
597
+ **Sorting rotates ascending, descending, then back to the order the rows arrived in.** That third press is the part most tables leave out and it is the one that matters: the arrival order is usually the order the server chose, and a table that can never be put back has thrown it away. The mark is drawn faintly on every sortable heading rather than appearing under the pointer, because a heading that only looks pressable once you are on it is a heading nobody presses. Values are compared as what they are, text with `localeCompare` — sorting by code point puts every capitalised word above every lower-case one — and **nothing sorts last in both directions**, because a blank in a column of amounts is not the smallest amount.
598
+
599
+ A column's `value` is what the sort and the search see, where `render` is what the reader sees. Most columns need neither; the moment a cell is _drawn_ rather than printed the two come apart, and a total column printing `$1,240.00` sorts as a string that puts `$89` after it.
600
+
601
+ **The selection hands back rows from every page**, not from the page on screen, and the header box goes indeterminate when part of a page is chosen — a half-filled page under a plain unticked box reads as the opposite of what is true. Shift extends the range in the order the rows are _currently_ in, which is what a reader dragging down a sorted page means by "these". A chosen row carries `aria-selected` as well as the tint, and a press on the tick is not also a press on the row.
602
+
603
+ `aria-sort` goes on the **heading**, not on the button inside it: the heading is what a screen reader reads on entering a cell in that column. The sort control is a bare `<button>` wearing the heading's own type, because a `PlButton` there would be a control on a control — a background, a radius and a height inside a cell whose job is to sit flush against the rule under it.
604
+
605
+ Four things it deliberately does not do, and the page says so rather than leaving them to be looked for: it does not virtualize (the honest answer is `paging="pages"`, which is also the only shape that works when the rows are being fetched), it does not drag-resize or reorder columns (both need somewhere to persist what the reader dragged), it does not export (the application's data and the application's filename — `toolbar` is where the button goes), and it sorts on one column (a sort three keys deep is a query the reader cannot see).
606
+
607
+ One new label, `selectRow`, in all seven packs.
608
+
609
+ Measured with `npm run size`: **+2.8 kB on the whole library and +0.0 kB on all four other scenarios**.
610
+
611
+ - **Locale bundles: the library's own words, translated.** Every component that says something of its own — a close button's name, a pager's landmark, the line an empty list shows — now reads from one set of sixty-four strings, and seven translations of that set ship with the package.
612
+
613
+ ```tsx
614
+ import { PlassProvider } from 'plass-ui';
615
+ import { ko } from 'plass-ui/locales';
616
+
617
+ <PlassProvider locale="ko-KR" labels={ko}>
618
+ <App />
619
+ </PlassProvider>;
620
+ ```
621
+
622
+ `de`, `en`, `es`, `fr`, `ja`, `ko` and `zhHans`. The list is short on purpose: a pack is worth shipping once somebody who reads the language has read it.
623
+
624
+ **Each pack is a named export from its own module rather than an entry in a `locales['ko']` table.** A lookup would be the shorter API and the wrong one, because for a key to be found in a table the table has to be in the build — a French application would ship the Korean strings, the Japanese strings and the rest. An import ships one, and an application that switches language while it runs imports the packs it offers and picks between them, which is what it already does for its own copy.
625
+
626
+ **A key is a meaning, not a component.** `close` is the × on a modal, a drawer, a popover and a toast, and it is translated once. A key exists per component only where the word genuinely differs: `paginationNext` moves by a page and `carouselNext` moves by a slide, so a language that distinguishes those has somewhere to put the distinction.
627
+
628
+ `PlassLabels` widens what used to be the pickers' own `PlassPickerLabels`, which had already grown past the pickers; the twenty-five components that were carrying their own English string now take it from the set. Nothing about the precedence changed — the component's own prop still wins, a partial `labels` still leaves the rest English — so an application that named its words by hand keeps working exactly as it did.
629
+
630
+ Measured with `npm run size`: **+0.7 kB on the five overlays and +0.8 kB on the whole library**, which is the registry being shared rather than inlined; `PlButton` and `PlTypography` alone are unchanged. A pack is about **0.9 kB gzipped** and is only in the build if it was imported.
631
+
632
+ - **Three hooks: `usePlDisclosure`, `usePlElementSize` and `usePlOnScreen`.** Each of them is a piece of machinery the library already ran on internally, and each is public because the hand-written version has a decision in it that is easy to get wrong.
633
+
634
+ **`usePlDisclosure`** is one boolean and the four callbacks that change it. It is worth a hook rather than a snippet for one reason: written by hand it is a `useState` _plus three arrow functions that are new on every render_, and an inline `() => setOpen(false)` handed to a memoised trigger defeats the memo it was handed to. Every callback here is stable, `onToggle` included, because it uses the updater form rather than `!open`. The names are the props — `setOpen` fits `onOpenChange` exactly — so the ordinary use is two lines.
635
+
636
+ **`usePlElementSize`** adds two things to a `ResizeObserver`. The **first measurement** is taken in a layout effect rather than waiting for the observer, whose first callback arrives after a frame has been painted — a component that laid itself out from `0 × 0` for that frame flashes. And it reports the **content box**: the room actually left inside the element once its padding has been taken off, where a hand-written version nearly always reports `getBoundingClientRect()` or `borderBoxSize` and gets a number that includes the padding and the border.
637
+
638
+ **`usePlOnScreen`** has `once` **on** by default. The question a caller almost always has is "has this been seen yet" rather than "is it on screen right now", and a hook that kept answering the second one would re-render a page of lazily loaded pictures every time the reader scrolled past any of them. It answers `false` before it knows, which is safe for both of its uses, and `true` where there is no `IntersectionObserver`, because a picture that never loads is worse than one that loads early.
639
+
640
+ Measured with `npm run size`: **+0.4 kB on the whole library and +0.0 kB on all four other scenarios**, the three of them together.
641
+
642
+ - **`PlAnimateSplit`.** A line of text arriving one part at a time. The other effects tell themselves off across their **children**, which a line of text does not have — so this one makes them, and hands the set to exactly the same `stagger` machinery a `PlAnimateFade` around a list of `<li>`s uses. `effect`, `stagger`, `durationStep` and `reverse` all mean what they mean everywhere else; the component is the splitting and nothing more.
643
+
644
+ **`by="character"` is not safe in every script**, and the page says so rather than leaving it to be discovered. A character part breaks the shaping between letters: Arabic stops joining, Devanagari conjuncts come apart, and an emoji built out of several code points is cut into its pieces. `word` has none of those problems and is the default.
645
+
646
+ **The gaps are not parts.** Whitespace is never given an entrance of its own — animating the space between two words is nothing arriving — and it does not take a step of the stagger with it either, so the second word starts one step after the first rather than two. That is why this component reaches for `useAnimationRun` directly rather than `useAnimateElement`, which is the arrangement `internal/animate.ts` already describes for the components that have to understand their own children.
647
+
648
+ Each part is `inline-block`, because a transform does not apply to a non-replaced inline element and a slide would otherwise fade without moving.
649
+
650
+ **A screen reader is told the line, once.** The parts are hidden from the accessibility tree and the whole line sits beside them, which is what stops a split headline being read out one word — or one letter — at a time: the defect this pattern is known for everywhere it appears without it.
651
+
652
+ Measured with `npm run size`: **+0.4 kB on the whole library and +0.0 kB on all four other scenarios**.
653
+
654
+ - **`PlAnimateScramble`.** A line of text resolving out of noise, and the second of the two effects that animate **content** rather than a box.
655
+
656
+ **The noise is made of the line's own characters.** Every scrambler that ships with a default alphabet ships an English one, and over a Korean, Greek or Arabic headline that is not a word resolving — it is a different script flickering where a word is about to be, and a reader watching their own language arrive out of somebody else's is watching a bug. Shuffling the line's own glyphs is right in every script and costs nothing; it also keeps the line's colour and width steady, because every frame is drawn out of exactly the characters the finished line is made of. `characters` overrides it for the caller who genuinely wants a terminal look.
657
+
658
+ It settles **left to right** rather than at random — a word arriving is something a reader can follow, and a reader who looks away and back has not lost their place — and **whitespace is never scrambled**, because the gaps between words are what keeps a line of noise looking like a sentence.
659
+
660
+ The redraw is stepped at `tick`, 45ms by default, rather than taken every frame. At sixty a second a line of changing glyphs strobes, which is unpleasant to look at and is exactly the flicker a reader with a sensitivity to it must never be handed.
661
+
662
+ It takes a `string` rather than a node for the reason `PlAnimateCounter` takes a number, waits to be seen for the same reason, and tells a screen reader the line once and never the noise.
663
+
664
+ Measured with `npm run size`: **+0.5 kB on the whole library and +0.0 kB on all four other scenarios**.
665
+
666
+ - **`PlAnimateCounter`.** A number counting up to what it is — the one effect in the group that animates **content** rather than a box.
667
+
668
+ **It is not a keyframe, and the reason is formatting.** A registered custom property and a CSS `counter()` tick a number perfectly well, and that would be the neater implementation; what they cannot do is put a thousands separator in one, or a currency symbol, or fold 1,200,000 into `1.2M`. A counter that cannot be formatted is a counter nobody can put on a dashboard, so the frame loop decides only which number is being drawn and `Intl.NumberFormat` decides what it looks like. `easing` is a **function** here for the same reason: there is no CSS animation running to hand a string to.
669
+
670
+ **`trigger` defaults to `visible`**, and it is the one component in the library that does not start on mount. An entrance played off screen has still delivered its content; a count that ran off screen delivered a number that was already sitting there, which is the one thing a counter cannot afford — being watched is the whole point of it.
671
+
672
+ **A screen reader is told the answer, once.** The ticking figure is hidden from the accessibility tree and the final number sits beside it in a clipped span, because a number changing sixty times a second in that tree is either silence or sixty announcements and neither of those is the figure. Until the count starts, what is shown is the number it will count _from_ — the same rule every keyframe here follows about its own first frame — so nothing claims a value it has not reached.
673
+
674
+ Changing `value` counts again from wherever the last one landed, so a figure that updates on a timer needs nothing said to it.
675
+
676
+ Measured with `npm run size`: **+0.3 kB on the whole library and +0.0 kB on all four other scenarios**.
677
+
678
+ - **`PlAnimateShake`.** A refusal. The one effect in the set that is a **response** rather than an entrance — what a form does when the password was wrong again, what a locked control does when it is pressed — so it starts held still, where every other effect here starts on mount.
679
+
680
+ **`replay` is the prop it exists around, and it is a new shape in the animation API.** A refusal can happen twice, and `play` — being a boolean — cannot say "again": replaying with it means toggling off and on, which is two renders for one event and a piece of state whose only job is to be flipped back. A value that has _changed_ is the closest React has to an event, and the count of failed attempts a form already keeps is exactly that value. It never plays on the first render, because a shake that shook itself on mount would be answering an event that has not happened.
681
+
682
+ It lands **exactly where it started** — three shudders either side of home and back to nothing. That matters more here than anywhere else in the group, because this is the one effect a caller will run over content that is still being typed into, and a field left a few pixels off its label would be a worse defect than the error it was reporting.
683
+
684
+ It is not in `PlassAnimation`, for `PlAnimateFloat`'s reason: that union is the set of ways content can arrive, and a response is not an arrival.
685
+
686
+ The page says the thing the component cannot enforce. **A reader who asked for less motion sees none of it**, so whatever the refusal is saying has to be said in text as well — the field's own `error` and `invalid` — and the shake is emphasis, never the message.
687
+
688
+ Measured with `npm run size`: **+0.2 kB on the whole library and +0.0 kB on all four other scenarios**.
689
+
690
+ - **`PlAnimateFloat`.** Content drifting gently, and not going anywhere. It is the odd one out in the group and the page says so: every other effect here is an **entrance**, played once when content arrives, and this one never finishes.
691
+
692
+ Three things follow from that. It repeats **infinitely** by default, because one drift out and back is a nudge and nobody asks for a nudge. It has **no `mode`**, because the cycle is symmetric and running a symmetric cycle backwards is the same cycle. And it is **not in `PlassAnimation`**.
693
+
694
+ That last one is the decision worth recording. The union is the set of ways content can _arrive_, and an object literal does not tree-shake per key — so every component importing the effect map pays for each row whether or not it uses it. An endless drift is not an arrival and is a row nothing else could ever want, which is exactly the line the `PlAnimateReveal` entry drew when it joined the union. It runs its own keyframe instead.
695
+
696
+ The cycle is home, out, home, so however many times it runs it ends where it started: a float stopped mid-cycle would leave the element permanently a few pixels out of place, which reads as a layout bug rather than as an effect that ended.
697
+
698
+ `easing` defaults to `ease-in-out`, and it is **the one component in the library that does not take the house curve**. The house curve is an entrance's — fast out of the gate, slow into place — and a drift with it lurches at each end of the cycle instead of turning around, because there is no gate: the element is already there and is only breathing.
699
+
700
+ Measured with `npm run size`: **+0.2 kB on the whole library and +0.0 kB on all four other scenarios**, which is what staying out of the union bought.
701
+
702
+ - **`PlHowToSteps` and `PlHowToStep`.** Instructions, numbered, with what to do under each one. Three components in the library now put things in order and the difference is not the drawing: a `PlStepper` and a `PlTimeline` both say **where you are** — one in a process the reader is moving through now, the other in a sequence that has already happened — and this one says **what to do**.
703
+
704
+ That is the shape everything else follows from: **every step's body is open at once.** Somebody following instructions reads ahead, goes back a step, and works at their own pace, so a guide that showed one step at a time would be hiding the answer to "what am I about to be asked for".
705
+
706
+ It is also why `active` is **optional** here. A guide that claimed to know how far a reader had got would be guessing; it is for the guide that genuinely knows, such as a setup wizard reporting what it has already done.
707
+
708
+ `numbered` is on by default, because "do this, then this" is an order and the number is how a reader finds their place again after looking away. Off — usually with `connector="none"` — is a checklist, which has no order. An `icon` replaces the number in the disc and keeps the step's place in it: a step never takes an index, the guide numbers its children as it walks them, and a conditional step that rendered nothing does not take a number with it.
709
+
710
+ It is a real `<ol>` of `<li>`s, and that is the accessibility of it. A screen reader announces "list, five items, item two" on its own — the position a heading per step would only approximate — so the numbers a reader sees are the ones the list carries.
711
+
712
+ The bullet, the halo and the connector come from `internal/steps.ts`, the same table the stepper and the timeline draw from, because a haloed bullet must not mean two things in one library.
713
+
714
+ Measured with `npm run size`: **+0.6 kB on the whole library and +0.0 kB on all four other scenarios**.
715
+
716
+ - **`PlFloatingActionButton`.** The one action a screen is about, floating over it. It is a `PlButton` in a corner and everything that makes it one is the button's — the three materials, the elevation ladder, the pointer light, `loading`, `readOnly`, `disabled`. What it adds is the pinning, the shape, and one rule.
717
+
718
+ **`label` is required and is always the accessible name.** A floating button is a disc with a mark in it nine times out of ten, and `extended` decides only whether the words are _also_ drawn — never whether they exist. An icon-only button with no name is the single most common accessibility defect this pattern ships with, and a required prop is the only fix that survives review.
719
+
720
+ The icon-only form is a **disc**, which is `PlIconButton`'s deliberate exception to the radius rule: the flat run along a control's edge is there for a line of text to sit on, and a glyph has no line of text. The extended form is **not a pill** for exactly that reason — it has words along its edge, so it takes the house fillet like every other labelled control.
721
+
722
+ `elevation` defaults to **3**, the top of the ladder, and unlike every other default in the library that is not a compromise: this is the one control that genuinely floats over the content rather than resting on it. `size` defaults to `lg`, one step up from a button's, because it is a target for a thumb.
723
+
724
+ The insets are **logical and inline**: `corner` is `start`/`end` as everywhere, so the button crosses the screen under RTL with everything else, and an inline declaration is the one form that wins over a utility deterministically — which a caller's `offset` needs, being a value rather than a class. `floating={false}` keeps the shape and the shadow and drops the positioning.
725
+
726
+ The page says the two things a component cannot enforce: **one per screen**, because two primary actions is none; and that it covers content, so a scrolling list needs room at its end for the row underneath it.
727
+
728
+ Measured with `npm run size`: **+0.1 kB on the whole library**, because it is a `PlButton` and a `PlIconButton` and nothing else.
729
+
730
+ - **`PlAnchor`.** A table of contents that follows the reader down the page. Anyone can draw a list of links; what is worth writing once is deciding **which one is lit**, and the rule is not the obvious one.
731
+
732
+ The lit row is the last heading whose top has **passed the reading line** — not whichever heading happens to be visible. Three can be on screen at once, and the one a reader is inside is the highest of them that is already above them. That is why the tracking is a measurement rather than an `IntersectionObserver`: an observer answers "is it visible", and the question here is "which one did I pass last".
733
+
734
+ Two ends are handled separately, and both are what a hand-rolled version gets wrong. **Above the first heading nothing is lit**, because the reader has not reached a section and lighting the first row before they arrive is a claim about where they are. **At the very bottom the last row is lit** whatever the measurement says, because a short final section never reaches the line and a list without this goes dead exactly where a reader is looking for it.
735
+
736
+ `offset` is where that line sits — the height of whatever is pinned over the page. Without it a heading goes on counting as the _next_ one after it has slid out of sight behind a sticky header, so the list sits a section behind the reader for the bar's whole height.
737
+
738
+ The headings arrive as a **flat array** rather than as children, which is the opposite of most of this library: a table of contents is generated, and generators produce a flat list in document order with a level on each entry. It stays flat, deliberately — real documents skip levels, so a nesting built from that list would be a guess at a shape nobody wrote.
739
+
740
+ Only where there is something to scroll: a page that fits on the screen is always at its own bottom, and lighting the last row there would say a reader had reached the end before they had read anything.
741
+
742
+ It measures at most once per frame, because scroll fires far more often than a page paints and the answer cannot change between two paints. The lit row carries `aria-current="location"` — where the reader is _within_ the document, and not `page` — with a rule down its leading edge, so colour is never the only thing saying where they are.
743
+
744
+ Measured with `npm run size`: **+0.6 kB on the whole library and +0.0 kB on all four other scenarios**.
745
+
746
+ - **`PlDataList` and `PlDataListItem`.** A list of labels and the values that go with them — the panel every detail screen ends with, and the whole reason it is a component is the **markup**. It is a real `<dl>` with real `<dt>`s and `<dd>`s, each pair grouped in a `<div>`, which the HTML specification allows and which is what lets a row be laid out side by side without giving up the grouping that makes it a pair.
747
+
748
+ **A details panel built as a two-column table is the mistake this replaces**, and it is not a styling one: a table claims a row-and-column relationship that is not there, so a reader navigating it by cell is told there are two columns of data when there is a column of names and a column of values. One thing and its fields is a description list; many things with the same fields is a `PlTable`; a run of items of the same kind is a `PlList`.
749
+
750
+ The label column is a **fixed width** rather than the width of the longest label, which is what makes two panels on one screen line up and what stops a value moving when somebody renames a field. `labelWidth` takes any length, and `'12ch'` is usually the right one — a label column is measured in characters and no ladder of `rem` can spell that.
751
+
752
+ `divider` rules between the rows and **only** between them: a line above the first or below the last would be a box drawn around a list that has no box.
753
+
754
+ The rows are children rather than data, unlike a `PlTable`'s columns. A details panel is written out once and read in source order, and every value in it is a different shape — a chip, a date, an avatar, a link — so a data array would be an array of `render` functions.
755
+
756
+ Measured with `npm run size`: **+0.4 kB on the whole library and +0.0 kB on all four other scenarios**.
757
+
758
+ - **`PlAppLogo`.** A product's mark and its name beside it. The whole component is the **framing**, and the default is the answer project after project gets wrong: `bare` draws the artwork as it was given, at the height `size` asks for and whatever width that comes to.
759
+
760
+ That is the default because most marks already have a frame. A mark drawn with its own background, its own margin, or the product's name set into it is finished artwork — putting it on a plate gives it two edges, and cropping it to a circle cuts the name in half. `plate` and `circle` are for a mark drawn as a bare glyph, which cannot sit next to anything else until it has been given an edge, and a plate insets the artwork to about seventy percent of the tile rather than filling it: the margin every app icon has.
761
+
762
+ It is **not a `PlAvatar`**, and the page says why. An avatar is a picture of a person or a thing, always a circle or a fillet, with initials behind it when the picture does not arrive, because there is always something to draw. A logo is artwork the product owns: it has no fallback worth inventing, and its shape is a decision somebody already made — which is why the shape is a prop here and a house rule there.
763
+
764
+ **With a `name`, the mark is taken off the accessibility tree.** The wordmark beside it already says what the product is called, and a picture that says it again is a screen reader reading the name twice. Without one the mark speaks, through an `alt` whose empty default is a real answer.
765
+
766
+ `variant` and `color` are read only when there is a plate: a bare mark is the product's own artwork and the library does not tint it. `render={<a href="/" />}` makes it the way home, which is nearly always what a logo is.
767
+
768
+ Measured with `npm run size`: **+0.5 kB on the whole library and +0.0 kB on all four other scenarios**.
769
+
770
+ - **`PlHoverCard`.** A preview of what is behind a link, shown when the pointer rests on it. The library now has all three floating surfaces, and the pages tell them apart by **what opens them and what a reader can do once they are open** rather than by how they look — all three are the same sheet. A tooltip names the thing under the pointer and nothing in it can be reached; a hover card previews what is behind it and the pointer can move _onto_ it; a popover was asked for, stays until it is dismissed, and can be typed into.
771
+
772
+ **The rule that decides whether it is the right component at all is written on the page: nothing may live only in here.** A card that opens on hover does not open for a finger, so a link, a button or a fact that exists nowhere else is one that every touch reader misses. Everything inside is a preview of something already reachable — which is what makes it safe to have, and why it needs no dismiss button, no focus trap and no scroll lock.
773
+
774
+ **The delays are the component.** `delay` is 600ms and deliberately long: a card that opens the moment a pointer crosses a link opens on every link a reader passes on the way somewhere else, which turns a page of prose into a page that flinches. `closeDelay` is 300ms and cannot be zero — the gap between the trigger and the card has no pointer in it, so a card that closed the instant the pointer left could never be reached, and reaching it is the whole difference from a tooltip.
775
+
776
+ It opens on **keyboard focus** as well as on hover, so a reader tabbing along a paragraph of links gets the same preview a pointer would. The trigger is rendered rather than wrapped, so a link keeps its `href`, its styling and its place in the tab order. The sheet is a `PlPopover`'s, one `size` rung wider at every step: a popover is a detail beside a control, and a preview squeezed to the width of a hint is a preview nobody reads.
777
+
778
+ Base UI's `PreviewCard` owns the anchoring, the flip at the window edge, the two delays and the dismissal.
779
+
780
+ Measured with `npm run size`: **+3.4 kB on the whole library and +0.0 kB on all four other scenarios** — the primitive, paid for only where the component is.
781
+
782
+ - **`PlScrollArea`.** A bounded box that scrolls, with the library's own scrollbar in it. The reason to reach for it over `overflow: auto` is the **bar**: a platform scrollbar is either an overlay that vanishes the moment the content stops moving or a strip of permanent grey furniture, and neither of them belongs beside a translucent sheet. The thumb is `--plass-track`, the same neutral ink a `PlSlider`'s rail and a progress groove are cut in, and the lane is overlaid so drawing it costs the content no width.
783
+
784
+ **It is not a `PlScrollZone`, and the pages say which is which.** A scroll zone is the other answer to the same fact — content that runs off the end of its box — and it takes the bar away entirely, fades the end that still has something behind it and adds a pair of buttons. That is right for a strip of tabs or chips, where a bar under one line of labels is heavier than the labels. A scroll area is right for a panel of content, where the bar is the honest signal and where a reader wants to know **how far through** they are, which a fade cannot say. There is deliberately no fade here for the same reason: two signals for one fact, one measured and one not, is one more than the box needs.
785
+
786
+ **`height` is a prop rather than a class**, which is the one API decision worth stating: a vertical scroll area has to be bounded by something or there is nothing for the content to overflow, so it is the measurement without which the component does nothing at all. `maxHeight` is the ceiling form for a panel that should shrink to short content; `width` and `maxWidth` are the pair for a horizontal one.
787
+
788
+ `orientation` takes `both`, which draws a lane on each edge with a corner in the join. `scrollbars` is `auto` — while the pointer is over the box or the content is moving — or `always`, which is right more often than it looks: for a panel whose whole point is that there is more below, a bar that appears on hover is a signal nobody standing back from the screen ever sees.
789
+
790
+ `label` is worth giving and the reason is not obvious. **A scrollable box is a tab stop when nothing inside it is focusable**, because somebody using a keyboard has to be able to scroll it, and a landing point with no name is announced as nothing at all. With a name it is a region; without one it claims no landmark, deliberately, because an unnamed region is something a screen reader lists as "region" and nothing else.
791
+
792
+ Base UI's `ScrollArea` owns the behaviour: the overlay measurement, the thumb's size and position, the drag, and making the viewport a tab stop exactly while there is something to scroll.
793
+
794
+ Measured with `npm run size`: **+4.7 kB on the whole library and +0.0 kB on all four other scenarios** — the Base UI primitive, which is the largest of the ones the library imports and is paid for only by the pages that import this component.
795
+
796
+ - **`PlMeter`.** A quantity inside a range, drawn as a bar. It is cut out of the same groove as a `PlProgressLinear` and it is deliberately not one, because the difference decides which to reach for: **progress is something advancing, and a meter is something already known.** Disk used, seats taken, a password's strength, how full a battery is — none of them is going anywhere on its own.
797
+
798
+ That follows into the API and into the semantics. `value` is **required** and there is no sweep, because a meter with nothing to report is not a meter but a bar that should not have been drawn yet; and the role is `meter` rather than `progressbar`, since telling somebody a static figure is in progress is telling them to wait for something that will never finish.
799
+
800
+ **`thresholds` is the prop it exists for.** Bands of `{ from, color }`, where the highest `from` at or below the value wins — read rather than walked, so the order they were written in does not matter — and `from` is in the meter's own **units** rather than a percentage. A quota bar that turns amber at three quarters and red at ninety percent says something a fixed colour cannot, and the colour comes from the value rather than from the moment somebody was looking. The page pairs it with `showValue` every time it appears: a band is a second way of saying how full something is and must never be the only one.
801
+
802
+ Base UI's `Meter` owns the semantics — the role, the range attributes, `aria-valuetext`, the formatting and the fill width — the same way its `Progress` owns a progress bar's, so what is left here is the material: the groove is `--plass-track` and the fill the family's gradient, travelling on the width. A value outside the range is clamped on both halves, the bar and the announced value together, so what is read out and what is on screen never disagree.
803
+
804
+ Measured with `npm run size`: **+1.0 kB on the whole library and +0.0 kB on all four other scenarios**, the Base UI primitive included.
805
+
806
+ - **`PlPortal`.** Children rendered somewhere else in the document. It is `createPortal` plus the three things a library has to add, and the first is the only real reason to reach for it.
807
+
808
+ **It carries `plass-portal`.** Every surface the library already sends through a portal — a modal, a drawer, a menu, a popover, a tooltip, a toast — lands with that class on it, because a portalled subtree leaves whatever element a host had scoped its CSS reset to and the class is how that host finds it again. A caller's own portal without it was the one subtree on the page the reset missed.
809
+
810
+ **It renders nothing until it has mounted.** There is no `document` on a server, so the HTML that ships never contains a portalled subtree and neither does the hydrating render. That is what a portal _is_ rather than a limitation to work around, and the page says so: anything that has to be in the server's HTML does not belong in one.
811
+
812
+ **`container` is resolved after mount**, which is what lets it be a **ref**. The element a portal targets is usually one React has not created yet at the moment the prop is written, so a ref is `null` and a `getElementById` finds nothing; reading the prop during render would get the wrong answer every time. An element and a `DocumentFragment` are taken as they are, a function is called, and anything that resolves to nothing falls back to `document.body` rather than dropping the children.
813
+
814
+ What it does **not** carry is the colour scheme, and that is written down rather than left to be discovered. The stylesheet answers to a `.dark` or a `[data-theme]` on an _ancestor_, and a portal to the body has left every ancestor it had. The library's own popups have the same gap and the same fix: point `container` at an element inside the theme. React context does cross a portal — the tree it is read from is the React one — so a `PlassProvider` above still decides `size`, `color`, `density` and `locale`.
815
+
816
+ It is **React-only**: an `overflow: hidden` ancestor and a stacking context that cannot be escaped from the inside are DOM problems, and Flutter's answer is the `Overlay` every app already has.
817
+
818
+ Measured with `npm run size`: **+0.1 kB on the whole library and +0.0 kB on all four other scenarios**.
819
+
820
+ - **`PlFlex`.** A row or a column and the gap between the things in it — the layout box with no arithmetic in it. A [PlGrid](https://plass.cdget.com/components/layout/grid) divides a row into twelve columns and needs a `PlGridItem` to take them; a [PlStack](https://plass.cdget.com/components/layout/stack) overlaps what it is handed. This one only decides which way its children run.
821
+
822
+ **`direction` is responsive and resolves in CSS**, which is the reason it is worth a component rather than three Tailwind classes. `direction` takes `{ xs: 'vertical', md: 'horizontal' }`, a form that stacks on a phone and lines up on a laptop, decided by the stylesheet: one `--p-dir-*` slot per rung the caller named, cascaded by the same `@variant` blocks a grid's columns already ride. So a server renders it correctly at every width, dragging a window costs no re-render, and no listener is installed. It is also the only way to say that in a project that imports `plass-ui/styles.css` and has no Tailwind of its own.
823
+
824
+ `reverse` folds into that same slot instead of taking one of its own, and that is why it is deliberately not responsive: one custom property carries the whole answer, so a breakpoint changes the axis without having to restate which end it starts from. It is a **painting order** and moves nothing in the document — what a screen reader reads and what the Tab key walks is unchanged — so it is for an arrangement and never for content whose order is the information.
825
+
826
+ `wrap` defaults to **`false`**, unlike a `PlGrid`'s. That is what a flex box already does, and the opposite default would silently reflow a toolbar somebody had sized to fit.
827
+
828
+ The two gutters are now declared for `.plass-grid` and `.plass-flex` together rather than copied. They are one question asked by two components — the space _between_ the things in a box — and `spacing` is the same Tailwind scale on both, so a flex box's gap and a grid's gutter of `4` are the same `1rem`.
829
+
830
+ It is **React-only**, and that is not an omission: `Row`, `Column` and `Wrap` are already in `package:flutter/widgets.dart` and already take a `spacing`. The half worth having in Dart is the responsive axis, and that is a `LayoutBuilder` rather than a widget. The component page shows it.
831
+
832
+ Measured with `npm run size`: **+0.2 kB on the whole library and +0.0 kB on all four other scenarios**, because nothing else imports it.
833
+
834
+ - **`PlHeader` and `PlFooter` hold their content to the same measure a `PlContainer` does, from one implementation.** All three took a `maxWidth` on "`PlContainer`'s ladder" and all three carried their own copy of it — three tables that could disagree, and a bar whose measure did not line up with the container under it is a page with a visible seam down one edge. There is one ladder now, in `internal/responsive.ts`, and all three take the same type: a rung, any CSS length, a number of pixels, or `none`, responsive, resolved in CSS.
835
+
836
+ - **Six components turn with the window.** `orientation` on `PlPanes`, `PlTabs`, `PlScrollZone`, `PlTimeline` and `PlStepper`, and `direction` on `PlStack`, all take a responsive value: `orientation` takes `{ xs: 'vertical', md: 'horizontal' }` and a tab bar is a column on a phone and a row on a laptop.
837
+
838
+ **These resolve in JavaScript rather than in CSS, and that is not an oversight.** An orientation decides which DOM a component builds, which `aria-orientation` it claims and which way its arrow keys walk; no stylesheet can do any of that. So they pay what every JavaScript answer about width pays — a server renders the `xs` entry and the browser corrects it on hydration — which is exactly why `PlContainer`'s `maxWidth`, which only decides style, does **not** come this way. The line is written down in `internal/responsive.ts` and on the [breakpoints](https://plass.cdget.com/design/breakpoints) page.
839
+
840
+ **A bare value subscribes to nothing.** `useMediaQuery(null)` is a hook that installs no listener, so the four queries are only asked when there is a map to answer with: twenty tab bars that never change shape install no listeners between them. Measured — 0 for `orientation="vertical"`, 4 for a map.
841
+
842
+ - **`PlShow`.** Content at some widths and not others — `from`, `until`, or both as a band. It decides in **CSS**, which is the whole component rather than an implementation note: a media query answered in JavaScript is `false` on a server and on the first frame a browser renders, so a `useMediaQuery` and a ternary draw the wrong half of a responsive layout and then throw it away. That is a flash on every page load, not an edge case. It is also the only way to gate on width for a project that imports `plass-ui/styles.css` and has no Tailwind of its own — there is no `md:hidden` to reach for there.
843
+
844
+ **It is not a box.** While showing it is `display: contents`, so its children take part in the surrounding layout exactly as they would have without it: a gate inside a flex row does not become a flex item. Which also means a `className` carrying a margin or a width lands on nothing — put your own element inside.
845
+
846
+ What it costs is that **both halves are in the document**. Hiding is `display: none`, which takes the subtree off the accessibility tree and out of the tab order, so nothing is read out twice and nothing is drawn — but both were rendered and both were sent. Right for two arrangements of the same content; wrong for a subtree that fetches, is expensive, or must not mount at all, and `usePlBreakpointValue` is what mounts exactly one of those at the cost of a server rendering the `xs` answer.
847
+
848
+ `until` is **exclusive**, so `until="md"` on one element and `from="md"` on another are the two halves of one decision with no width that draws both and none that draws neither. There is no `xs` bound — everything is at or above the bottom rung, so it would mean "always" and "never" — and there is no `PlHide`, because `until` is already the inverse of `from`.
849
+
850
+ - **A `PlContainer`'s `maxWidth` is responsive, and takes any length.** `maxWidth` takes `{ xs: 'none', md: 'lg' }` and narrows a page as the window widens, and `maxWidth="72ch"` is the measure a paragraph actually wants — the five rungs are `rem` and no ladder of `rem` can spell a measure in _characters_. A number is pixels; the rungs are unchanged.
851
+
852
+ It resolves in **CSS**, not in JavaScript, and that is the part worth knowing: one `--p-maxw-*` slot per rung the caller named, cascaded by the same `@variant` blocks the grid uses. So the first paint a server sends is already right at every width, and a window being dragged costs no re-render. Measured in a browser: `none` at 500px, `1024px` at 900px, `72ch` at 1400px, from one declaration.
853
+
854
+ The machinery moved out of `internal/grid.ts` into `internal/responsive.ts` with it, since it now has a second user. That file also writes down **where a responsive value can be resolved and where it cannot**: a value that only decides style belongs in CSS, and a value that decides structure — an orientation changes the DOM, the ARIA and the arrow keys — has to be JavaScript and pays the server's `xs` first render for it.
855
+
856
+ - **The breakpoint ladder is written down once, and a project can move it.** Every width in the library now comes from Tailwind's own `--breakpoint-*` theme, so one line moves both halves of it:
857
+
858
+ ```css
859
+ @theme {
860
+ --breakpoint-md: 50rem;
861
+ }
862
+ ```
863
+
864
+ The reason it takes an arrangement rather than a prop is worth stating: **a media query's condition cannot read a custom property.** `@media (width >= var(--x))` is not valid CSS, so no provider and no runtime value can move a breakpoint the stylesheet decides at. What the CSS half can do is resolve the theme at _build_ time, which is what `@variant` does — `PlGrid`'s four cascade blocks were hand-written `@media (width >= 48rem)` literals and are `@variant md` now. Measured: a `@theme` override moves a `@variant` block to 50rem and leaves a hand-written one at 48.
865
+
866
+ The JavaScript half asks `matchMedia`, where a breakpoint is a **value** rather than a condition — so it can read the same answer off the document. `--plass-breakpoint-sm` … `--plass-breakpoint-xl` are four new tokens carrying exactly that, each falling back to Tailwind's default so they are still right on a server, on a first paint before the CSS has arrived, and on the precompiled `plass-ui/styles.css` where our values are baked in. `usePlBreakpoint` and a `PlSidebar`'s `collapseBelow` both read them, and `internal/breakpoints.ts` is the one place that does.
867
+
868
+ A new package test fails the build if a width is ever written out by hand again, which is the failure that would otherwise be invisible: a page whose `md:` utilities change at one width and whose `PlGrid` changes at another still renders, still passes every component test, and is simply wrong on a laptop.
869
+
870
+ - **`PlAnimateReveal`.** Content uncovered behind a moving edge, and the only entrance in the set where **nothing moves and no colour changes**. A fade changes the ink, a slide changes the position, a grow changes the size; this changes how much of the element is drawn and leaves every pixel it has drawn exactly where it will finally be. That is what makes it the effect for anything whose _position is the information_ — a heading over the paragraph it belongs to, a divider between two sections, a chart's plot area, a column of figures that must not be read from the wrong place.
871
+
872
+ It is a `clip-path: inset()` rather than a mask or an `overflow` wrapper, and both alternatives were considered. A mask brings a gradient that has to be kept in step with the direction; a wrapper puts a second element into the layout, which is exactly what an effect for something positional must not do. `inset(0)` already means "nothing is cut off", so the to-state is the element's own natural one rather than a number somebody had to choose — and the page around it never learns that anything happened.
873
+
874
+ `from` is physical — `top`, `right`, `bottom`, `left` — as `PlassSide` is everywhere, and `mode="out"` closes from whichever edge it opened towards. **`fade` is off by default**, alone among the effects that offer it: fading a reveal asks for two entrances at once, and the reason to have reached for this one is usually that the first was the problem. It takes `stagger` and `timeline="view"` like the other five.
875
+
876
+ **The union it joined cost nothing**, which was the thing to check before adding to it. `reveal` is a seventh row in `PlassAnimation` and in the class map that backs it, and an object literal does not tree-shake per key — so every component importing that map pays for the row whether or not it uses the effect. Measured with `npm run size`: **+0.2 kB on the whole library and +0.0 kB on all four other scenarios**, because nothing outside the `PlAnimate*` family imports the map at all. An effect that was not an entrance — an endless drift, a response to a rejection — would not have gone in it.
877
+
878
+ - **`timeline="view"` hands an effect to the reader's scroll position.** Two CSS declarations behind an `@supports` give every keyframe in the library a scroll-linked mode: the effect stops being something that happens at a moment and becomes something that tracks where the element sits in the scrollport, so scrolling back up plays it backwards and a reader who stops halfway leaves it halfway. There is no new component and there should not be — this changes what _advances_ an effect, not what the effect is.
879
+
880
+ Four settings stop meaning anything and are **ignored** rather than half-working: `duration`, `delay` and `repeat` belong to a clock, and so does the whole idea of a `trigger`, since the scroll position is the trigger. That last one is load-bearing rather than tidy — an effect held `paused` waiting to be scrolled into view shows its own first frame and nothing else, however far it is scrolled — so a scroll-linked effect is always let go. `paused` still works, because that is a caller saying "hold it" rather than "wait for something". `range` replaces `duration`: an `animation-range` as CSS writes it, defaulting to `entry 0% cover 45%`, which finishes while the element is still arriving rather than when it reaches the middle of the screen.
881
+
882
+ **A browser with no `animation-timeline` gets one clock-driven run**, which is what the `@supports` is for: degraded is allowed, blank is not. The slots are written only when a caller asks for `view`, so the default costs nothing — `auto` is what the property already resolves to, and writing it into every inline style would be the same answer a hundred times. On the same six effects `stagger` is on and absent from the same four, because `animation-timeline` is a property of the element the keyframe runs on. **+0.1 kB on the whole library.**
883
+
884
+ - **Six effects can now be told off across their children.** `stagger` is milliseconds added to each child's delay, `durationStep` is the same for its duration, and `reverse` starts from the end of the set — on `PlAnimateFade`, `PlAnimateGrow`, `PlAnimateSlide`, `PlAnimateZoom`, `PlAnimateRotate` and `PlAnimateBlink`. `stagger` defaults to `0`, which plays the box, so nothing that exists changes.
885
+
886
+ **There is deliberately no `PlAnimateStagger`.** A stagger is a _differential_ rather than an effect, and a wrapper would be a second way to spell something all six can already say — the same rule that keeps a `Pulse` (`blink` + `alternate`) and a `Bounce` (`grow` + `alternate`) out of the library. Turning it on takes the animation **off the root entirely**: eight children fading in under a box that is also fading in is the same content faded twice, and the second one is not free.
887
+
888
+ The four effects that already read their children do not take these and cannot — a marquee lays its children down twice, a headline swaps between them, a typewriter counts their graphemes, and a lighting keeps its motion on a pseudo-element, which there is no way to put on somebody else's child. `PlAnimateAppear` has had the same three props under the same names since it existed and **now runs on the same code**: `animateChildren` and `staggerSlots` moved into `internal/animate.ts`, because two implementations of "one after another" would be two opinions about the arithmetic. It gains `durationStep` on the way past.
889
+
890
+ The effect is written onto the children themselves rather than onto wrappers, so a row of `<li>`s stays a row of `<li>`s and a grid's cells stay its direct children; the cost is that a child has to accept a `className` and a `style`. The `transform-origin` a `PlAnimateGrow` was given travels with it, since that property is not inherited and a staggered grow would otherwise unfold every child from its own middle. **+0.2 kB on the whole library**, nothing on any component that does not import an effect.
891
+
892
+ ### Fixed
893
+
894
+ - **The chart frame carried nine slot names this package does not declare.** Ported from the library the frame came from, renamed to the `--plass-` prefix, and never checked against anything: `--plass-grain`, `--plass-sheen`, `--plass-panel-press`, `--plass-plate-glass` and `--plass-disabled-fg` have no Plass equivalent under those names, and `--plass-chart-gap` was simply never written down. Nothing failed. An unresolvable `var()` computes to `none` in a `stroke` and to nothing at all in a shorthand, so what shipped was a **tooltip panel with no glass, no shadow and a `currentColor` border**, a legend whose switched-off entries were the same ink as the rest, and a line chart whose markers had no ring cut out of them.
895
+
896
+ The panel is now the sheet a `PlSelect`'s popup is, which is what it should always have been — a floating readout and a floating list are the same material. A switched-off legend entry is the muted ink with a line through it and its swatch keeps its own colour at 40%, which is what the Flutter build already drew. `--plass-chart-gap` is declared as the surface, and `PlassToken` names it.
897
+
898
+ **Two checks were added so this cannot ship again.** `test/package/tokens.test.ts` now asserts that every `--plass-*` a source file reads is declared in `styles.css` — the two checks already there compared the type against the stylesheet, so a token appearing in neither, which is exactly this case, agreed with itself. And `test/styles/standalone.test.tsx`, the one file in the suite that loads real CSS, now renders a chart and asserts that no `fill` or `stroke` on it computes to `none`. That catches the other half: a token that _is_ declared but resolves to nothing because its declaration sits under a selector no chart matches. Neither test asserts a shade.
899
+
900
+ - **The chart frame's focus ring and legend hover were unstyled.** Three of the ported frame's colour slots kept the names they had in the library they came from — `--n-soft`, `--n-ring` and `--n-line` — and nothing in this package declares any of them. A `var()` that cannot be resolved takes an `outline` shorthand down with it, so **the focus ring on a chart's plot and on every legend button was simply absent**: the plot is a tab stop, and a tab stop a keyboard reader cannot see is the accessibility failure the ring exists to prevent. The legend's hover tint was gone with it, and the tooltip panel's hairline fell back to `currentColor`, which drew the border in ink. All three now read the slots a `PlBox` actually sets — `--p-soft`, `--p-ring` and the glass hairline.
901
+
902
+ - **A chart ignored the `size` and `locale` a `PlassProvider` set.** `CartesianChart` took both as plain props with hard-coded fallbacks, so a provider that put the whole page on `sm` left every chart at `md`, and one that named a locale still got the reader's own number formatting on the axis. It resolves both from the defaults now. `density` deliberately does not go through it: the box resolves that one itself, and the single chart that needs it for arithmetic reads it directly.
903
+
904
+ - **A read-only `PlSelect` opened its popup again.** Base UI 1.8 redefined `readOnly` on a select to mean "nothing in the popup can be chosen" and lets it open regardless; every other picker in this library promises that a read-only control's popup does not open at all, and a select that opened beside a date picker that did not is the form that looks assembled rather than designed. `PlSelect` now holds the popup's open state itself and keeps it shut while `readOnly` — held rather than passed conditionally, so a field unlocked while it is on screen does not switch between controlled and uncontrolled underneath Base UI. An open the lock swallowed is dropped rather than remembered: recorded, it would spring the popup open the moment the read-only lifted, so a form that unlocks a section would drop a menu over it at nobody's asking.
905
+
906
+ - **A chart's grid, axis rules and baseline were invisible.** `internal/chart-frame` draws all three with `var(--plass-chart-grid)`, `var(--plass-chart-axis)` and `var(--plass-chart-baseline)`, and none of the three was ever declared — an unresolvable `var()` on a `stroke` computes to `none`, so `PlLineChart` and `PlAreaChart` shipped with no gridlines, no axis rule and no baseline at all. The faint horizontal lines visible in the docs were the demo canvas's own background showing through the translucent sheet.
907
+
908
+ The three are declared now, derived rather than picked so a project that re-tones its border moves the grid with it: the grid is the border at 70%, the axis is the border, and the baseline — which is where a bar starts from and where zero is, a fact about the data rather than furniture — is the muted foreground at 35%. `PlassToken` names them, which is what the package test that caught the omission checks.
909
+
910
+ - **A `PlTree`'s branch dropped in and vanished rather than opening.** It was the last fold in the library that did not travel: an accordion, a collapsible and a pill all move a height over 260ms, and this one swapped its rows in and out between two frames — under the twisty that had just been pressed, which is exactly where a reader is looking. The branch is a Base UI `Collapsible` now, written with the two lines a `PlCollapsible`'s panel is written with.
911
+
912
+ Folds nest exactly, which is the part worth checking rather than assuming: Base UI sizes an idle-open panel `auto` and only measures a pixel height for the length of the transition, so an outer branch contains an inner one opening inside it frame for frame with nothing to catch up to. Measured in a browser — 192px, then 224px with an inner branch open, with the outer at `auto` throughout.
913
+
914
+ The cost is that **a shut branch's rows are built and not mounted**, because rows dropped from the document on the frame the twisty turns have nothing to travel. React discards the elements, so it is a cost of building rather than of rendering, and `children: undefined` until a branch is opened is the answer for a tree with hundreds of closed folders in it. The Flutter build does not pay it — a fold there takes a callback — and the [tree](https://plass.cdget.com/components/display/tree) page says so. **+0.3 kB on the whole library** and nothing at all on a bundle that does not import `PlTree`, measured with `npm run size`: the primitive was already on the graph for `PlAccordion` and `PlCollapsible`.
915
+
916
+ - **A `PlSlider`'s thumb teleported to any value nobody dragged it to.** An arrow key, a press on the rail, a value set from outside — the thumb was in one place on one frame and somewhere else on the next, on the one control whose whole subject is _where along here_. It travels over the house duration now, and the run behind it fills at the same rate.
917
+
918
+ **Nothing at all while the pointer is down.** A thumb that eased towards a finger would be a thumb lagging behind it, which reads as the control being slow rather than as the motion being smooth, so `data-dragging` zeroes the duration on both parts. The properties are `inset-inline-start` / `bottom` and the indicator's `width` — the ones Base UI writes the value into — rather than a `translate`, which is why this does not bend the [no-transform rule](https://plass.cdget.com/design/design-language#controls-do-not-move): nothing is shifted off its own place, because the thing being moved is the value.
919
+
920
+ - **A `PlTree`'s twisty jumped between its two angles.** It carried the house transition and nothing else, and the house transition deliberately has no `rotate` in it — so on a span whose only colour is a constant it was a transition of nothing at all. The turn names itself now, the way an accordion's and a select's chevrons already do. It matters more here than the size of it suggests: the twisty is the only thing on a row that says whether the branch is open, and every other chevron in the library turns.
921
+
922
+ - **A tick and a radio dot arrived whole, on the frame the box filled.** Both were the last state change in the library still expressed as a swap: the mark was there or it was not, on a control whose whole job is to answer a click, and the mark is the part a reader is looking at.
923
+
924
+ Neither is scaled into place, because [controls do not move](https://plass.cdget.com/design/design-language#controls-do-not-move) and that rule is not bent for the small parts. **A tick draws itself along its own path** — the stroke is dashed at exactly its own length and slid out of view by exactly that, so how much of the mark exists is one number, and `pathLength="1"` renormalises the tick and the dash to the same 0-to-1 whatever their real geometry is. **A dot grows out of the middle of its ring** — the ring centres a fixed-size child, so a width and a height are laid out about the same point and nothing outside the ring can be moved by either.
925
+
926
+ Both marks are now **kept in the document at nothing** rather than unmounted, because Base UI waits for animations on the indicator itself and not on the `<path>` inside it — an exit would otherwise be cut off on its first frame, and a control that animates one way and cuts the other means two different things by the same click.
927
+
928
+ - **A `PlSelect` and a `PlCombobox` popup arrived and left between two frames.** Every other floating surface in the library fades — a modal, a drawer, a menu, a tooltip, a popover, a date picker's sheet — and these two were the ones that did not, so the same gesture had two different answers depending on which control the reader was standing on. They fade now, on the same one line the others use.
929
+
930
+ Opacity only, which is the point rather than a shortcut: a list that slid or grew in would be dragging its own options across the field they are being read against, and the options are the thing being looked at. Base UI keeps the popup in the document for the length of the ending transition, so a list closes the way it opened instead of vanishing.
931
+
932
+ - **Three things that were written as fades were not fading.** `opacity` was missing from the house transition's property list, and three states are expressed in this library as an opacity and nothing else: `opacity-50` is what every disabled control looks like, a `PlBackTop` arrives and leaves on `opacity-0`, and a `PlImage` uncovers itself the moment the picture decodes. All three changed between two frames — a button that blinked into the corner of the page, a photograph that replaced its own skeleton, a key that went grey the instant it was disabled.
933
+
934
+ It is one word in one array, and the reason it is worth a line here is the rule it restores rather than the size of it: **a value that carries a state has to be in the transition that carries states.** The list is otherwise unchanged, `transform` included — it is still not in it and still should not be.
935
+
936
+ - **A `PlTabs` bar that scrolls now says so.** The bar has always scrolled when there were more tabs than room — what it could not do was tell anyone. A macOS scrollbar is an overlay that appears while the strip is moving and is invisible the rest of the time, which is every moment a reader is deciding whether there is more to look at; the Windows one is fifteen pixels of permanent furniture under a row of labels. Both are taken away and the ends are faded instead — **only** the end that still has tabs behind it, which is what makes it a signal rather than a decoration. The fade is a `mask-image` rather than two gradient overlays because a bar can sit on anything, and an overlay would have to be painted in the colour of a background the component cannot know; taking the pixels away is right on every surface. It is dropped entirely while a tab inside is showing a focus ring, since focusing a tab scrolls it flush against the edge the fade is strongest at. Measured rather than declared — whether a bar overflows depends on the room it was given, so there is no prop that could answer it — and published as `data-overflow` on the tab list (`none` / `start` / `end` / `both`, in the reader's order) so a page can style against it. **+0.4 kB on the whole library** and nothing at all on a bundle that does not import `PlTabs`, measured with `npm run size`.
937
+
938
+ - **A `PlScrollZone`'s inline scroll button now stands in its lane instead of hiding in it.** With `buttons="auto"` and the default `inline` placement, the button with nowhere to go was drawn invisible so that its lane would not come and go and resize the strip under the pointer. The lane was right and the emptiness was not: the space is paid for either way, and a reserved empty lane beside a strip does not read as restraint — it reads as odd padding on one side of the box. It is `disabled` and visible now, which is what `buttons="always"` already drew in the same position, so the two settings no longer disagree about what "nowhere to go" looks like. What `auto` decides is whether the strip has scroll buttons at all. An `overlay` button is unchanged and still goes, because there the space costs nothing to give up — **remove a control when its absence is free, disable it when it is not**.
939
+
940
+ - **A `reversible` `PlSpoiler` changed height when it was opened, and again when it was closed.** The Hide row was drawn only once the spoiler was uncovered, so revealing grew the sheet by the height of a button and covering it again shrank it back — the page moving twice around the control somebody is pressing. The row is built from the start now and merely held invisible and out of reach under the cover, so its space is paid for once. The generalisation is worth keeping: **a control that appears with a state should reserve its space in the other state**, because the space is the part a reader notices.
941
+
942
+ - **A `ghost` `PlToggle` that was off drew itself as one that was on the moment the pointer arrived.** Its hover was `--p-soft`, and `--p-soft` is exactly what a `ghost` toggle _on_ is painted with — so the two states differed by their ink alone, which is not a difference a reader is going to read. The hover climbs the neutral glass ladder in all three variants now, the same two rungs `solid` and `glass` were already using and the same wash an unselected `PlBottomNavigation` item takes for the same reason. The rule the fix is written to is worth stating, because it is the one a two-state control fails quietly: **a control with two states cannot put the colour family on the false one**, and a hover is still the false state.
943
+ - **The build empties `dist/` before it writes to it.** `tsc` writes and never removes, so a component deleted from `src/` went on being published: `package.json` ships the whole directory and the `./*` export resolves `plass-ui/thing` straight out of it, which means a removed name goes on importing and goes on being typed for everybody but the person who removed it. Two more things read that directory rather than the source — `scripts/build-styles.mjs` writes one `plass-ui/css/*.css` scan manifest per folder it finds under `dist/components`, and `scripts/minify.mjs` counts `@__PURE__` annotations across whatever is there — so a leftover folder was also a stale stylesheet manifest and a count that quietly disagreed with the source it was a count of. None of it failed a build; it failed in somebody else's install. `npm run clean` is the first step of `npm run build` now, and is its own script so it can be run on its own.
944
+
945
+ ### Changed
946
+
947
+ - **A `PlButtonGroup` and a `PlToggleGroup` draw one run of keys rather than two copies of it.** The squared corners, the hairline overlap and the stacking context each key gets were written out in both components — the same three lists, and two places for the seam to stop lining up. They live in `internal/button-group.ts` now, beside the context both groups already publish. Nothing renders differently.
948
+
949
+ - **A surface that takes the page now opens at 260ms rather than 150ms.** `PlModal`, `PlDrawer`, `PlOverlay` and `PlCommandPalette` — the sheet and its scrim together, so the two arrive as one thing. `--plass-duration-slow` already existed and was already what a height travels over; it was never applied to the thing it describes best.
950
+
951
+ 150ms is a key going down. Across the whole window it is not a fade, it is a cut with a hint of blur on it, and a page that changes this completely that fast leaves a reader looking for what moved rather than reading what arrived. The line is written down on the [motion](https://plass.cdget.com/design/design-language#the-second-duration-is-for-what-takes-the-page) page and it is one question: **does the surface take the page, or hang off a control?** A menu, a popover, a tooltip, a select's list and a picker's sheet all still open at 150ms, because they are the size of the control they came from.
952
+
953
+ Nothing about the shape of the motion changed. It is still opacity and nothing else, in both directions, on the same curve.
954
+
955
+ - **`PlStack` replaces `PlAvatarGroup`, and this is a breaking change.** A row of overlapping faces is one arrangement of a pile rather than a component of its own: a deck of cards, a stack of documents, a fan of thumbnails and a row of avatars differ in what is in them, not in how they are laid out. `PlStack` takes whatever it is given and never looks inside.
956
+
957
+ **The overlap is real layout, never a transform**, and that is the whole component. A translated pile is laid out one item wide: it paints outside its own box, and every element after it on the page is placed against a size the reader never sees — so it cannot go in a paragraph, a table cell, or a flex row beside a label without pushing something out of place. A negative margin makes the box measure exactly what it draws. Five 32px items at 10px of overlap: **120×32 `horizontal`, 32×120 `vertical`, 120×72 `diagonal`**, measured in a browser with the stylesheet loaded rather than reasoned about.
958
+
959
+ That last one is the part most implementations get wrong. **A flow only overlaps on the axis it flows along**, so `diagonal` flows across like `horizontal` does and takes its vertical step per item instead — one fixed offset in a row would put every item at the same height. It is also why `diagonal` is a **fan** and says so: the horizontal advance is `item width − overlap`, and a component that takes arbitrary children does not know that width.
960
+
961
+ `max`, `total` and `overlap` carry over unchanged. `overflow` is new and is a **function** — `(n) => <PlAvatar initials={\`+${n}\`} />`— because the hidden count _is_ that item, and a node would have to be handed a number it has no way to work out.`direction`, `front`, `scaleStep`, `opacityStep`, `drop`and`ring`are new. Depth is applied on the standalone`scale`property inside a second box per item, which is not spare: every keyframe in this library that grows or zooms writes`scale`, and on one box an entrance would overwrite the depth on its first frame.
962
+
963
+ **What is lost is the group context, and it could not be kept.** `PlAvatarGroup` set `size`, `shape`, `variant`, `color` and `elevation` once for every face; a pile that accepts arbitrary children has no way to know one of them is an avatar. `size` and `color` are axes an application already sets once — put a `PlassProvider` around the stack. The other three belong on the avatar. `PlAvatar` no longer reads a group at all and resolves those five from its own props and the provider.
964
+
965
+ `ring` is a boolean and lands on **the element you passed**, so it takes that element's shape: wrap an avatar in something square and the ring is square. The alternative was copying a class onto the children, which stops working the moment one of them is a `PlTooltip`, a fragment, or somebody else's `.map()`. Measured with `npm run size`: **+0.1 kB on the whole library**, and nothing on the four other scenarios.
966
+
5
967
  ## 1.3.0 (2026-08-31)
6
968
 
7
969
  ### Added