stimeo-ui 0.3.0 → 0.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 (148) hide show
  1. package/CHANGELOG.md +124 -0
  2. package/dist/controllers/alert_dialog_controller.js +32 -5
  3. package/dist/controllers/alert_dialog_controller.js.map +1 -1
  4. package/dist/controllers/announcer_controller.d.ts +32 -6
  5. package/dist/controllers/announcer_controller.js +255 -20
  6. package/dist/controllers/announcer_controller.js.map +1 -1
  7. package/dist/controllers/aspect_ratio_controller.d.ts +2 -2
  8. package/dist/controllers/aspect_ratio_controller.js +1 -1
  9. package/dist/controllers/aspect_ratio_controller.js.map +1 -1
  10. package/dist/controllers/breadcrumb_controller.js +5 -1
  11. package/dist/controllers/breadcrumb_controller.js.map +1 -1
  12. package/dist/controllers/carousel_controller.js +5 -1
  13. package/dist/controllers/carousel_controller.js.map +1 -1
  14. package/dist/controllers/clipboard_controller.js +8 -3
  15. package/dist/controllers/clipboard_controller.js.map +1 -1
  16. package/dist/controllers/collapsible_controller.d.ts +1 -1
  17. package/dist/controllers/collapsible_controller.js +4 -1
  18. package/dist/controllers/collapsible_controller.js.map +1 -1
  19. package/dist/controllers/color_picker_controller.d.ts +4 -3
  20. package/dist/controllers/color_picker_controller.js +52 -2
  21. package/dist/controllers/color_picker_controller.js.map +1 -1
  22. package/dist/controllers/combobox_controller.js.map +1 -1
  23. package/dist/controllers/command_palette_controller.js +32 -5
  24. package/dist/controllers/command_palette_controller.js.map +1 -1
  25. package/dist/controllers/confirm_controller.js +32 -5
  26. package/dist/controllers/confirm_controller.js.map +1 -1
  27. package/dist/controllers/context_menu_controller.d.ts +1 -1
  28. package/dist/controllers/context_menu_controller.js +2 -2
  29. package/dist/controllers/context_menu_controller.js.map +1 -1
  30. package/dist/controllers/countdown_controller.d.ts +24 -8
  31. package/dist/controllers/countdown_controller.js +117 -13
  32. package/dist/controllers/countdown_controller.js.map +1 -1
  33. package/dist/controllers/date_range_picker_controller.d.ts +4 -1
  34. package/dist/controllers/date_range_picker_controller.js +55 -1
  35. package/dist/controllers/date_range_picker_controller.js.map +1 -1
  36. package/dist/controllers/dialog_controller.js +32 -5
  37. package/dist/controllers/dialog_controller.js.map +1 -1
  38. package/dist/controllers/direct_upload_controller.d.ts +1 -1
  39. package/dist/controllers/direct_upload_controller.js +3 -3
  40. package/dist/controllers/direct_upload_controller.js.map +1 -1
  41. package/dist/controllers/dismissible_controller.js.map +1 -1
  42. package/dist/controllers/drawer_controller.js +32 -5
  43. package/dist/controllers/drawer_controller.js.map +1 -1
  44. package/dist/controllers/empty_state_controller.d.ts +36 -11
  45. package/dist/controllers/empty_state_controller.js +128 -23
  46. package/dist/controllers/empty_state_controller.js.map +1 -1
  47. package/dist/controllers/flash_controller.d.ts +25 -5
  48. package/dist/controllers/flash_controller.js +161 -21
  49. package/dist/controllers/flash_controller.js.map +1 -1
  50. package/dist/controllers/focus_controller.js +32 -5
  51. package/dist/controllers/focus_controller.js.map +1 -1
  52. package/dist/controllers/form_validation_controller.js +8 -2
  53. package/dist/controllers/form_validation_controller.js.map +1 -1
  54. package/dist/controllers/frame_loading_controller.d.ts +31 -3
  55. package/dist/controllers/frame_loading_controller.js +261 -27
  56. package/dist/controllers/frame_loading_controller.js.map +1 -1
  57. package/dist/controllers/highlight_controller.d.ts +8 -4
  58. package/dist/controllers/highlight_controller.js +38 -1
  59. package/dist/controllers/highlight_controller.js.map +1 -1
  60. package/dist/controllers/idle_controller.d.ts +2 -1
  61. package/dist/controllers/idle_controller.js +13 -2
  62. package/dist/controllers/idle_controller.js.map +1 -1
  63. package/dist/controllers/listbox_controller.js.map +1 -1
  64. package/dist/controllers/local_time_controller.d.ts +19 -3
  65. package/dist/controllers/local_time_controller.js +102 -6
  66. package/dist/controllers/local_time_controller.js.map +1 -1
  67. package/dist/controllers/masonry_controller.d.ts +1 -1
  68. package/dist/controllers/masonry_controller.js +1 -1
  69. package/dist/controllers/masonry_controller.js.map +1 -1
  70. package/dist/controllers/meter_controller.d.ts +22 -2
  71. package/dist/controllers/meter_controller.js +147 -26
  72. package/dist/controllers/meter_controller.js.map +1 -1
  73. package/dist/controllers/multi_select_controller.js.map +1 -1
  74. package/dist/controllers/network_status_controller.d.ts +25 -11
  75. package/dist/controllers/network_status_controller.js +29 -11
  76. package/dist/controllers/network_status_controller.js.map +1 -1
  77. package/dist/controllers/number_input_controller.d.ts +8 -0
  78. package/dist/controllers/number_input_controller.js +191 -24
  79. package/dist/controllers/number_input_controller.js.map +1 -1
  80. package/dist/controllers/overflow_menu_controller.js +34 -7
  81. package/dist/controllers/overflow_menu_controller.js.map +1 -1
  82. package/dist/controllers/pagination_controller.js +5 -1
  83. package/dist/controllers/pagination_controller.js.map +1 -1
  84. package/dist/controllers/password_strength_controller.d.ts +1 -1
  85. package/dist/controllers/password_strength_controller.js +1 -1
  86. package/dist/controllers/password_strength_controller.js.map +1 -1
  87. package/dist/controllers/pointer_drag_controller.js +10 -0
  88. package/dist/controllers/pointer_drag_controller.js.map +1 -1
  89. package/dist/controllers/portal_controller.js +10 -0
  90. package/dist/controllers/portal_controller.js.map +1 -1
  91. package/dist/controllers/progress_controller.d.ts +17 -2
  92. package/dist/controllers/progress_controller.js +123 -12
  93. package/dist/controllers/progress_controller.js.map +1 -1
  94. package/dist/controllers/range_slider_controller.d.ts +27 -1
  95. package/dist/controllers/range_slider_controller.js +449 -93
  96. package/dist/controllers/range_slider_controller.js.map +1 -1
  97. package/dist/controllers/rating_controller.d.ts +4 -1
  98. package/dist/controllers/rating_controller.js +55 -0
  99. package/dist/controllers/rating_controller.js.map +1 -1
  100. package/dist/controllers/relative_time_controller.d.ts +13 -0
  101. package/dist/controllers/relative_time_controller.js +135 -12
  102. package/dist/controllers/relative_time_controller.js.map +1 -1
  103. package/dist/controllers/scroll_area_controller.d.ts +1 -1
  104. package/dist/controllers/scroll_area_controller.js +1 -1
  105. package/dist/controllers/scroll_area_controller.js.map +1 -1
  106. package/dist/controllers/separator_controller.js +13 -17
  107. package/dist/controllers/separator_controller.js.map +1 -1
  108. package/dist/controllers/sidebar_controller.d.ts +1 -11
  109. package/dist/controllers/sidebar_controller.js +37 -8
  110. package/dist/controllers/sidebar_controller.js.map +1 -1
  111. package/dist/controllers/skeleton_controller.d.ts +7 -2
  112. package/dist/controllers/skeleton_controller.js +143 -22
  113. package/dist/controllers/skeleton_controller.js.map +1 -1
  114. package/dist/controllers/slider_controller.d.ts +17 -1
  115. package/dist/controllers/slider_controller.js +342 -50
  116. package/dist/controllers/slider_controller.js.map +1 -1
  117. package/dist/controllers/spinner_controller.d.ts +50 -12
  118. package/dist/controllers/spinner_controller.js +244 -28
  119. package/dist/controllers/spinner_controller.js.map +1 -1
  120. package/dist/controllers/step_indicator_controller.d.ts +13 -2
  121. package/dist/controllers/step_indicator_controller.js +85 -6
  122. package/dist/controllers/step_indicator_controller.js.map +1 -1
  123. package/dist/controllers/stepper_controller.js +2 -0
  124. package/dist/controllers/stepper_controller.js.map +1 -1
  125. package/dist/controllers/stick_to_bottom_controller.d.ts +26 -2
  126. package/dist/controllers/stick_to_bottom_controller.js +60 -10
  127. package/dist/controllers/stick_to_bottom_controller.js.map +1 -1
  128. package/dist/controllers/switch_controller.d.ts +12 -8
  129. package/dist/controllers/switch_controller.js +162 -18
  130. package/dist/controllers/switch_controller.js.map +1 -1
  131. package/dist/controllers/textarea_autosize_controller.js +1 -1
  132. package/dist/controllers/textarea_autosize_controller.js.map +1 -1
  133. package/dist/controllers/time_picker_controller.d.ts +3 -0
  134. package/dist/controllers/time_picker_controller.js +6 -3
  135. package/dist/controllers/time_picker_controller.js.map +1 -1
  136. package/dist/controllers/tree_view_controller.d.ts +1 -2
  137. package/dist/controllers/tree_view_controller.js +19 -1
  138. package/dist/controllers/tree_view_controller.js.map +1 -1
  139. package/dist/index.js +2278 -596
  140. package/dist/index.js.map +1 -1
  141. package/dist/inspector/cli.d.ts +76 -1
  142. package/dist/inspector/cli.js +937 -76
  143. package/dist/inspector/cli.js.map +1 -1
  144. package/dist/inspector/cli_bin.js +997 -78
  145. package/dist/inspector/cli_bin.js.map +1 -1
  146. package/dist/inspector/examples.json +20 -20
  147. package/dist/inspector/manifest.json +428 -33
  148. package/package.json +3 -3
@@ -19,7 +19,7 @@
19
19
  },
20
20
  "stimeo--aspect-ratio": {
21
21
  "file": "examples/aspect_ratio/_demo.html.erb",
22
- "source": "<%# Markup for the aspect-ratio demo.\n The library only supplies the ratio Value as the --stimeo-aspect-ratio custom\n property; demo.css does the actual ratio rendering\n (aspect-ratio: var(--stimeo-aspect-ratio)). %>\n<div class=\"aspect-ratio-demo\">\n <figure class=\"aspect-ratio-demo__item\">\n <div class=\"ratio-box\" data-controller=\"stimeo--aspect-ratio\"\n data-stimeo--aspect-ratio-ratio-value=\"16/9\">\n <div class=\"ratio-box__content\" data-stimeo--aspect-ratio-target=\"content\">16 / 9</div>\n </div>\n <figcaption><%= t(\"components.aspect_ratio.demo.wide\") %></figcaption>\n </figure>\n\n <figure class=\"aspect-ratio-demo__item\">\n <div class=\"ratio-box\" data-controller=\"stimeo--aspect-ratio\"\n data-stimeo--aspect-ratio-ratio-value=\"1/1\">\n <div class=\"ratio-box__content\" data-stimeo--aspect-ratio-target=\"content\">1 / 1</div>\n </div>\n <figcaption><%= t(\"components.aspect_ratio.demo.square\") %></figcaption>\n </figure>\n\n <figure class=\"aspect-ratio-demo__item\">\n <div class=\"ratio-box\" data-controller=\"stimeo--aspect-ratio\"\n data-stimeo--aspect-ratio-ratio-value=\"3/4\">\n <div class=\"ratio-box__content\" data-stimeo--aspect-ratio-target=\"content\">3 / 4</div>\n </div>\n <figcaption><%= t(\"components.aspect_ratio.demo.portrait\") %></figcaption>\n </figure>\n</div>\n"
22
+ "source": "<%# Markup for the aspect-ratio demo.\n The library only supplies the ratio Value as the --stimeo--aspect-ratio custom\n property; demo.css does the actual ratio rendering\n (aspect-ratio: var(--stimeo--aspect-ratio)). %>\n<div class=\"aspect-ratio-demo\">\n <figure class=\"aspect-ratio-demo__item\">\n <div class=\"ratio-box\" data-controller=\"stimeo--aspect-ratio\"\n data-stimeo--aspect-ratio-ratio-value=\"16/9\">\n <div class=\"ratio-box__content\" data-stimeo--aspect-ratio-target=\"content\">16 / 9</div>\n </div>\n <figcaption><%= t(\"components.aspect_ratio.demo.wide\") %></figcaption>\n </figure>\n\n <figure class=\"aspect-ratio-demo__item\">\n <div class=\"ratio-box\" data-controller=\"stimeo--aspect-ratio\"\n data-stimeo--aspect-ratio-ratio-value=\"1/1\">\n <div class=\"ratio-box__content\" data-stimeo--aspect-ratio-target=\"content\">1 / 1</div>\n </div>\n <figcaption><%= t(\"components.aspect_ratio.demo.square\") %></figcaption>\n </figure>\n\n <figure class=\"aspect-ratio-demo__item\">\n <div class=\"ratio-box\" data-controller=\"stimeo--aspect-ratio\"\n data-stimeo--aspect-ratio-ratio-value=\"3/4\">\n <div class=\"ratio-box__content\" data-stimeo--aspect-ratio-target=\"content\">3 / 4</div>\n </div>\n <figcaption><%= t(\"components.aspect_ratio.demo.portrait\") %></figcaption>\n </figure>\n</div>\n"
23
23
  },
24
24
  "stimeo--auto-submit": {
25
25
  "file": "examples/auto_submit/_demo.html.erb",
@@ -63,7 +63,7 @@
63
63
  },
64
64
  "stimeo--color-picker": {
65
65
  "file": "examples/color_picker/_demo.html.erb",
66
- "source": "<%# Markup for the color-picker demo.\n The library manages each channel's (hue / saturation / lightness) role=\"slider\"\n value, composes them into a color two-way-synced with the hex input, and updates\n --stimeo-color for the preview. The 2D palette is decomposed into independent 1D\n sliders for accessibility. Thumb position and look are the consumer's CSS. %>\n<div\n class=\"color-picker\"\n data-controller=\"stimeo--color-picker\"\n data-stimeo--color-picker-value-value=\"#3366cc\">\n <div\n class=\"color-picker__preview\"\n data-stimeo--color-picker-target=\"preview\"\n aria-hidden=\"true\"></div>\n\n <div class=\"color-picker__channels\">\n <% { hue: [t(\"components.color_picker.demo.hue\"), 360, 210],\n saturation: [t(\"components.color_picker.demo.saturation\"), 100, 60],\n lightness: [t(\"components.color_picker.demo.lightness\"), 100, 50] }\n .each do |channel, (label, max, now)| %>\n <div class=\"color-picker__channel\">\n <span class=\"color-picker__channel-label\"><%= label %></span>\n <div\n class=\"color-picker__track color-picker__track--<%= channel %>\"\n role=\"slider\"\n aria-label=\"<%= label %>\"\n data-channel=\"<%= channel %>\"\n tabindex=\"0\"\n aria-valuemin=\"0\"\n aria-valuemax=\"<%= max %>\"\n aria-valuenow=\"<%= now %>\"\n data-stimeo--color-picker-target=\"slider\"\n data-action=\"\n keydown->stimeo--color-picker#onKeydown\n pointerdown->stimeo--color-picker#onPointerDown\">\n <span class=\"color-picker__thumb\" aria-hidden=\"true\"></span>\n </div>\n </div>\n <% end %>\n </div>\n\n <label class=\"color-picker__hex\">\n <span><%= t(\"components.color_picker.demo.hex\") %></span>\n <input\n type=\"text\"\n inputmode=\"text\"\n aria-label=\"<%= t(\"components.color_picker.demo.hex\") %>\"\n value=\"#3366cc\"\n data-stimeo--color-picker-target=\"hex\"\n data-action=\"change->stimeo--color-picker#onHexInput\" />\n </label>\n\n <input type=\"hidden\" name=\"brand_color\" data-stimeo--color-picker-target=\"field\" />\n</div>\n"
66
+ "source": "<%# Markup for the color-picker demo.\n The library manages each channel's (hue / saturation / lightness) role=\"slider\"\n value, composes them into a color two-way-synced with the hex input, and updates\n --stimeo--color for the preview. The 2D palette is decomposed into independent 1D\n sliders for accessibility. Thumb position and look are the consumer's CSS. %>\n<div\n class=\"color-picker\"\n data-controller=\"stimeo--color-picker\"\n data-stimeo--color-picker-value-value=\"#3366cc\">\n <div\n class=\"color-picker__preview\"\n data-stimeo--color-picker-target=\"preview\"\n aria-hidden=\"true\"></div>\n\n <div class=\"color-picker__channels\">\n <% { hue: [t(\"components.color_picker.demo.hue\"), 360, 210],\n saturation: [t(\"components.color_picker.demo.saturation\"), 100, 60],\n lightness: [t(\"components.color_picker.demo.lightness\"), 100, 50] }\n .each do |channel, (label, max, now)| %>\n <div class=\"color-picker__channel\">\n <span class=\"color-picker__channel-label\"><%= label %></span>\n <div\n class=\"color-picker__track color-picker__track--<%= channel %>\"\n role=\"slider\"\n aria-label=\"<%= label %>\"\n data-channel=\"<%= channel %>\"\n tabindex=\"0\"\n aria-valuemin=\"0\"\n aria-valuemax=\"<%= max %>\"\n aria-valuenow=\"<%= now %>\"\n data-stimeo--color-picker-target=\"slider\"\n data-action=\"\n keydown->stimeo--color-picker#onKeydown\n pointerdown->stimeo--color-picker#onPointerDown\">\n <span class=\"color-picker__thumb\" aria-hidden=\"true\"></span>\n </div>\n </div>\n <% end %>\n </div>\n\n <label class=\"color-picker__hex\">\n <span><%= t(\"components.color_picker.demo.hex\") %></span>\n <input\n type=\"text\"\n inputmode=\"text\"\n aria-label=\"<%= t(\"components.color_picker.demo.hex\") %>\"\n value=\"#3366cc\"\n data-stimeo--color-picker-target=\"hex\"\n data-action=\"change->stimeo--color-picker#onHexInput\" />\n </label>\n\n <input type=\"hidden\" name=\"brand_color\" data-stimeo--color-picker-target=\"field\" />\n</div>\n"
67
67
  },
68
68
  "stimeo--combobox": {
69
69
  "file": "examples/combobox/_demo.html.erb",
@@ -83,7 +83,7 @@
83
83
  },
84
84
  "stimeo--context-menu": {
85
85
  "file": "examples/context_menu/_demo.html.erb",
86
- "source": "<%# Markup for the context_menu demo.\n A right-click on the region (or the ContextMenu key / Shift+F10) opens a\n role=\"menu\" at the pointer position. The interaction model is the same as a Menu\n Button. The click coordinate is reflected on the menu as\n --stimeo-context-menu-x / -y, and the Playground CSS uses it for top/left\n placement (works even without the positioning module). %>\n<div class=\"context-menu\" data-controller=\"stimeo--context-menu\">\n <div\n class=\"context-menu__region\"\n data-stimeo--context-menu-target=\"region\"\n tabindex=\"0\"\n aria-haspopup=\"menu\"\n aria-controls=\"context-menu-list\"\n data-action=\"\n contextmenu->stimeo--context-menu#open\n keydown->stimeo--context-menu#onRegionKeydown\">\n <%= t(\"components.context_menu.demo.region\") %>\n </div>\n\n <ul\n class=\"context-menu__list\"\n id=\"context-menu-list\"\n role=\"menu\"\n aria-label=\"<%= t(\"components.context_menu.demo.label\") %>\"\n data-stimeo--context-menu-target=\"menu\"\n hidden>\n <li role=\"none\">\n <button type=\"button\" class=\"demo-menuitem\" role=\"menuitem\" tabindex=\"-1\"\n data-stimeo--context-menu-target=\"item\"\n data-action=\"\n click->stimeo--context-menu#activate\n keydown->stimeo--context-menu#onItemKeydown\">\n <%= t(\"components.context_menu.demo.copy\") %>\n </button>\n </li>\n <li role=\"none\">\n <button type=\"button\" class=\"demo-menuitem\" role=\"menuitem\" tabindex=\"-1\"\n data-stimeo--context-menu-target=\"item\"\n data-action=\"\n click->stimeo--context-menu#activate\n keydown->stimeo--context-menu#onItemKeydown\">\n <%= t(\"components.context_menu.demo.paste\") %>\n </button>\n </li>\n <li role=\"none\">\n <button type=\"button\" class=\"demo-menuitem\" role=\"menuitem\" tabindex=\"-1\"\n data-stimeo--context-menu-target=\"item\"\n data-action=\"\n click->stimeo--context-menu#activate\n keydown->stimeo--context-menu#onItemKeydown\">\n <%= t(\"components.context_menu.demo.delete\") %>\n </button>\n </li>\n </ul>\n</div>\n"
86
+ "source": "<%# Markup for the context_menu demo.\n A right-click on the region (or the ContextMenu key / Shift+F10) opens a\n role=\"menu\" at the pointer position. The interaction model is the same as a Menu\n Button. The click coordinate is reflected on the menu as\n --stimeo--context-menu-x / -y, and the Playground CSS uses it for top/left\n placement (works even without the positioning module). %>\n<div class=\"context-menu\" data-controller=\"stimeo--context-menu\">\n <div\n class=\"context-menu__region\"\n data-stimeo--context-menu-target=\"region\"\n tabindex=\"0\"\n aria-haspopup=\"menu\"\n aria-controls=\"context-menu-list\"\n data-action=\"\n contextmenu->stimeo--context-menu#open\n keydown->stimeo--context-menu#onRegionKeydown\">\n <%= t(\"components.context_menu.demo.region\") %>\n </div>\n\n <ul\n class=\"context-menu__list\"\n id=\"context-menu-list\"\n role=\"menu\"\n aria-label=\"<%= t(\"components.context_menu.demo.label\") %>\"\n data-stimeo--context-menu-target=\"menu\"\n hidden>\n <li role=\"none\">\n <button type=\"button\" class=\"demo-menuitem\" role=\"menuitem\" tabindex=\"-1\"\n data-stimeo--context-menu-target=\"item\"\n data-action=\"\n click->stimeo--context-menu#activate\n keydown->stimeo--context-menu#onItemKeydown\">\n <%= t(\"components.context_menu.demo.copy\") %>\n </button>\n </li>\n <li role=\"none\">\n <button type=\"button\" class=\"demo-menuitem\" role=\"menuitem\" tabindex=\"-1\"\n data-stimeo--context-menu-target=\"item\"\n data-action=\"\n click->stimeo--context-menu#activate\n keydown->stimeo--context-menu#onItemKeydown\">\n <%= t(\"components.context_menu.demo.paste\") %>\n </button>\n </li>\n <li role=\"none\">\n <button type=\"button\" class=\"demo-menuitem\" role=\"menuitem\" tabindex=\"-1\"\n data-stimeo--context-menu-target=\"item\"\n data-action=\"\n click->stimeo--context-menu#activate\n keydown->stimeo--context-menu#onItemKeydown\">\n <%= t(\"components.context_menu.demo.delete\") %>\n </button>\n </li>\n </ul>\n</div>\n"
87
87
  },
88
88
  "stimeo--count-up": {
89
89
  "file": "examples/count_up/_demo.html.erb",
@@ -91,7 +91,7 @@
91
91
  },
92
92
  "stimeo--countdown": {
93
93
  "file": "examples/countdown/_demo.html.erb",
94
- "source": "<%# Markup for the countdown (timer) demo.\n Fills the days/hours/minutes/seconds slots with the time remaining until the\n deadline, updating every interval (default 1000ms). role=\"timer\" + aria-live=\"off\"\n avoids per-second announcements; completion is announced by writing text into\n status (aria-live=\"polite\"). The deadline is computed in ERB to always be in the future.\n The pause/resume/reset buttons sit outside the timer (a role=\"timer\" live region\n should not wrap unrelated controls), so they drive it through the countdown:* events\n the controller listens for (see demo.js) instead of a Stimulus data-action, which\n only binds within the controller's own element.\n This demo behaves as a *duration* timer (a fixed length counted from \"now\"): the\n deadline is now + the duration below, and Reset re-anchors it to now + the same\n duration so the clock returns to the full time. The library only counts down to\n whatever deadline it is given; demo.js owns that duration→deadline policy and exposes\n the length via data-countdown-duration-ms. %>\n<% countdown_duration = 1.hour %>\n<div class=\"countdown-demo\" data-countdown-duration-ms=\"<%= countdown_duration.to_i * 1000 %>\">\n <div\n class=\"countdown\"\n data-controller=\"stimeo--countdown\"\n role=\"timer\"\n aria-live=\"off\"\n data-stimeo--countdown-deadline-value=\"<%= countdown_duration.from_now.iso8601 %>\"\n data-stimeo--countdown-complete-label-value=\"<%= t('components.countdown.demo.complete') %>\"\n data-action=\"\n countdown:pause->stimeo--countdown#pause\n countdown:resume->stimeo--countdown#resume\n countdown:reset->stimeo--countdown#reset\">\n <span class=\"countdown__unit\">\n <span class=\"countdown__value\" data-stimeo--countdown-target=\"days\">0</span>\n <span class=\"countdown__label\"><%= t(\"components.countdown.demo.days\") %></span>\n </span>\n <span class=\"countdown__unit\">\n <span class=\"countdown__value\" data-stimeo--countdown-target=\"hours\">00</span>\n <span class=\"countdown__label\"><%= t(\"components.countdown.demo.hours\") %></span>\n </span>\n <span class=\"countdown__unit\">\n <span class=\"countdown__value\" data-stimeo--countdown-target=\"minutes\">00</span>\n <span class=\"countdown__label\"><%= t(\"components.countdown.demo.minutes\") %></span>\n </span>\n <span class=\"countdown__unit\">\n <span class=\"countdown__value\" data-stimeo--countdown-target=\"seconds\">00</span>\n <span class=\"countdown__label\"><%= t(\"components.countdown.demo.seconds\") %></span>\n </span>\n <span class=\"countdown__status\" role=\"status\" aria-live=\"polite\"\n data-stimeo--countdown-target=\"status\"></span>\n </div>\n\n <div class=\"countdown-demo__controls\">\n <button class=\"demo-trigger\" type=\"button\" data-countdown-pause>\n <%= t(\"components.countdown.demo.pause\") %>\n </button>\n <button class=\"demo-trigger\" type=\"button\" data-countdown-resume>\n <%= t(\"components.countdown.demo.resume\") %>\n </button>\n <button class=\"demo-trigger\" type=\"button\" data-countdown-reset>\n <%= t(\"components.countdown.demo.reset\") %>\n </button>\n </div>\n</div>\n"
94
+ "source": "<%# Markup for the countdown (timer) demo.\n Fills the days/hours/minutes/seconds slots with the time remaining until the\n deadline, updating every interval (default 1000ms). role=\"timer\" + aria-live=\"off\"\n avoids per-second announcements; the status slot carries the completion text\n visually and the shared announcer reads it out, so it is said exactly once.\n The deadline is computed in ERB to always be in the future.\n The pause/resume/reset buttons sit outside the timer (a role=\"timer\" live region\n should not wrap unrelated controls), so they drive it through the countdown:* events\n the controller listens for (see demo.js) instead of a Stimulus data-action, which\n only binds within the controller's own element.\n This demo behaves as a *duration* timer (a fixed length counted from \"now\"): the\n deadline is now + the duration below, and Reset re-anchors it to now + the same\n duration so the clock returns to the full time. The library only counts down to\n whatever deadline it is given; demo.js owns that duration→deadline policy and exposes\n the length via data-countdown-duration-ms. %>\n<% countdown_duration = 1.hour %>\n<div class=\"countdown-demo\" data-countdown-duration-ms=\"<%= countdown_duration.to_i * 1000 %>\">\n <div\n class=\"countdown\"\n data-controller=\"stimeo--countdown\"\n role=\"timer\"\n aria-live=\"off\"\n data-stimeo--countdown-deadline-value=\"<%= countdown_duration.from_now.iso8601 %>\"\n data-stimeo--countdown-complete-label-value=\"<%= t('components.countdown.demo.complete') %>\"\n data-stimeo--countdown-announce-text-value=\"<%= t('components.countdown.demo.complete') %>\"\n data-action=\"\n countdown:pause->stimeo--countdown#pause\n countdown:resume->stimeo--countdown#resume\n countdown:reset->stimeo--countdown#reset\">\n <span class=\"countdown__unit\">\n <span class=\"countdown__value\" data-stimeo--countdown-target=\"days\">0</span>\n <span class=\"countdown__label\"><%= t(\"components.countdown.demo.days\") %></span>\n </span>\n <span class=\"countdown__unit\">\n <span class=\"countdown__value\" data-stimeo--countdown-target=\"hours\">00</span>\n <span class=\"countdown__label\"><%= t(\"components.countdown.demo.hours\") %></span>\n </span>\n <span class=\"countdown__unit\">\n <span class=\"countdown__value\" data-stimeo--countdown-target=\"minutes\">00</span>\n <span class=\"countdown__label\"><%= t(\"components.countdown.demo.minutes\") %></span>\n </span>\n <span class=\"countdown__unit\">\n <span class=\"countdown__value\" data-stimeo--countdown-target=\"seconds\">00</span>\n <span class=\"countdown__label\"><%= t(\"components.countdown.demo.seconds\") %></span>\n </span>\n <%# Visual only: the announcer says this once, so a live region here would repeat it. %>\n <span class=\"countdown__status\" data-stimeo--countdown-target=\"status\"></span>\n </div>\n\n <div class=\"countdown-demo__controls\">\n <button class=\"demo-trigger\" type=\"button\" data-countdown-pause>\n <%= t(\"components.countdown.demo.pause\") %>\n </button>\n <button class=\"demo-trigger\" type=\"button\" data-countdown-resume>\n <%= t(\"components.countdown.demo.resume\") %>\n </button>\n <button class=\"demo-trigger\" type=\"button\" data-countdown-reset>\n <%= t(\"components.countdown.demo.reset\") %>\n </button>\n </div>\n\n <%# Shared announcer: reaching zero is the transition worth reading; the ticking\n numbers in between are state the reader can query. %>\n <div data-controller=\"stimeo--announcer\">\n <div class=\"visually-hidden\"\n data-stimeo--announcer-target=\"polite\" aria-live=\"polite\" aria-atomic=\"true\"></div>\n </div>\n</div>\n"
95
95
  },
96
96
  "stimeo--currency-input": {
97
97
  "file": "examples/currency_input/_demo.html.erb",
@@ -111,7 +111,7 @@
111
111
  },
112
112
  "stimeo--direct-upload": {
113
113
  "file": "examples/direct_upload/_demo.html.erb",
114
- "source": "<%# Direct upload demo: there is no server, so demo.js fires the ActiveStorage\n direct-upload:* events to drive the rows. The library clones the row template per\n file, updates aria-valuenow / data-upload-state / the --stimeo-upload-progress\n var, and announces completion into the status live region. This demo styles the\n bars; the sr-only status carries the announcement. %>\n<div class=\"direct-upload-demo\">\n <button type=\"button\" class=\"demo-trigger\" data-direct-upload-start>\n <%= t(\"components.direct_upload.demo.start\") %>\n </button>\n <div\n class=\"direct-upload\"\n data-controller=\"stimeo--direct-upload\"\n data-stimeo--direct-upload-done-label-value=\"<%= t('components.direct_upload.demo.done') %>\"\n data-stimeo--direct-upload-error-label-value=\"<%= t('components.direct_upload.demo.error') %>\">\n <div class=\"direct-upload__list\" data-stimeo--direct-upload-target=\"list\"></div>\n <template data-stimeo--direct-upload-target=\"row\">\n <div class=\"direct-upload__row\" role=\"progressbar\" aria-valuemin=\"0\" aria-valuemax=\"100\">\n <span class=\"direct-upload__name\" data-field=\"name\"></span>\n <span class=\"direct-upload__track\"><span class=\"direct-upload__bar\"></span></span>\n <span class=\"direct-upload__percent\" data-field=\"percent\"></span>\n </div>\n </template>\n <span class=\"visually-hidden\" data-stimeo--direct-upload-target=\"status\"\n aria-live=\"polite\"></span>\n </div>\n</div>\n"
114
+ "source": "<%# Direct upload demo: there is no server, so demo.js fires the ActiveStorage\n direct-upload:* events to drive the rows. The library clones the row template per\n file, updates aria-valuenow / data-upload-state / the --stimeo--upload-progress\n var, and announces completion into the status live region. This demo styles the\n bars; the sr-only status carries the announcement. %>\n<div class=\"direct-upload-demo\">\n <button type=\"button\" class=\"demo-trigger\" data-direct-upload-start>\n <%= t(\"components.direct_upload.demo.start\") %>\n </button>\n <div\n class=\"direct-upload\"\n data-controller=\"stimeo--direct-upload\"\n data-stimeo--direct-upload-done-label-value=\"<%= t('components.direct_upload.demo.done') %>\"\n data-stimeo--direct-upload-error-label-value=\"<%= t('components.direct_upload.demo.error') %>\">\n <div class=\"direct-upload__list\" data-stimeo--direct-upload-target=\"list\"></div>\n <template data-stimeo--direct-upload-target=\"row\">\n <div class=\"direct-upload__row\" role=\"progressbar\" aria-valuemin=\"0\" aria-valuemax=\"100\">\n <span class=\"direct-upload__name\" data-field=\"name\"></span>\n <span class=\"direct-upload__track\"><span class=\"direct-upload__bar\"></span></span>\n <span class=\"direct-upload__percent\" data-field=\"percent\"></span>\n </div>\n </template>\n <span class=\"visually-hidden\" data-stimeo--direct-upload-target=\"status\"\n aria-live=\"polite\"></span>\n </div>\n</div>\n"
115
115
  },
116
116
  "stimeo--dirty-form": {
117
117
  "file": "examples/dirty_form/_demo.html.erb",
@@ -135,7 +135,7 @@
135
135
  },
136
136
  "stimeo--empty-state": {
137
137
  "file": "examples/empty_state/_demo.html.erb",
138
- "source": "<%# Empty-state demo: add/remove items and the empty placeholder toggles itself. The\n controller observes the list with a MutationObserver, toggles hidden on the\n list / empty targets at the 0 <-> 1+ boundary, and reflects data-empty / data-count.\n demo.js adds/removes <li> (standing in for Turbo Stream rows). This demo only styles\n it; the empty placeholder's copy is owned here. %>\n<div\n class=\"empty-state-demo\"\n data-controller=\"stimeo--empty-state\"\n data-stimeo--empty-state-announce-value=\"true\">\n <div class=\"empty-state-demo__bar\">\n <button\n type=\"button\"\n class=\"demo-trigger\"\n data-empty-state-add\n data-item-label=\"<%= t(\"components.empty_state.demo.item\") %>\">\n <%= t(\"components.empty_state.demo.add\") %>\n </button>\n <button type=\"button\" class=\"demo-trigger\" data-empty-state-clear>\n <%= t(\"components.empty_state.demo.clear\") %>\n </button>\n </div>\n <ul class=\"empty-state-demo__list\" data-stimeo--empty-state-target=\"list\">\n <li><%= t(\"components.empty_state.demo.item\") %> 1</li>\n <li><%= t(\"components.empty_state.demo.item\") %> 2</li>\n </ul>\n <p class=\"empty-state-demo__empty\" data-stimeo--empty-state-target=\"empty\" hidden>\n <%= t(\"components.empty_state.demo.empty\") %>\n </p>\n</div>\n"
138
+ "source": "<%# Empty-state demo: add/remove items and the empty placeholder toggles itself. The\n controller observes the list with a MutationObserver, toggles hidden on the\n list / empty targets at the 0 <-> 1+ boundary, and reflects data-empty / data-count.\n demo.js adds/removes <li> (standing in for Turbo Stream rows). This demo only styles\n it; the empty placeholder's copy is owned here, and the crossing is read out through\n the shared announcer at the bottom. %>\n<div\n class=\"empty-state-demo\"\n data-controller=\"stimeo--empty-state\"\n data-stimeo--empty-state-announce-text-value=\"<%=\n t(\"components.empty_state.demo.announce_empty\") %>\"\n data-stimeo--empty-state-announce-filled-text-value=\"<%=\n t(\"components.empty_state.demo.announce_filled\") %>\">\n <div class=\"empty-state-demo__bar\">\n <button\n type=\"button\"\n class=\"demo-trigger\"\n data-empty-state-add\n data-item-label=\"<%= t(\"components.empty_state.demo.item\") %>\">\n <%= t(\"components.empty_state.demo.add\") %>\n </button>\n <button type=\"button\" class=\"demo-trigger\" data-empty-state-clear>\n <%= t(\"components.empty_state.demo.clear\") %>\n </button>\n </div>\n <ul class=\"empty-state-demo__list\" data-stimeo--empty-state-target=\"list\">\n <li><%= t(\"components.empty_state.demo.item\") %> 1</li>\n <li><%= t(\"components.empty_state.demo.item\") %> 2</li>\n </ul>\n <p class=\"empty-state-demo__empty\" data-stimeo--empty-state-target=\"empty\" hidden>\n <%= t(\"components.empty_state.demo.empty\") %>\n </p>\n\n <%# Shared announcer: the crossing is read out without moving focus. A placeholder\n that only becomes visible at the moment of the change is not reliably read, so the\n wording goes to a region that stands in the page independently of it. %>\n <div data-controller=\"stimeo--announcer\">\n <div class=\"visually-hidden\"\n data-stimeo--announcer-target=\"polite\" aria-live=\"polite\" aria-atomic=\"true\"></div>\n </div>\n</div>\n"
139
139
  },
140
140
  "stimeo--file-dropzone": {
141
141
  "file": "examples/file_dropzone/_demo.html.erb",
@@ -163,7 +163,7 @@
163
163
  },
164
164
  "stimeo--frame-loading": {
165
165
  "file": "examples/frame_loading/_demo.html.erb",
166
- "source": "<%# Frame-loading demo: the controller subscribes to a turbo-frame's fetch lifecycle\n and toggles aria-busy / data-frame-loading, the skeleton, content inert, and focus.\n This catalog has no Turbo backend, so demo.js fires the turbo:before-fetch-request →\n turbo:frame-load pair to reproduce a frame fetch. The library only toggles state and\n hidden; demo.css owns the skeleton bars and the dimmed content. %>\n<div class=\"frame-demo\">\n <button type=\"button\" class=\"demo-trigger\" data-frame-demo-reload>\n <%= t(\"components.frame_loading.demo.reload\") %>\n </button>\n <div\n class=\"frame-demo__frame\"\n data-controller=\"stimeo--frame-loading\"\n data-stimeo--frame-loading-min-duration-value=\"600\">\n <div class=\"frame-demo__skeleton\" data-stimeo--frame-loading-target=\"skeleton\" hidden>\n <span class=\"visually-hidden\"><%= t(\"components.frame_loading.demo.loading\") %></span>\n <span class=\"frame-demo__bar\"></span>\n <span class=\"frame-demo__bar\"></span>\n <span class=\"frame-demo__bar\"></span>\n </div>\n <div class=\"frame-demo__content\" data-stimeo--frame-loading-target=\"content\">\n <p><%= t(\"components.frame_loading.demo.content\") %></p>\n <button\n type=\"button\"\n class=\"demo-trigger\"\n data-frame-demo-action\n data-result-template=\"<%= t(\"components.frame_loading.demo.action_result\") %>\">\n <%= t(\"components.frame_loading.demo.action\") %>\n </button>\n <span class=\"frame-demo__result\" data-frame-demo-result role=\"status\"></span>\n </div>\n </div>\n</div>\n"
166
+ "source": "<%# Frame-loading demo: the controller subscribes to a turbo-frame's fetch lifecycle\n and toggles aria-busy / data-frame-loading, the skeleton, content inert, and focus.\n This catalog has no Turbo backend, so demo.js fires the turbo:before-fetch-request →\n turbo:frame-load pair to reproduce a frame fetch. The library only toggles state and\n hidden; demo.css owns the skeleton bars and the dimmed content. %>\n<div class=\"frame-demo\">\n <button type=\"button\" class=\"demo-trigger\" data-frame-demo-reload>\n <%= t(\"components.frame_loading.demo.reload\") %>\n </button>\n <div\n class=\"frame-demo__frame\"\n data-controller=\"stimeo--frame-loading\"\n data-stimeo--frame-loading-min-duration-value=\"600\"\n data-stimeo--frame-loading-announce-text-value=\"<%=\n t(\"components.frame_loading.demo.announce_loading\") %>\"\n data-stimeo--frame-loading-announce-ready-text-value=\"<%=\n t(\"components.frame_loading.demo.announce_ready\") %>\">\n <div class=\"frame-demo__skeleton\" data-stimeo--frame-loading-target=\"skeleton\" hidden>\n <span class=\"visually-hidden\"><%= t(\"components.frame_loading.demo.loading\") %></span>\n <span class=\"frame-demo__bar\"></span>\n <span class=\"frame-demo__bar\"></span>\n <span class=\"frame-demo__bar\"></span>\n </div>\n <div class=\"frame-demo__content\" data-stimeo--frame-loading-target=\"content\">\n <p><%= t(\"components.frame_loading.demo.content\") %></p>\n <button\n type=\"button\"\n class=\"demo-trigger\"\n data-frame-demo-action\n data-result-template=\"<%= t(\"components.frame_loading.demo.action_result\") %>\">\n <%= t(\"components.frame_loading.demo.action\") %>\n </button>\n <span class=\"frame-demo__result\" data-frame-demo-result role=\"status\"></span>\n </div>\n </div>\n\n <%# Shared announcer: the frame's own skeleton is revealed at the moment of the\n change, which is not reliably read, so the loading / ready wording goes here. %>\n <div data-controller=\"stimeo--announcer\">\n <div class=\"visually-hidden\"\n data-stimeo--announcer-target=\"polite\" aria-live=\"polite\" aria-atomic=\"true\"></div>\n </div>\n</div>\n"
167
167
  },
168
168
  "stimeo--highlight": {
169
169
  "file": "examples/highlight/_demo.html.erb",
@@ -203,7 +203,7 @@
203
203
  },
204
204
  "stimeo--masonry": {
205
205
  "file": "examples/masonry/_demo.html.erb",
206
- "source": "<%# Markup for the masonry (tiled grid) demo.\n The library computes the column count from the container width and min-column-width,\n and updates --stimeo-masonry-columns and each item's data-column. The look (CSS\n multi-column) is the consumer's CSS; reading/focus order stays in DOM order. %>\n<div\n class=\"masonry-demo\"\n data-controller=\"stimeo--masonry\"\n data-stimeo--masonry-min-column-width-value=\"180\"\n data-stimeo--masonry-gap-value=\"12\">\n <div class=\"masonry-demo__item\" data-stimeo--masonry-target=\"item\">\n <h3 class=\"masonry-demo__title\"><%= t(\"components.masonry.demo.cards.short.title\") %></h3>\n <p><%= t(\"components.masonry.demo.cards.short.body\") %></p>\n </div>\n <div class=\"masonry-demo__item\" data-stimeo--masonry-target=\"item\">\n <h3 class=\"masonry-demo__title\"><%= t(\"components.masonry.demo.cards.tall.title\") %></h3>\n <p><%= t(\"components.masonry.demo.cards.tall.body\") %></p>\n <p><%= t(\"components.masonry.demo.cards.tall.body\") %></p>\n </div>\n <div class=\"masonry-demo__item\" data-stimeo--masonry-target=\"item\">\n <h3 class=\"masonry-demo__title\"><%= t(\"components.masonry.demo.cards.medium.title\") %></h3>\n <p><%= t(\"components.masonry.demo.cards.medium.body\") %></p>\n </div>\n <div class=\"masonry-demo__item\" data-stimeo--masonry-target=\"item\">\n <h3 class=\"masonry-demo__title\"><%= t(\"components.masonry.demo.cards.short.title\") %></h3>\n <p><%= t(\"components.masonry.demo.cards.short.body\") %></p>\n </div>\n <div class=\"masonry-demo__item\" data-stimeo--masonry-target=\"item\">\n <h3 class=\"masonry-demo__title\"><%= t(\"components.masonry.demo.cards.tall.title\") %></h3>\n <p><%= t(\"components.masonry.demo.cards.tall.body\") %></p>\n <p><%= t(\"components.masonry.demo.cards.medium.body\") %></p>\n </div>\n <div class=\"masonry-demo__item\" data-stimeo--masonry-target=\"item\">\n <h3 class=\"masonry-demo__title\"><%= t(\"components.masonry.demo.cards.medium.title\") %></h3>\n <p><%= t(\"components.masonry.demo.cards.medium.body\") %></p>\n </div>\n</div>\n<p\n class=\"masonry-demo__status\"\n data-masonry-status\n data-template=\"<%= t(\"components.masonry.demo.status_template\") %>\"\n aria-live=\"polite\"></p>\n"
206
+ "source": "<%# Markup for the masonry (tiled grid) demo.\n The library computes the column count from the container width and min-column-width,\n and updates --stimeo--masonry-columns and each item's data-column. The look (CSS\n multi-column) is the consumer's CSS; reading/focus order stays in DOM order. %>\n<div\n class=\"masonry-demo\"\n data-controller=\"stimeo--masonry\"\n data-stimeo--masonry-min-column-width-value=\"180\"\n data-stimeo--masonry-gap-value=\"12\">\n <div class=\"masonry-demo__item\" data-stimeo--masonry-target=\"item\">\n <h3 class=\"masonry-demo__title\"><%= t(\"components.masonry.demo.cards.short.title\") %></h3>\n <p><%= t(\"components.masonry.demo.cards.short.body\") %></p>\n </div>\n <div class=\"masonry-demo__item\" data-stimeo--masonry-target=\"item\">\n <h3 class=\"masonry-demo__title\"><%= t(\"components.masonry.demo.cards.tall.title\") %></h3>\n <p><%= t(\"components.masonry.demo.cards.tall.body\") %></p>\n <p><%= t(\"components.masonry.demo.cards.tall.body\") %></p>\n </div>\n <div class=\"masonry-demo__item\" data-stimeo--masonry-target=\"item\">\n <h3 class=\"masonry-demo__title\"><%= t(\"components.masonry.demo.cards.medium.title\") %></h3>\n <p><%= t(\"components.masonry.demo.cards.medium.body\") %></p>\n </div>\n <div class=\"masonry-demo__item\" data-stimeo--masonry-target=\"item\">\n <h3 class=\"masonry-demo__title\"><%= t(\"components.masonry.demo.cards.short.title\") %></h3>\n <p><%= t(\"components.masonry.demo.cards.short.body\") %></p>\n </div>\n <div class=\"masonry-demo__item\" data-stimeo--masonry-target=\"item\">\n <h3 class=\"masonry-demo__title\"><%= t(\"components.masonry.demo.cards.tall.title\") %></h3>\n <p><%= t(\"components.masonry.demo.cards.tall.body\") %></p>\n <p><%= t(\"components.masonry.demo.cards.medium.body\") %></p>\n </div>\n <div class=\"masonry-demo__item\" data-stimeo--masonry-target=\"item\">\n <h3 class=\"masonry-demo__title\"><%= t(\"components.masonry.demo.cards.medium.title\") %></h3>\n <p><%= t(\"components.masonry.demo.cards.medium.body\") %></p>\n </div>\n</div>\n<p\n class=\"masonry-demo__status\"\n data-masonry-status\n data-template=\"<%= t(\"components.masonry.demo.status_template\") %>\"\n aria-live=\"polite\"></p>\n"
207
207
  },
208
208
  "stimeo--menu": {
209
209
  "file": "examples/menu/_demo.html.erb",
@@ -215,7 +215,7 @@
215
215
  },
216
216
  "stimeo--meter": {
217
217
  "file": "examples/meter/_demo.html.erb",
218
- "source": "<%# Markup for the meter demo.\n Syncs role=\"meter\" and aria-value*, and classifies data-state into low/medium/high\n via the low/high thresholds. demo.css applies color based on data-state.\n aria-valuetext is generated by the library from the {percent}/{state} template.\n Like a progressbar, a meter must not contain interactive children, so the preset\n buttons sit beside it and update it through the meter:set CustomEvent the\n controller listens for (see demo.js) rather than a Stimulus data-action, which\n only binds within the controller's own element. %>\n<div class=\"meter-demo\">\n <div\n class=\"meter\"\n data-controller=\"stimeo--meter\"\n role=\"meter\"\n aria-label=\"<%= t('components.meter.demo.label') %>\"\n aria-valuemin=\"0\"\n aria-valuemax=\"100\"\n aria-valuenow=\"72\"\n data-stimeo--meter-value-value=\"72\"\n data-stimeo--meter-low-value=\"50\"\n data-stimeo--meter-high-value=\"80\"\n data-stimeo--meter-value-text-value=\"<%= t('components.meter.demo.value_text') %>\"\n data-action=\"meter:set->stimeo--meter#setValue\">\n <div class=\"meter__bar\" data-stimeo--meter-target=\"bar\"></div>\n </div>\n\n <div class=\"meter-demo__controls\" role=\"group\"\n aria-label=\"<%= t('components.meter.demo.controls_label') %>\">\n <% [20, 65, 90].each do |amount| %>\n <button\n class=\"demo-trigger\"\n type=\"button\"\n data-meter-amount=\"<%= amount %>\">\n <%= amount %>\n </button>\n <% end %>\n </div>\n</div>\n"
218
+ "source": "<%# Markup for the meter demo.\n Syncs role=\"meter\" and aria-value*, and classifies data-state into low/medium/high\n via the low/high thresholds. demo.css applies color based on data-state.\n aria-valuetext is generated by the library from the {percent}/{state} template.\n Like a progressbar, a meter must not contain interactive children, so the preset\n buttons sit beside it and update it through the meter:set CustomEvent the\n controller listens for (see demo.js) rather than a Stimulus data-action, which\n only binds within the controller's own element. %>\n<div class=\"meter-demo\">\n <div\n class=\"meter\"\n data-controller=\"stimeo--meter\"\n role=\"meter\"\n aria-label=\"<%= t('components.meter.demo.label') %>\"\n aria-valuemin=\"0\"\n aria-valuemax=\"100\"\n aria-valuenow=\"72\"\n data-stimeo--meter-value-value=\"72\"\n data-stimeo--meter-low-value=\"50\"\n data-stimeo--meter-high-value=\"80\"\n data-stimeo--meter-value-text-value=\"<%= t('components.meter.demo.value_text') %>\"\n data-stimeo--meter-announce-text-value=\"<%= t('components.meter.demo.announce_state') %>\"\n data-action=\"meter:set->stimeo--meter#setValue\">\n <div class=\"meter__bar\" data-stimeo--meter-target=\"bar\"></div>\n </div>\n\n <div class=\"meter-demo__controls\" role=\"group\"\n aria-label=\"<%= t('components.meter.demo.controls_label') %>\">\n <% [20, 65, 90].each do |amount| %>\n <button\n class=\"demo-trigger\"\n type=\"button\"\n data-meter-amount=\"<%= amount %>\">\n <%= amount %>\n </button>\n <% end %>\n </div>\n\n <%# Shared announcer: reading every value would be unusable, so only a move into\n another segment is read out. %>\n <div data-controller=\"stimeo--announcer\">\n <div class=\"visually-hidden\"\n data-stimeo--announcer-target=\"polite\" aria-live=\"polite\" aria-atomic=\"true\"></div>\n </div>\n</div>\n"
219
219
  },
220
220
  "stimeo--multi-select": {
221
221
  "file": "examples/multi_select/_demo.html.erb",
@@ -231,7 +231,7 @@
231
231
  },
232
232
  "stimeo--network-status": {
233
233
  "file": "examples/network_status/_demo.html.erb",
234
- "source": "<%# Markup for the network-status (offline-detection banner) demo.\n The library checks navigator.onLine initially and subscribes to the window's\n online/offline events, toggling the corresponding banner's hidden. Offline uses\n role=\"alert\" (assertive); coming back online uses role=\"status\" (polite). Since real\n online/offline is environment-dependent, the demo's demo.js fakes the window events\n to reproduce the behavior. %>\n<div class=\"network-demo\">\n <div class=\"network-demo__controls\">\n <button class=\"demo-trigger\" type=\"button\" data-network-demo=\"offline\">\n <%= t(\"components.network_status.demo.go_offline\") %>\n </button>\n <button class=\"demo-trigger\" type=\"button\" data-network-demo=\"online\">\n <%= t(\"components.network_status.demo.go_online\") %>\n </button>\n </div>\n\n <div data-controller=\"stimeo--network-status\"\n data-stimeo--network-status-online-auto-hide-value=\"3000\">\n <div\n class=\"network-banner network-banner--offline\"\n role=\"alert\"\n hidden\n data-stimeo--network-status-target=\"offline\">\n <%= t(\"components.network_status.demo.offline_message\") %>\n </div>\n <div\n class=\"network-banner network-banner--online\"\n role=\"status\"\n hidden\n data-stimeo--network-status-target=\"online\">\n <%= t(\"components.network_status.demo.online_message\") %>\n </div>\n </div>\n\n <p class=\"network-demo__note\"><%= t(\"components.network_status.demo.note\") %></p>\n</div>\n"
234
+ "source": "<%# Markup for the network-status (offline-detection banner) demo.\n The library checks navigator.onLine initially and subscribes to the window's\n online/offline events, toggling the corresponding banner's hidden. The banners are\n the visual half only: a region revealed at the moment of the change is not reliably\n read, so the wording goes to the shared announcer instead — assertively when the\n connection drops, politely when it comes back. Since real online/offline is\n environment-dependent, the demo's demo.js fakes the window events to reproduce the\n behavior. %>\n<div class=\"network-demo\">\n <div class=\"network-demo__controls\">\n <button class=\"demo-trigger\" type=\"button\" data-network-demo=\"offline\">\n <%= t(\"components.network_status.demo.go_offline\") %>\n </button>\n <button class=\"demo-trigger\" type=\"button\" data-network-demo=\"online\">\n <%= t(\"components.network_status.demo.go_online\") %>\n </button>\n </div>\n\n <div data-controller=\"stimeo--network-status\"\n data-stimeo--network-status-online-auto-hide-value=\"3000\"\n data-stimeo--network-status-announce-text-value=\"<%=\n t(\"components.network_status.demo.offline_message\") %>\"\n data-stimeo--network-status-announce-online-text-value=\"<%=\n t(\"components.network_status.demo.online_message\") %>\">\n <div\n class=\"network-banner network-banner--offline\"\n hidden\n data-stimeo--network-status-target=\"offline\">\n <%= t(\"components.network_status.demo.offline_message\") %>\n </div>\n <div\n class=\"network-banner network-banner--online\"\n hidden\n data-stimeo--network-status-target=\"online\">\n <%= t(\"components.network_status.demo.online_message\") %>\n </div>\n </div>\n\n <%# Shared announcer: losing the connection interrupts (assertive), regaining it\n waits its turn (polite). %>\n <div data-controller=\"stimeo--announcer\">\n <div class=\"visually-hidden\"\n data-stimeo--announcer-target=\"polite\" aria-live=\"polite\" aria-atomic=\"true\"></div>\n <div class=\"visually-hidden\"\n data-stimeo--announcer-target=\"assertive\" aria-live=\"assertive\" aria-atomic=\"true\"></div>\n </div>\n\n <p class=\"network-demo__note\"><%= t(\"components.network_status.demo.note\") %></p>\n</div>\n"
235
235
  },
236
236
  "stimeo--number-input": {
237
237
  "file": "examples/number_input/_demo.html.erb",
@@ -263,7 +263,7 @@
263
263
  },
264
264
  "stimeo--password-strength": {
265
265
  "file": "examples/password_strength/_demo.html.erb",
266
- "source": "<%# Markup for the password-strength demo.\n The controller scores the field with a lightweight zero-dependency heuristic and\n drives the meter (aria-valuenow), the data-strength token, and the 0–1 fill on the\n --stimeo-password-strength custom property. The input sits inside the controller so\n its data-action input->...#evaluate binds directly. Level labels are localized via\n the levels value; demo.css colors the bar by the locale-independent data-strength band,\n so the look never depends on the (translated) label text. The library writes no CSS. %>\n<% levels = t(\"components.password_strength.demo.levels\").to_json %>\n<div\n class=\"password-strength\"\n data-controller=\"stimeo--password-strength\"\n data-stimeo--password-strength-levels-value=\"<%= levels %>\">\n <label class=\"password-strength__label\" for=\"password-strength-demo-input\">\n <%= t(\"components.password_strength.demo.label\") %>\n </label>\n <input\n class=\"password-strength__input\"\n id=\"password-strength-demo-input\"\n type=\"password\"\n autocomplete=\"new-password\"\n aria-describedby=\"password-strength-demo-readout\"\n data-stimeo--password-strength-target=\"input\"\n data-action=\"input->stimeo--password-strength#evaluate\">\n <div\n class=\"password-strength__meter\"\n role=\"meter\"\n aria-valuemin=\"0\"\n aria-valuemax=\"4\"\n aria-valuenow=\"0\"\n aria-label=\"<%= t('components.password_strength.demo.meter_label') %>\"\n data-stimeo--password-strength-target=\"meter\">\n <span class=\"password-strength__bar\"></span>\n </div>\n <span\n class=\"password-strength__readout\"\n id=\"password-strength-demo-readout\"\n aria-live=\"polite\"\n data-stimeo--password-strength-target=\"label\"></span>\n</div>\n<p class=\"password-strength__hint\"><%= t(\"components.password_strength.demo.hint\") %></p>\n"
266
+ "source": "<%# Markup for the password-strength demo.\n The controller scores the field with a lightweight zero-dependency heuristic and\n drives the meter (aria-valuenow), the data-strength token, and the 0–1 fill on the\n --stimeo--password-strength custom property. The input sits inside the controller so\n its data-action input->...#evaluate binds directly. Level labels are localized via\n the levels value; demo.css colors the bar by the locale-independent data-strength band,\n so the look never depends on the (translated) label text. The library writes no CSS. %>\n<% levels = t(\"components.password_strength.demo.levels\").to_json %>\n<div\n class=\"password-strength\"\n data-controller=\"stimeo--password-strength\"\n data-stimeo--password-strength-levels-value=\"<%= levels %>\">\n <label class=\"password-strength__label\" for=\"password-strength-demo-input\">\n <%= t(\"components.password_strength.demo.label\") %>\n </label>\n <input\n class=\"password-strength__input\"\n id=\"password-strength-demo-input\"\n type=\"password\"\n autocomplete=\"new-password\"\n aria-describedby=\"password-strength-demo-readout\"\n data-stimeo--password-strength-target=\"input\"\n data-action=\"input->stimeo--password-strength#evaluate\">\n <div\n class=\"password-strength__meter\"\n role=\"meter\"\n aria-valuemin=\"0\"\n aria-valuemax=\"4\"\n aria-valuenow=\"0\"\n aria-label=\"<%= t('components.password_strength.demo.meter_label') %>\"\n data-stimeo--password-strength-target=\"meter\">\n <span class=\"password-strength__bar\"></span>\n </div>\n <span\n class=\"password-strength__readout\"\n id=\"password-strength-demo-readout\"\n aria-live=\"polite\"\n data-stimeo--password-strength-target=\"label\"></span>\n</div>\n<p class=\"password-strength__hint\"><%= t(\"components.password_strength.demo.hint\") %></p>\n"
267
267
  },
268
268
  "stimeo--persist": {
269
269
  "file": "examples/persist/_demo.html.erb",
@@ -291,7 +291,7 @@
291
291
  },
292
292
  "stimeo--progress": {
293
293
  "file": "examples/progress/_demo.html.erb",
294
- "source": "<%# Markup for the progress (progress bar) demo.\n The library syncs role=\"progressbar\" and aria-value*, and exposes the ratio as\n --stimeo-progress-ratio (0–1). The bar's width and color live in demo.css.\n The control buttons sit outside the progressbar (a progressbar must not contain\n interactive children), so they can't drive it through a Stimulus data-action —\n that only binds inside the controller's own element. Instead they dispatch the\n progress:set CustomEvent the controller listens for (see demo.js), the same\n contract a consumer would use to update progress from elsewhere. %>\n<div class=\"progress-demo\">\n <div\n class=\"progress\"\n data-controller=\"stimeo--progress\"\n role=\"progressbar\"\n aria-label=\"<%= t('components.progress.demo.label') %>\"\n aria-valuemin=\"0\"\n aria-valuemax=\"100\"\n aria-valuenow=\"40\"\n data-stimeo--progress-value-value=\"40\"\n data-stimeo--progress-value-text-value=\"<%= t('components.progress.demo.value_text') %>\"\n data-action=\"progress:set->stimeo--progress#setValue\">\n <div class=\"progress__bar\" data-stimeo--progress-target=\"bar\"></div>\n </div>\n\n <div\n class=\"progress-demo__controls\"\n role=\"group\"\n aria-label=\"<%= t('components.progress.demo.controls_label') %>\">\n <% [0, 25, 50, 75, 100].each do |amount| %>\n <button\n class=\"demo-trigger\"\n type=\"button\"\n data-progress-amount=\"<%= amount %>\">\n <%= amount %>%\n </button>\n <% end %>\n </div>\n\n <%# Indeterminate example: drop aria-valuenow and let the consumer apply a looping\n animation via data-state=\"indeterminate\". %>\n <div\n class=\"progress progress--indeterminate\"\n data-controller=\"stimeo--progress\"\n role=\"progressbar\"\n aria-label=\"<%= t('components.progress.demo.indeterminate_label') %>\"\n aria-valuemin=\"0\"\n aria-valuemax=\"100\"\n data-stimeo--progress-indeterminate-value=\"true\">\n <div class=\"progress__bar\" data-stimeo--progress-target=\"bar\"></div>\n </div>\n</div>\n"
294
+ "source": "<%# Markup for the progress (progress bar) demo.\n The library syncs role=\"progressbar\" and aria-value*, and exposes the ratio as\n --stimeo--progress-ratio (0–1). The bar's width and color live in demo.css.\n The control buttons sit outside the progressbar (a progressbar must not contain\n interactive children), so they can't drive it through a Stimulus data-action —\n that only binds inside the controller's own element. Instead they dispatch the\n progress:set CustomEvent the controller listens for (see demo.js), the same\n contract a consumer would use to update progress from elsewhere. %>\n<div class=\"progress-demo\">\n <div\n class=\"progress\"\n data-controller=\"stimeo--progress\"\n role=\"progressbar\"\n aria-label=\"<%= t('components.progress.demo.label') %>\"\n aria-valuemin=\"0\"\n aria-valuemax=\"100\"\n aria-valuenow=\"40\"\n data-stimeo--progress-value-value=\"40\"\n data-stimeo--progress-value-text-value=\"<%= t('components.progress.demo.value_text') %>\"\n data-stimeo--progress-announce-text-value=\"<%=\n t('components.progress.demo.announce_complete') %>\"\n data-action=\"progress:set->stimeo--progress#setValue\">\n <div class=\"progress__bar\" data-stimeo--progress-target=\"bar\"></div>\n </div>\n\n <div\n class=\"progress-demo__controls\"\n role=\"group\"\n aria-label=\"<%= t('components.progress.demo.controls_label') %>\">\n <% [0, 25, 50, 75, 100].each do |amount| %>\n <button\n class=\"demo-trigger\"\n type=\"button\"\n data-progress-amount=\"<%= amount %>\">\n <%= amount %>%\n </button>\n <% end %>\n </div>\n\n <%# Indeterminate example: drop aria-valuenow and let the consumer apply a looping\n animation via data-state=\"indeterminate\". %>\n <div\n class=\"progress progress--indeterminate\"\n data-controller=\"stimeo--progress\"\n role=\"progressbar\"\n aria-label=\"<%= t('components.progress.demo.indeterminate_label') %>\"\n aria-valuemin=\"0\"\n aria-valuemax=\"100\"\n data-stimeo--progress-indeterminate-value=\"true\">\n <div class=\"progress__bar\" data-stimeo--progress-target=\"bar\"></div>\n </div>\n\n <%# Shared announcer: aria-valuenow is state a reader can query, not a notification.\n Only reaching the end is news, so that is the one thing read out. %>\n <div data-controller=\"stimeo--announcer\">\n <div class=\"visually-hidden\"\n data-stimeo--announcer-target=\"polite\" aria-live=\"polite\" aria-atomic=\"true\"></div>\n </div>\n</div>\n"
295
295
  },
296
296
  "stimeo--radio-group": {
297
297
  "file": "examples/radio_group/_demo.html.erb",
@@ -299,7 +299,7 @@
299
299
  },
300
300
  "stimeo--range-slider": {
301
301
  "file": "examples/range_slider/_demo.html.erb",
302
- "source": "<%# Markup for the range-slider (APG Slider, multi-thumb) demo.\n stimeo--range-slider handles the two thumbs' (start / end) mutual constraint\n (lower ≤ upper), keyboard / drag operation, and dynamic updates of each thumb's\n aria-valuemin/max/now, and exposes the range as the root CSS variables\n --stimeo-range-start / --stimeo-range-end (0..1). The thumb and range placement\n is the Playground's CSS using those variables. %>\n<div\n class=\"range-slider\"\n data-controller=\"stimeo--range-slider\"\n data-stimeo--range-slider-min-value=\"0\"\n data-stimeo--range-slider-max-value=\"100\"\n data-stimeo--range-slider-step-value=\"1\"\n data-stimeo--range-slider-start-value=\"20\"\n data-stimeo--range-slider-end-value=\"80\">\n <div class=\"range-slider__track\" data-stimeo--range-slider-target=\"track\"\n data-action=\"pointerdown->stimeo--range-slider#onPointerDown\">\n <div class=\"range-slider__range\" aria-hidden=\"true\"></div>\n <div class=\"range-slider__thumb\" role=\"slider\" tabindex=\"0\"\n aria-label=\"<%= t(\"components.range_slider.demo.min_label\") %>\"\n data-stimeo--range-slider-target=\"startThumb\"\n data-action=\"keydown->stimeo--range-slider#onKeydown\"></div>\n <div class=\"range-slider__thumb\" role=\"slider\" tabindex=\"0\"\n aria-label=\"<%= t(\"components.range_slider.demo.max_label\") %>\"\n data-stimeo--range-slider-target=\"endThumb\"\n data-action=\"keydown->stimeo--range-slider#onKeydown\"></div>\n </div>\n <output class=\"range-slider__output\" data-range-slider-output aria-live=\"polite\">20 – 80</output>\n</div>\n"
302
+ "source": "<%# Markup for the range-slider (APG Slider, multi-thumb) demo.\n stimeo--range-slider handles the two thumbs' (start / end) mutual constraint\n (lower ≤ upper), keyboard / drag operation, and dynamic updates of each thumb's\n aria-valuemin/max/now, and exposes the range as the root CSS variables\n --stimeo--range-slider-start / --stimeo--range-slider-end (0..1). The thumb\n and range placement is the Playground's CSS using those variables. %>\n<div\n class=\"range-slider\"\n data-controller=\"stimeo--range-slider\"\n data-stimeo--range-slider-min-value=\"0\"\n data-stimeo--range-slider-max-value=\"100\"\n data-stimeo--range-slider-step-value=\"1\"\n data-stimeo--range-slider-start-value=\"20\"\n data-stimeo--range-slider-end-value=\"80\">\n <div class=\"range-slider__track\" data-stimeo--range-slider-target=\"track\"\n data-action=\"pointerdown->stimeo--range-slider#onPointerDown\">\n <div class=\"range-slider__range\" aria-hidden=\"true\"></div>\n <div class=\"range-slider__thumb\" role=\"slider\" tabindex=\"0\"\n aria-label=\"<%= t(\"components.range_slider.demo.min_label\") %>\"\n data-stimeo--range-slider-target=\"startThumb\"\n data-action=\"keydown->stimeo--range-slider#onKeydown\"></div>\n <div class=\"range-slider__thumb\" role=\"slider\" tabindex=\"0\"\n aria-label=\"<%= t(\"components.range_slider.demo.max_label\") %>\"\n data-stimeo--range-slider-target=\"endThumb\"\n data-action=\"keydown->stimeo--range-slider#onKeydown\"></div>\n </div>\n <output class=\"range-slider__output\" data-range-slider-output aria-live=\"polite\">20 – 80</output>\n</div>\n"
303
303
  },
304
304
  "stimeo--rating": {
305
305
  "file": "examples/rating/_demo.html.erb",
@@ -311,7 +311,7 @@
311
311
  },
312
312
  "stimeo--reading-progress": {
313
313
  "file": "examples/reading_progress/_demo.html.erb",
314
- "source": "<%# reading-progress: the article publishes its progress as a CSS custom property\n on itself AND on :root; the bar here consumes it with pure CSS (no consumer\n JS). Scroll the page to move the bar. %>\n<div class=\"reading-progress-demo\">\n <p class=\"reading-progress-demo__hint\"><%= t(\"components.reading_progress.demo.hint\") %></p>\n <div class=\"reading-progress-demo__bar\" aria-hidden=\"true\"></div>\n <article class=\"reading-progress-demo__article\" data-controller=\"stimeo--reading-progress\">\n <% 14.times do |i| %>\n <p><%= t(\"components.reading_progress.demo.paragraph\", index: i + 1) %></p>\n <% end %>\n </article>\n</div>\n"
314
+ "source": "<%# reading-progress: the article publishes its progress as a CSS custom property\n on itself AND on :root; the bar here consumes it with pure CSS (no consumer\n JS). Scroll the page to move the bar. %>\n<div class=\"reading-progress-demo\">\n <p class=\"reading-progress-demo__hint\"><%= t(\"components.reading_progress.demo.hint\") %></p>\n <div class=\"reading-progress-demo__bar\" aria-hidden=\"true\"></div>\n <article class=\"reading-progress-demo__article\" data-controller=\"stimeo--reading-progress\">\n <% 28.times do |i| %>\n <p><%= t(\"components.reading_progress.demo.paragraph\", index: i + 1) %></p>\n <% end %>\n </article>\n</div>\n"
315
315
  },
316
316
  "stimeo--relative-time": {
317
317
  "file": "examples/relative_time/_demo.html.erb",
@@ -331,7 +331,7 @@
331
331
  },
332
332
  "stimeo--scroll-area": {
333
333
  "file": "examples/scroll_area/_demo.html.erb",
334
- "source": "<%# Markup for the scroll-area demo.\n The consumer (this markup) puts aria-label on the viewport. On overflow the library\n adds tabindex to the viewport and (only when it has an accessible name) role=\"region\",\n and exposes data-scroll (start/middle/end) and --stimeo-scroll-progress.\n Drawing the scroll shadow is demo.css's job, reading data-scroll. %>\n<div class=\"scroll-area\" data-controller=\"stimeo--scroll-area\"\n data-stimeo--scroll-area-orientation-value=\"vertical\">\n <div class=\"scroll-area__viewport\"\n data-stimeo--scroll-area-target=\"viewport\"\n aria-label=\"<%= t(\"components.scroll_area.demo.label\") %>\">\n <p><%= t(\"components.scroll_area.demo.intro\") %></p>\n <% 12.times do |i| %>\n <p class=\"scroll-area__line\"><%= t(\"components.scroll_area.demo.line\", number: i + 1) %></p>\n <% end %>\n </div>\n</div>\n"
334
+ "source": "<%# Markup for the scroll-area demo.\n The consumer (this markup) puts aria-label on the viewport. On overflow the library\n adds tabindex to the viewport and (only when it has an accessible name) role=\"region\",\n and exposes data-scroll (start/middle/end) and --stimeo--scroll-progress.\n Drawing the scroll shadow is demo.css's job, reading data-scroll. %>\n<div class=\"scroll-area\" data-controller=\"stimeo--scroll-area\"\n data-stimeo--scroll-area-orientation-value=\"vertical\">\n <div class=\"scroll-area__viewport\"\n data-stimeo--scroll-area-target=\"viewport\"\n aria-label=\"<%= t(\"components.scroll_area.demo.label\") %>\">\n <p><%= t(\"components.scroll_area.demo.intro\") %></p>\n <% 12.times do |i| %>\n <p class=\"scroll-area__line\"><%= t(\"components.scroll_area.demo.line\", number: i + 1) %></p>\n <% end %>\n </div>\n</div>\n"
335
335
  },
336
336
  "stimeo--scroll-restore": {
337
337
  "file": "examples/scroll_restore/_demo.html.erb",
@@ -351,11 +351,11 @@
351
351
  },
352
352
  "stimeo--sidebar": {
353
353
  "file": "examples/sidebar/_demo.html.erb",
354
- "source": "<%# Markup for the sidebar (collapsible sidebar) demo.\n This demo focuses on the INLINE rail: the toggle collapses / expands it and the\n preference persists in localStorage across reloads. (The same sidebar can also\n switch to a modal off-canvas panel below a breakpoint — that document-level modal\n mode is described in the text and is the same off-canvas pattern the Drawer demo\n shows; it is not staged here because a page-level modal inside a catalog card reads\n as taking over the page.) breakpoint=0 keeps this demo always inline. The library\n syncs aria-expanded, data-mode, and data-state; the rail width / animation are\n demo.css's. role=\"dialog\" is intentionally not applied — the panel stays a nav. %>\n<div\n class=\"sidebar-demo\"\n data-controller=\"stimeo--sidebar\"\n data-action=\"turbo:before-cache@document->stimeo--sidebar#beforeCache\"\n data-stimeo--sidebar-breakpoint-value=\"0\"\n data-stimeo--sidebar-key-value=\"catalog-demo\">\n <div class=\"sidebar-demo__bar\">\n <button\n type=\"button\"\n class=\"sidebar-demo__toggle\"\n data-stimeo--sidebar-target=\"trigger\"\n data-action=\"click->stimeo--sidebar#toggle\"\n aria-expanded=\"true\"\n aria-controls=\"sidebar-demo-panel\">\n <%= t(\"components.sidebar.demo.toggle\") %>\n </button>\n <span class=\"sidebar-demo__brand\"><%= t(\"components.sidebar.demo.brand\") %></span>\n </div>\n\n <div class=\"sidebar-demo__body\">\n <nav\n id=\"sidebar-demo-panel\"\n class=\"sidebar-demo__panel\"\n data-stimeo--sidebar-target=\"panel\"\n aria-label=\"<%= t('components.sidebar.demo.nav_label') %>\"\n data-mode=\"inline\"\n data-state=\"expanded\">\n <% t(\"components.sidebar.demo.items\").each do |label| %>\n <a class=\"sidebar-demo__link\" href=\"#\"><%= label %></a>\n <% end %>\n </nav>\n\n <main class=\"sidebar-demo__content\">\n <p><%= t(\"components.sidebar.demo.hint\") %></p>\n </main>\n </div>\n</div>\n"
354
+ "source": "<%# Markup for the sidebar (collapsible sidebar) demo.\n This demo focuses on the INLINE rail: the toggle collapses / expands it and the\n preference persists in localStorage across reloads. (The same sidebar can also\n switch to a modal off-canvas panel below a breakpoint — that document-level modal\n mode is described in the text and is the same off-canvas pattern the Drawer demo\n shows; it is not staged here because a page-level modal inside a catalog card reads\n as taking over the page.) breakpoint=0 keeps this demo always inline. The library\n syncs aria-expanded, data-mode, and data-state; the rail width / animation are\n demo.css's. role=\"dialog\" is intentionally not applied — the panel stays a nav. %>\n<div\n class=\"sidebar-demo\"\n data-controller=\"stimeo--sidebar\"\n data-stimeo--sidebar-breakpoint-value=\"0\"\n data-stimeo--sidebar-key-value=\"catalog-demo\">\n <div class=\"sidebar-demo__bar\">\n <button\n type=\"button\"\n class=\"sidebar-demo__toggle\"\n data-stimeo--sidebar-target=\"trigger\"\n data-action=\"click->stimeo--sidebar#toggle\"\n aria-expanded=\"true\"\n aria-controls=\"sidebar-demo-panel\">\n <%= t(\"components.sidebar.demo.toggle\") %>\n </button>\n <span class=\"sidebar-demo__brand\"><%= t(\"components.sidebar.demo.brand\") %></span>\n </div>\n\n <div class=\"sidebar-demo__body\">\n <nav\n id=\"sidebar-demo-panel\"\n class=\"sidebar-demo__panel\"\n data-stimeo--sidebar-target=\"panel\"\n aria-label=\"<%= t('components.sidebar.demo.nav_label') %>\"\n data-mode=\"inline\"\n data-state=\"expanded\">\n <% t(\"components.sidebar.demo.items\").each do |label| %>\n <a class=\"sidebar-demo__link\" href=\"#\"><%= label %></a>\n <% end %>\n </nav>\n\n <main class=\"sidebar-demo__content\">\n <p><%= t(\"components.sidebar.demo.hint\") %></p>\n </main>\n </div>\n</div>\n"
355
355
  },
356
356
  "stimeo--skeleton": {
357
357
  "file": "examples/skeleton/_demo.html.erb",
358
- "source": "<%# Markup for the skeleton (skeleton toggle control) demo.\n Initially the skeleton is shown, the real content hidden, and aria-busy=\"true\". On\n ready it swaps and sets aria-busy=\"false\". The skeleton is decorative\n (aria-hidden=\"true\"). A minimum display time prevents flicker.\n The Load / Reset buttons sit outside the busy card (so aria-busy stays scoped to the\n loading region, not the controls), so they drive it through events the controller\n listens for (see demo.js): Load fires content:ready — the same signal a consumer\n dispatches when their real content arrives — and Reset returns it to loading. A\n Stimulus data-action on the buttons would not bind, since actions only wire up\n within the controller's own element. %>\n<div class=\"skeleton-demo\">\n <div class=\"skeleton-demo__controls\">\n <button\n class=\"demo-trigger\"\n type=\"button\"\n data-skeleton-load>\n <%= t(\"components.skeleton.demo.load\") %>\n </button>\n <button\n class=\"demo-trigger\"\n type=\"button\"\n data-skeleton-reset>\n <%= t(\"components.skeleton.demo.reset\") %>\n </button>\n </div>\n\n <div\n class=\"skeleton-card\"\n data-controller=\"stimeo--skeleton\"\n aria-busy=\"true\"\n data-stimeo--skeleton-min-duration-value=\"400\"\n data-action=\"content:ready->stimeo--skeleton#ready skeleton:reset->stimeo--skeleton#reset\">\n <div\n class=\"skeleton\"\n aria-hidden=\"true\"\n data-stimeo--skeleton-target=\"placeholder\">\n <span class=\"skeleton__line skeleton__line--title\"></span>\n <span class=\"skeleton__line\"></span>\n <span class=\"skeleton__line skeleton__line--short\"></span>\n </div>\n\n <div class=\"skeleton-content\" hidden data-stimeo--skeleton-target=\"content\">\n <h3 class=\"skeleton-content__title\"><%= t(\"components.skeleton.demo.title\") %></h3>\n <p><%= t(\"components.skeleton.demo.body\") %></p>\n </div>\n </div>\n</div>\n"
358
+ "source": "<%# Markup for the skeleton (skeleton toggle control) demo.\n Initially the skeleton is shown, the real content hidden, and aria-busy=\"true\". On\n ready it swaps and sets aria-busy=\"false\". The skeleton is decorative\n (aria-hidden=\"true\"). A minimum display time prevents flicker.\n The Load / Reset buttons sit outside the busy card (so aria-busy stays scoped to the\n loading region, not the controls), so they drive it through events the controller\n listens for (see demo.js): Load fires content:ready — the same signal a consumer\n dispatches when their real content arrives — and Reset returns it to loading. A\n Stimulus data-action on the buttons would not bind, since actions only wire up\n within the controller's own element. %>\n<div class=\"skeleton-demo\">\n <div class=\"skeleton-demo__controls\">\n <button\n class=\"demo-trigger\"\n type=\"button\"\n data-skeleton-load>\n <%= t(\"components.skeleton.demo.load\") %>\n </button>\n <button\n class=\"demo-trigger\"\n type=\"button\"\n data-skeleton-reset>\n <%= t(\"components.skeleton.demo.reset\") %>\n </button>\n </div>\n\n <div\n class=\"skeleton-card\"\n data-controller=\"stimeo--skeleton\"\n aria-busy=\"true\"\n data-stimeo--skeleton-min-duration-value=\"400\"\n data-stimeo--skeleton-announce-ready-text-value=\"<%=\n t(\"components.skeleton.demo.announce_ready\") %>\"\n data-action=\"content:ready->stimeo--skeleton#ready skeleton:reset->stimeo--skeleton#reset\">\n <div\n class=\"skeleton\"\n aria-hidden=\"true\"\n data-stimeo--skeleton-target=\"placeholder\">\n <span class=\"skeleton__line skeleton__line--title\"></span>\n <span class=\"skeleton__line\"></span>\n <span class=\"skeleton__line skeleton__line--short\"></span>\n </div>\n\n <div class=\"skeleton-content\" hidden data-stimeo--skeleton-target=\"content\">\n <h3 class=\"skeleton-content__title\"><%= t(\"components.skeleton.demo.title\") %></h3>\n <p><%= t(\"components.skeleton.demo.body\") %></p>\n </div>\n </div>\n\n <%# Shared announcer: aria-busy going false is state, not a notification, so the\n arrival of the content is read out through a region that stands in the page\n independently of the card. %>\n <div data-controller=\"stimeo--announcer\">\n <div class=\"visually-hidden\"\n data-stimeo--announcer-target=\"polite\" aria-live=\"polite\" aria-atomic=\"true\"></div>\n </div>\n</div>\n"
359
359
  },
360
360
  "stimeo--slider": {
361
361
  "file": "examples/slider/_demo.html.erb",
@@ -371,11 +371,11 @@
371
371
  },
372
372
  "stimeo--spinner": {
373
373
  "file": "examples/spinner/_demo.html.erb",
374
- "source": "<%# Markup for the spinner (loading indicator) demo.\n The indicator holds text in a role=\"status\" live region, with the visual spinner\n placed alongside as aria-hidden=\"true\". The region reflects aria-busy. A show delay\n and a minimum display time suppress flicker. The Start/Stop buttons call the\n controller's methods directly. %>\n<div\n class=\"spinner-demo\"\n data-controller=\"stimeo--spinner\"\n data-stimeo--spinner-delay-value=\"150\"\n data-stimeo--spinner-min-duration-value=\"600\">\n <div class=\"spinner-demo__controls\">\n <button\n class=\"demo-trigger\"\n type=\"button\"\n data-action=\"click->stimeo--spinner#start\">\n <%= t(\"components.spinner.demo.start\") %>\n </button>\n <button\n class=\"demo-trigger\"\n type=\"button\"\n data-action=\"click->stimeo--spinner#stop\">\n <%= t(\"components.spinner.demo.stop\") %>\n </button>\n </div>\n\n <p class=\"spinner-demo__hint\"><%= t(\"components.spinner.demo.hint\") %></p>\n\n <div\n class=\"spinner\"\n role=\"status\"\n aria-live=\"polite\"\n hidden\n data-stimeo--spinner-target=\"indicator\">\n <span class=\"spinner__icon\" aria-hidden=\"true\"></span>\n <span data-stimeo--spinner-target=\"message\"><%= t(\"components.spinner.demo.loading\") %></span>\n </div>\n\n <div\n class=\"spinner-demo__region\"\n aria-busy=\"false\"\n data-stimeo--spinner-target=\"region\">\n <%= t(\"components.spinner.demo.content\") %>\n </div>\n</div>\n"
374
+ "source": "<%# Markup for the spinner (loading indicator) demo.\n The indicator is the visual half its text plus a spinner marked aria-hidden=\"true\" —\n and the controlled region reflects aria-busy. An indicator revealed at the moment of\n the change is not reliably read, so the loading / ready wording goes to the shared\n announcer and is said exactly once. A show delay and a minimum display time suppress\n flicker. The Start/Stop buttons call the controller's methods directly. %>\n<div\n class=\"spinner-demo\"\n data-controller=\"stimeo--spinner\"\n data-stimeo--spinner-delay-value=\"150\"\n data-stimeo--spinner-min-duration-value=\"600\"\n data-stimeo--spinner-announce-text-value=\"<%= t(\"components.spinner.demo.loading\") %>\"\n data-stimeo--spinner-announce-ready-text-value=\"<%=\n t(\"components.spinner.demo.announce_ready\") %>\">\n <div class=\"spinner-demo__controls\">\n <button\n class=\"demo-trigger\"\n type=\"button\"\n data-action=\"click->stimeo--spinner#start\">\n <%= t(\"components.spinner.demo.start\") %>\n </button>\n <button\n class=\"demo-trigger\"\n type=\"button\"\n data-action=\"click->stimeo--spinner#stop\">\n <%= t(\"components.spinner.demo.stop\") %>\n </button>\n </div>\n\n <p class=\"spinner-demo__hint\"><%= t(\"components.spinner.demo.hint\") %></p>\n\n <%# Visual only: the announcer says this once, so a live region here would repeat it. %>\n <div\n class=\"spinner\"\n hidden\n data-stimeo--spinner-target=\"indicator\">\n <span class=\"spinner__icon\" aria-hidden=\"true\"></span>\n <span data-stimeo--spinner-target=\"message\"><%= t(\"components.spinner.demo.loading\") %></span>\n </div>\n\n <div\n class=\"spinner-demo__region\"\n aria-busy=\"false\"\n data-stimeo--spinner-target=\"region\">\n <%= t(\"components.spinner.demo.content\") %>\n </div>\n\n <%# Shared announcer: loading ↔ ready is the transition worth reading; aria-busy is\n state the reader can query, not a notification. %>\n <div data-controller=\"stimeo--announcer\">\n <div class=\"visually-hidden\"\n data-stimeo--announcer-target=\"polite\" aria-live=\"polite\" aria-atomic=\"true\"></div>\n </div>\n</div>\n"
375
375
  },
376
376
  "stimeo--step-indicator": {
377
377
  "file": "examples/step_indicator/_demo.html.erb",
378
- "source": "<%# Markup for the step-indicator (read-only progress) demo.\n The library holds the current step number, sets data-state on each li, sets\n aria-current=\"step\" on the current li, and updates the progress CSS variable\n (--stimeo-step-indicator-ratio). The steps themselves aren't interactive, so the\n current step is updated via the external step:set event (fired by demo.js from the\n prev/next buttons). %>\n<div class=\"step-indicator-demo\">\n <ol\n class=\"step-indicator\"\n data-controller=\"stimeo--step-indicator\"\n aria-label=\"<%= t(\"components.step_indicator.demo.label\") %>\"\n data-stimeo--step-indicator-current-value=\"1\"\n data-action=\"step:set->stimeo--step-indicator#setCurrent\">\n <% %w[cart shipping payment confirm].each_with_index do |step, index| %>\n <li\n class=\"step-indicator__step\"\n data-stimeo--step-indicator-target=\"step\"\n <%= \"data-state=\\\"complete\\\"\".html_safe if index < 1 %>\n <%= \"data-state=\\\"current\\\" aria-current=\\\"step\\\"\".html_safe if index == 1 %>\n <%= \"data-state=\\\"upcoming\\\"\".html_safe if index > 1 %>>\n <span class=\"step-indicator__dot\"></span>\n <span class=\"step-indicator__label\"><%= t(\n \"components.step_indicator.demo.steps.#{step}\"\n ) %></span>\n </li>\n <% end %>\n </ol>\n\n <div class=\"step-indicator__controls\">\n <button type=\"button\" class=\"demo-trigger\" data-step-indicator-prev>\n <%= t(\"components.step_indicator.demo.back\") %>\n </button>\n <button type=\"button\" class=\"demo-trigger\" data-step-indicator-next>\n <%= t(\"components.step_indicator.demo.advance\") %>\n </button>\n </div>\n</div>\n"
378
+ "source": "<%# Markup for the step-indicator (read-only progress) demo.\n The library holds the current step number, sets data-state on each li, sets\n aria-current=\"step\" on the current li, and updates the progress CSS variable\n (--stimeo--step-indicator-ratio). The steps themselves aren't interactive, so the\n current step is updated via the external step:set event (fired by demo.js from the\n prev/next buttons). %>\n<div class=\"step-indicator-demo\">\n <%# --steps lets the CSS put the track between the first and last dot centres:\n the library's ratio is \"how far along the range\", and the range runs from\n centre to centre, not edge to edge. %>\n <ol\n class=\"step-indicator\"\n style=\"--steps: 4\"\n data-controller=\"stimeo--step-indicator\"\n aria-label=\"<%= t(\"components.step_indicator.demo.label\") %>\"\n data-stimeo--step-indicator-current-value=\"1\"\n data-action=\"step:set->stimeo--step-indicator#setCurrent\">\n <% %w[cart shipping payment confirm].each_with_index do |step, index| %>\n <li\n class=\"step-indicator__step\"\n data-stimeo--step-indicator-target=\"step\"\n <%= \"data-state=\\\"complete\\\"\".html_safe if index < 1 %>\n <%= \"data-state=\\\"current\\\" aria-current=\\\"step\\\"\".html_safe if index == 1 %>\n <%= \"data-state=\\\"upcoming\\\"\".html_safe if index > 1 %>>\n <span class=\"step-indicator__dot\"></span>\n <span class=\"step-indicator__label\"><%= t(\n \"components.step_indicator.demo.steps.#{step}\"\n ) %></span>\n </li>\n <% end %>\n </ol>\n\n <div class=\"step-indicator__controls\">\n <button type=\"button\" class=\"demo-trigger\" data-step-indicator-prev>\n <%= t(\"components.step_indicator.demo.back\") %>\n </button>\n <button type=\"button\" class=\"demo-trigger\" data-step-indicator-next>\n <%= t(\"components.step_indicator.demo.advance\") %>\n </button>\n </div>\n</div>\n"
379
379
  },
380
380
  "stimeo--stepper": {
381
381
  "file": "examples/stepper/_demo.html.erb",
@@ -383,7 +383,7 @@
383
383
  },
384
384
  "stimeo--stick-to-bottom": {
385
385
  "file": "examples/stick_to_bottom/_demo.html.erb",
386
- "source": "<%# Stick-to-bottom demo: the scrollable log follows new messages while you're at the\n bottom, but holds and shows the \"new messages\" jump button if you've scrolled up. This\n catalog has no Action Cable / Turbo Stream, so the Add button appends a <li> — exactly\n the mutation a Turbo Stream broadcast would make — and the controller reacts. The jump\n button lives inside the log (a descendant, wired with a plain data-action) and CSS\n reveals it only while data-has-new. demo.js starts the log scrolled to the bottom. The\n library only follows/flags and reflects data-pinned / data-has-new; demo.css owns the look.\n\n The log scrolls but its only control — the jump button — is hidden until there is\n something to jump to, so most of the time a keyboard user cannot reach it at all\n (WCAG 2.1.1). Rather than hand-write a tab stop, compose stimeo--scroll-area over it:\n that controller exists for exactly this and gives the viewport tabindex=\"0\" only while\n it holds no rendered focusable content, so the tab stop appears and disappears in step\n with the jump button instead of doubling up with it. It goes on a wrapper because\n stick-to-bottom must sit on the scrolling element itself, and it takes the log as its\n viewport target. role=\"region\" comes free with the label below. %>\n<div class=\"stb-demo\">\n <div class=\"stb-demo__log-wrap\" data-controller=\"stimeo--scroll-area\">\n <div\n class=\"stb-demo__log\"\n data-controller=\"stimeo--stick-to-bottom\"\n data-stimeo--stick-to-bottom-behavior-value=\"smooth\"\n data-stimeo--scroll-area-target=\"viewport\"\n aria-label=\"<%= t(\"components.stick_to_bottom.demo.viewport_label\") %>\">\n <ul class=\"stb-demo__messages\" data-stimeo--stick-to-bottom-target=\"content\">\n <% (1..8).each do |i| %>\n <li><%= t(\"components.stick_to_bottom.demo.message\") %> <%= i %></li>\n <% end %>\n </ul>\n <button\n type=\"button\"\n class=\"stb-demo__jump\"\n data-action=\"click->stimeo--stick-to-bottom#scrollToBottom\">\n <%= t(\"components.stick_to_bottom.demo.jump\") %>\n </button>\n </div>\n </div>\n <button\n type=\"button\"\n class=\"demo-trigger\"\n data-stb-demo-add\n data-message-label=\"<%= t(\"components.stick_to_bottom.demo.message\") %>\">\n <%= t(\"components.stick_to_bottom.demo.add\") %>\n </button>\n</div>\n"
386
+ "source": "<%# Stick-to-bottom demo: the scrollable log follows new messages while you're at the\n bottom, but holds and shows the \"new messages\" jump button if you've scrolled up. This\n catalog has no Action Cable / Turbo Stream, so the Add button appends a <li> — exactly\n the mutation a Turbo Stream broadcast would make — and the controller reacts. The jump\n button lives inside the log (a descendant, wired with a plain data-action) and CSS\n reveals it only while data-has-new. pin-on-connect starts the log at the bottom so the\n first message is followed rather than flagged — a fresh scroll container renders at the\n top, and the controller does this on every connect, which a once-per-document page event\n cannot. The library only follows/flags and reflects data-pinned / data-has-new; demo.css\n owns the look.\n\n The log scrolls but its only control — the jump button — is hidden until there is\n something to jump to, so most of the time a keyboard user cannot reach it at all\n (WCAG 2.1.1). Rather than hand-write a tab stop, compose stimeo--scroll-area over it:\n that controller exists for exactly this and gives the viewport tabindex=\"0\" only while\n it holds no rendered focusable content, so the tab stop appears and disappears in step\n with the jump button instead of doubling up with it. It goes on a wrapper because\n stick-to-bottom must sit on the scrolling element itself, and it takes the log as its\n viewport target. role=\"region\" comes free with the label below. %>\n<div class=\"stb-demo\">\n <div class=\"stb-demo__log-wrap\" data-controller=\"stimeo--scroll-area\">\n <div\n class=\"stb-demo__log\"\n data-controller=\"stimeo--stick-to-bottom\"\n data-stimeo--stick-to-bottom-behavior-value=\"smooth\"\n data-stimeo--stick-to-bottom-pin-on-connect-value=\"true\"\n data-stimeo--scroll-area-target=\"viewport\"\n aria-label=\"<%= t(\"components.stick_to_bottom.demo.viewport_label\") %>\">\n <ul class=\"stb-demo__messages\" data-stimeo--stick-to-bottom-target=\"content\">\n <% (1..8).each do |i| %>\n <li><%= t(\"components.stick_to_bottom.demo.message\") %> <%= i %></li>\n <% end %>\n </ul>\n <button\n type=\"button\"\n class=\"stb-demo__jump\"\n data-action=\"click->stimeo--stick-to-bottom#scrollToBottom\">\n <%= t(\"components.stick_to_bottom.demo.jump\") %>\n </button>\n </div>\n </div>\n <button\n type=\"button\"\n class=\"demo-trigger\"\n data-stb-demo-add\n data-message-label=\"<%= t(\"components.stick_to_bottom.demo.message\") %>\">\n <%= t(\"components.stick_to_bottom.demo.add\") %>\n </button>\n</div>\n"
387
387
  },
388
388
  "stimeo--sticky-observer": {
389
389
  "file": "examples/sticky_observer/_demo.html.erb",