shadcn-rails 0.1.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.
- checksums.yaml +7 -0
- data/.dockerignore +40 -0
- data/CHANGELOG.md +54 -0
- data/CLAUDE.md +463 -0
- data/PROGRESS.md +485 -0
- data/README.md +1483 -0
- data/Rakefile +29 -0
- data/__tests__/controllers/__snapshots__/calendar_controller.test.js.snap +13 -0
- data/__tests__/controllers/__snapshots__/popover_controller.test.js.snap +46 -0
- data/__tests__/controllers/__snapshots__/sheet_controller.test.js.snap +111 -0
- data/__tests__/controllers/__snapshots__/tabs_controller.test.js.snap +27 -0
- data/__tests__/controllers/accordion_controller.test.js +904 -0
- data/__tests__/controllers/calendar_controller.test.js +1370 -0
- data/__tests__/controllers/carousel_controller.test.js +912 -0
- data/__tests__/controllers/checkbox_controller.test.js +454 -0
- data/__tests__/controllers/collapsible_controller.test.js +407 -0
- data/__tests__/controllers/combobox_controller.test.js +966 -0
- data/__tests__/controllers/context_menu_controller.test.js +627 -0
- data/__tests__/controllers/date_picker_controller.test.js +636 -0
- data/__tests__/controllers/dialog_controller.test.js +878 -0
- data/__tests__/controllers/drawer_controller.test.js +995 -0
- data/__tests__/controllers/menubar_controller.test.js +736 -0
- data/__tests__/controllers/navigation_menu_controller.test.js +598 -0
- data/__tests__/controllers/popover_controller.test.js +1007 -0
- data/__tests__/controllers/radio_group_controller.test.js +640 -0
- data/__tests__/controllers/resizable_controller.test.js +680 -0
- data/__tests__/controllers/select_controller.test.js +674 -0
- data/__tests__/controllers/sheet_controller.test.js +986 -0
- data/__tests__/controllers/slider_controller.test.js +1036 -0
- data/__tests__/controllers/switch_controller.test.js +424 -0
- data/__tests__/controllers/tabs_controller.test.js +907 -0
- data/__tests__/controllers/toggle_group_controller.test.js +839 -0
- data/__tests__/controllers/tooltip_controller.test.js +808 -0
- data/__tests__/helpers/stimulus-test-helper.js +203 -0
- data/app/assets/config/manifest.js +1 -0
- data/app/assets/javascripts/shadcn/controllers/accordion_controller.d.ts +53 -0
- data/app/assets/javascripts/shadcn/controllers/accordion_controller.js +140 -0
- data/app/assets/javascripts/shadcn/controllers/avatar_controller.d.ts +22 -0
- data/app/assets/javascripts/shadcn/controllers/avatar_controller.js +26 -0
- data/app/assets/javascripts/shadcn/controllers/calendar_controller.js +592 -0
- data/app/assets/javascripts/shadcn/controllers/carousel_controller.js +263 -0
- data/app/assets/javascripts/shadcn/controllers/checkbox_controller.d.ts +31 -0
- data/app/assets/javascripts/shadcn/controllers/checkbox_controller.js +48 -0
- data/app/assets/javascripts/shadcn/controllers/collapsible_controller.d.ts +43 -0
- data/app/assets/javascripts/shadcn/controllers/collapsible_controller.js +73 -0
- data/app/assets/javascripts/shadcn/controllers/combobox_controller.js +234 -0
- data/app/assets/javascripts/shadcn/controllers/command_controller.js +141 -0
- data/app/assets/javascripts/shadcn/controllers/command_dialog_controller.js +162 -0
- data/app/assets/javascripts/shadcn/controllers/context_menu_controller.js +202 -0
- data/app/assets/javascripts/shadcn/controllers/date_picker_controller.js +282 -0
- data/app/assets/javascripts/shadcn/controllers/dialog_controller.d.ts +67 -0
- data/app/assets/javascripts/shadcn/controllers/dialog_controller.js +187 -0
- data/app/assets/javascripts/shadcn/controllers/drawer_controller.d.ts +58 -0
- data/app/assets/javascripts/shadcn/controllers/drawer_controller.js +112 -0
- data/app/assets/javascripts/shadcn/controllers/dropdown_controller.d.ts +83 -0
- data/app/assets/javascripts/shadcn/controllers/dropdown_controller.js +225 -0
- data/app/assets/javascripts/shadcn/controllers/hover_card_controller.d.ts +59 -0
- data/app/assets/javascripts/shadcn/controllers/hover_card_controller.js +143 -0
- data/app/assets/javascripts/shadcn/controllers/input_otp_controller.d.ts +44 -0
- data/app/assets/javascripts/shadcn/controllers/input_otp_controller.js +206 -0
- data/app/assets/javascripts/shadcn/controllers/menubar_controller.js +323 -0
- data/app/assets/javascripts/shadcn/controllers/navigation_menu_controller.js +251 -0
- data/app/assets/javascripts/shadcn/controllers/popover_controller.d.ts +56 -0
- data/app/assets/javascripts/shadcn/controllers/popover_controller.js +141 -0
- data/app/assets/javascripts/shadcn/controllers/radio_group_controller.d.ts +47 -0
- data/app/assets/javascripts/shadcn/controllers/radio_group_controller.js +108 -0
- data/app/assets/javascripts/shadcn/controllers/resizable_controller.js +272 -0
- data/app/assets/javascripts/shadcn/controllers/scroll_area_controller.d.ts +44 -0
- data/app/assets/javascripts/shadcn/controllers/scroll_area_controller.js +74 -0
- data/app/assets/javascripts/shadcn/controllers/select_controller.d.ts +84 -0
- data/app/assets/javascripts/shadcn/controllers/select_controller.js +222 -0
- data/app/assets/javascripts/shadcn/controllers/sheet_controller.d.ts +60 -0
- data/app/assets/javascripts/shadcn/controllers/sheet_controller.js +151 -0
- data/app/assets/javascripts/shadcn/controllers/sidebar_controller.js +148 -0
- data/app/assets/javascripts/shadcn/controllers/slider_controller.d.ts +102 -0
- data/app/assets/javascripts/shadcn/controllers/slider_controller.js +364 -0
- data/app/assets/javascripts/shadcn/controllers/switch_controller.d.ts +46 -0
- data/app/assets/javascripts/shadcn/controllers/switch_controller.js +78 -0
- data/app/assets/javascripts/shadcn/controllers/tabs_controller.d.ts +51 -0
- data/app/assets/javascripts/shadcn/controllers/tabs_controller.js +126 -0
- data/app/assets/javascripts/shadcn/controllers/toast_controller.d.ts +37 -0
- data/app/assets/javascripts/shadcn/controllers/toast_controller.js +58 -0
- data/app/assets/javascripts/shadcn/controllers/toggle_controller.d.ts +27 -0
- data/app/assets/javascripts/shadcn/controllers/toggle_controller.js +42 -0
- data/app/assets/javascripts/shadcn/controllers/toggle_group_controller.d.ts +44 -0
- data/app/assets/javascripts/shadcn/controllers/toggle_group_controller.js +68 -0
- data/app/assets/javascripts/shadcn/controllers/tooltip_controller.d.ts +56 -0
- data/app/assets/javascripts/shadcn/controllers/tooltip_controller.js +117 -0
- data/app/assets/javascripts/shadcn/index.d.ts +74 -0
- data/app/assets/javascripts/shadcn/index.js +133 -0
- data/app/assets/stylesheets/.keep +0 -0
- data/app/assets/stylesheets/shadcn/base.css +445 -0
- data/app/assets/stylesheets/shadcn/components.css +513 -0
- data/app/assets/stylesheets/shadcn/index.css +18 -0
- data/app/assets/stylesheets/shadcn/themes/gray.css +68 -0
- data/app/assets/stylesheets/shadcn/themes/slate.css +68 -0
- data/app/assets/stylesheets/shadcn/themes/stone.css +68 -0
- data/app/assets/stylesheets/shadcn/themes/zinc.css +68 -0
- data/app/components/shadcn/accordion_component.rb +63 -0
- data/app/components/shadcn/accordion_content_component.rb +29 -0
- data/app/components/shadcn/accordion_item_component.rb +40 -0
- data/app/components/shadcn/accordion_trigger_component.rb +49 -0
- data/app/components/shadcn/alert_component.rb +75 -0
- data/app/components/shadcn/alert_description_component.rb +12 -0
- data/app/components/shadcn/alert_dialog_action_component.rb +24 -0
- data/app/components/shadcn/alert_dialog_cancel_component.rb +24 -0
- data/app/components/shadcn/alert_dialog_component.rb +71 -0
- data/app/components/shadcn/alert_dialog_content_component.rb +57 -0
- data/app/components/shadcn/alert_dialog_description_component.rb +12 -0
- data/app/components/shadcn/alert_dialog_footer_component.rb +19 -0
- data/app/components/shadcn/alert_dialog_header_component.rb +19 -0
- data/app/components/shadcn/alert_dialog_title_component.rb +12 -0
- data/app/components/shadcn/alert_title_component.rb +12 -0
- data/app/components/shadcn/aspect_ratio_component.rb +49 -0
- data/app/components/shadcn/avatar_component.rb +107 -0
- data/app/components/shadcn/avatar_fallback_component.rb +17 -0
- data/app/components/shadcn/badge_component.rb +49 -0
- data/app/components/shadcn/base_component.rb +100 -0
- data/app/components/shadcn/breadcrumb_component.rb +70 -0
- data/app/components/shadcn/breadcrumb_item_component.rb +50 -0
- data/app/components/shadcn/button_component.rb +141 -0
- data/app/components/shadcn/button_group_component.rb +69 -0
- data/app/components/shadcn/calendar_component.rb +337 -0
- data/app/components/shadcn/card_action_component.rb +10 -0
- data/app/components/shadcn/card_component.rb +63 -0
- data/app/components/shadcn/card_content_component.rb +19 -0
- data/app/components/shadcn/card_description_component.rb +12 -0
- data/app/components/shadcn/card_footer_component.rb +12 -0
- data/app/components/shadcn/card_header_component.rb +24 -0
- data/app/components/shadcn/card_title_component.rb +18 -0
- data/app/components/shadcn/carousel_component.rb +275 -0
- data/app/components/shadcn/checkbox_component.rb +103 -0
- data/app/components/shadcn/collapsible_component.rb +66 -0
- data/app/components/shadcn/collapsible_content_component.rb +28 -0
- data/app/components/shadcn/combobox_component.rb +322 -0
- data/app/components/shadcn/command_component.rb +52 -0
- data/app/components/shadcn/command_dialog_component.rb +76 -0
- data/app/components/shadcn/command_empty_component.rb +12 -0
- data/app/components/shadcn/command_group_component.rb +34 -0
- data/app/components/shadcn/command_input_component.rb +59 -0
- data/app/components/shadcn/command_item_component.rb +48 -0
- data/app/components/shadcn/command_list_component.rb +38 -0
- data/app/components/shadcn/command_separator_component.rb +12 -0
- data/app/components/shadcn/command_shortcut_component.rb +12 -0
- data/app/components/shadcn/context_menu_component.rb +64 -0
- data/app/components/shadcn/context_menu_content_component.rb +44 -0
- data/app/components/shadcn/context_menu_item_component.rb +63 -0
- data/app/components/shadcn/context_menu_label_component.rb +18 -0
- data/app/components/shadcn/context_menu_separator_component.rb +12 -0
- data/app/components/shadcn/context_menu_shortcut_component.rb +12 -0
- data/app/components/shadcn/date_picker_component.rb +368 -0
- data/app/components/shadcn/dialog_component.rb +77 -0
- data/app/components/shadcn/dialog_content_component.rb +91 -0
- data/app/components/shadcn/dialog_description_component.rb +12 -0
- data/app/components/shadcn/dialog_footer_component.rb +12 -0
- data/app/components/shadcn/dialog_header_component.rb +19 -0
- data/app/components/shadcn/dialog_title_component.rb +12 -0
- data/app/components/shadcn/drawer_component.rb +72 -0
- data/app/components/shadcn/drawer_content_component.rb +76 -0
- data/app/components/shadcn/drawer_description_component.rb +12 -0
- data/app/components/shadcn/drawer_footer_component.rb +12 -0
- data/app/components/shadcn/drawer_header_component.rb +19 -0
- data/app/components/shadcn/drawer_title_component.rb +12 -0
- data/app/components/shadcn/dropdown_menu_component.rb +75 -0
- data/app/components/shadcn/dropdown_menu_content_component.rb +49 -0
- data/app/components/shadcn/dropdown_menu_group_component.rb +10 -0
- data/app/components/shadcn/dropdown_menu_item_component.rb +63 -0
- data/app/components/shadcn/dropdown_menu_label_component.rb +18 -0
- data/app/components/shadcn/dropdown_menu_separator_component.rb +12 -0
- data/app/components/shadcn/dropdown_menu_shortcut_component.rb +12 -0
- data/app/components/shadcn/empty_component.rb +48 -0
- data/app/components/shadcn/empty_content_component.rb +12 -0
- data/app/components/shadcn/empty_description_component.rb +12 -0
- data/app/components/shadcn/empty_header_component.rb +29 -0
- data/app/components/shadcn/empty_media_component.rb +21 -0
- data/app/components/shadcn/empty_title_component.rb +12 -0
- data/app/components/shadcn/field_component.rb +113 -0
- data/app/components/shadcn/hover_card_component.rb +64 -0
- data/app/components/shadcn/hover_card_content_component.rb +36 -0
- data/app/components/shadcn/input_component.rb +108 -0
- data/app/components/shadcn/input_group_component.rb +70 -0
- data/app/components/shadcn/input_otp_component.rb +183 -0
- data/app/components/shadcn/item_actions_component.rb +12 -0
- data/app/components/shadcn/item_component.rb +98 -0
- data/app/components/shadcn/item_content_component.rb +24 -0
- data/app/components/shadcn/item_description_component.rb +12 -0
- data/app/components/shadcn/item_footer_component.rb +12 -0
- data/app/components/shadcn/item_group_component.rb +24 -0
- data/app/components/shadcn/item_header_component.rb +12 -0
- data/app/components/shadcn/item_media_component.rb +22 -0
- data/app/components/shadcn/item_separator_component.rb +12 -0
- data/app/components/shadcn/item_title_component.rb +12 -0
- data/app/components/shadcn/kbd_component.rb +36 -0
- data/app/components/shadcn/label_component.rb +49 -0
- data/app/components/shadcn/menubar_checkbox_item_component.rb +76 -0
- data/app/components/shadcn/menubar_component.rb +56 -0
- data/app/components/shadcn/menubar_content_component.rb +64 -0
- data/app/components/shadcn/menubar_item_component.rb +65 -0
- data/app/components/shadcn/menubar_label_component.rb +27 -0
- data/app/components/shadcn/menubar_menu_component.rb +34 -0
- data/app/components/shadcn/menubar_radio_group_component.rb +42 -0
- data/app/components/shadcn/menubar_radio_item_component.rb +76 -0
- data/app/components/shadcn/menubar_separator_component.rb +22 -0
- data/app/components/shadcn/menubar_shortcut_component.rb +21 -0
- data/app/components/shadcn/menubar_sub_component.rb +38 -0
- data/app/components/shadcn/menubar_sub_content_component.rb +45 -0
- data/app/components/shadcn/menubar_sub_trigger_component.rb +59 -0
- data/app/components/shadcn/menubar_trigger_component.rb +31 -0
- data/app/components/shadcn/native_select_component.rb +150 -0
- data/app/components/shadcn/navigation_menu_component.rb +76 -0
- data/app/components/shadcn/navigation_menu_content_component.rb +30 -0
- data/app/components/shadcn/navigation_menu_item_component.rb +39 -0
- data/app/components/shadcn/navigation_menu_link_component.rb +38 -0
- data/app/components/shadcn/navigation_menu_list_component.rb +29 -0
- data/app/components/shadcn/navigation_menu_trigger_component.rb +59 -0
- data/app/components/shadcn/pagination_component.rb +195 -0
- data/app/components/shadcn/pagination_content_component.rb +47 -0
- data/app/components/shadcn/pagination_ellipsis_component.rb +30 -0
- data/app/components/shadcn/pagination_item_component.rb +53 -0
- data/app/components/shadcn/pagination_next_component.rb +48 -0
- data/app/components/shadcn/pagination_previous_component.rb +48 -0
- data/app/components/shadcn/popover_component.rb +76 -0
- data/app/components/shadcn/popover_content_component.rb +25 -0
- data/app/components/shadcn/progress_component.rb +77 -0
- data/app/components/shadcn/radio_group_component.rb +129 -0
- data/app/components/shadcn/radio_group_item_component.rb +109 -0
- data/app/components/shadcn/resizable_handle_component.rb +98 -0
- data/app/components/shadcn/resizable_panel_component.rb +56 -0
- data/app/components/shadcn/resizable_panel_group_component.rb +94 -0
- data/app/components/shadcn/scroll_area_component.rb +110 -0
- data/app/components/shadcn/select_component.rb +151 -0
- data/app/components/shadcn/select_group_component.rb +32 -0
- data/app/components/shadcn/select_item_component.rb +59 -0
- data/app/components/shadcn/select_separator_component.rb +12 -0
- data/app/components/shadcn/separator_component.rb +54 -0
- data/app/components/shadcn/sheet_component.rb +82 -0
- data/app/components/shadcn/sheet_content_component.rb +95 -0
- data/app/components/shadcn/sheet_description_component.rb +12 -0
- data/app/components/shadcn/sheet_footer_component.rb +12 -0
- data/app/components/shadcn/sheet_header_component.rb +19 -0
- data/app/components/shadcn/sheet_title_component.rb +12 -0
- data/app/components/shadcn/sidebar_component.rb +180 -0
- data/app/components/shadcn/sidebar_content_component.rb +32 -0
- data/app/components/shadcn/sidebar_footer_component.rb +24 -0
- data/app/components/shadcn/sidebar_group_action_component.rb +26 -0
- data/app/components/shadcn/sidebar_group_component.rb +38 -0
- data/app/components/shadcn/sidebar_group_content_component.rb +32 -0
- data/app/components/shadcn/sidebar_group_label_component.rb +25 -0
- data/app/components/shadcn/sidebar_header_component.rb +24 -0
- data/app/components/shadcn/sidebar_inset_component.rb +25 -0
- data/app/components/shadcn/sidebar_menu_action_component.rb +37 -0
- data/app/components/shadcn/sidebar_menu_badge_component.rb +25 -0
- data/app/components/shadcn/sidebar_menu_button_component.rb +52 -0
- data/app/components/shadcn/sidebar_menu_component.rb +32 -0
- data/app/components/shadcn/sidebar_menu_item_component.rb +41 -0
- data/app/components/shadcn/sidebar_menu_skeleton_component.rb +46 -0
- data/app/components/shadcn/sidebar_menu_sub_button_component.rb +43 -0
- data/app/components/shadcn/sidebar_menu_sub_component.rb +33 -0
- data/app/components/shadcn/sidebar_menu_sub_item_component.rb +30 -0
- data/app/components/shadcn/sidebar_provider_component.rb +57 -0
- data/app/components/shadcn/sidebar_rail_component.rb +30 -0
- data/app/components/shadcn/sidebar_separator_component.rb +24 -0
- data/app/components/shadcn/sidebar_trigger_component.rb +51 -0
- data/app/components/shadcn/skeleton_component.rb +29 -0
- data/app/components/shadcn/slider_component.rb +76 -0
- data/app/components/shadcn/spinner_component.rb +67 -0
- data/app/components/shadcn/switch_component.rb +147 -0
- data/app/components/shadcn/table_body_component.rb +16 -0
- data/app/components/shadcn/table_caption_component.rb +12 -0
- data/app/components/shadcn/table_cell_component.rb +12 -0
- data/app/components/shadcn/table_component.rb +57 -0
- data/app/components/shadcn/table_footer_component.rb +16 -0
- data/app/components/shadcn/table_head_component.rb +12 -0
- data/app/components/shadcn/table_header_component.rb +16 -0
- data/app/components/shadcn/table_row_component.rb +40 -0
- data/app/components/shadcn/tabs_component.rb +78 -0
- data/app/components/shadcn/tabs_content_component.rb +32 -0
- data/app/components/shadcn/tabs_list_component.rb +30 -0
- data/app/components/shadcn/tabs_trigger_component.rb +37 -0
- data/app/components/shadcn/textarea_component.rb +84 -0
- data/app/components/shadcn/toast_action_component.rb +18 -0
- data/app/components/shadcn/toast_component.rb +114 -0
- data/app/components/shadcn/toast_description_component.rb +12 -0
- data/app/components/shadcn/toast_title_component.rb +12 -0
- data/app/components/shadcn/toast_viewport_component.rb +12 -0
- data/app/components/shadcn/toggle_component.rb +77 -0
- data/app/components/shadcn/toggle_group_component.rb +96 -0
- data/app/components/shadcn/toggle_group_item_component.rb +62 -0
- data/app/components/shadcn/tooltip_component.rb +89 -0
- data/app/components/shadcn/typography_component.rb +112 -0
- data/babel.config.cjs +5 -0
- data/bin/console +11 -0
- data/bin/setup +8 -0
- data/config/importmap.rb +5 -0
- data/fly.toml +26 -0
- data/jest.config.js +19 -0
- data/jest.setup.js +8 -0
- data/lib/generators/shadcn/component/component_generator.rb +188 -0
- data/lib/generators/shadcn/install/install_generator.rb +140 -0
- data/lib/generators/shadcn/install/templates/initializer.rb.tt +35 -0
- data/lib/generators/shadcn/install/templates/shadcn.yml.tt +35 -0
- data/lib/generators/shadcn/theme/theme_generator.rb +128 -0
- data/lib/shadcn/rails/class_merger.rb +228 -0
- data/lib/shadcn/rails/configuration.rb +341 -0
- data/lib/shadcn/rails/engine.rb +59 -0
- data/lib/shadcn/rails/helpers/class_name_helper.rb +35 -0
- data/lib/shadcn/rails/helpers/component_helper.rb +60 -0
- data/lib/shadcn/rails/helpers/pagination_helper.rb +187 -0
- data/lib/shadcn/rails/version.rb +7 -0
- data/lib/shadcn/rails.rb +179 -0
- data/package-lock.json +7415 -0
- data/package.json +68 -0
- data/rollup.config.js +29 -0
- data/sig/shadcn/rails.rbs +6 -0
- metadata +526 -0
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Slider Controller
|
|
5
|
+
*
|
|
6
|
+
* Handles slider value selection with drag and keyboard support
|
|
7
|
+
*
|
|
8
|
+
* Targets:
|
|
9
|
+
* - track: The slider track
|
|
10
|
+
* - range: The filled range portion
|
|
11
|
+
* - thumb: The draggable thumb
|
|
12
|
+
* - input: Hidden input for form submission
|
|
13
|
+
* - output: Optional element to display the current value (auto-synced)
|
|
14
|
+
*
|
|
15
|
+
* Values:
|
|
16
|
+
* - min: Minimum value
|
|
17
|
+
* - max: Maximum value
|
|
18
|
+
* - step: Step increment
|
|
19
|
+
* - value: Current value
|
|
20
|
+
* - name: Input name
|
|
21
|
+
* - disabled: Whether slider is disabled
|
|
22
|
+
* - outputFormat: Format string for output (use {value} for value, {percent} for percentage)
|
|
23
|
+
*
|
|
24
|
+
* Data attributes for native <input type="range">:
|
|
25
|
+
* - data-output-target: ID of element to display value (one-way: slider → output)
|
|
26
|
+
* - data-output-format: Format string with {value} and {percent} placeholders
|
|
27
|
+
* - data-input-target: ID of input element for two-way binding (slider ↔ input)
|
|
28
|
+
*/
|
|
29
|
+
export default class extends Controller {
|
|
30
|
+
static targets = ["track", "range", "thumb", "input", "output"]
|
|
31
|
+
static values = {
|
|
32
|
+
min: { type: Number, default: 0 },
|
|
33
|
+
max: { type: Number, default: 100 },
|
|
34
|
+
step: { type: Number, default: 1 },
|
|
35
|
+
value: { type: Number, default: 0 },
|
|
36
|
+
name: String,
|
|
37
|
+
disabled: { type: Boolean, default: false },
|
|
38
|
+
outputFormat: { type: String, default: "{value}" }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
connect() {
|
|
42
|
+
this.isDragging = false
|
|
43
|
+
this.updateVisuals()
|
|
44
|
+
this.setupTwoWayBindings()
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
disconnect() {
|
|
48
|
+
this.teardownTwoWayBindings()
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Set up two-way bindings for native range inputs with data-input-target
|
|
53
|
+
*/
|
|
54
|
+
setupTwoWayBindings() {
|
|
55
|
+
this.inputBindings = []
|
|
56
|
+
|
|
57
|
+
// Check if the controller element itself is a range input with data-input-target
|
|
58
|
+
// (This is the case when data-controller is on the input element directly)
|
|
59
|
+
if (this.element.matches &&
|
|
60
|
+
this.element.matches('input[type="range"][data-input-target]')) {
|
|
61
|
+
this.setupBindingForInput(this.element)
|
|
62
|
+
return
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Otherwise, find all native range inputs with data-input-target attribute within the element
|
|
66
|
+
const rangeInputs = this.element.querySelectorAll('input[type="range"][data-input-target]')
|
|
67
|
+
rangeInputs.forEach(rangeInput => {
|
|
68
|
+
this.setupBindingForInput(rangeInput)
|
|
69
|
+
})
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Set up two-way binding for a single range input
|
|
74
|
+
* @param {HTMLInputElement} rangeInput - The range input element
|
|
75
|
+
*/
|
|
76
|
+
setupBindingForInput(rangeInput) {
|
|
77
|
+
const inputTargetId = rangeInput.dataset.inputTarget
|
|
78
|
+
const linkedInput = document.getElementById(inputTargetId)
|
|
79
|
+
|
|
80
|
+
if (linkedInput) {
|
|
81
|
+
// Create bound handler for this specific pair
|
|
82
|
+
const handler = this.handleLinkedInputChange.bind(this, rangeInput)
|
|
83
|
+
|
|
84
|
+
// Store binding info for cleanup
|
|
85
|
+
this.inputBindings.push({
|
|
86
|
+
rangeInput,
|
|
87
|
+
linkedInput,
|
|
88
|
+
handler
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
// Listen for changes on the linked input
|
|
92
|
+
linkedInput.addEventListener('input', handler)
|
|
93
|
+
linkedInput.addEventListener('change', handler)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Clean up event listeners when disconnecting
|
|
99
|
+
*/
|
|
100
|
+
teardownTwoWayBindings() {
|
|
101
|
+
if (this.inputBindings) {
|
|
102
|
+
this.inputBindings.forEach(({ linkedInput, handler }) => {
|
|
103
|
+
linkedInput.removeEventListener('input', handler)
|
|
104
|
+
linkedInput.removeEventListener('change', handler)
|
|
105
|
+
})
|
|
106
|
+
this.inputBindings = []
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Handle changes from a linked input element (input → slider sync)
|
|
112
|
+
* @param {HTMLInputElement} rangeInput - The range input to update
|
|
113
|
+
* @param {Event} event - The input/change event from the linked input
|
|
114
|
+
*/
|
|
115
|
+
handleLinkedInputChange(rangeInput, event) {
|
|
116
|
+
const linkedInput = event.target
|
|
117
|
+
let value = parseFloat(linkedInput.value)
|
|
118
|
+
|
|
119
|
+
// Validate and clamp the value
|
|
120
|
+
const min = parseFloat(rangeInput.min) || 0
|
|
121
|
+
const max = parseFloat(rangeInput.max) || 100
|
|
122
|
+
const step = parseFloat(rangeInput.step) || 1
|
|
123
|
+
|
|
124
|
+
// Handle invalid input
|
|
125
|
+
if (isNaN(value)) {
|
|
126
|
+
value = min
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Clamp to min/max
|
|
130
|
+
value = Math.max(min, Math.min(max, value))
|
|
131
|
+
|
|
132
|
+
// Snap to step
|
|
133
|
+
const steps = Math.round((value - min) / step)
|
|
134
|
+
value = min + steps * step
|
|
135
|
+
|
|
136
|
+
// Update range input
|
|
137
|
+
rangeInput.value = value
|
|
138
|
+
|
|
139
|
+
// Update CSS custom property for fill
|
|
140
|
+
const percentage = ((value - min) / (max - min)) * 100
|
|
141
|
+
rangeInput.style.setProperty("--slider-fill", `${percentage}%`)
|
|
142
|
+
|
|
143
|
+
// Update the linked input if value was clamped/snapped
|
|
144
|
+
if (parseFloat(linkedInput.value) !== value) {
|
|
145
|
+
linkedInput.value = value
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Also update output if present
|
|
149
|
+
const outputTargetId = rangeInput.dataset.outputTarget
|
|
150
|
+
if (outputTargetId) {
|
|
151
|
+
const outputElement = document.getElementById(outputTargetId)
|
|
152
|
+
if (outputElement) {
|
|
153
|
+
const format = rangeInput.dataset.outputFormat || "{value}"
|
|
154
|
+
const formattedValue = format
|
|
155
|
+
.replace("{value}", value)
|
|
156
|
+
.replace("{percent}", Math.round(percentage))
|
|
157
|
+
outputElement.textContent = formattedValue
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Dispatch change event
|
|
162
|
+
this.dispatch("change", {
|
|
163
|
+
detail: { value: value, percentage: percentage }
|
|
164
|
+
})
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
startDrag(event) {
|
|
168
|
+
if (this.disabledValue) return
|
|
169
|
+
|
|
170
|
+
event.preventDefault()
|
|
171
|
+
this.isDragging = true
|
|
172
|
+
|
|
173
|
+
// Handle both mouse and touch events
|
|
174
|
+
const moveEvent = event.type === "touchstart" ? "touchmove" : "mousemove"
|
|
175
|
+
const endEvent = event.type === "touchstart" ? "touchend" : "mouseup"
|
|
176
|
+
|
|
177
|
+
this.handleDrag(event)
|
|
178
|
+
|
|
179
|
+
this.boundHandleDrag = this.handleDrag.bind(this)
|
|
180
|
+
this.boundStopDrag = this.stopDrag.bind(this)
|
|
181
|
+
|
|
182
|
+
document.addEventListener(moveEvent, this.boundHandleDrag)
|
|
183
|
+
document.addEventListener(endEvent, this.boundStopDrag)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
handleDrag(event) {
|
|
187
|
+
if (!this.isDragging && event.type !== "mousedown" && event.type !== "touchstart") return
|
|
188
|
+
|
|
189
|
+
const track = this.trackTarget
|
|
190
|
+
const rect = track.getBoundingClientRect()
|
|
191
|
+
|
|
192
|
+
// Get clientX from either mouse or touch event
|
|
193
|
+
const clientX = event.type.includes("touch")
|
|
194
|
+
? event.touches[0].clientX
|
|
195
|
+
: event.clientX
|
|
196
|
+
|
|
197
|
+
const percentage = Math.max(0, Math.min(1, (clientX - rect.left) / rect.width))
|
|
198
|
+
const rawValue = this.minValue + percentage * (this.maxValue - this.minValue)
|
|
199
|
+
const steppedValue = this.snapToStep(rawValue)
|
|
200
|
+
|
|
201
|
+
this.valueValue = steppedValue
|
|
202
|
+
this.updateVisuals()
|
|
203
|
+
this.dispatchChange()
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
stopDrag() {
|
|
207
|
+
this.isDragging = false
|
|
208
|
+
document.removeEventListener("mousemove", this.boundHandleDrag)
|
|
209
|
+
document.removeEventListener("mouseup", this.boundStopDrag)
|
|
210
|
+
document.removeEventListener("touchmove", this.boundHandleDrag)
|
|
211
|
+
document.removeEventListener("touchend", this.boundStopDrag)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
handleKeydown(event) {
|
|
215
|
+
if (this.disabledValue) return
|
|
216
|
+
|
|
217
|
+
let newValue = this.valueValue
|
|
218
|
+
const bigStep = (this.maxValue - this.minValue) / 10
|
|
219
|
+
|
|
220
|
+
switch (event.key) {
|
|
221
|
+
case "ArrowRight":
|
|
222
|
+
case "ArrowUp":
|
|
223
|
+
event.preventDefault()
|
|
224
|
+
newValue = Math.min(this.maxValue, this.valueValue + this.stepValue)
|
|
225
|
+
break
|
|
226
|
+
case "ArrowLeft":
|
|
227
|
+
case "ArrowDown":
|
|
228
|
+
event.preventDefault()
|
|
229
|
+
newValue = Math.max(this.minValue, this.valueValue - this.stepValue)
|
|
230
|
+
break
|
|
231
|
+
case "PageUp":
|
|
232
|
+
event.preventDefault()
|
|
233
|
+
newValue = Math.min(this.maxValue, this.valueValue + bigStep)
|
|
234
|
+
break
|
|
235
|
+
case "PageDown":
|
|
236
|
+
event.preventDefault()
|
|
237
|
+
newValue = Math.max(this.minValue, this.valueValue - bigStep)
|
|
238
|
+
break
|
|
239
|
+
case "Home":
|
|
240
|
+
event.preventDefault()
|
|
241
|
+
newValue = this.minValue
|
|
242
|
+
break
|
|
243
|
+
case "End":
|
|
244
|
+
event.preventDefault()
|
|
245
|
+
newValue = this.maxValue
|
|
246
|
+
break
|
|
247
|
+
default:
|
|
248
|
+
return
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
this.valueValue = this.snapToStep(newValue)
|
|
252
|
+
this.updateVisuals()
|
|
253
|
+
this.dispatchChange()
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
snapToStep(value) {
|
|
257
|
+
const steps = Math.round((value - this.minValue) / this.stepValue)
|
|
258
|
+
return Math.max(this.minValue, Math.min(this.maxValue, this.minValue + steps * this.stepValue))
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
updateVisuals() {
|
|
262
|
+
const percentage = this.percentage
|
|
263
|
+
|
|
264
|
+
if (this.hasRangeTarget) {
|
|
265
|
+
this.rangeTarget.style.width = `${percentage}%`
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (this.hasThumbTarget) {
|
|
269
|
+
this.thumbTarget.style.left = `calc(${percentage}% - 8px)`
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Update ARIA attributes
|
|
273
|
+
this.element.setAttribute("aria-valuenow", this.valueValue)
|
|
274
|
+
|
|
275
|
+
// Update hidden input
|
|
276
|
+
if (this.hasInputTarget) {
|
|
277
|
+
this.inputTarget.value = this.valueValue
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Update output element if present (for syncing value labels)
|
|
281
|
+
if (this.hasOutputTarget) {
|
|
282
|
+
this.updateOutput()
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
updateOutput() {
|
|
287
|
+
const formattedValue = this.outputFormatValue
|
|
288
|
+
.replace("{value}", this.valueValue)
|
|
289
|
+
.replace("{percent}", Math.round(this.percentage))
|
|
290
|
+
|
|
291
|
+
this.outputTarget.textContent = formattedValue
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
dispatchChange() {
|
|
295
|
+
this.dispatch("change", {
|
|
296
|
+
detail: { value: this.valueValue, name: this.nameValue }
|
|
297
|
+
})
|
|
298
|
+
|
|
299
|
+
// Dispatch native input event for form compatibility
|
|
300
|
+
if (this.hasInputTarget) {
|
|
301
|
+
this.inputTarget.dispatchEvent(new Event("input", { bubbles: true }))
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
get percentage() {
|
|
306
|
+
if (this.maxValue === this.minValue) return 0
|
|
307
|
+
return ((this.valueValue - this.minValue) / (this.maxValue - this.minValue)) * 100
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
valueValueChanged() {
|
|
311
|
+
this.updateVisuals()
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Update style for native input range element
|
|
316
|
+
* Called on input event from native <input type="range">
|
|
317
|
+
* Updates CSS custom property for fill and syncs output element
|
|
318
|
+
*/
|
|
319
|
+
updateStyle(event) {
|
|
320
|
+
const input = event.target
|
|
321
|
+
const value = parseFloat(input.value)
|
|
322
|
+
const min = parseFloat(input.min) || 0
|
|
323
|
+
const max = parseFloat(input.max) || 100
|
|
324
|
+
|
|
325
|
+
// Calculate percentage and update CSS custom property
|
|
326
|
+
const percentage = ((value - min) / (max - min)) * 100
|
|
327
|
+
input.style.setProperty("--slider-fill", `${percentage}%`)
|
|
328
|
+
|
|
329
|
+
// Update value for output sync
|
|
330
|
+
this.valueValue = value
|
|
331
|
+
|
|
332
|
+
// Update output if present (Stimulus target)
|
|
333
|
+
if (this.hasOutputTarget) {
|
|
334
|
+
this.updateOutput()
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Also check for data-output-target attribute (ID-based targeting)
|
|
338
|
+
const outputTargetId = input.dataset.outputTarget
|
|
339
|
+
if (outputTargetId) {
|
|
340
|
+
const outputElement = document.getElementById(outputTargetId)
|
|
341
|
+
if (outputElement) {
|
|
342
|
+
const format = input.dataset.outputFormat || "{value}"
|
|
343
|
+
const formattedValue = format
|
|
344
|
+
.replace("{value}", value)
|
|
345
|
+
.replace("{percent}", Math.round(percentage))
|
|
346
|
+
outputElement.textContent = formattedValue
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Sync to linked input element for two-way binding (slider → input)
|
|
351
|
+
const inputTargetId = input.dataset.inputTarget
|
|
352
|
+
if (inputTargetId) {
|
|
353
|
+
const linkedInput = document.getElementById(inputTargetId)
|
|
354
|
+
if (linkedInput) {
|
|
355
|
+
linkedInput.value = value
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Dispatch change event
|
|
360
|
+
this.dispatch("change", {
|
|
361
|
+
detail: { value: value, percentage: percentage }
|
|
362
|
+
})
|
|
363
|
+
}
|
|
364
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Switch Controller
|
|
5
|
+
* Handles toggle switch with hidden input sync for form submission
|
|
6
|
+
*/
|
|
7
|
+
export default class SwitchController extends Controller {
|
|
8
|
+
static targets: ["button", "thumb", "input"];
|
|
9
|
+
static values: {
|
|
10
|
+
checked: { type: "Boolean"; default: false };
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/** Switch button target */
|
|
14
|
+
readonly buttonTarget: HTMLButtonElement;
|
|
15
|
+
readonly hasButtonTarget: boolean;
|
|
16
|
+
|
|
17
|
+
/** Switch thumb target */
|
|
18
|
+
readonly thumbTarget: HTMLElement;
|
|
19
|
+
readonly hasThumbTarget: boolean;
|
|
20
|
+
|
|
21
|
+
/** Hidden input target */
|
|
22
|
+
readonly inputTarget: HTMLInputElement;
|
|
23
|
+
readonly hasInputTarget: boolean;
|
|
24
|
+
|
|
25
|
+
/** Whether the switch is checked */
|
|
26
|
+
checkedValue: boolean;
|
|
27
|
+
readonly hasCheckedValue: boolean;
|
|
28
|
+
|
|
29
|
+
/** Toggle the switch */
|
|
30
|
+
toggle(): void;
|
|
31
|
+
|
|
32
|
+
/** Handle keyboard events (Space, Enter) */
|
|
33
|
+
handleKeydown(event: KeyboardEvent): void;
|
|
34
|
+
|
|
35
|
+
/** Update visual state */
|
|
36
|
+
updateVisuals(): void;
|
|
37
|
+
|
|
38
|
+
/** Sync hidden input value */
|
|
39
|
+
syncInput(): void;
|
|
40
|
+
|
|
41
|
+
/** Dispatch change event */
|
|
42
|
+
dispatchChange(): void;
|
|
43
|
+
|
|
44
|
+
/** Called when checkedValue changes */
|
|
45
|
+
checkedValueChanged(): void;
|
|
46
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Switch Controller
|
|
5
|
+
*
|
|
6
|
+
* Handles toggle switch with hidden input sync for form submission
|
|
7
|
+
*
|
|
8
|
+
* Targets:
|
|
9
|
+
* - button: The visual switch button element
|
|
10
|
+
* - thumb: The sliding thumb element
|
|
11
|
+
* - input: Hidden checkbox input for form submission
|
|
12
|
+
*
|
|
13
|
+
* Values:
|
|
14
|
+
* - checked: Boolean indicating current state
|
|
15
|
+
*/
|
|
16
|
+
export default class extends Controller {
|
|
17
|
+
static targets = ["button", "thumb", "input"]
|
|
18
|
+
static values = {
|
|
19
|
+
checked: { type: Boolean, default: false }
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
connect() {
|
|
23
|
+
this.updateVisuals()
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
toggle() {
|
|
27
|
+
if (this.hasButtonTarget && this.buttonTarget.disabled) return
|
|
28
|
+
|
|
29
|
+
this.checkedValue = !this.checkedValue
|
|
30
|
+
this.updateVisuals()
|
|
31
|
+
this.syncInput()
|
|
32
|
+
this.dispatchChange()
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
handleKeydown(event) {
|
|
36
|
+
if (event.key === " " || event.key === "Enter") {
|
|
37
|
+
event.preventDefault()
|
|
38
|
+
this.toggle()
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
updateVisuals() {
|
|
43
|
+
const state = this.checkedValue ? "checked" : "unchecked"
|
|
44
|
+
|
|
45
|
+
// Update button state
|
|
46
|
+
if (this.hasButtonTarget) {
|
|
47
|
+
this.buttonTarget.dataset.state = state
|
|
48
|
+
this.buttonTarget.setAttribute("aria-checked", this.checkedValue.toString())
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Update thumb position
|
|
52
|
+
if (this.hasThumbTarget) {
|
|
53
|
+
this.thumbTarget.dataset.state = state
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Update wrapper element state
|
|
57
|
+
this.element.dataset.state = state
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
syncInput() {
|
|
61
|
+
if (this.hasInputTarget) {
|
|
62
|
+
this.inputTarget.checked = this.checkedValue
|
|
63
|
+
// Dispatch native change event for form compatibility
|
|
64
|
+
this.inputTarget.dispatchEvent(new Event("change", { bubbles: true }))
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
dispatchChange() {
|
|
69
|
+
this.dispatch("change", {
|
|
70
|
+
detail: { checked: this.checkedValue }
|
|
71
|
+
})
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
checkedValueChanged() {
|
|
75
|
+
this.updateVisuals()
|
|
76
|
+
this.syncInput()
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Tabs controller for tabbed interfaces
|
|
5
|
+
* Handles tab selection, keyboard navigation, content switching, and URL sync
|
|
6
|
+
*/
|
|
7
|
+
export default class TabsController extends Controller {
|
|
8
|
+
static targets: ["list", "trigger", "content"];
|
|
9
|
+
static values: {
|
|
10
|
+
defaultValue: "String";
|
|
11
|
+
urlParam: "String";
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/** Tab list container target */
|
|
15
|
+
readonly listTarget: HTMLElement;
|
|
16
|
+
readonly hasListTarget: boolean;
|
|
17
|
+
|
|
18
|
+
/** Tab trigger targets */
|
|
19
|
+
readonly triggerTargets: HTMLElement[];
|
|
20
|
+
readonly hasTriggerTarget: boolean;
|
|
21
|
+
|
|
22
|
+
/** Tab content panel targets */
|
|
23
|
+
readonly contentTargets: HTMLElement[];
|
|
24
|
+
readonly hasContentTarget: boolean;
|
|
25
|
+
|
|
26
|
+
/** Default tab value to select */
|
|
27
|
+
defaultValueValue: string;
|
|
28
|
+
readonly hasDefaultValueValue: boolean;
|
|
29
|
+
|
|
30
|
+
/** URL parameter name for syncing tab state */
|
|
31
|
+
urlParamValue: string;
|
|
32
|
+
readonly hasUrlParamValue: boolean;
|
|
33
|
+
|
|
34
|
+
/** Handle browser back/forward navigation */
|
|
35
|
+
handlePopState(): void;
|
|
36
|
+
|
|
37
|
+
/** Get current value from URL */
|
|
38
|
+
getValueFromUrl(): string | null;
|
|
39
|
+
|
|
40
|
+
/** Update URL with current tab value */
|
|
41
|
+
updateUrl(value: string): void;
|
|
42
|
+
|
|
43
|
+
/** Select a tab via click event */
|
|
44
|
+
selectTab(event: Event): void;
|
|
45
|
+
|
|
46
|
+
/** Select a tab by its value */
|
|
47
|
+
selectTabByValue(value: string, updateUrl?: boolean): void;
|
|
48
|
+
|
|
49
|
+
/** Handle keyboard navigation (Arrow keys, Home, End) */
|
|
50
|
+
handleKeydown(event: KeyboardEvent): void;
|
|
51
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Tabs controller for tabbed interfaces
|
|
5
|
+
* Handles tab selection, keyboard navigation, content switching, and URL sync
|
|
6
|
+
*/
|
|
7
|
+
export default class extends Controller {
|
|
8
|
+
static targets = ["list", "trigger", "content"]
|
|
9
|
+
static values = {
|
|
10
|
+
defaultValue: String,
|
|
11
|
+
urlParam: String // Query parameter name for URL sync (e.g., "tab")
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
connect() {
|
|
15
|
+
// Determine initial tab value
|
|
16
|
+
let initialValue = this.getValueFromUrl() || this.defaultValueValue || this.triggerTargets[0]?.dataset.value
|
|
17
|
+
|
|
18
|
+
if (initialValue) {
|
|
19
|
+
// Validate that the value exists in our triggers
|
|
20
|
+
const validValues = this.triggerTargets.map(t => t.dataset.value)
|
|
21
|
+
if (!validValues.includes(initialValue)) {
|
|
22
|
+
initialValue = this.defaultValueValue || this.triggerTargets[0]?.dataset.value
|
|
23
|
+
}
|
|
24
|
+
this.selectTabByValue(initialValue, false) // Don't update URL on initial load
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Listen for browser back/forward navigation
|
|
28
|
+
if (this.hasUrlParamValue) {
|
|
29
|
+
window.addEventListener("popstate", this.handlePopState.bind(this))
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
disconnect() {
|
|
34
|
+
if (this.hasUrlParamValue) {
|
|
35
|
+
window.removeEventListener("popstate", this.handlePopState.bind(this))
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
handlePopState() {
|
|
40
|
+
const value = this.getValueFromUrl()
|
|
41
|
+
if (value) {
|
|
42
|
+
this.selectTabByValue(value, false)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
getValueFromUrl() {
|
|
47
|
+
if (!this.hasUrlParamValue) return null
|
|
48
|
+
|
|
49
|
+
const url = new URL(window.location.href)
|
|
50
|
+
return url.searchParams.get(this.urlParamValue)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
updateUrl(value) {
|
|
54
|
+
if (!this.hasUrlParamValue) return
|
|
55
|
+
|
|
56
|
+
const url = new URL(window.location.href)
|
|
57
|
+
url.searchParams.set(this.urlParamValue, value)
|
|
58
|
+
window.history.replaceState({}, "", url.toString())
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
selectTab(event) {
|
|
62
|
+
const trigger = event.currentTarget
|
|
63
|
+
const value = trigger.dataset.value
|
|
64
|
+
this.selectTabByValue(value, true)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
selectTabByValue(value, updateUrl = true) {
|
|
68
|
+
// Update triggers
|
|
69
|
+
this.triggerTargets.forEach(trigger => {
|
|
70
|
+
const isSelected = trigger.dataset.value === value
|
|
71
|
+
trigger.dataset.state = isSelected ? "active" : "inactive"
|
|
72
|
+
trigger.setAttribute("aria-selected", isSelected.toString())
|
|
73
|
+
trigger.tabIndex = isSelected ? 0 : -1
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
// Update content panels
|
|
77
|
+
this.contentTargets.forEach(content => {
|
|
78
|
+
const isSelected = content.dataset.value === value
|
|
79
|
+
content.dataset.state = isSelected ? "active" : "inactive"
|
|
80
|
+
content.hidden = !isSelected
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
// Update URL if enabled
|
|
84
|
+
if (updateUrl) {
|
|
85
|
+
this.updateUrl(value)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
this.dispatch("change", { detail: { value } })
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Keyboard navigation
|
|
92
|
+
handleKeydown(event) {
|
|
93
|
+
const triggers = this.triggerTargets.filter(t => !t.disabled)
|
|
94
|
+
const currentIndex = triggers.findIndex(t => t === document.activeElement)
|
|
95
|
+
|
|
96
|
+
if (currentIndex === -1) return
|
|
97
|
+
|
|
98
|
+
let newIndex = currentIndex
|
|
99
|
+
|
|
100
|
+
switch (event.key) {
|
|
101
|
+
case "ArrowLeft":
|
|
102
|
+
case "ArrowUp":
|
|
103
|
+
event.preventDefault()
|
|
104
|
+
newIndex = currentIndex === 0 ? triggers.length - 1 : currentIndex - 1
|
|
105
|
+
break
|
|
106
|
+
case "ArrowRight":
|
|
107
|
+
case "ArrowDown":
|
|
108
|
+
event.preventDefault()
|
|
109
|
+
newIndex = currentIndex === triggers.length - 1 ? 0 : currentIndex + 1
|
|
110
|
+
break
|
|
111
|
+
case "Home":
|
|
112
|
+
event.preventDefault()
|
|
113
|
+
newIndex = 0
|
|
114
|
+
break
|
|
115
|
+
case "End":
|
|
116
|
+
event.preventDefault()
|
|
117
|
+
newIndex = triggers.length - 1
|
|
118
|
+
break
|
|
119
|
+
default:
|
|
120
|
+
return
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
triggers[newIndex].focus()
|
|
124
|
+
triggers[newIndex].click()
|
|
125
|
+
}
|
|
126
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Toast controller for notification toasts
|
|
5
|
+
*/
|
|
6
|
+
export default class ToastController extends Controller {
|
|
7
|
+
static values: {
|
|
8
|
+
duration: { type: "Number"; default: 5000 };
|
|
9
|
+
open: { type: "Boolean"; default: true };
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/** Auto-dismiss duration in milliseconds (0 to disable) */
|
|
13
|
+
durationValue: number;
|
|
14
|
+
readonly hasDurationValue: boolean;
|
|
15
|
+
|
|
16
|
+
/** Whether the toast is currently visible */
|
|
17
|
+
openValue: boolean;
|
|
18
|
+
readonly hasOpenValue: boolean;
|
|
19
|
+
|
|
20
|
+
/** Dismiss timeout handle */
|
|
21
|
+
dismissTimeout: ReturnType<typeof setTimeout> | null;
|
|
22
|
+
|
|
23
|
+
/** Close the toast */
|
|
24
|
+
close(): void;
|
|
25
|
+
|
|
26
|
+
/** Start the auto-dismiss timer */
|
|
27
|
+
startDismissTimer(): void;
|
|
28
|
+
|
|
29
|
+
/** Clear the dismiss timer */
|
|
30
|
+
clearDismissTimer(): void;
|
|
31
|
+
|
|
32
|
+
/** Pause the dismiss timer (on hover) */
|
|
33
|
+
pause(): void;
|
|
34
|
+
|
|
35
|
+
/** Resume the dismiss timer (on hover end) */
|
|
36
|
+
resume(): void;
|
|
37
|
+
}
|