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,282 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Date Picker controller
|
|
5
|
+
* Handles opening/closing the calendar popover and date selection
|
|
6
|
+
*
|
|
7
|
+
* API inspired by React DayPicker (https://daypicker.dev/)
|
|
8
|
+
*
|
|
9
|
+
* Disabled dates:
|
|
10
|
+
* - minDate/maxDate: Disable dates outside a range
|
|
11
|
+
* - disabledDates: Comma-separated list of YYYY-MM-DD dates
|
|
12
|
+
* - disabledDaysOfWeek: Comma-separated list of day numbers (0=Sun, 6=Sat)
|
|
13
|
+
*/
|
|
14
|
+
export default class extends Controller {
|
|
15
|
+
static targets = ["trigger", "content", "grid", "monthYear", "day", "displayValue", "hiddenInput"]
|
|
16
|
+
static values = {
|
|
17
|
+
open: { type: Boolean, default: false },
|
|
18
|
+
month: String,
|
|
19
|
+
selected: String,
|
|
20
|
+
format: { type: String, default: "medium" },
|
|
21
|
+
placeholder: { type: String, default: "Pick a date" },
|
|
22
|
+
minDate: String,
|
|
23
|
+
maxDate: String,
|
|
24
|
+
disabledDates: String, // comma-separated YYYY-MM-DD
|
|
25
|
+
disabledDaysOfWeek: String, // comma-separated 0-6
|
|
26
|
+
showOutsideDays: { type: Boolean, default: true },
|
|
27
|
+
weekStartsOn: { type: Number, default: 0 } // 0 = Sunday, 1 = Monday, etc.
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
static MONTHS = [
|
|
31
|
+
"January", "February", "March", "April", "May", "June",
|
|
32
|
+
"July", "August", "September", "October", "November", "December"
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
connect() {
|
|
36
|
+
this.currentMonth = this.monthValue ? this.parseLocalDate(this.monthValue) : new Date()
|
|
37
|
+
this.selectedDate = this.selectedValue ? this.parseLocalDate(this.selectedValue) : null
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Parse a date string (YYYY-MM-DD) as local date, not UTC
|
|
42
|
+
* This prevents timezone issues where "2024-11-26" becomes Nov 25 in western timezones
|
|
43
|
+
*/
|
|
44
|
+
parseLocalDate(dateStr) {
|
|
45
|
+
if (!dateStr) return null
|
|
46
|
+
const [year, month, day] = dateStr.split('-').map(Number)
|
|
47
|
+
return new Date(year, month - 1, day)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Format a date as YYYY-MM-DD using local date components
|
|
52
|
+
*/
|
|
53
|
+
formatDateString(date) {
|
|
54
|
+
if (!date) return ''
|
|
55
|
+
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Check if a date is disabled
|
|
60
|
+
*/
|
|
61
|
+
isDateDisabled(date) {
|
|
62
|
+
const dateStr = this.formatDateString(date)
|
|
63
|
+
|
|
64
|
+
// Check min/max date
|
|
65
|
+
if (this.minDateValue) {
|
|
66
|
+
const minDate = this.parseLocalDate(this.minDateValue)
|
|
67
|
+
if (date < minDate) return true
|
|
68
|
+
}
|
|
69
|
+
if (this.maxDateValue) {
|
|
70
|
+
const maxDate = this.parseLocalDate(this.maxDateValue)
|
|
71
|
+
if (date > maxDate) return true
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Check disabled dates list
|
|
75
|
+
if (this.disabledDatesValue) {
|
|
76
|
+
const disabledDates = this.disabledDatesValue.split(",").map(d => d.trim())
|
|
77
|
+
if (disabledDates.includes(dateStr)) return true
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Check disabled days of week
|
|
81
|
+
if (this.disabledDaysOfWeekValue) {
|
|
82
|
+
const disabledDays = this.disabledDaysOfWeekValue.split(",").map(d => parseInt(d.trim(), 10))
|
|
83
|
+
if (disabledDays.includes(date.getDay())) return true
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return false
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
toggle() {
|
|
90
|
+
this.openValue = !this.openValue
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
open() {
|
|
94
|
+
this.openValue = true
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
close() {
|
|
98
|
+
this.openValue = false
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
openValueChanged() {
|
|
102
|
+
if (this.hasContentTarget) {
|
|
103
|
+
this.contentTarget.style.display = this.openValue ? "block" : "none"
|
|
104
|
+
}
|
|
105
|
+
if (this.hasTriggerTarget) {
|
|
106
|
+
this.triggerTarget.setAttribute("aria-expanded", this.openValue.toString())
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
closeOnClickOutside(event) {
|
|
111
|
+
if (!this.openValue) return
|
|
112
|
+
if (this.element.contains(event.target)) return
|
|
113
|
+
|
|
114
|
+
this.close()
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
previousMonth() {
|
|
118
|
+
this.currentMonth.setMonth(this.currentMonth.getMonth() - 1)
|
|
119
|
+
this.render()
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
nextMonth() {
|
|
123
|
+
this.currentMonth.setMonth(this.currentMonth.getMonth() + 1)
|
|
124
|
+
this.render()
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
selectDay(event) {
|
|
128
|
+
const dateStr = event.currentTarget.dataset.date
|
|
129
|
+
if (!dateStr) return
|
|
130
|
+
|
|
131
|
+
const date = this.parseLocalDate(dateStr)
|
|
132
|
+
|
|
133
|
+
// Check if disabled
|
|
134
|
+
if (this.isDateDisabled(date)) return
|
|
135
|
+
|
|
136
|
+
this.selectedDate = date
|
|
137
|
+
this.selectedValue = dateStr
|
|
138
|
+
|
|
139
|
+
// Update hidden input
|
|
140
|
+
if (this.hasHiddenInputTarget) {
|
|
141
|
+
this.hiddenInputTarget.value = dateStr
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Update display value
|
|
145
|
+
if (this.hasDisplayValueTarget) {
|
|
146
|
+
this.displayValueTarget.textContent = this.formatDate(this.selectedDate)
|
|
147
|
+
this.displayValueTarget.classList.remove("text-muted-foreground")
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Re-render calendar to update selection styling
|
|
151
|
+
this.render()
|
|
152
|
+
|
|
153
|
+
// Close the popover
|
|
154
|
+
this.close()
|
|
155
|
+
|
|
156
|
+
// Dispatch custom event
|
|
157
|
+
this.dispatch("select", {
|
|
158
|
+
detail: {
|
|
159
|
+
date: this.selectedDate,
|
|
160
|
+
dateString: dateStr
|
|
161
|
+
}
|
|
162
|
+
})
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
formatDate(date) {
|
|
166
|
+
switch (this.formatValue) {
|
|
167
|
+
case "short":
|
|
168
|
+
return `${(date.getMonth() + 1).toString().padStart(2, '0')}/${date.getDate().toString().padStart(2, '0')}/${date.getFullYear()}`
|
|
169
|
+
case "long":
|
|
170
|
+
return date.toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric', year: 'numeric' })
|
|
171
|
+
case "iso":
|
|
172
|
+
// Use local date components to avoid timezone issues
|
|
173
|
+
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`
|
|
174
|
+
case "medium":
|
|
175
|
+
default:
|
|
176
|
+
return date.toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric' })
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
render() {
|
|
181
|
+
// Update month/year label
|
|
182
|
+
if (this.hasMonthYearTarget) {
|
|
183
|
+
const monthName = this.constructor.MONTHS[this.currentMonth.getMonth()]
|
|
184
|
+
const year = this.currentMonth.getFullYear()
|
|
185
|
+
this.monthYearTarget.textContent = `${monthName} ${year}`
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Render days grid
|
|
189
|
+
if (this.hasGridTarget) {
|
|
190
|
+
this.gridTarget.innerHTML = this.renderDays()
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
renderDays() {
|
|
195
|
+
const year = this.currentMonth.getFullYear()
|
|
196
|
+
const month = this.currentMonth.getMonth()
|
|
197
|
+
|
|
198
|
+
// Get first and last day of month
|
|
199
|
+
const firstDay = new Date(year, month, 1)
|
|
200
|
+
const lastDay = new Date(year, month + 1, 0)
|
|
201
|
+
|
|
202
|
+
// Get start date based on weekStartsOn
|
|
203
|
+
const startDate = new Date(firstDay)
|
|
204
|
+
const dayOffset = (firstDay.getDay() - this.weekStartsOnValue + 7) % 7
|
|
205
|
+
startDate.setDate(firstDay.getDate() - dayOffset)
|
|
206
|
+
|
|
207
|
+
// Get end date (complete the last week)
|
|
208
|
+
const endDate = new Date(lastDay)
|
|
209
|
+
const endDayOffset = (6 - lastDay.getDay() + this.weekStartsOnValue) % 7
|
|
210
|
+
endDate.setDate(lastDay.getDate() + endDayOffset)
|
|
211
|
+
|
|
212
|
+
const today = new Date()
|
|
213
|
+
today.setHours(0, 0, 0, 0)
|
|
214
|
+
|
|
215
|
+
let html = ""
|
|
216
|
+
const currentDate = new Date(startDate)
|
|
217
|
+
|
|
218
|
+
while (currentDate <= endDate) {
|
|
219
|
+
const isOutside = currentDate.getMonth() !== month
|
|
220
|
+
const isToday = currentDate.getTime() === today.getTime()
|
|
221
|
+
const isSelected = this.selectedDate &&
|
|
222
|
+
currentDate.toDateString() === this.selectedDate.toDateString()
|
|
223
|
+
const isDisabled = this.isDateDisabled(currentDate)
|
|
224
|
+
|
|
225
|
+
// Use local date components to avoid timezone issues with toISOString()
|
|
226
|
+
const dateStr = this.formatDateString(currentDate)
|
|
227
|
+
|
|
228
|
+
// Skip outside days if showOutsideDays is false
|
|
229
|
+
if (isOutside && !this.showOutsideDaysValue) {
|
|
230
|
+
html += '<div class="h-8 w-8"></div>'
|
|
231
|
+
currentDate.setDate(currentDate.getDate() + 1)
|
|
232
|
+
continue
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
let classes = "h-8 w-8 text-center text-sm p-0 relative flex items-center justify-center rounded-md focus:outline-none focus:ring-1 focus:ring-ring"
|
|
236
|
+
|
|
237
|
+
if (isDisabled) {
|
|
238
|
+
classes += " text-muted-foreground opacity-50 cursor-not-allowed"
|
|
239
|
+
} else if (isSelected) {
|
|
240
|
+
classes += " bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground cursor-pointer"
|
|
241
|
+
} else if (isToday) {
|
|
242
|
+
classes += " bg-accent text-accent-foreground cursor-pointer hover:bg-accent hover:text-accent-foreground"
|
|
243
|
+
} else {
|
|
244
|
+
classes += " cursor-pointer hover:bg-accent hover:text-accent-foreground"
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (isOutside && !isDisabled) {
|
|
248
|
+
classes += " text-muted-foreground opacity-50"
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const ariaAttrs = []
|
|
252
|
+
if (isSelected) ariaAttrs.push('aria-selected="true"')
|
|
253
|
+
if (isDisabled) {
|
|
254
|
+
ariaAttrs.push('aria-disabled="true"')
|
|
255
|
+
ariaAttrs.push('disabled')
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Only add click action for non-disabled days
|
|
259
|
+
const dataAction = isDisabled
|
|
260
|
+
? ''
|
|
261
|
+
: 'data-action="click->shadcn--date-picker#selectDay"'
|
|
262
|
+
|
|
263
|
+
html += `<button type="button" class="${classes}" data-date="${dateStr}" data-shadcn--date-picker-target="day" ${dataAction} ${ariaAttrs.join(" ")}>${currentDate.getDate()}</button>`
|
|
264
|
+
|
|
265
|
+
currentDate.setDate(currentDate.getDate() + 1)
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return html
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
monthValueChanged() {
|
|
272
|
+
if (this.monthValue) {
|
|
273
|
+
this.currentMonth = this.parseLocalDate(this.monthValue)
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
selectedValueChanged() {
|
|
278
|
+
if (this.selectedValue) {
|
|
279
|
+
this.selectedDate = this.parseLocalDate(this.selectedValue)
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Dialog controller for modal dialogs
|
|
5
|
+
* Handles opening, closing, focus trapping, and keyboard navigation
|
|
6
|
+
*/
|
|
7
|
+
export default class DialogController extends Controller {
|
|
8
|
+
static targets: ["trigger", "template", "overlay", "content"];
|
|
9
|
+
static values: {
|
|
10
|
+
open: { type: "Boolean"; default: false };
|
|
11
|
+
modal: { type: "Boolean"; default: true };
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/** Dialog trigger target */
|
|
15
|
+
readonly triggerTarget: HTMLElement;
|
|
16
|
+
readonly hasTriggerTarget: boolean;
|
|
17
|
+
|
|
18
|
+
/** Template containing dialog content */
|
|
19
|
+
readonly templateTarget: HTMLTemplateElement;
|
|
20
|
+
readonly hasTemplateTarget: boolean;
|
|
21
|
+
|
|
22
|
+
/** Dialog overlay target */
|
|
23
|
+
readonly overlayTarget: HTMLElement;
|
|
24
|
+
readonly hasOverlayTarget: boolean;
|
|
25
|
+
|
|
26
|
+
/** Dialog content target */
|
|
27
|
+
readonly contentTarget: HTMLElement;
|
|
28
|
+
readonly hasContentTarget: boolean;
|
|
29
|
+
|
|
30
|
+
/** Whether the dialog is open */
|
|
31
|
+
openValue: boolean;
|
|
32
|
+
readonly hasOpenValue: boolean;
|
|
33
|
+
|
|
34
|
+
/** Whether the dialog is modal (traps focus, prevents body scroll) */
|
|
35
|
+
modalValue: boolean;
|
|
36
|
+
readonly hasModalValue: boolean;
|
|
37
|
+
|
|
38
|
+
/** Portal element (created dynamically) */
|
|
39
|
+
portal: HTMLDivElement | null;
|
|
40
|
+
|
|
41
|
+
/** Previously focused element */
|
|
42
|
+
previousActiveElement: Element | null;
|
|
43
|
+
|
|
44
|
+
/** Open the dialog */
|
|
45
|
+
open(): void;
|
|
46
|
+
|
|
47
|
+
/** Close the dialog */
|
|
48
|
+
close(): void;
|
|
49
|
+
|
|
50
|
+
/** Toggle dialog open/closed */
|
|
51
|
+
toggle(): void;
|
|
52
|
+
|
|
53
|
+
/** Handle keydown events (Escape to close, Tab for focus trapping) */
|
|
54
|
+
handleKeydown(event: KeyboardEvent): void;
|
|
55
|
+
|
|
56
|
+
/** Handle clicks outside the dialog content */
|
|
57
|
+
handleClickOutside(event: MouseEvent): void;
|
|
58
|
+
|
|
59
|
+
/** Focus the first focusable element in the dialog */
|
|
60
|
+
focusFirstElement(): void;
|
|
61
|
+
|
|
62
|
+
/** Trap focus within the dialog */
|
|
63
|
+
trapFocus(event: KeyboardEvent): void;
|
|
64
|
+
|
|
65
|
+
/** Called when openValue changes */
|
|
66
|
+
openValueChanged(): void;
|
|
67
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Dialog controller for modal dialogs
|
|
5
|
+
* Handles opening, closing, focus trapping, and keyboard navigation
|
|
6
|
+
*/
|
|
7
|
+
export default class extends Controller {
|
|
8
|
+
static targets = ["trigger", "template", "overlay", "content"]
|
|
9
|
+
static values = {
|
|
10
|
+
open: { type: Boolean, default: false },
|
|
11
|
+
modal: { type: Boolean, default: true }
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
connect() {
|
|
15
|
+
this.portal = null
|
|
16
|
+
this.previousActiveElement = null
|
|
17
|
+
this.boundHandleKeydown = this.handleKeydown.bind(this)
|
|
18
|
+
this.boundHandleClickOutside = this.handleClickOutside.bind(this)
|
|
19
|
+
|
|
20
|
+
if (this.openValue) {
|
|
21
|
+
this.open()
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
disconnect() {
|
|
26
|
+
this.close()
|
|
27
|
+
if (this.portal) {
|
|
28
|
+
this.portal.remove()
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
open() {
|
|
33
|
+
if (this.openValue) return
|
|
34
|
+
|
|
35
|
+
this.previousActiveElement = document.activeElement
|
|
36
|
+
this.openValue = true
|
|
37
|
+
|
|
38
|
+
// Move template content to body
|
|
39
|
+
if (this.hasTemplateTarget && !this.portal) {
|
|
40
|
+
this.portal = document.createElement("div")
|
|
41
|
+
this.portal.className = "shadcn-dialog-portal"
|
|
42
|
+
this.portal.innerHTML = this.templateTarget.innerHTML
|
|
43
|
+
document.body.appendChild(this.portal)
|
|
44
|
+
|
|
45
|
+
// Re-query targets from portal
|
|
46
|
+
this.portalOverlay = this.portal.querySelector('[data-shadcn--dialog-target="overlay"]')
|
|
47
|
+
this.portalContent = this.portal.querySelector('[data-shadcn--dialog-target="content"]')
|
|
48
|
+
|
|
49
|
+
// Wire up close actions on portal elements (since they're outside controller scope)
|
|
50
|
+
this.portal.querySelectorAll('[data-action*="shadcn--dialog#close"]').forEach(el => {
|
|
51
|
+
el.addEventListener("click", (e) => {
|
|
52
|
+
e.preventDefault()
|
|
53
|
+
this.close()
|
|
54
|
+
})
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
// Also handle overlay click
|
|
58
|
+
if (this.portalOverlay) {
|
|
59
|
+
this.portalOverlay.addEventListener("click", () => this.close())
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Show overlay and content
|
|
64
|
+
requestAnimationFrame(() => {
|
|
65
|
+
if (this.portalOverlay) {
|
|
66
|
+
this.portalOverlay.dataset.state = "open"
|
|
67
|
+
this.portalOverlay.removeAttribute("hidden")
|
|
68
|
+
}
|
|
69
|
+
if (this.portalContent) {
|
|
70
|
+
this.portalContent.dataset.state = "open"
|
|
71
|
+
this.portalContent.removeAttribute("hidden")
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Setup event listeners
|
|
75
|
+
document.addEventListener("keydown", this.boundHandleKeydown)
|
|
76
|
+
|
|
77
|
+
// Focus first focusable element
|
|
78
|
+
this.focusFirstElement()
|
|
79
|
+
|
|
80
|
+
// Prevent body scroll
|
|
81
|
+
if (this.modalValue) {
|
|
82
|
+
document.body.style.overflow = "hidden"
|
|
83
|
+
}
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
this.dispatch("opened")
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
close() {
|
|
90
|
+
if (!this.openValue) return
|
|
91
|
+
|
|
92
|
+
this.openValue = false
|
|
93
|
+
|
|
94
|
+
if (this.portalOverlay) {
|
|
95
|
+
this.portalOverlay.dataset.state = "closed"
|
|
96
|
+
}
|
|
97
|
+
if (this.portalContent) {
|
|
98
|
+
this.portalContent.dataset.state = "closed"
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Remove event listeners
|
|
102
|
+
document.removeEventListener("keydown", this.boundHandleKeydown)
|
|
103
|
+
|
|
104
|
+
// Restore body scroll
|
|
105
|
+
document.body.style.overflow = ""
|
|
106
|
+
|
|
107
|
+
// Return focus
|
|
108
|
+
if (this.previousActiveElement) {
|
|
109
|
+
this.previousActiveElement.focus()
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Remove portal after animation
|
|
113
|
+
setTimeout(() => {
|
|
114
|
+
if (this.portal) {
|
|
115
|
+
this.portal.remove()
|
|
116
|
+
this.portal = null
|
|
117
|
+
}
|
|
118
|
+
}, 200)
|
|
119
|
+
|
|
120
|
+
this.dispatch("closed")
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
toggle() {
|
|
124
|
+
if (this.openValue) {
|
|
125
|
+
this.close()
|
|
126
|
+
} else {
|
|
127
|
+
this.open()
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
handleKeydown(event) {
|
|
132
|
+
if (event.key === "Escape") {
|
|
133
|
+
this.close()
|
|
134
|
+
} else if (event.key === "Tab" && this.modalValue) {
|
|
135
|
+
this.trapFocus(event)
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
handleClickOutside(event) {
|
|
140
|
+
if (this.portalContent && !this.portalContent.contains(event.target)) {
|
|
141
|
+
this.close()
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
focusFirstElement() {
|
|
146
|
+
if (!this.portalContent) return
|
|
147
|
+
|
|
148
|
+
const focusable = this.portalContent.querySelectorAll(
|
|
149
|
+
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
|
|
150
|
+
)
|
|
151
|
+
if (focusable.length > 0) {
|
|
152
|
+
focusable[0].focus()
|
|
153
|
+
} else {
|
|
154
|
+
this.portalContent.focus()
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
trapFocus(event) {
|
|
159
|
+
if (!this.portalContent) return
|
|
160
|
+
|
|
161
|
+
const focusable = this.portalContent.querySelectorAll(
|
|
162
|
+
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
|
|
163
|
+
)
|
|
164
|
+
const firstFocusable = focusable[0]
|
|
165
|
+
const lastFocusable = focusable[focusable.length - 1]
|
|
166
|
+
|
|
167
|
+
if (event.shiftKey) {
|
|
168
|
+
if (document.activeElement === firstFocusable) {
|
|
169
|
+
lastFocusable.focus()
|
|
170
|
+
event.preventDefault()
|
|
171
|
+
}
|
|
172
|
+
} else {
|
|
173
|
+
if (document.activeElement === lastFocusable) {
|
|
174
|
+
firstFocusable.focus()
|
|
175
|
+
event.preventDefault()
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
openValueChanged() {
|
|
181
|
+
if (this.openValue) {
|
|
182
|
+
this.open()
|
|
183
|
+
} else {
|
|
184
|
+
this.close()
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Drawer Controller
|
|
5
|
+
* Handles opening/closing drawer panels
|
|
6
|
+
*/
|
|
7
|
+
export default class DrawerController extends Controller {
|
|
8
|
+
static targets: ["trigger", "template", "overlay", "content"];
|
|
9
|
+
static values: {
|
|
10
|
+
open: { type: "Boolean"; default: false };
|
|
11
|
+
direction: { type: "String"; default: "bottom" };
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/** Drawer trigger target */
|
|
15
|
+
readonly triggerTarget: HTMLElement;
|
|
16
|
+
readonly hasTriggerTarget: boolean;
|
|
17
|
+
|
|
18
|
+
/** Template containing drawer content */
|
|
19
|
+
readonly templateTarget: HTMLTemplateElement;
|
|
20
|
+
readonly hasTemplateTarget: boolean;
|
|
21
|
+
|
|
22
|
+
/** Drawer overlay target */
|
|
23
|
+
readonly overlayTarget: HTMLElement;
|
|
24
|
+
readonly hasOverlayTarget: boolean;
|
|
25
|
+
|
|
26
|
+
/** Drawer content target */
|
|
27
|
+
readonly contentTarget: HTMLElement;
|
|
28
|
+
readonly hasContentTarget: boolean;
|
|
29
|
+
|
|
30
|
+
/** Whether the drawer is open */
|
|
31
|
+
openValue: boolean;
|
|
32
|
+
readonly hasOpenValue: boolean;
|
|
33
|
+
|
|
34
|
+
/** Direction the drawer slides from: "top", "right", "bottom", "left" */
|
|
35
|
+
directionValue: "top" | "right" | "bottom" | "left";
|
|
36
|
+
readonly hasDirectionValue: boolean;
|
|
37
|
+
|
|
38
|
+
/** Portal element (created dynamically) */
|
|
39
|
+
portal: HTMLDivElement | null;
|
|
40
|
+
|
|
41
|
+
/** Open the drawer */
|
|
42
|
+
open(): void;
|
|
43
|
+
|
|
44
|
+
/** Close the drawer */
|
|
45
|
+
close(): void;
|
|
46
|
+
|
|
47
|
+
/** Toggle drawer open/closed */
|
|
48
|
+
toggle(): void;
|
|
49
|
+
|
|
50
|
+
/** Handle keydown events (Escape to close) */
|
|
51
|
+
handleKeydown(event: KeyboardEvent): void;
|
|
52
|
+
|
|
53
|
+
/** Remove the portal from the DOM */
|
|
54
|
+
removePortal(): void;
|
|
55
|
+
|
|
56
|
+
/** Called when openValue changes */
|
|
57
|
+
openValueChanged(): void;
|
|
58
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Drawer Controller
|
|
5
|
+
* Handles opening/closing drawer panels with swipe support
|
|
6
|
+
*/
|
|
7
|
+
export default class extends Controller {
|
|
8
|
+
static targets = ["trigger", "template", "overlay", "content"]
|
|
9
|
+
static values = {
|
|
10
|
+
open: { type: Boolean, default: false },
|
|
11
|
+
direction: { type: String, default: "bottom" }
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
connect() {
|
|
15
|
+
this.portal = null
|
|
16
|
+
this.boundHandleKeydown = this.handleKeydown.bind(this)
|
|
17
|
+
|
|
18
|
+
if (this.openValue) {
|
|
19
|
+
this.open()
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
disconnect() {
|
|
24
|
+
this.removePortal()
|
|
25
|
+
document.removeEventListener("keydown", this.boundHandleKeydown)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
open() {
|
|
29
|
+
if (!this.hasTemplateTarget) return
|
|
30
|
+
|
|
31
|
+
// Create portal at body level
|
|
32
|
+
this.portal = document.createElement("div")
|
|
33
|
+
this.portal.innerHTML = this.templateTarget.innerHTML
|
|
34
|
+
document.body.appendChild(this.portal)
|
|
35
|
+
|
|
36
|
+
// Get references to portal elements
|
|
37
|
+
const overlay = this.portal.querySelector("[data-shadcn--drawer-target='overlay']")
|
|
38
|
+
const content = this.portal.querySelector("[data-shadcn--drawer-target='content']")
|
|
39
|
+
|
|
40
|
+
// Add click handler to overlay
|
|
41
|
+
if (overlay) {
|
|
42
|
+
overlay.addEventListener("click", () => this.close())
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Set open state
|
|
46
|
+
requestAnimationFrame(() => {
|
|
47
|
+
if (overlay) overlay.setAttribute("data-state", "open")
|
|
48
|
+
if (content) {
|
|
49
|
+
content.setAttribute("data-state", "open")
|
|
50
|
+
content.focus()
|
|
51
|
+
}
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
// Prevent body scroll
|
|
55
|
+
document.body.style.overflow = "hidden"
|
|
56
|
+
document.addEventListener("keydown", this.boundHandleKeydown)
|
|
57
|
+
|
|
58
|
+
this.openValue = true
|
|
59
|
+
this.dispatch("open")
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
close() {
|
|
63
|
+
if (!this.portal) return
|
|
64
|
+
|
|
65
|
+
const overlay = this.portal.querySelector("[data-shadcn--drawer-target='overlay']")
|
|
66
|
+
const content = this.portal.querySelector("[data-shadcn--drawer-target='content']")
|
|
67
|
+
|
|
68
|
+
// Set closing state
|
|
69
|
+
if (overlay) overlay.setAttribute("data-state", "closed")
|
|
70
|
+
if (content) content.setAttribute("data-state", "closed")
|
|
71
|
+
|
|
72
|
+
// Wait for animation then remove portal
|
|
73
|
+
setTimeout(() => {
|
|
74
|
+
this.removePortal()
|
|
75
|
+
}, 200)
|
|
76
|
+
|
|
77
|
+
document.body.style.overflow = ""
|
|
78
|
+
document.removeEventListener("keydown", this.boundHandleKeydown)
|
|
79
|
+
|
|
80
|
+
this.openValue = false
|
|
81
|
+
this.dispatch("close")
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
toggle() {
|
|
85
|
+
if (this.openValue) {
|
|
86
|
+
this.close()
|
|
87
|
+
} else {
|
|
88
|
+
this.open()
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
handleKeydown(event) {
|
|
93
|
+
if (event.key === "Escape") {
|
|
94
|
+
this.close()
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
removePortal() {
|
|
99
|
+
if (this.portal) {
|
|
100
|
+
this.portal.remove()
|
|
101
|
+
this.portal = null
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
openValueChanged() {
|
|
106
|
+
if (this.openValue && !this.portal) {
|
|
107
|
+
this.open()
|
|
108
|
+
} else if (!this.openValue && this.portal) {
|
|
109
|
+
this.close()
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|