stimeo-ui 0.7.0 → 0.9.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.
- package/CHANGELOG.md +151 -0
- package/dist/controllers/auto_submit_controller.d.ts +14 -3
- package/dist/controllers/auto_submit_controller.js +94 -16
- package/dist/controllers/auto_submit_controller.js.map +1 -1
- package/dist/controllers/bulk_select_controller.d.ts +62 -15
- package/dist/controllers/bulk_select_controller.js +139 -28
- package/dist/controllers/bulk_select_controller.js.map +1 -1
- package/dist/controllers/carousel_controller.d.ts +81 -47
- package/dist/controllers/carousel_controller.js +451 -100
- package/dist/controllers/carousel_controller.js.map +1 -1
- package/dist/controllers/clipboard_controller.d.ts +48 -15
- package/dist/controllers/clipboard_controller.js +102 -20
- package/dist/controllers/clipboard_controller.js.map +1 -1
- package/dist/controllers/color_picker_controller.d.ts +37 -6
- package/dist/controllers/color_picker_controller.js +180 -43
- package/dist/controllers/color_picker_controller.js.map +1 -1
- package/dist/controllers/currency_input_controller.d.ts +39 -5
- package/dist/controllers/currency_input_controller.js +305 -74
- package/dist/controllers/currency_input_controller.js.map +1 -1
- package/dist/controllers/data_grid_controller.d.ts +25 -9
- package/dist/controllers/data_grid_controller.js +150 -24
- package/dist/controllers/data_grid_controller.js.map +1 -1
- package/dist/controllers/direct_upload_controller.d.ts +3 -1
- package/dist/controllers/direct_upload_controller.js +12 -2
- package/dist/controllers/direct_upload_controller.js.map +1 -1
- package/dist/controllers/editable_controller.d.ts +34 -9
- package/dist/controllers/editable_controller.js +83 -30
- package/dist/controllers/editable_controller.js.map +1 -1
- package/dist/controllers/file_dropzone_controller.d.ts +123 -29
- package/dist/controllers/file_dropzone_controller.js +386 -63
- package/dist/controllers/file_dropzone_controller.js.map +1 -1
- package/dist/controllers/filter_controller.d.ts +15 -3
- package/dist/controllers/filter_controller.js +32 -1
- package/dist/controllers/filter_controller.js.map +1 -1
- package/dist/controllers/flash_controller.d.ts +3 -1
- package/dist/controllers/flash_controller.js +3 -1
- package/dist/controllers/flash_controller.js.map +1 -1
- package/dist/controllers/frame_loading_controller.d.ts +4 -2
- package/dist/controllers/frame_loading_controller.js +2 -1
- package/dist/controllers/frame_loading_controller.js.map +1 -1
- package/dist/controllers/input_mask_controller.d.ts +42 -16
- package/dist/controllers/input_mask_controller.js +251 -76
- package/dist/controllers/input_mask_controller.js.map +1 -1
- package/dist/controllers/masonry_controller.d.ts +34 -5
- package/dist/controllers/masonry_controller.js +129 -17
- package/dist/controllers/masonry_controller.js.map +1 -1
- package/dist/controllers/nested_form_controller.d.ts +51 -14
- package/dist/controllers/nested_form_controller.js +450 -42
- package/dist/controllers/nested_form_controller.js.map +1 -1
- package/dist/controllers/otp_controller.d.ts +64 -25
- package/dist/controllers/otp_controller.js +485 -114
- package/dist/controllers/otp_controller.js.map +1 -1
- package/dist/controllers/reset_before_cache_controller.d.ts +25 -2
- package/dist/controllers/reset_before_cache_controller.js +51 -5
- package/dist/controllers/reset_before_cache_controller.js.map +1 -1
- package/dist/controllers/resizable_controller.d.ts +23 -7
- package/dist/controllers/resizable_controller.js +128 -55
- package/dist/controllers/resizable_controller.js.map +1 -1
- package/dist/controllers/spinner_controller.d.ts +3 -2
- package/dist/controllers/spinner_controller.js +7 -5
- package/dist/controllers/spinner_controller.js.map +1 -1
- package/dist/controllers/submit_once_controller.d.ts +4 -1
- package/dist/controllers/submit_once_controller.js +3 -1
- package/dist/controllers/submit_once_controller.js.map +1 -1
- package/dist/controllers/textarea_autosize_controller.d.ts +30 -10
- package/dist/controllers/textarea_autosize_controller.js +131 -3
- package/dist/controllers/textarea_autosize_controller.js.map +1 -1
- package/dist/index.js +2764 -923
- package/dist/index.js.map +1 -1
- package/dist/inspector/cli.d.ts +8 -1
- package/dist/inspector/cli.js +5 -1
- package/dist/inspector/cli.js.map +1 -1
- package/dist/inspector/cli_bin.js +5 -1
- package/dist/inspector/cli_bin.js.map +1 -1
- package/dist/inspector/examples.json +24 -24
- package/dist/inspector/manifest.json +193 -30
- package/package.json +2 -2
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
},
|
|
24
24
|
"stimeo--auto-submit": {
|
|
25
25
|
"file": "examples/auto_submit/_demo.html.erb",
|
|
26
|
-
"source": "<%# Markup for the debounced auto-submit demo.\n The controller debounces input and then calls form.requestSubmit(). This\n Playground has no search backend, so demo.js cancels the native submit, filters\n the list in the browser, completes the Turbo lifecycle with a synthetic\n turbo:submit-end (so the controller clears aria-busy and emits done), and\n announces the result count through
|
|
26
|
+
"source": "<%# Markup for the debounced auto-submit demo.\n The controller debounces input and then calls form.requestSubmit(). This\n Playground has no search backend, so demo.js cancels the native submit, filters\n the list in the browser, completes the Turbo lifecycle with a synthetic\n turbo:submit-end (so the controller clears aria-busy and emits done), and\n announces the result count through the shared stimeo--announcer your app seats\n once, in its layout (the result count is read out without moving focus). %>\n<%\n items = t(\"components.auto_submit.demo.items\")\n count_template = t(\"components.auto_submit.demo.count_template\")\n%>\n<div class=\"auto-submit-demo\">\n <form class=\"auto-submit-demo__form\" role=\"search\" action=\"#\"\n data-controller=\"stimeo--auto-submit\"\n data-stimeo--auto-submit-debounce-value=\"400\"\n data-action=\"input->stimeo--auto-submit#submit\"\n data-auto-submit-count=\"<%= count_template %>\">\n <label class=\"auto-submit-demo__label\" for=\"auto-submit-q\">\n <%= t(\"components.auto_submit.demo.label\") %>\n </label>\n <input class=\"demo-input\" id=\"auto-submit-q\" type=\"search\" name=\"q\"\n autocomplete=\"off\" data-auto-submit-demo=\"input\"\n placeholder=\"<%= t(\"components.auto_submit.demo.placeholder\") %>\">\n </form>\n\n <ul class=\"auto-submit-demo__list\" data-auto-submit-demo=\"list\">\n <% items.each do |item| %>\n <li data-auto-submit-value=\"<%= item.downcase %>\"><%= item %></li>\n <% end %>\n </ul>\n</div>\n"
|
|
27
27
|
},
|
|
28
28
|
"stimeo--avatar": {
|
|
29
29
|
"file": "examples/avatar/_demo.html.erb",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
},
|
|
36
36
|
"stimeo--bulk-select": {
|
|
37
37
|
"file": "examples/bulk_select/_demo.html.erb",
|
|
38
|
-
"source": "<%# Markup for the bulk-select / batch action bar demo.\n The controller links the select-all box to the rows (with indeterminate), reveals\n the sticky bar once one or more rows are selected, keeps the count, and
|
|
38
|
+
"source": "<%# Markup for the bulk-select / batch action bar demo.\n The controller links the select-all box to the rows (with indeterminate), reveals\n the sticky bar once one or more rows are selected, keeps the count, and hands that\n count to the shared stimeo--announcer your app seats once, in its layout, using the\n wording in announce-text-value (the bar is revealed with its count already in place,\n which a live region of its own could not announce). Row changes are handled by\n delegation, so the \"Add row\" button (demo.js) works without per-row wiring, and rows\n it appends are reconciled on their own. %>\n<%# Sample rows come from the locale file so the table reads in the page language. %>\n<%\n rows = t(\"components.bulk_select.demo.rows\")\n announce_text = t(\"components.bulk_select.demo.announce_text\")\n%>\n<%# The two-stage pattern only reads if the page says it is one page of many, so the\n demo states where total-count-value's 42 comes from. Nothing here is required by\n the controller — a single-page table simply omits the selectAllPages control. %>\n<div class=\"bulk-demo\" data-controller=\"stimeo--bulk-select\"\n data-stimeo--bulk-select-total-count-value=\"42\"\n data-stimeo--bulk-select-announce-text-value=\"<%= announce_text %>\">\n <p class=\"bulk-demo__context\"><%= t(\"components.bulk_select.demo.page_context\") %></p>\n\n <table class=\"bulk-demo__table\">\n <thead>\n <tr>\n <th scope=\"col\">\n <input type=\"checkbox\" data-stimeo--bulk-select-target=\"all\"\n aria-label=\"<%= t(\"components.bulk_select.demo.select_all\") %>\">\n </th>\n <th scope=\"col\"><%= t(\"components.bulk_select.demo.col_name\") %></th>\n <th scope=\"col\"><%= t(\"components.bulk_select.demo.col_role\") %></th>\n </tr>\n </thead>\n <tbody data-bulk-demo=\"rows\">\n <% rows.each do |row| %>\n <% row_label = t(\"components.bulk_select.demo.select_row\", name: row[:name]) %>\n <tr>\n <td>\n <input type=\"checkbox\" data-stimeo--bulk-select-target=\"item\"\n aria-label=\"<%= row_label %>\">\n </td>\n <td><%= row[:name] %></td>\n <td><%= row[:role] %></td>\n </tr>\n <% end %>\n </tbody>\n </table>\n\n <%# Localized templates for demo.js's appended rows ({n} = row number, {name} = the\n generated name), so dynamically-added rows match the page locale too. %>\n <button class=\"demo-trigger\" type=\"button\" data-bulk-demo=\"add\"\n data-bulk-add-name=\"<%= t(\"components.bulk_select.demo.add_name_template\") %>\"\n data-bulk-add-role=\"<%= t(\"components.bulk_select.demo.add_role\") %>\"\n data-bulk-add-label=\"<%= t(\"components.bulk_select.demo.add_row_label\") %>\">\n <%= t(\"components.bulk_select.demo.add_row\") %>\n </button>\n\n <%# Sticky batch action bar. It carries role=\"toolbar\", so stimeo--toolbar is composed\n onto it for the arrow-key movement and single tab stop that role calls for. %>\n <div class=\"bulk-demo__bar\" data-stimeo--bulk-select-target=\"bar\" hidden\n role=\"toolbar\" data-controller=\"stimeo--toolbar\"\n aria-label=\"<%= t(\"components.bulk_select.demo.bar_label\") %>\">\n <span class=\"bulk-demo__count\">\n <span data-stimeo--bulk-select-target=\"count\">0</span>\n <%= t(\"components.bulk_select.demo.selected_suffix\") %>\n </span>\n <button class=\"demo-trigger\" type=\"button\"\n data-stimeo--bulk-select-target=\"selectAllPages\"\n data-stimeo--toolbar-target=\"control\"\n data-action=\"click->stimeo--bulk-select#selectAllPages\">\n <%= t(\"components.bulk_select.demo.select_all_pages\") %>\n </button>\n <button class=\"demo-trigger\" type=\"button\"\n data-stimeo--toolbar-target=\"control\"\n data-action=\"click->stimeo--bulk-select#clear\">\n <%= t(\"components.bulk_select.demo.clear\") %>\n </button>\n </div>\n\n <%# Shown only while the whole set is selected. CSS reads data-all-pages on the\n root, so the demo needs no script to keep this in step with the mode. %>\n <p class=\"bulk-demo__all-pages-note\"><%= t(\"components.bulk_select.demo.all_pages_note\") %></p>\n</div>\n"
|
|
39
39
|
},
|
|
40
40
|
"stimeo--calendar": {
|
|
41
41
|
"file": "examples/calendar/_demo.html.erb",
|
|
@@ -43,11 +43,11 @@
|
|
|
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,
|
|
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",
|
|
50
|
-
"source": "<%# Markup for the character-counter demo. The visible output updates on every\n keystroke, while the settled remaining count is debounced into the shared\n stimeo--announcer
|
|
50
|
+
"source": "<%# Markup for the character-counter demo. The visible output updates on every\n keystroke, while the settled remaining count is debounced into the shared\n stimeo--announcer your app seats once, in its layout (the settled count is the\n transition worth reading; the visible output is not itself a live region). This\n demo's CSS styles the root's near/over hooks and the over-limit aria-invalid;\n watching the field needs no consumer JavaScript. %>\n<% announcement = t(\"components.character_counter.demo.announcement\") %>\n<div class=\"character-counter-demo\">\n <label class=\"character-counter-demo__label\" for=\"cc-message\">\n <%= t(\"components.character_counter.demo.label\") %>\n </label>\n <div\n class=\"character-counter\"\n data-controller=\"stimeo--character-counter\"\n data-stimeo--character-counter-max-value=\"140\"\n data-stimeo--character-counter-warn-at-value=\"20\"\n data-stimeo--character-counter-announce-text-value=\"<%= announcement %>\">\n <textarea\n id=\"cc-message\"\n class=\"character-counter__input\"\n rows=\"3\"\n data-stimeo--character-counter-target=\"input\"\n aria-describedby=\"cc-message-count\"\n placeholder=\"<%= t('components.character_counter.demo.placeholder') %>\"></textarea>\n <span\n id=\"cc-message-count\"\n class=\"character-counter__output\"\n data-stimeo--character-counter-target=\"output\"></span>\n </div>\n</div>\n"
|
|
51
51
|
},
|
|
52
52
|
"stimeo--checkbox": {
|
|
53
53
|
"file": "examples/checkbox/_demo.html.erb",
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
},
|
|
56
56
|
"stimeo--clipboard": {
|
|
57
57
|
"file": "examples/clipboard/_demo.html.erb",
|
|
58
|
-
"source": "<%# Markup for the clipboard (copy & completion feedback) demo.\n Copies via navigator.clipboard and
|
|
58
|
+
"source": "<%# Markup for the clipboard (copy & completion feedback) demo.\n Copies via navigator.clipboard, shows the result in the visible feedback slot,\n and hands the announcement to the shared stimeo--announcer your app seats once,\n in its layout. Keep the feedback slot free of live-region semantics — the\n announcer already reads the result, so a role=\"status\" here would say it twice.\n For localization the visible labels come from the copied-label / error-label\n Values and the spoken ones from the announce-copied-text / announce-error-text\n Values. The look per data-state (idle / copied / error) lives in demo.css. %>\n<div\n class=\"clipboard\"\n data-controller=\"stimeo--clipboard\"\n data-stimeo--clipboard-feedback-duration-value=\"2000\"\n data-stimeo--clipboard-copied-label-value=\"<%= t('components.clipboard.demo.copied') %>\"\n data-stimeo--clipboard-error-label-value=\"<%= t('components.clipboard.demo.error') %>\"\n data-stimeo--clipboard-announce-copied-text-value=\"<%= t(\n 'components.clipboard.demo.announce_copied'\n ) %>\"\n data-stimeo--clipboard-announce-error-text-value=\"<%= t(\n 'components.clipboard.demo.announce_error'\n ) %>\">\n <input\n class=\"clipboard__source\"\n type=\"text\"\n readonly\n aria-label=\"<%= t('components.clipboard.demo.source_label') %>\"\n value=\"https://stimeo.example/share/abc123\"\n data-stimeo--clipboard-target=\"source\">\n <button\n class=\"clipboard__button\"\n type=\"button\"\n data-stimeo--clipboard-target=\"button\"\n data-action=\"click->stimeo--clipboard#copy\">\n <%= t(\"components.clipboard.demo.button\") %>\n </button>\n <span class=\"clipboard__feedback\" data-stimeo--clipboard-target=\"feedback\"></span>\n</div>\n"
|
|
59
59
|
},
|
|
60
60
|
"stimeo--collapsible": {
|
|
61
61
|
"file": "examples/collapsible/_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 data-value-text carries the announced wording, so this catalog announces the\n channel in the reader's own language instead of the library's English. %>\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 data-value-text=\"<%= t(\"components.color_picker.demo.value_text.#{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",
|
|
@@ -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; 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
|
|
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 stimeo--announcer your app seats once, in its layout,\n reads it out, so it is said exactly once (reaching zero is the transition worth\n reading; the ticking numbers in between are state the reader can query).\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</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 — including one upload that fails\n (error followed by end, the order ActiveStorage really uses). The library\n clones the row template per file, updates aria-valuenow / aria-valuetext /\n data-upload-state / the --stimeo--upload-progress var, and hands completion\n and failure announcements to the shared announcer
|
|
114
|
+
"source": "<%# Direct upload demo: there is no server, so demo.js fires the ActiveStorage\n direct-upload:* events to drive the rows — including one upload that fails\n (error followed by end, the order ActiveStorage really uses). The library\n clones the row template per file, updates aria-valuenow / aria-valuetext /\n data-upload-state / the --stimeo--upload-progress var, and hands completion\n and failure announcements to the shared stimeo--announcer your app seats once,\n in its layout (completion and failure are the transitions worth reading;\n per-tick progress stays on each row's aria-valuenow). This demo styles the\n bars. %>\n<% done_text = t(\"components.direct_upload.demo.done\") %>\n<% error_text = t(\"components.direct_upload.demo.error\") %>\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-announce-done-text-value=\"<%= done_text %>\"\n data-stimeo--direct-upload-announce-error-text-value=\"<%= error_text %>\">\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 </div>\n</div>\n"
|
|
115
115
|
},
|
|
116
116
|
"stimeo--dirty-form": {
|
|
117
117
|
"file": "examples/dirty_form/_demo.html.erb",
|
|
@@ -131,15 +131,15 @@
|
|
|
131
131
|
},
|
|
132
132
|
"stimeo--editable": {
|
|
133
133
|
"file": "examples/editable/_demo.html.erb",
|
|
134
|
-
"source": "<%# Markup for the editable (inline editing) demo.\n Click / Enter / F2 on the display element (a button) enters edit mode, focusing
|
|
134
|
+
"source": "<%# Markup for the editable (inline editing) demo.\n Click / Enter / Space / F2 on the display element (a button) enters edit mode, focusing\n the input and selecting all. The library handles mode switching (data-mode), focus /\n select-all, and the commit event; the look lives in demo.css.\n Two variants are shown:\n 1. Single-line <input> with Save / Cancel buttons: Enter saves, Escape cancels, and\n the buttons reach the same paths without a keyboard. Focus moving to a button\n inside the editor does not commit, so Cancel really cancels.\n 2. Multiline <textarea>: Enter inserts a newline, Ctrl+Enter saves, Escape cancels.\n submitOnBlur=true also saves on blur for both, when focus leaves the editor. %>\n<div class=\"editable-demo\">\n <%# 1. Single-line inline edit. %>\n <div class=\"editable\" data-controller=\"stimeo--editable\"\n data-stimeo--editable-submit-on-blur-value=\"true\">\n <span class=\"editable__hint\"><%= t(\"components.editable.demo.hint\") %></span>\n <button\n type=\"button\"\n class=\"editable__display\"\n aria-label=\"<%= t(\"components.editable.demo.aria_label\") %>\"\n data-placeholder=\"<%= t(\"components.editable.demo.placeholder\") %>\"\n data-stimeo--editable-target=\"display\"\n data-action=\"click->stimeo--editable#edit keydown->stimeo--editable#onDisplayKeydown\">\n <%= t(\"components.editable.demo.value\") %>\n </button>\n <input\n type=\"text\"\n class=\"editable__input\"\n hidden\n aria-label=\"<%= t(\"components.editable.demo.aria_label\") %>\"\n data-stimeo--editable-target=\"input\"\n data-action=\"keydown->stimeo--editable#onKeydown\" />\n <%# Only meaningful while editing; demo.css hides the row in display mode. %>\n <div class=\"editable__controls\">\n <button\n type=\"button\"\n class=\"demo-trigger demo-trigger--primary\"\n data-action=\"click->stimeo--editable#save\"><%= t(\n \"components.editable.demo.save\"\n ) %></button>\n <button\n type=\"button\"\n class=\"demo-trigger\"\n data-action=\"click->stimeo--editable#cancel\"><%= t(\n \"components.editable.demo.cancel\"\n ) %></button>\n </div>\n </div>\n\n <%# 2. Multiline inline edit. The controller detects the <textarea> and treats Enter as a\n newline, so saving is Ctrl+Enter (or blur). The display uses white-space: pre-line so\n saved line breaks render. %>\n <div class=\"editable\" data-controller=\"stimeo--editable\"\n data-stimeo--editable-submit-on-blur-value=\"true\">\n <span class=\"editable__hint\"><%= t(\"components.editable.demo.multiline.hint\") %></span>\n <button\n type=\"button\"\n class=\"editable__display editable__display--multiline\"\n aria-label=\"<%= t(\"components.editable.demo.multiline.aria_label\") %>\"\n data-placeholder=\"<%= t(\"components.editable.demo.multiline.placeholder\") %>\"\n data-stimeo--editable-target=\"display\"\n data-action=\"click->stimeo--editable#edit\n keydown->stimeo--editable#onDisplayKeydown\"><%= t(\n \"components.editable.demo.multiline.value\"\n ) %></button>\n <textarea\n class=\"editable__input editable__input--multiline\"\n hidden\n rows=\"3\"\n aria-label=\"<%= t(\"components.editable.demo.multiline.aria_label\") %>\"\n data-stimeo--editable-target=\"input\"\n data-action=\"keydown->stimeo--editable#onKeydown\"></textarea>\n </div>\n</div>\n"
|
|
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, and the crossing is read out
|
|
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 — without\n moving focus — through the shared stimeo--announcer your app seats once, in its layout\n (a placeholder that only becomes visible at the moment of the change is not reliably\n read, so the wording goes to a region that stands in the page independently of it). %>\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</div>\n"
|
|
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
|
|
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 stimeo--announcer your app seats once,\n in its layout (additions, rejections, removals, and the drag affordance are the\n transitions worth reading; the dropzone owns no live region of its own).\n Drag & drop is 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</div>\n"
|
|
143
143
|
},
|
|
144
144
|
"stimeo--filter": {
|
|
145
145
|
"file": "examples/filter/_demo.html.erb",
|
|
@@ -155,15 +155,15 @@
|
|
|
155
155
|
},
|
|
156
156
|
"stimeo--form-field": {
|
|
157
157
|
"file": "examples/form_field/_demo.html.erb",
|
|
158
|
-
"source": "<%# Markup for the form-field demo.\n Associates label / description / error with the control via aria-describedby /\n aria-invalid / aria-errormessage. Showing/clearing the error uses the action\n parameter (message) and #clearError. The look switches on
|
|
158
|
+
"source": "<%# Markup for the form-field demo.\n Associates label / description / error with the control via aria-describedby /\n aria-invalid / aria-errormessage. Showing/clearing the error uses the action\n parameter (message) and #clearError. The error text is spoken assertively by the\n shared stimeo--announcer your app seats once, in its layout — a region that stands\n in the accessibility tree ahead of the first error, which is what makes the message\n observable. The look switches on data-stimeo--form-field-invalid and aria-invalid. %>\n<div class=\"form-field-demo\" data-controller=\"stimeo--form-field\"\n data-stimeo--form-field-focus-on-error-value=\"true\">\n <label class=\"form-field-demo__label\" for=\"ff-email\">\n <%= t(\"components.form_field.demo.label\") %>\n </label>\n\n <input class=\"form-field-demo__control\" id=\"ff-email\" type=\"email\" aria-invalid=\"false\"\n placeholder=\"<%= t('components.form_field.demo.placeholder') %>\"\n data-stimeo--form-field-target=\"control\" />\n\n <%# Helper description. It is assigned an id and linked into the control's\n aria-describedby. %>\n <p class=\"form-field-demo__description\" data-stimeo--form-field-target=\"description\">\n <%= t(\"components.form_field.demo.hint\") %>\n </p>\n\n <%# Note clarifying the demo's intent (this part only wires up the error state;\n it doesn't validate or clear the value). %>\n <p class=\"form-field-demo__note\"><%= t(\"components.form_field.demo.note\") %></p>\n\n <%# Visual error region. Starts hidden; speech is delegated to the shared announcer. %>\n <p class=\"form-field-demo__error\" hidden\n data-stimeo--form-field-target=\"error\"></p>\n\n <div class=\"form-field-demo__actions\">\n <%# setError reads the message from the action parameter (no consumer JS needed). %>\n <button type=\"button\" class=\"demo-trigger\"\n data-stimeo--form-field-message-param=\"<%= t('components.form_field.demo.error') %>\"\n data-action=\"click->stimeo--form-field#setError\">\n <%= t(\"components.form_field.demo.validate\") %>\n </button>\n <button type=\"button\" class=\"demo-trigger\"\n data-action=\"click->stimeo--form-field#clearError\">\n <%= t(\"components.form_field.demo.clear\") %>\n </button>\n </div>\n\n <%# Helper region that subscribes to the validate event and shows the valid\n state only (updated by demo.js). %>\n <p class=\"form-field-demo__status\" id=\"ff-status\" aria-live=\"polite\"\n data-valid-text=\"<%= t('components.form_field.demo.valid') %>\"></p>\n</div>\n"
|
|
159
159
|
},
|
|
160
160
|
"stimeo--form-validation": {
|
|
161
161
|
"file": "examples/form_validation/_demo.html.erb",
|
|
162
|
-
"source": "<%# Markup for the form-validation demo.\n stimeo--form-validation orchestrates WHEN native constraint validation runs\n (submit / blur / change / input) and routes each control's message into its\n stimeo--form-field visual error — the ARIA wiring stays in form-field, and\n the speech goes to the shared announcer
|
|
162
|
+
"source": "<%# Markup for the form-validation demo.\n stimeo--form-validation orchestrates WHEN native constraint validation runs\n (submit / blur / change / input) and routes each control's message into its\n stimeo--form-field visual error — the ARIA wiring stays in form-field, and\n the speech goes to the shared stimeo--announcer your app seats once, in its\n layout (one assertive receiver serves every field, and it is already in the\n accessibility tree before the first error, which is what makes the message\n observable).\n Rules are plain markup attributes (required, type=\"email\", plus the\n declarative disallow=\"whitespace\"); the wording comes from localized\n data-stimeo--form-validation-message-* overrides rather than the browser, so\n the catalog reads the same in both locales. The plan listbox joins through a\n validatable mirror: <input type=\"text\" hidden required> — the hidden\n ATTRIBUTE, not type=\"hidden\", which is barred from constraint validation.\n The authored novalidate keeps the no-JS fallback graceful (the controller\n preserves it). Valid submissions are kept on the page by demo.js so the\n catalog can show a success status. %>\n<form class=\"form-validation-demo\" action=\"#\" method=\"get\" novalidate\n data-controller=\"stimeo--form-validation\"\n data-stimeo--form-validation-stimeo--form-field-outlet=\".form-validation-demo__field\">\n\n <div class=\"form-validation-demo__field\" data-controller=\"stimeo--form-field\">\n <label class=\"form-validation-demo__label\" for=\"fv-email\">\n <%= t(\"components.form_validation.demo.email_label\") %>\n </label>\n <input class=\"form-validation-demo__control\" id=\"fv-email\" name=\"email\" type=\"email\"\n required placeholder=\"<%= t('components.form_validation.demo.email_placeholder') %>\"\n data-stimeo--form-validation-message-value-missing=\n \"<%= t('components.form_validation.demo.messages.email_required') %>\"\n data-stimeo--form-validation-message-type-mismatch=\n \"<%= t('components.form_validation.demo.messages.email_type') %>\"\n data-stimeo--form-field-target=\"control\" />\n <%# Visual error only. Speech goes through the shared stimeo--announcer. %>\n <p class=\"form-validation-demo__error\" hidden\n data-stimeo--form-field-target=\"error\"></p>\n </div>\n\n <div class=\"form-validation-demo__field\" data-controller=\"stimeo--form-field\">\n <label class=\"form-validation-demo__label\" for=\"fv-name\">\n <%= t(\"components.form_validation.demo.name_label\") %>\n </label>\n <input class=\"form-validation-demo__control\" id=\"fv-name\" name=\"name\" type=\"text\"\n required placeholder=\"<%= t('components.form_validation.demo.name_placeholder') %>\"\n data-stimeo--form-validation-disallow=\"whitespace\"\n data-stimeo--form-validation-message-value-missing=\n \"<%= t('components.form_validation.demo.messages.name_required') %>\"\n data-stimeo--form-validation-message-whitespace=\n \"<%= t('components.form_validation.demo.messages.name_whitespace') %>\"\n data-stimeo--form-field-target=\"control\" />\n <p class=\"form-validation-demo__error\" hidden\n data-stimeo--form-field-target=\"error\"></p>\n </div>\n\n <%# The listbox keeps its committed value in the mirror below. The form-field\n control is the visible trigger, so aria-invalid and invalid focus land there. %>\n <div class=\"form-validation-demo__field listbox\"\n data-controller=\"stimeo--form-field stimeo--listbox\">\n <span id=\"fv-plan-label\" class=\"form-validation-demo__label\">\n <%= t(\"components.form_validation.demo.plan_label\") %>\n </span>\n <button\n type=\"button\"\n class=\"demo-trigger listbox__trigger\"\n role=\"combobox\"\n aria-haspopup=\"listbox\"\n aria-expanded=\"false\"\n aria-controls=\"fv-plan-options\"\n aria-labelledby=\"fv-plan-label fv-plan-value\"\n data-stimeo--listbox-target=\"trigger\"\n data-stimeo--form-field-target=\"control\"\n data-action=\"click->stimeo--listbox#toggle keydown->stimeo--listbox#onTriggerKeydown\">\n <span id=\"fv-plan-value\" data-stimeo--listbox-target=\"value\">\n <%= t(\"components.form_validation.demo.plan_placeholder\") %>\n </span>\n <span class=\"listbox__chevron\" aria-hidden=\"true\">▾</span>\n </button>\n <ul id=\"fv-plan-options\" class=\"listbox__list\" role=\"listbox\"\n aria-labelledby=\"fv-plan-label\" hidden data-stimeo--listbox-target=\"list\">\n <% %w[free pro team].each_with_index do |plan, index| %>\n <li id=\"fv-plan-opt-<%= index %>\" class=\"listbox__option\" role=\"option\"\n aria-selected=\"false\" data-value=\"<%= plan %>\"\n data-stimeo--listbox-target=\"option\"\n data-action=\"click->stimeo--listbox#select\">\n <%= t(\"components.form_validation.demo.plans.#{plan}\") %>\n </li>\n <% end %>\n </ul>\n <%# Validatable mirror: the hidden attribute participates in constraint\n validation and submits with the form — type=\"hidden\" would do neither. %>\n <input type=\"text\" hidden required name=\"plan\"\n data-stimeo--form-validation-message-value-missing=\n \"<%= t('components.form_validation.demo.messages.plan_required') %>\"\n data-stimeo--listbox-target=\"field\" />\n <p class=\"form-validation-demo__error\" hidden\n data-stimeo--form-field-target=\"error\"></p>\n </div>\n\n <p class=\"form-validation-demo__note\"><%= t(\"components.form_validation.demo.note\") %></p>\n\n <button type=\"submit\" class=\"demo-trigger\">\n <%= t(\"components.form_validation.demo.submit\") %>\n </button>\n\n <%# Status line updated by demo.js; templates ride data-* so the copy stays bilingual. %>\n <p class=\"form-validation-demo__status\" id=\"fv-status\" aria-live=\"polite\"\n data-valid-text=\"<%= t('components.form_validation.demo.valid_status') %>\"\n data-invalid-template=\"<%= t('components.form_validation.demo.invalid_template') %>\"></p>\n</form>\n"
|
|
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
|
|
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 The skeleton is revealed at the moment of the change, which is not reliably read, so\n the loading / ready wording goes to the shared stimeo--announcer your app seats once,\n in its layout. This catalog has no Turbo backend, so demo.js fires the\n turbo:before-fetch-request → turbo:frame-load pair to reproduce a frame fetch. The\n library only toggles state and hidden; demo.css owns the skeleton bars and the dimmed\n 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</div>\n"
|
|
167
167
|
},
|
|
168
168
|
"stimeo--highlight": {
|
|
169
169
|
"file": "examples/highlight/_demo.html.erb",
|
|
@@ -215,11 +215,11 @@
|
|
|
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-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
|
|
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 Only a move into another segment reaches the shared stimeo--announcer your app seats\n once, in its layout — reading every value would be unusable.\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</div>\n"
|
|
219
219
|
},
|
|
220
220
|
"stimeo--multi-select": {
|
|
221
221
|
"file": "examples/multi_select/_demo.html.erb",
|
|
222
|
-
"source": "<%# Markup for the multi-select (multiple-selection combobox) demo.\n Filter candidates as you type, select several, and show the selected ones as chips.\n Focus stays on the input; the active candidate is shown via aria-activedescendant\n and selection via aria-selected. The library handles filtering, multi-select\n toggling, chip create/remove, roving, shared
|
|
222
|
+
"source": "<%# Markup for the multi-select (multiple-selection combobox) demo.\n Filter candidates as you type, select several, and show the selected ones as chips.\n Focus stays on the input; the active candidate is shown via aria-activedescendant\n and selection via aria-selected. The library handles filtering, multi-select\n toggling, chip create/remove, roving, dismiss, and the selection announcements read\n by the shared stimeo--announcer your app seats once, in its layout (selecting and\n removing a value are the transitions worth reading; the multi-select owns no live\n region of its own). Placement lives in demo.css. %>\n<% announce_selected = t(\"components.multi_select.demo.announce_selected\") %>\n<% announce_removed = t(\"components.multi_select.demo.announce_removed\") %>\n<% remove_label = t(\"components.multi_select.demo.remove_label\") %>\n<div\n class=\"multi-select\"\n data-controller=\"stimeo--multi-select\"\n data-stimeo--multi-select-announce-text-value=\"<%= announce_selected %>\"\n data-stimeo--multi-select-announce-removed-text-value=\"<%= announce_removed %>\">\n <span id=\"multi-select-label\" class=\"multi-select__label\">\n <%= t(\"components.multi_select.demo.label\") %>\n </span>\n\n <ul\n class=\"multi-select__tags\"\n aria-label=\"<%= t(\"components.multi_select.demo.selected_label\") %>\"\n data-stimeo--multi-select-target=\"tags\"></ul>\n\n <input\n type=\"text\"\n class=\"multi-select__input\"\n role=\"combobox\"\n aria-expanded=\"false\"\n aria-autocomplete=\"list\"\n aria-controls=\"multi-select-list\"\n aria-labelledby=\"multi-select-label\"\n placeholder=\"<%= t(\"components.multi_select.demo.placeholder\") %>\"\n data-stimeo--multi-select-target=\"input\"\n data-action=\"input->stimeo--multi-select#filter\n keydown->stimeo--multi-select#onKeydown\n focus->stimeo--multi-select#open\" />\n\n <ul\n id=\"multi-select-list\"\n class=\"multi-select__list\"\n role=\"listbox\"\n aria-multiselectable=\"true\"\n aria-label=\"<%= t(\"components.multi_select.demo.options_label\") %>\"\n hidden\n data-stimeo--multi-select-target=\"list\">\n <% %w[apple banana cherry grape orange melon].each_with_index do |fruit, index| %>\n <li\n id=\"multi-select-opt-<%= index %>\"\n class=\"multi-select__option\"\n role=\"option\"\n aria-selected=\"false\"\n data-value=\"<%= fruit %>\"\n data-stimeo--multi-select-target=\"option\"\n data-action=\"click->stimeo--multi-select#toggleOption\">\n <%= t(\"components.multi_select.demo.options.#{fruit}\") %>\n </li>\n <% end %>\n </ul>\n\n <template data-stimeo--multi-select-target=\"tagTemplate\">\n <li class=\"multi-select__tag\" data-stimeo--multi-select-target=\"tag\">\n <span data-stimeo--multi-select-target=\"label\"></span>\n <%# Removal is handled by a delegated listener on the tags 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=\"multi-select__remove\"\n tabindex=\"-1\"\n aria-label=\"<%= remove_label %>\"\n data-stimeo--multi-select-target=\"remove\">×</button>\n </li>\n </template>\n</div>\n"
|
|
223
223
|
},
|
|
224
224
|
"stimeo--navigation-menu": {
|
|
225
225
|
"file": "examples/navigation_menu/_demo.html.erb",
|
|
@@ -227,11 +227,11 @@
|
|
|
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 (
|
|
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 hands the row count to the shared stimeo--announcer your app seats once, in its\n layout (count changes are read out without moving focus away; the row list owns no\n live region of its own). Remove buttons are handled by delegation, so cloned rows\n 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</div>\n"
|
|
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. 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
|
|
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 instead to the shared stimeo--announcer your app seats once,\n in its layout — losing the connection interrupts (assertive), regaining it waits its\n turn (polite). Since real online/offline is environment-dependent, the demo's demo.js\n fakes the window events 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 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 <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",
|
|
@@ -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
|
|
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",
|
|
@@ -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-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
|
|
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 Completion is handed to the shared stimeo--announcer your app seats once, in its\n layout — aria-valuenow is state a reader can query, not a notification, so reaching\n the end is the one thing read out.\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</div>\n"
|
|
295
295
|
},
|
|
296
296
|
"stimeo--radio-group": {
|
|
297
297
|
"file": "examples/radio_group/_demo.html.erb",
|
|
@@ -355,7 +355,7 @@
|
|
|
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-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
|
|
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 arrival of the content is handed to the shared stimeo--announcer your app seats\n once, in its layout — aria-busy going false is state, not a notification, so it is\n read out from a region that stands in the page independently of the card.\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</div>\n"
|
|
359
359
|
},
|
|
360
360
|
"stimeo--slider": {
|
|
361
361
|
"file": "examples/slider/_demo.html.erb",
|
|
@@ -371,7 +371,7 @@
|
|
|
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 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
|
|
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 stimeo--announcer your app seats once, in its layout, and is said exactly once\n (loading ↔ ready is the transition worth reading; aria-busy is state the reader can\n query, not a notification). A show delay and a minimum display time suppress flicker.\n 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</div>\n"
|
|
375
375
|
},
|
|
376
376
|
"stimeo--step-indicator": {
|
|
377
377
|
"file": "examples/step_indicator/_demo.html.erb",
|
|
@@ -391,7 +391,7 @@
|
|
|
391
391
|
},
|
|
392
392
|
"stimeo--submit-once": {
|
|
393
393
|
"file": "examples/submit_once/_demo.html.erb",
|
|
394
|
-
"source": "<%# Submit-once demo. A structured idle / busy target pair keeps the DOM the\n consumer authored while the button is disabled. This catalog has no server
|
|
394
|
+
"source": "<%# Submit-once demo. A structured idle / busy target pair keeps the DOM the\n consumer authored while the button is disabled. The busy ↔ ready transition\n goes to the shared stimeo--announcer your app seats once, in its layout,\n where it is read without moving focus; aria-busy stays state the reader can\n query, not a notification. This catalog has no server, so demo.js fires\n Turbo's start / end lifecycle around a short fake round trip. %>\n<% announce_start = t(\"components.submit_once.demo.announce_start\") %>\n<% announce_ready = t(\"components.submit_once.demo.announce_ready\") %>\n<% submit_label = t(\"components.submit_once.demo.submit\") %>\n<% busy_label = t(\"components.submit_once.demo.busy\") %>\n<div class=\"submit-once-demo\">\n <form\n data-controller=\"stimeo--submit-once\"\n data-stimeo--submit-once-announce-text-value=\"<%= announce_start %>\"\n data-stimeo--submit-once-announce-ready-text-value=\"<%= announce_ready %>\">\n <label class=\"submit-once-demo__field\">\n <span><%= t(\"components.submit_once.demo.label\") %></span>\n <input type=\"text\" name=\"title\" class=\"submit-once-demo__input\">\n </label>\n <button type=\"submit\" class=\"demo-trigger\" data-stimeo--submit-once-target=\"submit\">\n <span data-stimeo--submit-once-target=\"idle\"><%= submit_label %></span>\n <span data-stimeo--submit-once-target=\"busy\" hidden><%= busy_label %></span>\n </button>\n </form>\n</div>\n"
|
|
395
395
|
},
|
|
396
396
|
"stimeo--switch": {
|
|
397
397
|
"file": "examples/switch/_demo.html.erb",
|
|
@@ -403,7 +403,7 @@
|
|
|
403
403
|
},
|
|
404
404
|
"stimeo--tags-input": {
|
|
405
405
|
"file": "examples/tags_input/_demo.html.erb",
|
|
406
|
-
"source": "<%# Markup for the tags-input demo.\n Enter or a comma (delimiter) turns the input value into a tag, blocking duplicates /\n empties / over-limit. Tag remove buttons move via roving (left/right keys), and\n Backspace on an empty input deletes the previous tag. The library handles tag\n create/remove, hidden-field sync, focus handoff, and shared
|
|
406
|
+
"source": "<%# Markup for the tags-input demo.\n Enter or a comma (delimiter) turns the input value into a tag, blocking duplicates /\n empties / over-limit. Tag remove buttons move via roving (left/right keys), and\n Backspace on an empty input deletes the previous tag. The library handles tag\n create/remove, hidden-field sync, focus handoff, and the add / remove announcements\n read by the shared stimeo--announcer your app seats once, in its layout (the tag list\n owns no live region of its own). %>\n<% announce_added = t(\"components.tags_input.demo.announce_added\") %>\n<% announce_removed = t(\"components.tags_input.demo.announce_removed\") %>\n<% remove_label = t(\"components.tags_input.demo.remove_label\") %>\n<div class=\"tags-input\" data-controller=\"stimeo--tags-input\"\n data-stimeo--tags-input-delimiter-value=\",\"\n data-stimeo--tags-input-name-value=\"frameworks[]\"\n data-stimeo--tags-input-announce-text-value=\"<%= announce_added %>\"\n data-stimeo--tags-input-announce-removed-text-value=\"<%= announce_removed %>\">\n <ul\n class=\"tags-input__tags\"\n role=\"list\"\n aria-label=\"<%= t(\"components.tags_input.demo.tags_label\") %>\"\n data-stimeo--tags-input-target=\"tags\"></ul>\n\n <input\n type=\"text\"\n class=\"tags-input__input\"\n aria-label=\"<%= t(\"components.tags_input.demo.input_label\") %>\"\n aria-describedby=\"tags-input-help\"\n data-stimeo--tags-input-target=\"input\"\n data-action=\"keydown->stimeo--tags-input#onKeydown\" />\n\n <span id=\"tags-input-help\" class=\"tags-input__help\">\n <%= t(\"components.tags_input.demo.help\") %>\n </span>\n\n <%# The library creates and syncs the submit hidden inputs as name=\"frameworks[]\". %>\n <div hidden data-stimeo--tags-input-target=\"fields\"></div>\n\n <template data-stimeo--tags-input-target=\"tagTemplate\">\n <li class=\"tags-input__tag\" role=\"listitem\" data-stimeo--tags-input-target=\"tag\">\n <span data-stimeo--tags-input-target=\"label\"></span>\n <%# Removal is handled by a delegated listener on the tags 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=\"tags-input__remove\"\n tabindex=\"-1\"\n aria-label=\"<%= remove_label %>\"\n data-stimeo--tags-input-target=\"remove\">×</button>\n </li>\n </template>\n</div>\n"
|
|
407
407
|
},
|
|
408
408
|
"stimeo--textarea-autosize": {
|
|
409
409
|
"file": "examples/textarea_autosize/_demo.html.erb",
|