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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +66 -2
- data/README.md +21 -8
- data/__mocks__/@floating-ui/dom.js +67 -0
- data/app/assets/javascripts/shadcn/controllers/combobox_controller.js +23 -2
- data/app/assets/javascripts/shadcn/controllers/context_menu_controller.js +4 -31
- data/app/assets/javascripts/shadcn/controllers/dropdown_controller.js +32 -41
- data/app/assets/javascripts/shadcn/controllers/hover_card_controller.js +29 -55
- data/app/assets/javascripts/shadcn/controllers/popover_controller.js +29 -54
- data/app/assets/javascripts/shadcn/controllers/select_controller.js +26 -8
- data/app/assets/javascripts/shadcn/controllers/tooltip_controller.js +28 -59
- data/app/assets/javascripts/shadcn/index.js +7 -1
- data/app/assets/javascripts/shadcn/utils/floating.js +179 -0
- data/app/assets/stylesheets/shadcn/base.css +32 -0
- data/app/components/shadcn/accordion_component.html.erb +8 -0
- data/app/components/shadcn/accordion_component.rb +6 -15
- data/app/components/shadcn/alert_component.html.erb +6 -0
- data/app/components/shadcn/alert_component.rb +0 -18
- data/app/components/shadcn/alert_dialog_component.html.erb +12 -0
- data/app/components/shadcn/alert_dialog_component.rb +7 -27
- data/app/components/shadcn/aspect_ratio_component.html.erb +7 -0
- data/app/components/shadcn/aspect_ratio_component.rb +4 -19
- data/app/components/shadcn/avatar_component.html.erb +20 -0
- data/app/components/shadcn/avatar_component.rb +8 -36
- data/app/components/shadcn/badge_component.html.erb +1 -0
- data/app/components/shadcn/badge_component.rb +0 -11
- data/app/components/shadcn/base_component.rb +15 -2
- data/app/components/shadcn/breadcrumb_component.html.erb +5 -0
- data/app/components/shadcn/breadcrumb_component.rb +6 -16
- data/app/components/shadcn/button_component.html.erb +18 -0
- data/app/components/shadcn/button_component.rb +1 -41
- data/app/components/shadcn/card_component.html.erb +8 -0
- data/app/components/shadcn/card_component.rb +2 -6
- data/app/components/shadcn/checkbox_component.html.erb +32 -0
- data/app/components/shadcn/checkbox_component.rb +4 -43
- data/app/components/shadcn/collapsible_component.html.erb +8 -0
- data/app/components/shadcn/collapsible_component.rb +6 -15
- data/app/components/shadcn/context_menu_component.html.erb +11 -0
- data/app/components/shadcn/context_menu_component.rb +6 -26
- data/app/components/shadcn/dialog_component.html.erb +14 -0
- data/app/components/shadcn/dialog_component.rb +8 -29
- data/app/components/shadcn/drawer_component.html.erb +12 -0
- data/app/components/shadcn/drawer_component.rb +7 -27
- data/app/components/shadcn/dropdown_menu_component.html.erb +14 -0
- data/app/components/shadcn/dropdown_menu_component.rb +9 -29
- data/app/components/shadcn/field_component.rb +7 -8
- data/app/components/shadcn/hover_card_component.html.erb +12 -0
- data/app/components/shadcn/hover_card_component.rb +7 -26
- data/app/components/shadcn/input_component.html.erb +18 -0
- data/app/components/shadcn/input_component.rb +2 -27
- data/app/components/shadcn/input_otp_component.rb +3 -3
- data/app/components/shadcn/kbd_component.html.erb +1 -0
- data/app/components/shadcn/kbd_component.rb +3 -10
- data/app/components/shadcn/label_component.html.erb +3 -0
- data/app/components/shadcn/label_component.rb +2 -18
- data/app/components/shadcn/menubar_component.html.erb +6 -0
- data/app/components/shadcn/menubar_component.rb +4 -15
- data/app/components/shadcn/native_select_component.html.erb +22 -0
- data/app/components/shadcn/native_select_component.rb +9 -39
- data/app/components/shadcn/navigation_menu_component.html.erb +6 -0
- data/app/components/shadcn/navigation_menu_component.rb +4 -15
- data/app/components/shadcn/pagination_component.html.erb +5 -0
- data/app/components/shadcn/pagination_component.rb +11 -15
- data/app/components/shadcn/popover_component.html.erb +15 -0
- data/app/components/shadcn/popover_component.rb +10 -30
- data/app/components/shadcn/progress_component.html.erb +13 -0
- data/app/components/shadcn/progress_component.rb +6 -26
- data/app/components/shadcn/radio_group_component.html.erb +8 -0
- data/app/components/shadcn/radio_group_component.rb +12 -26
- data/app/components/shadcn/scroll_area_component.html.erb +7 -0
- data/app/components/shadcn/scroll_area_component.rb +4 -16
- data/app/components/shadcn/select_component.html.erb +46 -0
- data/app/components/shadcn/select_component.rb +6 -80
- data/app/components/shadcn/separator_component.html.erb +5 -0
- data/app/components/shadcn/separator_component.rb +6 -14
- data/app/components/shadcn/sheet_component.html.erb +12 -0
- data/app/components/shadcn/sheet_component.rb +7 -27
- data/app/components/shadcn/sidebar_component.rb +2 -2
- data/app/components/shadcn/skeleton_component.html.erb +1 -0
- data/app/components/shadcn/skeleton_component.rb +4 -2
- data/app/components/shadcn/slider_component.html.erb +12 -0
- data/app/components/shadcn/slider_component.rb +2 -21
- data/app/components/shadcn/spinner_component.html.erb +18 -0
- data/app/components/shadcn/spinner_component.rb +2 -30
- data/app/components/shadcn/switch_component.html.erb +72 -0
- data/app/components/shadcn/switch_component.rb +4 -82
- data/app/components/shadcn/table_component.html.erb +9 -0
- data/app/components/shadcn/table_component.rb +2 -10
- data/app/components/shadcn/tabs_component.html.erb +8 -0
- data/app/components/shadcn/tabs_component.rb +4 -17
- data/app/components/shadcn/textarea_component.html.erb +13 -0
- data/app/components/shadcn/textarea_component.rb +6 -22
- data/app/components/shadcn/toast_component.html.erb +36 -0
- data/app/components/shadcn/toast_component.rb +6 -54
- data/app/components/shadcn/toggle_component.html.erb +12 -0
- data/app/components/shadcn/toggle_component.rb +6 -21
- data/app/components/shadcn/toggle_group_component.html.erb +14 -0
- data/app/components/shadcn/toggle_group_component.rb +6 -29
- data/app/components/shadcn/tooltip_component.html.erb +20 -0
- data/app/components/shadcn/tooltip_component.rb +13 -38
- data/lib/generators/shadcn/add/USAGE +24 -0
- data/lib/generators/shadcn/add/add_generator.rb +279 -0
- data/lib/generators/shadcn/install/USAGE +22 -0
- data/lib/generators/shadcn/install/install_generator.rb +8 -3
- data/lib/generators/shadcn/install/templates/initializer.rb.tt +7 -27
- data/lib/generators/shadcn/install/templates/shadcn.yml.tt +15 -31
- data/lib/shadcn/rails/version.rb +1 -1
- metadata +47 -45
- data/.dockerignore +0 -40
- data/CLAUDE.md +0 -612
- data/PROGRESS.md +0 -495
- data/Rakefile +0 -95
- data/__tests__/controllers/__snapshots__/calendar_controller.test.js.snap +0 -13
- data/__tests__/controllers/__snapshots__/popover_controller.test.js.snap +0 -46
- data/__tests__/controllers/__snapshots__/sheet_controller.test.js.snap +0 -111
- data/__tests__/controllers/__snapshots__/tabs_controller.test.js.snap +0 -27
- data/__tests__/controllers/accordion_controller.test.js +0 -904
- data/__tests__/controllers/calendar_controller.test.js +0 -1370
- data/__tests__/controllers/carousel_controller.test.js +0 -912
- data/__tests__/controllers/checkbox_controller.test.js +0 -454
- data/__tests__/controllers/collapsible_controller.test.js +0 -407
- data/__tests__/controllers/combobox_controller.test.js +0 -971
- data/__tests__/controllers/context_menu_controller.test.js +0 -905
- data/__tests__/controllers/date_picker_controller.test.js +0 -636
- data/__tests__/controllers/dialog_controller.test.js +0 -878
- data/__tests__/controllers/drawer_controller.test.js +0 -995
- data/__tests__/controllers/menubar_controller.test.js +0 -737
- data/__tests__/controllers/navigation_menu_controller.test.js +0 -599
- data/__tests__/controllers/popover_controller.test.js +0 -982
- data/__tests__/controllers/radio_group_controller.test.js +0 -640
- data/__tests__/controllers/resizable_controller.test.js +0 -680
- data/__tests__/controllers/select_controller.test.js +0 -678
- data/__tests__/controllers/sheet_controller.test.js +0 -986
- data/__tests__/controllers/slider_controller.test.js +0 -1036
- data/__tests__/controllers/switch_controller.test.js +0 -424
- data/__tests__/controllers/tabs_controller.test.js +0 -907
- data/__tests__/controllers/toggle_group_controller.test.js +0 -839
- data/__tests__/controllers/tooltip_controller.test.js +0 -808
- data/__tests__/helpers/stimulus-test-helper.js +0 -203
- data/babel.config.cjs +0 -5
- data/bin/bump +0 -321
- data/bin/console +0 -11
- data/bin/release +0 -205
- data/bin/setup +0 -8
- data/bin/test +0 -75
- data/jest.config.js +0 -19
- data/jest.setup.js +0 -8
- data/lib/generators/shadcn/component/component_generator.rb +0 -188
- data/lib/generators/shadcn/theme/theme_generator.rb +0 -128
- data/package-lock.json +0 -7438
- data/package.json +0 -71
- data/rollup.config.js +0 -29
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 169b88e9623277a0a148a98510ea1b5f0fbe56f8edd12bd160c406aa6516c681
|
|
4
|
+
data.tar.gz: 0da7fa6d9b3e1e20a2ebd081dec32140a44641c4aadb2fd096ba0de5545ae848
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
|
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
|
-
|
|
128
|
-
|
|
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
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
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
|
-
|
|
48
|
-
|
|
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
|
}
|