shadcn-rails 0.2.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +66 -2
  3. data/README.md +21 -8
  4. data/__mocks__/@floating-ui/dom.js +67 -0
  5. data/app/assets/javascripts/shadcn/controllers/combobox_controller.js +23 -2
  6. data/app/assets/javascripts/shadcn/controllers/context_menu_controller.js +4 -31
  7. data/app/assets/javascripts/shadcn/controllers/dropdown_controller.js +32 -41
  8. data/app/assets/javascripts/shadcn/controllers/hover_card_controller.js +29 -55
  9. data/app/assets/javascripts/shadcn/controllers/popover_controller.js +29 -54
  10. data/app/assets/javascripts/shadcn/controllers/select_controller.js +26 -8
  11. data/app/assets/javascripts/shadcn/controllers/tooltip_controller.js +28 -59
  12. data/app/assets/javascripts/shadcn/index.js +7 -1
  13. data/app/assets/javascripts/shadcn/utils/floating.js +179 -0
  14. data/app/assets/stylesheets/shadcn/base.css +32 -0
  15. data/app/components/shadcn/accordion_component.html.erb +8 -0
  16. data/app/components/shadcn/accordion_component.rb +6 -15
  17. data/app/components/shadcn/alert_component.html.erb +6 -0
  18. data/app/components/shadcn/alert_component.rb +0 -18
  19. data/app/components/shadcn/alert_dialog_component.html.erb +12 -0
  20. data/app/components/shadcn/alert_dialog_component.rb +7 -27
  21. data/app/components/shadcn/aspect_ratio_component.html.erb +7 -0
  22. data/app/components/shadcn/aspect_ratio_component.rb +4 -19
  23. data/app/components/shadcn/avatar_component.html.erb +20 -0
  24. data/app/components/shadcn/avatar_component.rb +8 -36
  25. data/app/components/shadcn/badge_component.html.erb +1 -0
  26. data/app/components/shadcn/badge_component.rb +0 -11
  27. data/app/components/shadcn/base_component.rb +15 -2
  28. data/app/components/shadcn/breadcrumb_component.html.erb +5 -0
  29. data/app/components/shadcn/breadcrumb_component.rb +6 -16
  30. data/app/components/shadcn/button_component.html.erb +18 -0
  31. data/app/components/shadcn/button_component.rb +1 -41
  32. data/app/components/shadcn/card_component.html.erb +8 -0
  33. data/app/components/shadcn/card_component.rb +2 -6
  34. data/app/components/shadcn/checkbox_component.html.erb +32 -0
  35. data/app/components/shadcn/checkbox_component.rb +4 -43
  36. data/app/components/shadcn/collapsible_component.html.erb +8 -0
  37. data/app/components/shadcn/collapsible_component.rb +6 -15
  38. data/app/components/shadcn/context_menu_component.html.erb +11 -0
  39. data/app/components/shadcn/context_menu_component.rb +6 -26
  40. data/app/components/shadcn/dialog_component.html.erb +14 -0
  41. data/app/components/shadcn/dialog_component.rb +8 -29
  42. data/app/components/shadcn/drawer_component.html.erb +12 -0
  43. data/app/components/shadcn/drawer_component.rb +7 -27
  44. data/app/components/shadcn/dropdown_menu_component.html.erb +14 -0
  45. data/app/components/shadcn/dropdown_menu_component.rb +9 -29
  46. data/app/components/shadcn/field_component.rb +7 -8
  47. data/app/components/shadcn/hover_card_component.html.erb +12 -0
  48. data/app/components/shadcn/hover_card_component.rb +7 -26
  49. data/app/components/shadcn/input_component.html.erb +18 -0
  50. data/app/components/shadcn/input_component.rb +2 -27
  51. data/app/components/shadcn/input_otp_component.rb +3 -3
  52. data/app/components/shadcn/kbd_component.html.erb +1 -0
  53. data/app/components/shadcn/kbd_component.rb +3 -10
  54. data/app/components/shadcn/label_component.html.erb +3 -0
  55. data/app/components/shadcn/label_component.rb +2 -18
  56. data/app/components/shadcn/menubar_component.html.erb +6 -0
  57. data/app/components/shadcn/menubar_component.rb +4 -15
  58. data/app/components/shadcn/native_select_component.html.erb +22 -0
  59. data/app/components/shadcn/native_select_component.rb +9 -39
  60. data/app/components/shadcn/navigation_menu_component.html.erb +6 -0
  61. data/app/components/shadcn/navigation_menu_component.rb +4 -15
  62. data/app/components/shadcn/pagination_component.html.erb +5 -0
  63. data/app/components/shadcn/pagination_component.rb +11 -15
  64. data/app/components/shadcn/popover_component.html.erb +15 -0
  65. data/app/components/shadcn/popover_component.rb +10 -30
  66. data/app/components/shadcn/progress_component.html.erb +13 -0
  67. data/app/components/shadcn/progress_component.rb +6 -26
  68. data/app/components/shadcn/radio_group_component.html.erb +8 -0
  69. data/app/components/shadcn/radio_group_component.rb +12 -26
  70. data/app/components/shadcn/scroll_area_component.html.erb +7 -0
  71. data/app/components/shadcn/scroll_area_component.rb +4 -16
  72. data/app/components/shadcn/select_component.html.erb +46 -0
  73. data/app/components/shadcn/select_component.rb +6 -80
  74. data/app/components/shadcn/separator_component.html.erb +5 -0
  75. data/app/components/shadcn/separator_component.rb +6 -14
  76. data/app/components/shadcn/sheet_component.html.erb +12 -0
  77. data/app/components/shadcn/sheet_component.rb +7 -27
  78. data/app/components/shadcn/sidebar_component.rb +2 -2
  79. data/app/components/shadcn/skeleton_component.html.erb +1 -0
  80. data/app/components/shadcn/skeleton_component.rb +4 -2
  81. data/app/components/shadcn/slider_component.html.erb +12 -0
  82. data/app/components/shadcn/slider_component.rb +2 -21
  83. data/app/components/shadcn/spinner_component.html.erb +18 -0
  84. data/app/components/shadcn/spinner_component.rb +2 -30
  85. data/app/components/shadcn/switch_component.html.erb +72 -0
  86. data/app/components/shadcn/switch_component.rb +4 -82
  87. data/app/components/shadcn/table_component.html.erb +9 -0
  88. data/app/components/shadcn/table_component.rb +2 -10
  89. data/app/components/shadcn/tabs_component.html.erb +8 -0
  90. data/app/components/shadcn/tabs_component.rb +4 -17
  91. data/app/components/shadcn/textarea_component.html.erb +13 -0
  92. data/app/components/shadcn/textarea_component.rb +6 -22
  93. data/app/components/shadcn/toast_component.html.erb +36 -0
  94. data/app/components/shadcn/toast_component.rb +6 -54
  95. data/app/components/shadcn/toggle_component.html.erb +12 -0
  96. data/app/components/shadcn/toggle_component.rb +6 -21
  97. data/app/components/shadcn/toggle_group_component.html.erb +14 -0
  98. data/app/components/shadcn/toggle_group_component.rb +6 -29
  99. data/app/components/shadcn/tooltip_component.html.erb +20 -0
  100. data/app/components/shadcn/tooltip_component.rb +13 -38
  101. data/lib/generators/shadcn/add/USAGE +24 -0
  102. data/lib/generators/shadcn/add/add_generator.rb +279 -0
  103. data/lib/generators/shadcn/install/USAGE +22 -0
  104. data/lib/generators/shadcn/install/install_generator.rb +8 -3
  105. data/lib/generators/shadcn/install/templates/initializer.rb.tt +7 -27
  106. data/lib/generators/shadcn/install/templates/shadcn.yml.tt +15 -31
  107. data/lib/shadcn/rails/version.rb +1 -1
  108. metadata +47 -45
  109. data/.dockerignore +0 -40
  110. data/CLAUDE.md +0 -612
  111. data/PROGRESS.md +0 -495
  112. data/Rakefile +0 -95
  113. data/__tests__/controllers/__snapshots__/calendar_controller.test.js.snap +0 -13
  114. data/__tests__/controllers/__snapshots__/popover_controller.test.js.snap +0 -46
  115. data/__tests__/controllers/__snapshots__/sheet_controller.test.js.snap +0 -111
  116. data/__tests__/controllers/__snapshots__/tabs_controller.test.js.snap +0 -27
  117. data/__tests__/controllers/accordion_controller.test.js +0 -904
  118. data/__tests__/controllers/calendar_controller.test.js +0 -1370
  119. data/__tests__/controllers/carousel_controller.test.js +0 -912
  120. data/__tests__/controllers/checkbox_controller.test.js +0 -454
  121. data/__tests__/controllers/collapsible_controller.test.js +0 -407
  122. data/__tests__/controllers/combobox_controller.test.js +0 -971
  123. data/__tests__/controllers/context_menu_controller.test.js +0 -905
  124. data/__tests__/controllers/date_picker_controller.test.js +0 -636
  125. data/__tests__/controllers/dialog_controller.test.js +0 -878
  126. data/__tests__/controllers/drawer_controller.test.js +0 -995
  127. data/__tests__/controllers/menubar_controller.test.js +0 -737
  128. data/__tests__/controllers/navigation_menu_controller.test.js +0 -599
  129. data/__tests__/controllers/popover_controller.test.js +0 -982
  130. data/__tests__/controllers/radio_group_controller.test.js +0 -640
  131. data/__tests__/controllers/resizable_controller.test.js +0 -680
  132. data/__tests__/controllers/select_controller.test.js +0 -678
  133. data/__tests__/controllers/sheet_controller.test.js +0 -986
  134. data/__tests__/controllers/slider_controller.test.js +0 -1036
  135. data/__tests__/controllers/switch_controller.test.js +0 -424
  136. data/__tests__/controllers/tabs_controller.test.js +0 -907
  137. data/__tests__/controllers/toggle_group_controller.test.js +0 -839
  138. data/__tests__/controllers/tooltip_controller.test.js +0 -808
  139. data/__tests__/helpers/stimulus-test-helper.js +0 -203
  140. data/babel.config.cjs +0 -5
  141. data/bin/bump +0 -321
  142. data/bin/console +0 -11
  143. data/bin/release +0 -205
  144. data/bin/setup +0 -8
  145. data/bin/test +0 -75
  146. data/jest.config.js +0 -19
  147. data/jest.setup.js +0 -8
  148. data/lib/generators/shadcn/component/component_generator.rb +0 -188
  149. data/lib/generators/shadcn/theme/theme_generator.rb +0 -128
  150. data/package-lock.json +0 -7438
  151. data/package.json +0 -71
  152. data/rollup.config.js +0 -29
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6ed3ecfa5b3774e31da3ea629fe24151b003bdb80ab5e3032e726987e7756327
4
- data.tar.gz: bdb60df75e3eac0c4e3e1eeea2a1d121865964d2aaf878ab3a037ef435f0f333
3
+ metadata.gz: 169b88e9623277a0a148a98510ea1b5f0fbe56f8edd12bd160c406aa6516c681
4
+ data.tar.gz: 0da7fa6d9b3e1e20a2ebd081dec32140a44641c4aadb2fd096ba0de5545ae848
5
5
  SHA512:
6
- metadata.gz: 7f2ac29443aecaa53f1a989d070bb8d390e4b96308f0509b021129af1a8e27f1016b87938a17cc831fd0aca2e54f821c3a24561bb930637da0d5e5efe6fba5c2
7
- data.tar.gz: '09d5d7e5293bf5ea755ac9911a049799d0a38464e16432c1597d18d860f401a874980a5f6f304e8ee9e9b2c8a1f1046957eabce7883d71f964b8cbdb09a7e96e'
6
+ metadata.gz: 4adc7d30a2a186e968569e0c11a899bea9efe640b6782e227aba2608fec962e2701355351d4fc56333dae4ef582fe8a9ec0f0662c06da01994593b2d2efb9376
7
+ data.tar.gz: 3f754ff7d630b0e630101da96f1a9255625bb919dc7ad3d9e7ff11b62ee575d3d434cd0dba0de35f3b9c7652a173eec730b826c136d9a9b209f468fbc1a2b791
data/CHANGELOG.md CHANGED
@@ -7,9 +7,72 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.2.1] - 2025-11-27
11
+
12
+ ### Added
13
+
14
+ - **Clipboard controller** for documentation site - Copy buttons now work with "Copied!" feedback
15
+ - **Jest tests for ClipboardController** - Comprehensive test suite for copy functionality
16
+ - **Sidecar templates** for all 47 components - Each component now has its own `.html.erb` template file
17
+ - **Components page** in documentation - New `/docs/components` listing page
18
+ - **Flexible class name support** - Components now accept both `class` and `class_name` attributes
19
+
20
+ ### Changed
21
+
22
+ - **Floating UI migration started** - Begun work on migrating popover/dropdown positioning to Floating UI
23
+ - **Tooltip animation polish** - Smoother entrance/exit animations
24
+ - **Generator improvements** - Updated component generators with better defaults
25
+ - **Field component updates** - Improved form field wrapper component
26
+
27
+ ### Fixed
28
+
29
+ - **Copy button styling** - Fixed code block padding to prevent text overlap with Copy button
30
+ - **Select dropdown clipping** - Fixed overflow issue causing white line through dropdown borders
31
+ - **Sidebar component** - Fixed layout issues in sidebar navigation
32
+ - **OTPSeparatorComponent** - Fixed rendering issues with OTP input separators
33
+ - **Tooltip component styling** - Fixed visual styling issues
34
+ - **Dashboard layout** - Fixed layout issues in example dashboard
35
+ - **HTML escaping** - Use `escape_once` instead of `escape` to prevent double-escaping
36
+
37
+ ### Documentation
38
+
39
+ - Added consistent widths to Select component examples
40
+ - Fixed Form Integration example spacing between label and select
41
+ - Updated links throughout docs to point to correct demo app URLs
42
+ - Simplified homepage navigation structure
43
+ - README updates with improved installation instructions
44
+
10
45
  ## [0.2.0] - 2025-11-27
11
46
 
12
- ## [0.1.0] - 2024-11-27
47
+ ### Added
48
+
49
+ - **Context Menu component** - Right-click context menus with full keyboard navigation
50
+ - **stimulus-use integration** - Added `useClickOutside` for better click-outside detection across menu components
51
+ - **BaseMenuController** - Shared base controller for menu components (DropdownMenu, ContextMenu, Menubar)
52
+ - **Polymorphic menu items** - Menu items can render as buttons, links, or custom elements
53
+ - **Two-way slider binding** - Sliders can now sync bidirectionally with input fields via `data-input-target`
54
+
55
+ ### Changed
56
+
57
+ - Refactored menu controllers to use shared `BaseMenuController`
58
+ - Improved click-outside handling using stimulus-use library
59
+ - Better keyboard navigation across all menu components
60
+
61
+ ### Fixed
62
+
63
+ - Combobox debouncing now works correctly
64
+ - Context menu positioning and click handling
65
+ - Radio group label click behavior
66
+ - Interleaved content rendering in menus
67
+
68
+ ### Developer Experience
69
+
70
+ - New `bin/bump` script for unified version bumping (replaces `bin/bump-version`)
71
+ - New `bin/test` script to run both Ruby and JavaScript tests
72
+ - Improved gemspec to exclude development files from published gem
73
+ - Added comprehensive Jest test suite for Stimulus controllers
74
+
75
+ ## [0.1.0] - 2025-11-27
13
76
 
14
77
  ### Added
15
78
 
@@ -52,6 +115,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
52
115
  - Accessibility guidelines
53
116
  - Installation instructions for various JavaScript bundlers
54
117
 
55
- [Unreleased]: https://github.com/iheanyi/shadcn-rails/compare/v0.2.0...HEAD
118
+ [Unreleased]: https://github.com/iheanyi/shadcn-rails/compare/v0.2.1...HEAD
119
+ [0.2.1]: https://github.com/iheanyi/shadcn-rails/compare/v0.2.0...v0.2.1
56
120
  [0.2.0]: https://github.com/iheanyi/shadcn-rails/compare/v0.1.0...v0.2.0
57
121
  [0.1.0]: https://github.com/iheanyi/shadcn-rails/releases/tag/v0.1.0
data/README.md CHANGED
@@ -41,6 +41,26 @@ const application = Application.start()
41
41
  registerShadcnControllers(application)
42
42
  ```
43
43
 
44
+ ## Adding Components
45
+
46
+ Copy components into your app for customization:
47
+
48
+ ```bash
49
+ # List all available components
50
+ rails generate shadcn:add --list
51
+
52
+ # Add specific components
53
+ rails generate shadcn:add button dialog tabs
54
+
55
+ # Add all components
56
+ rails generate shadcn:add --all
57
+
58
+ # Add without Stimulus controllers
59
+ rails generate shadcn:add dialog --exclude-controllers
60
+ ```
61
+
62
+ Components are copied to `app/components/shadcn/` and controllers to `app/javascript/controllers/shadcn/`. Local files take precedence over the gem's built-in components.
63
+
44
64
  ## Quick Start
45
65
 
46
66
  ```erb
@@ -91,17 +111,10 @@ Configure colors in your initializer:
91
111
  # config/initializers/shadcn.rb
92
112
  Shadcn::Rails.configure do |config|
93
113
  config.base_color = "slate" # neutral, slate, stone, gray, zinc
94
- config.dark_mode = :class # :class, :media
95
- config.radius = "0.5rem"
114
+ config.dark_mode = :class # :class, :media, :both
96
115
  end
97
116
  ```
98
117
 
99
- Or use the generator:
100
-
101
- ```bash
102
- rails generate shadcn:theme slate
103
- ```
104
-
105
118
  ## Stimulus Controllers
106
119
 
107
120
  All interactive components have corresponding Stimulus controllers:
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Mock for @floating-ui/dom
3
+ * Used in tests since JSDOM doesn't properly support the DOM APIs Floating UI needs
4
+ */
5
+
6
+ // Store references for size middleware to apply
7
+ let _computeRef = null
8
+ let _computeFloating = null
9
+
10
+ export const computePosition = async (reference, floating, options = {}) => {
11
+ _computeRef = reference
12
+ _computeFloating = floating
13
+
14
+ // Call size middleware apply if present
15
+ if (options.middleware) {
16
+ for (const mw of options.middleware) {
17
+ if (mw.name === 'size' && mw.applyFn) {
18
+ mw.applyFn({
19
+ availableWidth: 400,
20
+ availableHeight: 300,
21
+ elements: { floating },
22
+ rects: {
23
+ reference: {
24
+ width: reference?.getBoundingClientRect?.()?.width || 100,
25
+ height: reference?.getBoundingClientRect?.()?.height || 40
26
+ }
27
+ }
28
+ })
29
+ }
30
+ }
31
+ }
32
+
33
+ return {
34
+ x: 100,
35
+ y: 140,
36
+ placement: options.placement || 'bottom-start',
37
+ middlewareData: {}
38
+ }
39
+ }
40
+
41
+ export const autoUpdate = (reference, floating, update) => {
42
+ // Call update once immediately
43
+ update()
44
+ // Return cleanup function
45
+ return () => {}
46
+ }
47
+
48
+ export const flip = (options = {}) => ({
49
+ name: 'flip',
50
+ options
51
+ })
52
+
53
+ export const shift = (options = {}) => ({
54
+ name: 'shift',
55
+ options
56
+ })
57
+
58
+ export const offset = (value = 0) => ({
59
+ name: 'offset',
60
+ options: { mainAxis: value }
61
+ })
62
+
63
+ export const size = (options = {}) => ({
64
+ name: 'size',
65
+ options,
66
+ applyFn: options.apply
67
+ })
@@ -1,10 +1,11 @@
1
1
  import { Controller } from "@hotwired/stimulus"
2
2
  import { useClickOutside, useDebounce } from "stimulus-use"
3
+ import { positionFloating } from "../utils/floating"
3
4
 
4
5
  /**
5
6
  * Combobox controller for searchable select dropdown
6
7
  * Handles open/close, filtering, keyboard navigation, and item selection
7
- * Uses stimulus-use for click outside detection and debounced filtering
8
+ * Uses Floating UI for smart positioning and stimulus-use for utilities
8
9
  */
9
10
  export default class extends Controller {
10
11
  static targets = ["trigger", "content", "input", "list", "item", "empty", "displayValue", "hiddenInput"]
@@ -12,12 +13,14 @@ export default class extends Controller {
12
13
  open: { type: Boolean, default: false },
13
14
  value: { type: String, default: "" },
14
15
  selectedIndex: { type: Number, default: -1 },
15
- debounceWait: { type: Number, default: 150 }
16
+ debounceWait: { type: Number, default: 150 },
17
+ placement: { type: String, default: "bottom-start" }
16
18
  }
17
19
  static debounces = ["filter"]
18
20
 
19
21
  connect() {
20
22
  this.boundHandleKeydown = this.handleKeydown.bind(this)
23
+ this.cleanupFloating = null
21
24
 
22
25
  // Use stimulus-use for click outside detection
23
26
  useClickOutside(this)
@@ -27,6 +30,14 @@ export default class extends Controller {
27
30
 
28
31
  disconnect() {
29
32
  document.removeEventListener("keydown", this.boundHandleKeydown)
33
+ this.cleanupPositioning()
34
+ }
35
+
36
+ cleanupPositioning() {
37
+ if (this.cleanupFloating) {
38
+ this.cleanupFloating()
39
+ this.cleanupFloating = null
40
+ }
30
41
  }
31
42
 
32
43
  toggle() {
@@ -45,6 +56,13 @@ export default class extends Controller {
45
56
  this.contentTarget.dataset.state = "open"
46
57
  this.triggerTarget.setAttribute("aria-expanded", "true")
47
58
 
59
+ // Use Floating UI for smart positioning
60
+ this.cleanupFloating = positionFloating(this.triggerTarget, this.contentTarget, {
61
+ placement: this.placementValue,
62
+ sameWidth: true,
63
+ maxHeight: 384 // max-h-96
64
+ })
65
+
48
66
  // Focus the input
49
67
  requestAnimationFrame(() => {
50
68
  if (this.hasInputTarget) {
@@ -67,6 +85,9 @@ export default class extends Controller {
67
85
  this.contentTarget.dataset.state = "closed"
68
86
  this.triggerTarget.setAttribute("aria-expanded", "false")
69
87
 
88
+ // Cleanup Floating UI
89
+ this.cleanupPositioning()
90
+
70
91
  // Hide after animation completes, then reset filter state
71
92
  const hideAndReset = () => {
72
93
  this.contentTarget.hidden = true
@@ -1,8 +1,9 @@
1
1
  import BaseMenuController from "./base_menu_controller"
2
+ import { positionAtPoint } from "../utils/floating"
2
3
 
3
4
  /**
4
5
  * Context Menu controller for right-click menus
5
- * Extends BaseMenuController with context menu-specific positioning and event handling
6
+ * Extends BaseMenuController with Floating UI positioning at cursor location
6
7
  */
7
8
  export default class extends BaseMenuController {
8
9
  static targets = [...BaseMenuController.targets]
@@ -124,35 +125,7 @@ export default class extends BaseMenuController {
124
125
  positionContent() {
125
126
  if (!this.hasContentTarget) return
126
127
 
127
- const content = this.contentTarget
128
- const viewportWidth = window.innerWidth
129
- const viewportHeight = window.innerHeight
130
-
131
- // Reset position to measure actual size
132
- content.style.left = "0"
133
- content.style.top = "0"
134
-
135
- const contentRect = content.getBoundingClientRect()
136
-
137
- // Calculate position, keeping menu within viewport
138
- let x = this.mouseX
139
- let y = this.mouseY
140
-
141
- // Adjust if menu would overflow right edge
142
- if (x + contentRect.width > viewportWidth) {
143
- x = viewportWidth - contentRect.width - 8
144
- }
145
-
146
- // Adjust if menu would overflow bottom edge
147
- if (y + contentRect.height > viewportHeight) {
148
- y = viewportHeight - contentRect.height - 8
149
- }
150
-
151
- // Ensure menu doesn't go off left or top edge
152
- x = Math.max(8, x)
153
- y = Math.max(8, y)
154
-
155
- content.style.left = `${x}px`
156
- content.style.top = `${y}px`
128
+ // Use Floating UI for smart positioning at cursor location
129
+ positionAtPoint(this.contentTarget, this.mouseX, this.mouseY)
157
130
  }
158
131
  }
@@ -1,8 +1,9 @@
1
1
  import BaseMenuController from "./base_menu_controller"
2
+ import { positionFloating } from "../utils/floating"
2
3
 
3
4
  /**
4
5
  * Dropdown controller for dropdown menus
5
- * Extends BaseMenuController with dropdown-specific positioning
6
+ * Extends BaseMenuController with Floating UI positioning
6
7
  */
7
8
  export default class extends BaseMenuController {
8
9
  static targets = [...BaseMenuController.targets]
@@ -12,53 +13,43 @@ export default class extends BaseMenuController {
12
13
  side: { type: String, default: "bottom" }
13
14
  }
14
15
 
15
- show(event) {
16
- // Store side value for positioning before showing
17
- if (this.hasContentTarget) {
18
- this.contentTarget.dataset.side = this.sideValue
16
+ connect() {
17
+ this.cleanupFloating = null
18
+ super.connect()
19
+ }
20
+
21
+ disconnect() {
22
+ this.cleanupPositioning()
23
+ super.disconnect()
24
+ }
25
+
26
+ cleanupPositioning() {
27
+ if (this.cleanupFloating) {
28
+ this.cleanupFloating()
29
+ this.cleanupFloating = null
19
30
  }
20
- super.show(event)
31
+ }
32
+
33
+ get placement() {
34
+ // Convert side/align to Floating UI placement
35
+ const align = this.alignValue === "center" ? "" : `-${this.alignValue}`
36
+ return `${this.sideValue}${align}`
21
37
  }
22
38
 
23
39
  positionContent() {
24
40
  if (!this.hasContentTarget || !this.hasTriggerTarget) return
25
41
 
26
- const trigger = this.triggerTarget.getBoundingClientRect()
27
- const content = this.contentTarget
28
-
29
- // Position based on side and align
30
- content.style.position = "absolute"
31
- content.style.minWidth = `${trigger.width}px`
32
-
33
- switch (this.sideValue) {
34
- case "top":
35
- content.style.bottom = "100%"
36
- content.style.top = "auto"
37
- content.style.marginBottom = "4px"
38
- break
39
- case "bottom":
40
- default:
41
- content.style.top = "100%"
42
- content.style.bottom = "auto"
43
- content.style.marginTop = "4px"
44
- break
45
- }
42
+ // Use Floating UI for smart positioning
43
+ this.cleanupFloating = positionFloating(this.triggerTarget, this.contentTarget, {
44
+ placement: this.placement,
45
+ offset: 4,
46
+ sameWidth: false
47
+ })
48
+ }
46
49
 
47
- switch (this.alignValue) {
48
- case "start":
49
- content.style.left = "0"
50
- content.style.right = "auto"
51
- break
52
- case "center":
53
- content.style.left = "50%"
54
- content.style.transform = "translateX(-50%)"
55
- break
56
- case "end":
57
- default:
58
- content.style.right = "0"
59
- content.style.left = "auto"
60
- break
61
- }
50
+ hideMenu() {
51
+ this.cleanupPositioning()
52
+ super.hideMenu()
62
53
  }
63
54
 
64
55
  toggleCheckbox(event) {
@@ -1,20 +1,25 @@
1
1
  import { Controller } from "@hotwired/stimulus"
2
+ import { positionFloating } from "../utils/floating"
2
3
 
3
4
  /**
4
5
  * Hover Card Controller
5
6
  * Handles showing/hiding content on hover with delays
7
+ * Uses Floating UI for smart positioning
6
8
  */
7
9
  export default class extends Controller {
8
10
  static targets = ["trigger", "content"]
9
11
  static values = {
10
12
  openDelay: { type: Number, default: 700 },
11
- closeDelay: { type: Number, default: 300 }
13
+ closeDelay: { type: Number, default: 300 },
14
+ side: { type: String, default: "bottom" },
15
+ align: { type: String, default: "center" }
12
16
  }
13
17
 
14
18
  connect() {
15
19
  this.openTimeout = null
16
20
  this.closeTimeout = null
17
21
  this.isOpen = false
22
+ this.cleanupFloating = null
18
23
 
19
24
  this.triggerTarget.addEventListener("mouseenter", this.scheduleOpen.bind(this))
20
25
  this.triggerTarget.addEventListener("mouseleave", this.scheduleClose.bind(this))
@@ -27,6 +32,20 @@ export default class extends Controller {
27
32
 
28
33
  disconnect() {
29
34
  this.clearTimeouts()
35
+ this.cleanupPositioning()
36
+ }
37
+
38
+ cleanupPositioning() {
39
+ if (this.cleanupFloating) {
40
+ this.cleanupFloating()
41
+ this.cleanupFloating = null
42
+ }
43
+ }
44
+
45
+ get placement() {
46
+ // Convert side/align to Floating UI placement
47
+ const align = this.alignValue === "center" ? "" : `-${this.alignValue}`
48
+ return `${this.sideValue}${align}`
30
49
  }
31
50
 
32
51
  scheduleOpen() {
@@ -67,7 +86,12 @@ export default class extends Controller {
67
86
  this.isOpen = true
68
87
  this.contentTarget.style.display = "block"
69
88
  this.contentTarget.setAttribute("data-state", "open")
70
- this.positionContent()
89
+
90
+ // Use Floating UI for smart positioning
91
+ this.cleanupFloating = positionFloating(this.triggerTarget, this.contentTarget, {
92
+ placement: this.placement,
93
+ offset: 8
94
+ })
71
95
 
72
96
  this.dispatch("open")
73
97
  }
@@ -78,6 +102,9 @@ export default class extends Controller {
78
102
  this.isOpen = false
79
103
  this.contentTarget.setAttribute("data-state", "closed")
80
104
 
105
+ // Cleanup Floating UI
106
+ this.cleanupPositioning()
107
+
81
108
  // Wait for animation to complete
82
109
  setTimeout(() => {
83
110
  if (!this.isOpen) {
@@ -87,57 +114,4 @@ export default class extends Controller {
87
114
 
88
115
  this.dispatch("close")
89
116
  }
90
-
91
- positionContent() {
92
- const trigger = this.triggerTarget.getBoundingClientRect()
93
- const content = this.contentTarget
94
- const side = content.dataset.side || "bottom"
95
- const align = content.dataset.align || "center"
96
-
97
- // Reset position
98
- content.style.top = ""
99
- content.style.left = ""
100
- content.style.right = ""
101
- content.style.bottom = ""
102
-
103
- const gap = 8 // Gap between trigger and content
104
-
105
- switch (side) {
106
- case "top":
107
- content.style.bottom = "100%"
108
- content.style.marginBottom = `${gap}px`
109
- break
110
- case "bottom":
111
- content.style.top = "100%"
112
- content.style.marginTop = `${gap}px`
113
- break
114
- case "left":
115
- content.style.right = "100%"
116
- content.style.marginRight = `${gap}px`
117
- content.style.top = "0"
118
- break
119
- case "right":
120
- content.style.left = "100%"
121
- content.style.marginLeft = `${gap}px`
122
- content.style.top = "0"
123
- break
124
- }
125
-
126
- // Handle alignment for top/bottom
127
- if (side === "top" || side === "bottom") {
128
- switch (align) {
129
- case "start":
130
- content.style.left = "0"
131
- break
132
- case "end":
133
- content.style.right = "0"
134
- break
135
- case "center":
136
- default:
137
- content.style.left = "50%"
138
- content.style.transform = "translateX(-50%)"
139
- break
140
- }
141
- }
142
- }
143
117
  }
@@ -1,9 +1,10 @@
1
1
  import { Controller } from "@hotwired/stimulus"
2
2
  import { useClickOutside } from "stimulus-use"
3
+ import { positionFloating } from "../utils/floating"
3
4
 
4
5
  /**
5
6
  * Popover controller for rich content overlays
6
- * Uses stimulus-use for click outside detection
7
+ * Uses Floating UI for smart positioning and stimulus-use for click outside detection
7
8
  */
8
9
  export default class extends Controller {
9
10
  static targets = ["trigger", "content"]
@@ -15,6 +16,8 @@ export default class extends Controller {
15
16
  }
16
17
 
17
18
  connect() {
19
+ this.cleanupFloating = null
20
+
18
21
  // Use stimulus-use for click outside detection
19
22
  useClickOutside(this)
20
23
 
@@ -25,6 +28,20 @@ export default class extends Controller {
25
28
 
26
29
  disconnect() {
27
30
  this.hide()
31
+ this.cleanupPositioning()
32
+ }
33
+
34
+ cleanupPositioning() {
35
+ if (this.cleanupFloating) {
36
+ this.cleanupFloating()
37
+ this.cleanupFloating = null
38
+ }
39
+ }
40
+
41
+ get placement() {
42
+ // Convert side/align to Floating UI placement
43
+ const align = this.alignValue === "center" ? "" : `-${this.alignValue}`
44
+ return `${this.sideValue}${align}`
28
45
  }
29
46
 
30
47
  toggle(event) {
@@ -44,8 +61,14 @@ export default class extends Controller {
44
61
  if (this.hasContentTarget) {
45
62
  this.contentTarget.hidden = false
46
63
  this.contentTarget.dataset.state = "open"
47
- this.contentTarget.dataset.side = this.sideValue
48
- this.positionContent()
64
+
65
+ // Use Floating UI for smart positioning
66
+ if (this.hasTriggerTarget) {
67
+ this.cleanupFloating = positionFloating(this.triggerTarget, this.contentTarget, {
68
+ placement: this.placement,
69
+ offset: 8
70
+ })
71
+ }
49
72
  }
50
73
 
51
74
  if (this.modalValue) {
@@ -61,6 +84,9 @@ export default class extends Controller {
61
84
 
62
85
  this.openValue = false
63
86
 
87
+ // Cleanup Floating UI auto-update
88
+ this.cleanupPositioning()
89
+
64
90
  if (this.hasContentTarget) {
65
91
  this.contentTarget.dataset.state = "closed"
66
92
  setTimeout(() => {
@@ -87,55 +113,4 @@ export default class extends Controller {
87
113
  this.hide()
88
114
  }
89
115
  }
90
-
91
- positionContent() {
92
- if (!this.hasContentTarget || !this.hasTriggerTarget) return
93
-
94
- const trigger = this.triggerTarget.getBoundingClientRect()
95
- const content = this.contentTarget
96
-
97
- content.style.position = "absolute"
98
-
99
- const gap = 8
100
-
101
- switch (this.sideValue) {
102
- case "top":
103
- content.style.bottom = "100%"
104
- content.style.top = "auto"
105
- content.style.marginBottom = `${gap}px`
106
- break
107
- case "bottom":
108
- content.style.top = "100%"
109
- content.style.bottom = "auto"
110
- content.style.marginTop = `${gap}px`
111
- break
112
- case "left":
113
- content.style.right = "100%"
114
- content.style.left = "auto"
115
- content.style.marginRight = `${gap}px`
116
- break
117
- case "right":
118
- content.style.left = "100%"
119
- content.style.right = "auto"
120
- content.style.marginLeft = `${gap}px`
121
- break
122
- }
123
-
124
- switch (this.alignValue) {
125
- case "start":
126
- content.style.left = "0"
127
- content.style.right = "auto"
128
- break
129
- case "center":
130
- if (this.sideValue === "top" || this.sideValue === "bottom") {
131
- content.style.left = "50%"
132
- content.style.transform = "translateX(-50%)"
133
- }
134
- break
135
- case "end":
136
- content.style.right = "0"
137
- content.style.left = "auto"
138
- break
139
- }
140
- }
141
116
  }