stimeo-ui 0.7.0 → 0.8.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 (50) hide show
  1. package/CHANGELOG.md +62 -0
  2. package/dist/controllers/auto_submit_controller.d.ts +14 -3
  3. package/dist/controllers/auto_submit_controller.js +94 -16
  4. package/dist/controllers/auto_submit_controller.js.map +1 -1
  5. package/dist/controllers/carousel_controller.d.ts +81 -47
  6. package/dist/controllers/carousel_controller.js +451 -100
  7. package/dist/controllers/carousel_controller.js.map +1 -1
  8. package/dist/controllers/currency_input_controller.d.ts +39 -5
  9. package/dist/controllers/currency_input_controller.js +305 -74
  10. package/dist/controllers/currency_input_controller.js.map +1 -1
  11. package/dist/controllers/direct_upload_controller.d.ts +3 -1
  12. package/dist/controllers/direct_upload_controller.js +12 -2
  13. package/dist/controllers/direct_upload_controller.js.map +1 -1
  14. package/dist/controllers/file_dropzone_controller.d.ts +123 -29
  15. package/dist/controllers/file_dropzone_controller.js +386 -63
  16. package/dist/controllers/file_dropzone_controller.js.map +1 -1
  17. package/dist/controllers/flash_controller.d.ts +3 -1
  18. package/dist/controllers/flash_controller.js +3 -1
  19. package/dist/controllers/flash_controller.js.map +1 -1
  20. package/dist/controllers/frame_loading_controller.d.ts +4 -2
  21. package/dist/controllers/frame_loading_controller.js +2 -1
  22. package/dist/controllers/frame_loading_controller.js.map +1 -1
  23. package/dist/controllers/input_mask_controller.d.ts +42 -16
  24. package/dist/controllers/input_mask_controller.js +251 -76
  25. package/dist/controllers/input_mask_controller.js.map +1 -1
  26. package/dist/controllers/nested_form_controller.d.ts +51 -14
  27. package/dist/controllers/nested_form_controller.js +450 -42
  28. package/dist/controllers/nested_form_controller.js.map +1 -1
  29. package/dist/controllers/otp_controller.d.ts +61 -25
  30. package/dist/controllers/otp_controller.js +472 -114
  31. package/dist/controllers/otp_controller.js.map +1 -1
  32. package/dist/controllers/spinner_controller.d.ts +3 -2
  33. package/dist/controllers/spinner_controller.js +7 -5
  34. package/dist/controllers/spinner_controller.js.map +1 -1
  35. package/dist/controllers/submit_once_controller.d.ts +4 -1
  36. package/dist/controllers/submit_once_controller.js +3 -1
  37. package/dist/controllers/submit_once_controller.js.map +1 -1
  38. package/dist/controllers/textarea_autosize_controller.d.ts +30 -10
  39. package/dist/controllers/textarea_autosize_controller.js +131 -3
  40. package/dist/controllers/textarea_autosize_controller.js.map +1 -1
  41. package/dist/index.js +1927 -607
  42. package/dist/index.js.map +1 -1
  43. package/dist/inspector/cli.d.ts +8 -1
  44. package/dist/inspector/cli.js +3 -1
  45. package/dist/inspector/cli.js.map +1 -1
  46. package/dist/inspector/cli_bin.js +3 -1
  47. package/dist/inspector/cli_bin.js.map +1 -1
  48. package/dist/inspector/examples.json +4 -4
  49. package/dist/inspector/manifest.json +182 -23
  50. package/package.json +2 -2
@@ -43,7 +43,7 @@
43
43
  },
44
44
  "stimeo--carousel": {
45
45
  "file": "examples/carousel/_demo.html.erb",
46
- "source": "<%# Markup for the carousel demo.\n The library handles slide advance, autoplay, syncing the current slide's\n data-state / hidden, and the pickers' (tabs) aria-selected and roving.\n Autoplay deliberately demonstrates WCAG 2.2.2: it pauses while the pointer hovers\n and hard-stops when keyboard focus enters the carousel (it does not auto-resume on\n focus out — press play). This is intentional and is spelled out in the on-screen\n note below the carousel. Transitions and layout are the consumer's CSS. %>\n<section\n class=\"carousel\"\n data-controller=\"stimeo--carousel\"\n aria-roledescription=\"carousel\"\n aria-label=\"<%= t(\"components.carousel.demo.label\") %>\"\n data-stimeo--carousel-autoplay-value=\"false\"\n data-stimeo--carousel-interval-value=\"2500\"\n data-stimeo--carousel-loop-value=\"true\"\n data-action=\"\n mouseenter->stimeo--carousel#pause\n mouseleave->stimeo--carousel#resume\n focusin->stimeo--carousel#pause\n focusout->stimeo--carousel#resume\">\n <div class=\"carousel__bar\">\n <button\n type=\"button\"\n class=\"carousel__play\"\n aria-pressed=\"false\"\n aria-label=\"<%= t(\"components.carousel.demo.autoplay\") %>\"\n data-stimeo--carousel-target=\"playToggle\"\n data-action=\"click->stimeo--carousel#togglePlay\">\n <%# The library toggles aria-pressed; demo.css swaps the glyph off it so the\n control visibly reflects play (❚❚) vs. paused (▶). The icons are decorative;\n the accessible name comes from aria-label above. %>\n <span class=\"carousel__play-icon carousel__play-icon--play\" aria-hidden=\"true\">▶</span>\n <span class=\"carousel__play-icon carousel__play-icon--pause\" aria-hidden=\"true\">❚❚</span>\n </button>\n <p\n class=\"carousel__status\"\n data-carousel-status\n data-template=\"<%= t(\"components.carousel.demo.status_template\") %>\"\n data-playing-suffix=\"<%= t(\"components.carousel.demo.status_playing\") %>\"\n aria-live=\"polite\"></p>\n </div>\n\n <div class=\"carousel__viewport\" data-stimeo--carousel-target=\"viewport\">\n <% t(\"components.carousel.demo.slides\").each_with_index do |slide, i| %>\n <div\n id=\"carousel-slide-<%= i + 1 %>\"\n class=\"carousel__slide\"\n role=\"tabpanel\"\n aria-roledescription=\"slide\"\n aria-label=\"<%= \"#{i + 1} of #{t('components.carousel.demo.slides').size}\" %>\"\n aria-labelledby=\"carousel-dot-<%= i + 1 %>\"\n data-stimeo--carousel-target=\"slide\"\n <%= \"hidden\" if i.positive? %>>\n <h3 class=\"carousel__title\"><%= slide[:title] %></h3>\n <p><%= slide[:body] %></p>\n </div>\n <% end %>\n </div>\n\n <div class=\"carousel__nav\">\n <button type=\"button\" class=\"carousel__arrow\"\n aria-label=\"<%= t(\"components.carousel.demo.prev\") %>\"\n data-stimeo--carousel-target=\"prev\"\n data-action=\"click->stimeo--carousel#prev\">‹</button>\n <button type=\"button\" class=\"carousel__arrow\"\n aria-label=\"<%= t(\"components.carousel.demo.next\") %>\"\n data-stimeo--carousel-target=\"next\"\n data-action=\"click->stimeo--carousel#next\">›</button>\n </div>\n\n <div class=\"carousel__dots\" role=\"tablist\"\n aria-label=\"<%= t(\"components.carousel.demo.tablist\") %>\">\n <% t(\"components.carousel.demo.slides\").each_with_index do |slide, i| %>\n <button\n id=\"carousel-dot-<%= i + 1 %>\"\n class=\"carousel__dot\"\n role=\"tab\"\n aria-selected=\"<%= i.zero? %>\"\n aria-controls=\"carousel-slide-<%= i + 1 %>\"\n aria-label=\"<%= slide[:title] %>\"\n tabindex=\"<%= i.zero? ? 0 : -1 %>\"\n data-stimeo--carousel-target=\"picker\"\n data-action=\"click->stimeo--carousel#goto\n keydown->stimeo--carousel#onPickerKeydown\"></button>\n <% end %>\n </div>\n</section>\n\n<%# On-screen explanation of the deliberate autoplay accessibility behavior, so the\n pause-on-hover / hard-stop-on-focus is understood as intentional (WCAG 2.2.2), not a bug.\n Kept outside the <section> so reading it doesn't itself pause the carousel. %>\n<p class=\"carousel__hint\"><%= t(\"components.carousel.demo.hint\") %></p>\n"
46
+ "source": "<%# Markup for the carousel demo.\n The library handles slide advance, autoplay, syncing the current slide's\n data-state / hidden / inert, the pickers' (tabs) aria-selected and roving, the\n aria-disabled of controls that cannot be reached, and the viewport's aria-live.\n There is no data-action anywhere: the controller delegates clicks, picker keys,\n hover, and focus from its own element, so the markup only declares targets.\n Autoplay deliberately demonstrates WCAG 2.2.2: the play toggle is what stops it for\n good, while hovering, focusing, or backgrounding the tab only suspends it until that\n condition ends. This is spelled out in the on-screen note below the carousel.\n Transitions and layout are the consumer's CSS. %>\n<section\n class=\"carousel\"\n data-controller=\"stimeo--carousel\"\n aria-roledescription=\"carousel\"\n aria-label=\"<%= t(\"components.carousel.demo.label\") %>\"\n data-stimeo--carousel-autoplay-value=\"false\"\n data-stimeo--carousel-interval-value=\"2500\"\n data-stimeo--carousel-loop-value=\"true\">\n <div class=\"carousel__bar\">\n <button\n type=\"button\"\n class=\"carousel__play\"\n aria-label=\"<%= t(\"components.carousel.demo.autoplay\") %>\"\n data-stimeo--carousel-target=\"playToggle\">\n <%# The library owns aria-pressed (the autoplay Value is the intent it mirrors);\n demo.css swaps the glyph off it so the control visibly reflects play (❚❚) vs.\n paused (▶). The icons are decorative; the accessible name comes from aria-label\n above. %>\n <span class=\"carousel__play-icon carousel__play-icon--play\" aria-hidden=\"true\">▶</span>\n <span class=\"carousel__play-icon carousel__play-icon--pause\" aria-hidden=\"true\">❚❚</span>\n </button>\n <%# A visual read-out only. The library makes the viewport the live region, so a\n second one here would make a screen reader announce the change twice. %>\n <p\n class=\"carousel__status\"\n data-carousel-status\n data-template=\"<%= t(\"components.carousel.demo.status_template\") %>\"\n data-playing-suffix=\"<%= t(\"components.carousel.demo.status_playing\") %>\"></p>\n </div>\n\n <div class=\"carousel__viewport\" data-stimeo--carousel-target=\"viewport\">\n <% t(\"components.carousel.demo.slides\").each_with_index do |slide, i| %>\n <div\n id=\"carousel-slide-<%= i + 1 %>\"\n class=\"carousel__slide\"\n role=\"tabpanel\"\n aria-roledescription=\"slide\"\n aria-label=\"<%= \"#{i + 1} of #{t('components.carousel.demo.slides').size}\" %>\"\n aria-labelledby=\"carousel-dot-<%= i + 1 %>\"\n data-stimeo--carousel-target=\"slide\"\n <%= \"hidden inert\" if i.positive? %>>\n <h3 class=\"carousel__title\"><%= slide[:title] %></h3>\n <p><%= slide[:body] %></p>\n </div>\n <% end %>\n </div>\n\n <div class=\"carousel__nav\">\n <button type=\"button\" class=\"carousel__arrow\"\n aria-label=\"<%= t(\"components.carousel.demo.prev\") %>\"\n data-stimeo--carousel-target=\"prev\">‹</button>\n <button type=\"button\" class=\"carousel__arrow\"\n aria-label=\"<%= t(\"components.carousel.demo.next\") %>\"\n data-stimeo--carousel-target=\"next\">›</button>\n </div>\n\n <div class=\"carousel__dots\" role=\"tablist\"\n aria-label=\"<%= t(\"components.carousel.demo.tablist\") %>\">\n <% t(\"components.carousel.demo.slides\").each_with_index do |slide, i| %>\n <button\n id=\"carousel-dot-<%= i + 1 %>\"\n class=\"carousel__dot\"\n role=\"tab\"\n aria-selected=\"<%= i.zero? %>\"\n aria-controls=\"carousel-slide-<%= i + 1 %>\"\n aria-label=\"<%= slide[:title] %>\"\n tabindex=\"<%= i.zero? ? 0 : -1 %>\"\n data-stimeo--carousel-target=\"picker\"></button>\n <% end %>\n </div>\n</section>\n\n<%# On-screen explanation of the deliberate autoplay accessibility behavior, so the\n pause-on-hover / hard-stop-on-focus is understood as intentional (WCAG 2.2.2), not a bug.\n Kept outside the <section> so reading it doesn't itself pause the carousel. %>\n<p class=\"carousel__hint\"><%= t(\"components.carousel.demo.hint\") %></p>\n"
47
47
  },
48
48
  "stimeo--character-counter": {
49
49
  "file": "examples/character_counter/_demo.html.erb",
@@ -139,7 +139,7 @@
139
139
  },
140
140
  "stimeo--file-dropzone": {
141
141
  "file": "examples/file_dropzone/_demo.html.erb",
142
- "source": "<%# Markup for the file-dropzone (file drag & drop / image preview) demo.\n Click or keyboard launches the native file input, and drag & drop also adds files.\n It validates accept / maxSize / count and shows image thumbnails via objectURL.\n The library handles the drop bridge, validation, preview create/revoke, focus\n handoff on removal, and live announcements. Drag & drop is only an aid — selection\n is always possible via click / keyboard. %>\n<div class=\"file-dropzone\" data-controller=\"stimeo--file-dropzone\"\n data-stimeo--file-dropzone-max-size-value=\"5242880\"\n data-stimeo--file-dropzone-max-files-value=\"4\"\n data-stimeo--file-dropzone-drag-label-value=\"<%= t(\n 'components.file_dropzone.demo.drag_label'\n ) %>\">\n <div\n class=\"file-dropzone__zone\"\n data-stimeo--file-dropzone-target=\"zone\"\n data-action=\"dragover->stimeo--file-dropzone#onDragOver\n dragleave->stimeo--file-dropzone#onDragLeave\n drop->stimeo--file-dropzone#onDrop\">\n <button\n type=\"button\"\n class=\"demo-trigger file-dropzone__trigger\"\n data-stimeo--file-dropzone-target=\"trigger\"\n data-action=\"click->stimeo--file-dropzone#openDialog\">\n <%= t(\"components.file_dropzone.demo.trigger\") %>\n </button>\n <input\n type=\"file\"\n accept=\"image/*\"\n multiple\n aria-label=\"<%= t('components.file_dropzone.demo.input_label') %>\"\n class=\"file-dropzone__input visually-hidden\"\n data-stimeo--file-dropzone-target=\"input\"\n data-action=\"change->stimeo--file-dropzone#onChange\" />\n </div>\n\n <ul\n class=\"file-dropzone__list\"\n aria-label=\"<%= t('components.file_dropzone.demo.list_label') %>\"\n data-stimeo--file-dropzone-target=\"list\"></ul>\n\n <%# Demo-only: the library accepts only images within the limits (5 MB / 4 files) and\n fires stimeo--file-dropzone:reject for the rest — accepted images get a thumbnail\n preview. Surface the reason visibly so a rejected file (e.g. an over-size photo) is\n not mistaken for a broken preview. demo.js fills this from the reject event; the\n reason copy is localized here. %>\n <p class=\"file-dropzone__error\"\n data-file-dropzone-error\n hidden\n data-reason-type=\"<%= t('components.file_dropzone.demo.reject_type') %>\"\n data-reason-size=\"<%= t('components.file_dropzone.demo.reject_size') %>\"\n data-reason-count=\"<%= t('components.file_dropzone.demo.reject_count') %>\"></p>\n\n <span\n role=\"status\"\n aria-live=\"polite\"\n class=\"file-dropzone__status visually-hidden\"\n data-stimeo--file-dropzone-target=\"status\"></span>\n\n <template data-stimeo--file-dropzone-target=\"itemTemplate\">\n <li class=\"file-dropzone__item\" data-stimeo--file-dropzone-target=\"item\">\n <img class=\"file-dropzone__thumb\" data-file-dropzone-slot=\"thumb\" alt=\"\" hidden />\n <span class=\"file-dropzone__name\" data-file-dropzone-slot=\"name\"></span>\n <%# Removal is handled by a delegated listener on the list container, so it\n works instantly without waiting on Stimulus wiring a data-action onto the\n dynamically-added button. %>\n <button\n type=\"button\"\n class=\"file-dropzone__remove\">\n <%= t(\"components.file_dropzone.demo.remove\") %>\n </button>\n </li>\n </template>\n</div>\n"
142
+ "source": "<%# Markup for the file-dropzone (file drag & drop / image preview) demo.\n Click or keyboard launches the native file input, and drag & drop also adds files.\n It validates accept / maxSize / duplicates / count and shows image thumbnails via\n objectURL. The library handles the drop bridge, validation, preview create/revoke,\n focus handoff on removal, mirroring the accepted files back onto the native input,\n and the announcements read by the shared announcer seated below. Drag & drop is\n only an aid — selection is always possible via click / keyboard. %>\n<div class=\"file-dropzone\" data-controller=\"stimeo--file-dropzone\"\n data-stimeo--file-dropzone-max-size-value=\"5242880\"\n data-stimeo--file-dropzone-max-files-value=\"4\"\n data-stimeo--file-dropzone-announce-drag-text-value=\"<%= t(\n 'components.file_dropzone.demo.drag_label'\n ) %>\"\n data-stimeo--file-dropzone-announce-added-text-value=\"<%= t(\n 'components.file_dropzone.demo.announce_added'\n ) %>\"\n data-stimeo--file-dropzone-announce-removed-text-value=\"<%= t(\n 'components.file_dropzone.demo.announce_removed'\n ) %>\"\n data-stimeo--file-dropzone-announce-rejected-type-text-value=\"<%= t(\n 'components.file_dropzone.demo.announce_reject_type'\n ) %>\"\n data-stimeo--file-dropzone-announce-rejected-size-text-value=\"<%= t(\n 'components.file_dropzone.demo.announce_reject_size'\n ) %>\"\n data-stimeo--file-dropzone-announce-rejected-duplicate-text-value=\"<%= t(\n 'components.file_dropzone.demo.announce_reject_duplicate'\n ) %>\"\n data-stimeo--file-dropzone-announce-rejected-count-text-value=\"<%= t(\n 'components.file_dropzone.demo.announce_reject_count'\n ) %>\">\n <div\n class=\"file-dropzone__zone\"\n data-stimeo--file-dropzone-target=\"zone\"\n data-action=\"dragover->stimeo--file-dropzone#onDragOver\n dragleave->stimeo--file-dropzone#onDragLeave\n drop->stimeo--file-dropzone#onDrop\">\n <button\n type=\"button\"\n class=\"demo-trigger file-dropzone__trigger\"\n data-stimeo--file-dropzone-target=\"trigger\"\n data-action=\"click->stimeo--file-dropzone#openDialog\">\n <%= t(\"components.file_dropzone.demo.trigger\") %>\n </button>\n <input\n type=\"file\"\n accept=\"image/*\"\n multiple\n aria-label=\"<%= t('components.file_dropzone.demo.input_label') %>\"\n class=\"file-dropzone__input visually-hidden\"\n data-stimeo--file-dropzone-target=\"input\"\n data-action=\"change->stimeo--file-dropzone#onChange\" />\n </div>\n\n <ul\n class=\"file-dropzone__list\"\n aria-label=\"<%= t('components.file_dropzone.demo.list_label') %>\"\n data-stimeo--file-dropzone-target=\"list\"></ul>\n\n <%# Demo-only: the library accepts only unique images within the limits (5 MB / 4 files)\n and fires stimeo--file-dropzone:reject for the rest — accepted images get a thumbnail\n preview. The reason is already spoken by the shared announcer; this repeats it on\n screen so a rejected file (e.g. an over-size photo) is not mistaken for a broken\n preview. demo.js fills this from the reject event; the reason copy is localized here. %>\n <p class=\"file-dropzone__error\"\n data-file-dropzone-error\n hidden\n data-reason-type=\"<%= t('components.file_dropzone.demo.reject_type') %>\"\n data-reason-size=\"<%= t('components.file_dropzone.demo.reject_size') %>\"\n data-reason-duplicate=\"<%= t('components.file_dropzone.demo.reject_duplicate') %>\"\n data-reason-count=\"<%= t('components.file_dropzone.demo.reject_count') %>\"></p>\n\n <template data-stimeo--file-dropzone-target=\"itemTemplate\">\n <li class=\"file-dropzone__item\" data-stimeo--file-dropzone-target=\"item\">\n <img class=\"file-dropzone__thumb\" data-stimeo--file-dropzone-target=\"thumb\" alt=\"\" hidden />\n <span class=\"file-dropzone__name\" data-stimeo--file-dropzone-target=\"name\"></span>\n <%# Removal is handled by a delegated listener on the list container, so it\n works instantly without waiting on Stimulus wiring a data-action onto the\n dynamically-added button. The library keeps this aria-label and only\n expands {name}, so the accessible name stays in the page's language. %>\n <button\n type=\"button\"\n class=\"file-dropzone__remove\"\n aria-label=\"<%= t('components.file_dropzone.demo.remove_label') %>\"\n data-stimeo--file-dropzone-target=\"remove\">\n <%= t(\"components.file_dropzone.demo.remove\") %>\n </button>\n </li>\n </template>\n\n <%# Shared announcer: additions, rejections, removals, and the drag affordance are\n the transitions worth reading. The dropzone owns no live region of its own. %>\n <div data-controller=\"stimeo--announcer\">\n <div class=\"visually-hidden\"\n data-stimeo--announcer-target=\"polite\"\n aria-live=\"polite\"\n aria-atomic=\"true\"></div>\n </div>\n</div>\n"
143
143
  },
144
144
  "stimeo--filter": {
145
145
  "file": "examples/filter/_demo.html.erb",
@@ -227,7 +227,7 @@
227
227
  },
228
228
  "stimeo--nested-form": {
229
229
  "file": "examples/nested_form/_demo.html.erb",
230
- "source": "<%# Markup for the nested / dynamic fields demo (cocoon-style line items).\n The controller clones the <template> row (renumbering __INDEX__), removes rows by\n dropping unsaved ones / flagging _destroy on persisted ones, enforces min/max, and\n announces the count through a shared stimeo--announcer. Remove buttons are handled\n by delegation, so cloned rows need no per-row wiring. %>\n<%\n item_label = t(\"components.nested_form.demo.item_label\")\n remove_label = t(\"components.nested_form.demo.remove\")\n count_message = t(\"components.nested_form.demo.count_message\")\n%>\n<div class=\"nested-demo\" data-controller=\"stimeo--nested-form\"\n data-stimeo--nested-form-min-value=\"1\"\n data-stimeo--nested-form-max-value=\"5\"\n data-stimeo--nested-form-count-message-value=\"<%= count_message %>\">\n <div class=\"nested-demo__list\" data-stimeo--nested-form-target=\"list\">\n <fieldset class=\"nested-demo__row\">\n <label class=\"nested-demo__field\">\n <span><%= item_label %></span>\n <input type=\"text\" class=\"demo-input\" name=\"order[items_attributes][0][name]\">\n </label>\n <button type=\"button\" class=\"demo-trigger\"\n data-stimeo--nested-form-target=\"remove\"><%= remove_label %></button>\n </fieldset>\n </div>\n\n <template data-stimeo--nested-form-target=\"template\">\n <fieldset class=\"nested-demo__row\">\n <label class=\"nested-demo__field\">\n <span><%= item_label %></span>\n <input type=\"text\" class=\"demo-input\" name=\"order[items_attributes][__INDEX__][name]\">\n </label>\n <button type=\"button\" class=\"demo-trigger\"\n data-stimeo--nested-form-target=\"remove\"><%= remove_label %></button>\n </fieldset>\n </template>\n\n <button type=\"button\" class=\"demo-trigger\" data-stimeo--nested-form-target=\"add\"\n data-action=\"click->stimeo--nested-form#add\">\n <%= t(\"components.nested_form.demo.add\") %>\n </button>\n\n <%# Shared announcer: row count changes are read out without moving focus away. %>\n <div data-controller=\"stimeo--announcer\">\n <div class=\"visually-hidden\" data-stimeo--announcer-target=\"polite\"\n aria-live=\"polite\" aria-atomic=\"true\"></div>\n </div>\n</div>\n"
230
+ "source": "<%# Markup for the nested / dynamic fields demo (invoice-style line items).\n The controller clones the <template> row (renumbering __INDEX__), removes rows by\n dropping unsaved ones / flagging _destroy on persisted ones, enforces min/max, and\n announces the count through a shared stimeo--announcer. Remove buttons are handled\n by delegation, so cloned rows need no per-row wiring. %>\n<%\n item_label = t(\"components.nested_form.demo.item_label\")\n remove_label = t(\"components.nested_form.demo.remove\")\n count_message = t(\"components.nested_form.demo.count_message\")\n persisted_value = t(\"components.nested_form.demo.persisted_value\")\n%>\n<div class=\"nested-demo\" data-controller=\"stimeo--nested-form\"\n data-stimeo--nested-form-min-value=\"1\"\n data-stimeo--nested-form-max-value=\"5\"\n data-stimeo--nested-form-count-message-value=\"<%= count_message %>\">\n <div class=\"nested-demo__list\" data-stimeo--nested-form-target=\"list\">\n <fieldset class=\"nested-demo__row\">\n <label class=\"nested-demo__field\">\n <span><%= item_label %></span>\n <input type=\"text\" class=\"demo-input\" name=\"order[items_attributes][0][name]\">\n </label>\n <button type=\"button\" class=\"demo-trigger\"\n data-stimeo--nested-form-target=\"remove\"><%= remove_label %></button>\n </fieldset>\n <%# A persisted row: its _destroy flag makes Remove hide it instead of deleting it. %>\n <fieldset class=\"nested-demo__row\">\n <input type=\"hidden\" name=\"order[items_attributes][1][_destroy]\" value=\"0\"\n data-stimeo--nested-form-target=\"destroyFlag\">\n <label class=\"nested-demo__field\">\n <span><%= item_label %></span>\n <input type=\"text\" class=\"demo-input\" value=\"<%= persisted_value %>\"\n name=\"order[items_attributes][1][name]\">\n </label>\n <button type=\"button\" class=\"demo-trigger\"\n data-stimeo--nested-form-target=\"remove\"><%= remove_label %></button>\n </fieldset>\n </div>\n\n <template data-stimeo--nested-form-target=\"template\">\n <fieldset class=\"nested-demo__row\">\n <label class=\"nested-demo__field\">\n <span><%= item_label %></span>\n <input type=\"text\" class=\"demo-input\" name=\"order[items_attributes][__INDEX__][name]\">\n </label>\n <button type=\"button\" class=\"demo-trigger\"\n data-stimeo--nested-form-target=\"remove\"><%= remove_label %></button>\n </fieldset>\n </template>\n\n <button type=\"button\" class=\"demo-trigger\" data-stimeo--nested-form-target=\"add\"\n data-action=\"click->stimeo--nested-form#add\">\n <%= t(\"components.nested_form.demo.add\") %>\n </button>\n\n <%# Shared announcer: row count changes are read out without moving focus away. %>\n <div data-controller=\"stimeo--announcer\">\n <div class=\"visually-hidden\" data-stimeo--announcer-target=\"polite\"\n aria-live=\"polite\" aria-atomic=\"true\"></div>\n </div>\n</div>\n"
231
231
  },
232
232
  "stimeo--network-status": {
233
233
  "file": "examples/network_status/_demo.html.erb",
@@ -243,7 +243,7 @@
243
243
  },
244
244
  "stimeo--otp": {
245
245
  "file": "examples/otp/_demo.html.erb",
246
- "source": "<%# Markup for the otp (OTP PIN passcode) demo.\n Provides auto-advance on entry in each field, Backspace to go back, paste\n distribution, and value sync. %>\n<div\n class=\"otp-demo\"\n id=\"otp-passcode\"\n data-controller=\"stimeo--otp\"\n data-stimeo--otp-length-value=\"6\"\n data-stimeo--otp-pattern-value=\"[0-9]\"\n role=\"group\"\n aria-label=\"<%= t(\"components.otp.demo.label\") %>\"\n>\n \n <div class=\"otp-demo__label\"><%= t(\"components.otp.demo.label\") %></div>\n \n <div class=\"otp-demo__fields\">\n <% 6.times do |i| %>\n <input\n class=\"otp-demo__field\"\n type=\"text\"\n inputmode=\"numeric\"\n maxlength=\"1\"\n data-stimeo--otp-target=\"field\"\n data-action=\"\n input->stimeo--otp#onInput\n keydown->stimeo--otp#onKeydown\n paste->stimeo--otp#onPaste\n \"\n aria-label=\"<%= t(\"components.otp.demo.digit_label\", position: i + 1) %>\"\n />\n <% end %>\n </div>\n\n <input\n type=\"hidden\"\n data-stimeo--otp-target=\"value\"\n name=\"otp\"\n />\n\n <%# Region for invalid-input / full-width-character warning messages. %>\n <div\n data-stimeo--otp-target=\"error\"\n id=\"otp-error-message\"\n class=\"otp-demo__error\"\n role=\"alert\"\n aria-live=\"polite\"\n hidden\n >\n <span class=\"otp-demo__error-badge\">!</span>\n <span class=\"otp-demo__error-text\">\n <%= t(\"components.otp.demo.invalid_msg\") %>\n </span>\n </div>\n\n <%# Placeholder for the completion celebration message. %>\n <div id=\"otp-complete-message\" class=\"otp-demo__message\" style=\"display: none;\">\n <span class=\"otp-demo__success-badge\">✓</span>\n <span class=\"otp-demo__success-text\">\n <%= t(\"components.otp.demo.complete_msg\") %>\n <strong id=\"otp-success-code\"></strong>\n </span>\n </div>\n</div>\n"
246
+ "source": "<%# Markup for the otp (OTP PIN passcode) demo.\n Provides auto-advance on entry in each field, Backspace to clear, paste and\n autofill splitting, and value sync. The digit count is the number of fields. %>\n<div\n class=\"otp-demo\"\n id=\"otp-passcode\"\n data-controller=\"stimeo--otp\"\n data-stimeo--otp-pattern-value=\"[0-9]\"\n role=\"group\"\n aria-label=\"<%= t(\"components.otp.demo.label\") %>\"\n>\n\n <div class=\"otp-demo__label\"><%= t(\"components.otp.demo.label\") %></div>\n\n <div class=\"otp-demo__fields\">\n <% 6.times do |i| %>\n <input\n class=\"otp-demo__field\"\n type=\"text\"\n inputmode=\"numeric\"\n maxlength=\"1\"\n autocomplete=\"one-time-code\"\n data-stimeo--otp-target=\"field\"\n data-action=\"\n input->stimeo--otp#onInput\n keydown->stimeo--otp#onKeydown\n paste->stimeo--otp#onPaste\n pointerdown->stimeo--otp#onPointerDown\n \"\n aria-label=\"<%= t(\"components.otp.demo.digit_label\", position: i + 1) %>\"\n />\n <% end %>\n </div>\n\n <input\n type=\"hidden\"\n data-stimeo--otp-target=\"value\"\n name=\"otp\"\n />\n\n <%# Region for invalid-input / full-width-character warning messages. %>\n <div\n data-stimeo--otp-target=\"error\"\n id=\"otp-error-message\"\n class=\"otp-demo__error\"\n role=\"alert\"\n aria-live=\"polite\"\n hidden\n >\n <span class=\"otp-demo__error-badge\">!</span>\n <span class=\"otp-demo__error-text\">\n <%= t(\"components.otp.demo.invalid_msg\") %>\n </span>\n </div>\n\n <%# The completion message is shown by CSS reading data-state on the root, so the\n demo script only has to fill in the code itself. %>\n <div id=\"otp-complete-message\" class=\"otp-demo__message\">\n <span class=\"otp-demo__success-badge\">✓</span>\n <span class=\"otp-demo__success-text\">\n <%= t(\"components.otp.demo.complete_msg\") %>\n <strong id=\"otp-success-code\"></strong>\n </span>\n </div>\n\n <button type=\"button\" class=\"demo-trigger otp-demo__clear\" data-action=\"stimeo--otp#clear\">\n <%= t(\"components.otp.demo.clear\") %>\n </button>\n</div>\n"
247
247
  },
248
248
  "stimeo--overflow-indicator": {
249
249
  "file": "examples/overflow_indicator/_demo.html.erb",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schemaVersion": 12,
3
- "packageVersion": "0.7.0",
3
+ "packageVersion": "0.8.0",
4
4
  "controllers": {
5
5
  "stimeo--accordion": {
6
6
  "targets": [
@@ -167,7 +167,8 @@
167
167
  ],
168
168
  "events": [
169
169
  "submit",
170
- "done"
170
+ "done",
171
+ "reconcile"
171
172
  ],
172
173
  "requiredTargets": [],
173
174
  "conditionalTargets": [],
@@ -349,7 +350,15 @@
349
350
  "interval",
350
351
  "loop"
351
352
  ],
352
- "valueConstraints": [],
353
+ "valueConstraints": [
354
+ {
355
+ "value": "interval",
356
+ "type": "number",
357
+ "finite": true,
358
+ "greaterThan": 0,
359
+ "suggestion": "Set interval to a finite number of milliseconds greater than 0 — anything else falls back to 5000 at runtime."
360
+ }
361
+ ],
353
362
  "valueRelations": [],
354
363
  "actions": [
355
364
  "goto",
@@ -367,10 +376,24 @@
367
376
  "reconcile"
368
377
  ],
369
378
  "requiredTargets": [
370
- "slide",
371
- "picker"
379
+ "slide"
380
+ ],
381
+ "conditionalTargets": [
382
+ {
383
+ "whenPresent": "prev",
384
+ "require": [
385
+ "next"
386
+ ],
387
+ "suggestion": "Add the \"next\" button that pairs with \"prev\" — a carousel the reader can only step backwards through cannot reach the slides ahead of it."
388
+ },
389
+ {
390
+ "whenPresent": "next",
391
+ "require": [
392
+ "prev"
393
+ ],
394
+ "suggestion": "Add the \"prev\" button that pairs with \"next\" — a carousel the reader can only step forwards through cannot return to a slide they passed."
395
+ }
372
396
  ],
373
- "conditionalTargets": [],
374
397
  "requiredActions": [],
375
398
  "actionCompletion": [],
376
399
  "a11y": [
@@ -400,7 +423,27 @@
400
423
  "values": [
401
424
  "tabpanel"
402
425
  ],
403
- "suggestion": "Add role=\"tabpanel\" to each slide target."
426
+ "whenContains": {
427
+ "target": "picker",
428
+ "within": "scope",
429
+ "min": 1
430
+ },
431
+ "suggestion": "Add role=\"tabpanel\" to each slide target — its picker is the tab."
432
+ },
433
+ {
434
+ "target": "slide",
435
+ "attrs": [
436
+ "role"
437
+ ],
438
+ "values": [
439
+ "group"
440
+ ],
441
+ "whenContains": {
442
+ "target": "picker",
443
+ "within": "scope",
444
+ "max": 0
445
+ },
446
+ "suggestion": "Add role=\"group\" to each slide target — a carousel with no pickers has no tabs, so its slides are groups rather than tabpanels."
404
447
  },
405
448
  {
406
449
  "target": "slide",
@@ -433,7 +476,44 @@
433
476
  ],
434
477
  "keyboard": [],
435
478
  "hosts": [],
436
- "managedAria": [],
479
+ "managedAria": [
480
+ {
481
+ "target": "playToggle",
482
+ "attrs": [
483
+ "aria-pressed"
484
+ ],
485
+ "suggestion": "Remove aria-pressed from the playToggle target — the controller mirrors the autoplay Value onto it. Set data-stimeo--carousel-autoplay-value to choose the initial state."
486
+ },
487
+ {
488
+ "target": "playToggle",
489
+ "attrs": [
490
+ "aria-disabled"
491
+ ],
492
+ "suggestion": "Remove aria-disabled from the playToggle target — the controller marks it when the carousel has no slide left to rotate to."
493
+ },
494
+ {
495
+ "target": "prev",
496
+ "attrs": [
497
+ "aria-disabled"
498
+ ],
499
+ "suggestion": "Remove aria-disabled from the prev target — the controller marks it at the first slide of a non-looping carousel."
500
+ },
501
+ {
502
+ "target": "next",
503
+ "attrs": [
504
+ "aria-disabled"
505
+ ],
506
+ "suggestion": "Remove aria-disabled from the next target — the controller marks it at the last slide of a non-looping carousel."
507
+ },
508
+ {
509
+ "target": "viewport",
510
+ "attrs": [
511
+ "aria-live",
512
+ "aria-atomic"
513
+ ],
514
+ "suggestion": "Remove aria-live and aria-atomic from the viewport target — the controller switches the region between off while rotating and polite while stopped."
515
+ }
516
+ ],
437
517
  "compositions": [],
438
518
  "companions": [],
439
519
  "targetDeclarations": [],
@@ -447,6 +527,20 @@
447
527
  ],
448
528
  "max": 1,
449
529
  "suggestion": "Leave aria-selected=\"true\" on exactly one picker and set the others to \"false\" — the carousel shows one slide, and connect keeps the first."
530
+ },
531
+ {
532
+ "within": "",
533
+ "target": "playToggle",
534
+ "min": 1,
535
+ "when": {
536
+ "value": "autoplay",
537
+ "type": "boolean",
538
+ "equals": [
539
+ "true"
540
+ ],
541
+ "default": "false"
542
+ },
543
+ "suggestion": "Add a \"playToggle\" button wired to stimeo--carousel#togglePlay — a carousel that rotates on its own needs a control that stops it (WCAG 2.2.2)."
450
544
  }
451
545
  ],
452
546
  "forbiddenAria": []
@@ -1468,7 +1562,8 @@
1468
1562
  "events": [
1469
1563
  "progress",
1470
1564
  "done",
1471
- "error"
1565
+ "error",
1566
+ "reconcile"
1472
1567
  ],
1473
1568
  "requiredTargets": [
1474
1569
  "list",
@@ -1719,12 +1814,21 @@
1719
1814
  "list",
1720
1815
  "item",
1721
1816
  "itemTemplate",
1722
- "status"
1817
+ "name",
1818
+ "thumb",
1819
+ "remove"
1723
1820
  ],
1724
1821
  "values": [
1725
1822
  "maxSize",
1726
1823
  "maxFiles",
1727
- "dragLabel"
1824
+ "allowDuplicates",
1825
+ "announceDragText",
1826
+ "announceAddedText",
1827
+ "announceRemovedText",
1828
+ "announceRejectedTypeText",
1829
+ "announceRejectedSizeText",
1830
+ "announceRejectedDuplicateText",
1831
+ "announceRejectedCountText"
1728
1832
  ],
1729
1833
  "valueConstraints": [],
1730
1834
  "valueRelations": [],
@@ -1737,7 +1841,8 @@
1737
1841
  ],
1738
1842
  "events": [
1739
1843
  "change",
1740
- "reject"
1844
+ "reject",
1845
+ "reconcile"
1741
1846
  ],
1742
1847
  "requiredTargets": [
1743
1848
  "input",
@@ -1757,6 +1862,16 @@
1757
1862
  "itemTemplate"
1758
1863
  ],
1759
1864
  "suggestion": "Add an \"itemTemplate\" target — a \"list\" has no item markup to clone without it."
1865
+ },
1866
+ {
1867
+ "whenPresent": "itemTemplate",
1868
+ "require": [
1869
+ "item",
1870
+ "name",
1871
+ "remove"
1872
+ ],
1873
+ "requireInside": true,
1874
+ "suggestion": "Complete the item template: inside it, it needs an \"item\" root, a \"name\" element for the file name, and a \"remove\" button. Parts declared outside the template never reach the clone, so no file can render."
1760
1875
  }
1761
1876
  ],
1762
1877
  "requiredActions": [],
@@ -1823,7 +1938,8 @@
1823
1938
  ],
1824
1939
  "events": [
1825
1940
  "show",
1826
- "dismiss"
1941
+ "dismiss",
1942
+ "reconcile"
1827
1943
  ],
1828
1944
  "requiredTargets": [
1829
1945
  "region"
@@ -1964,7 +2080,8 @@
1964
2080
  "actions": [],
1965
2081
  "events": [
1966
2082
  "start",
1967
- "end"
2083
+ "end",
2084
+ "reconcile"
1968
2085
  ],
1969
2086
  "requiredTargets": [],
1970
2087
  "conditionalTargets": [],
@@ -2083,7 +2200,8 @@
2083
2200
  "format"
2084
2201
  ],
2085
2202
  "events": [
2086
- "change"
2203
+ "change",
2204
+ "reconcile"
2087
2205
  ],
2088
2206
  "requiredTargets": [],
2089
2207
  "conditionalTargets": [],
@@ -2801,7 +2919,8 @@
2801
2919
  ],
2802
2920
  "events": [
2803
2921
  "add",
2804
- "remove"
2922
+ "remove",
2923
+ "reconcile"
2805
2924
  ],
2806
2925
  "requiredTargets": [
2807
2926
  "list",
@@ -2932,7 +3051,6 @@
2932
3051
  "error"
2933
3052
  ],
2934
3053
  "values": [
2935
- "length",
2936
3054
  "pattern"
2937
3055
  ],
2938
3056
  "valueConstraints": [],
@@ -2940,12 +3058,15 @@
2940
3058
  "actions": [
2941
3059
  "onInput",
2942
3060
  "onKeydown",
2943
- "onPaste"
3061
+ "onPaste",
3062
+ "onPointerDown",
3063
+ "clear"
2944
3064
  ],
2945
3065
  "events": [
2946
3066
  "change",
2947
3067
  "complete",
2948
- "invalid"
3068
+ "invalid",
3069
+ "reconcile"
2949
3070
  ],
2950
3071
  "requiredTargets": [
2951
3072
  "field"
@@ -2953,10 +3074,46 @@
2953
3074
  "conditionalTargets": [],
2954
3075
  "requiredActions": [],
2955
3076
  "actionCompletion": [],
2956
- "a11y": [],
3077
+ "a11y": [
3078
+ {
3079
+ "target": "",
3080
+ "attrs": [
3081
+ "role"
3082
+ ],
3083
+ "values": [
3084
+ "group"
3085
+ ],
3086
+ "suggestion": "Add role=\"group\" to the controller element."
3087
+ },
3088
+ {
3089
+ "target": "",
3090
+ "attrs": [
3091
+ "aria-label",
3092
+ "aria-labelledby"
3093
+ ],
3094
+ "suggestion": "Name the passcode group via aria-label or aria-labelledby."
3095
+ },
3096
+ {
3097
+ "target": "field",
3098
+ "attrs": [
3099
+ "aria-label",
3100
+ "aria-labelledby"
3101
+ ],
3102
+ "suggestion": "Name each field target after its position via aria-label or aria-labelledby."
3103
+ }
3104
+ ],
2957
3105
  "keyboard": [],
2958
3106
  "hosts": [],
2959
- "managedAria": [],
3107
+ "managedAria": [
3108
+ {
3109
+ "target": "field",
3110
+ "attrs": [
3111
+ "aria-invalid",
3112
+ "aria-errormessage"
3113
+ ],
3114
+ "suggestion": "Remove aria-invalid and aria-errormessage from the field target — the controller writes them while it reports discarded input and returns them once the input is accepted."
3115
+ }
3116
+ ],
2960
3117
  "compositions": [],
2961
3118
  "companions": [],
2962
3119
  "targetDeclarations": [],
@@ -4428,7 +4585,8 @@
4428
4585
  "events": [
4429
4586
  "hide",
4430
4587
  "show",
4431
- "timeout"
4588
+ "timeout",
4589
+ "reconcile"
4432
4590
  ],
4433
4591
  "requiredTargets": [],
4434
4592
  "conditionalTargets": [],
@@ -4595,7 +4753,8 @@
4595
4753
  ],
4596
4754
  "events": [
4597
4755
  "start",
4598
- "end"
4756
+ "end",
4757
+ "reconcile"
4599
4758
  ],
4600
4759
  "requiredTargets": [],
4601
4760
  "conditionalTargets": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stimeo-ui",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "Headless Stimulus UI framework for Ruby on Rails — behavior-only, accessible components driven by HTML attributes.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -84,7 +84,7 @@
84
84
  }
85
85
  },
86
86
  "devDependencies": {
87
- "@biomejs/biome": "2.5.7",
87
+ "@biomejs/biome": "2.5.10",
88
88
  "@floating-ui/dom": "1.8.0",
89
89
  "@rails/actioncable": "8.1.301",
90
90
  "@types/rails__actioncable": "8.0.3",