shadcn-rails 0.1.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 +69 -2
- data/README.md +102 -1398
- data/__mocks__/@floating-ui/dom.js +67 -0
- data/app/assets/javascripts/shadcn/controllers/base_menu_controller.js +266 -0
- data/app/assets/javascripts/shadcn/controllers/combobox_controller.js +34 -8
- data/app/assets/javascripts/shadcn/controllers/command_controller.js +5 -1
- data/app/assets/javascripts/shadcn/controllers/context_menu_controller.js +64 -135
- data/app/assets/javascripts/shadcn/controllers/dropdown_controller.js +56 -186
- data/app/assets/javascripts/shadcn/controllers/hover_card_controller.js +29 -55
- data/app/assets/javascripts/shadcn/controllers/menubar_controller.js +10 -7
- data/app/assets/javascripts/shadcn/controllers/navigation_menu_controller.js +10 -6
- data/app/assets/javascripts/shadcn/controllers/popover_controller.js +35 -60
- data/app/assets/javascripts/shadcn/controllers/select_controller.js +37 -17
- data/app/assets/javascripts/shadcn/controllers/sidebar_controller.js +24 -14
- data/app/assets/javascripts/shadcn/controllers/tooltip_controller.js +28 -59
- data/app/assets/javascripts/shadcn/index.js +9 -1
- data/app/assets/javascripts/shadcn/utils/floating.js +179 -0
- data/app/assets/stylesheets/shadcn/base.css +32 -0
- data/app/assets/stylesheets/shadcn/components.css +12 -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/command_list_component.rb +29 -14
- data/app/components/shadcn/context_menu_checkbox_item_component.rb +76 -0
- 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/context_menu_content_component.rb +37 -14
- data/app/components/shadcn/context_menu_item_component.rb +3 -2
- data/app/components/shadcn/context_menu_radio_group_component.rb +42 -0
- data/app/components/shadcn/context_menu_radio_item_component.rb +76 -0
- 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_checkbox_item_component.rb +76 -0
- 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/dropdown_menu_content_component.rb +45 -16
- data/app/components/shadcn/dropdown_menu_radio_group_component.rb +42 -0
- data/app/components/shadcn/dropdown_menu_radio_item_component.rb +76 -0
- 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/menubar_content_component.rb +45 -20
- data/app/components/shadcn/menubar_sub_content_component.rb +21 -8
- 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/radio_group_item_component.rb +32 -6
- data/app/components/shadcn/resizable_panel_group_component.rb +27 -16
- 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 +29 -86
- 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 +54 -42
- data/.dockerignore +0 -40
- data/CLAUDE.md +0 -463
- data/PROGRESS.md +0 -485
- data/Rakefile +0 -29
- 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 -966
- data/__tests__/controllers/context_menu_controller.test.js +0 -627
- 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 -736
- data/__tests__/controllers/navigation_menu_controller.test.js +0 -598
- data/__tests__/controllers/popover_controller.test.js +0 -1007
- 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 -674
- 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/console +0 -11
- data/bin/setup +0 -8
- 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 -7415
- data/package.json +0 -68
- data/rollup.config.js +0 -29
|
@@ -1,18 +1,26 @@
|
|
|
1
1
|
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
import { useClickOutside } from "stimulus-use"
|
|
3
|
+
import { positionFloating } from "../utils/floating"
|
|
2
4
|
|
|
3
5
|
/**
|
|
4
6
|
* Select controller for custom select dropdowns
|
|
7
|
+
* Uses Floating UI for smart positioning and stimulus-use for click outside detection
|
|
5
8
|
*/
|
|
6
9
|
export default class extends Controller {
|
|
7
10
|
static targets = ["trigger", "content", "input", "item", "display", "checkIcon"]
|
|
8
11
|
static values = {
|
|
9
|
-
value: String
|
|
12
|
+
value: String,
|
|
13
|
+
placement: { type: String, default: "bottom-start" },
|
|
14
|
+
sameWidth: { type: Boolean, default: true }
|
|
10
15
|
}
|
|
11
16
|
|
|
12
17
|
connect() {
|
|
13
18
|
this.isOpen = false
|
|
14
19
|
this.focusedIndex = -1
|
|
15
|
-
this.
|
|
20
|
+
this.cleanupFloating = null
|
|
21
|
+
|
|
22
|
+
// Use stimulus-use for click outside detection
|
|
23
|
+
useClickOutside(this)
|
|
16
24
|
|
|
17
25
|
// Set initial value display
|
|
18
26
|
if (this.valueValue) {
|
|
@@ -22,6 +30,14 @@ export default class extends Controller {
|
|
|
22
30
|
|
|
23
31
|
disconnect() {
|
|
24
32
|
this.close()
|
|
33
|
+
this.cleanupPositioning()
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
cleanupPositioning() {
|
|
37
|
+
if (this.cleanupFloating) {
|
|
38
|
+
this.cleanupFloating()
|
|
39
|
+
this.cleanupFloating = null
|
|
40
|
+
}
|
|
25
41
|
}
|
|
26
42
|
|
|
27
43
|
toggle(event) {
|
|
@@ -38,23 +54,24 @@ export default class extends Controller {
|
|
|
38
54
|
|
|
39
55
|
this.isOpen = true
|
|
40
56
|
|
|
41
|
-
// Set trigger width as CSS variable for dropdown sizing
|
|
42
|
-
if (this.hasTriggerTarget && this.hasContentTarget) {
|
|
43
|
-
const triggerWidth = this.triggerTarget.offsetWidth
|
|
44
|
-
this.contentTarget.style.setProperty('--radix-select-trigger-width', `${triggerWidth}px`)
|
|
45
|
-
}
|
|
46
|
-
|
|
47
57
|
if (this.hasContentTarget) {
|
|
48
58
|
this.contentTarget.hidden = false
|
|
49
59
|
this.contentTarget.dataset.state = "open"
|
|
60
|
+
|
|
61
|
+
// Use Floating UI for smart positioning
|
|
62
|
+
if (this.hasTriggerTarget) {
|
|
63
|
+
this.cleanupFloating = positionFloating(this.triggerTarget, this.contentTarget, {
|
|
64
|
+
placement: this.placementValue,
|
|
65
|
+
sameWidth: this.sameWidthValue,
|
|
66
|
+
maxHeight: 384 // max-h-96
|
|
67
|
+
})
|
|
68
|
+
}
|
|
50
69
|
}
|
|
51
70
|
|
|
52
71
|
if (this.hasTriggerTarget) {
|
|
53
72
|
this.triggerTarget.setAttribute("aria-expanded", "true")
|
|
54
73
|
}
|
|
55
74
|
|
|
56
|
-
document.addEventListener("click", this.boundHandleClickOutside)
|
|
57
|
-
|
|
58
75
|
// Focus current value or first item
|
|
59
76
|
this.focusedIndex = -1
|
|
60
77
|
const currentItem = this.itemTargets.find(item => item.dataset.value === this.valueValue)
|
|
@@ -73,6 +90,9 @@ export default class extends Controller {
|
|
|
73
90
|
|
|
74
91
|
this.isOpen = false
|
|
75
92
|
|
|
93
|
+
// Cleanup Floating UI auto-update
|
|
94
|
+
this.cleanupPositioning()
|
|
95
|
+
|
|
76
96
|
if (this.hasContentTarget) {
|
|
77
97
|
this.contentTarget.dataset.state = "closed"
|
|
78
98
|
setTimeout(() => {
|
|
@@ -86,12 +106,18 @@ export default class extends Controller {
|
|
|
86
106
|
this.triggerTarget.setAttribute("aria-expanded", "false")
|
|
87
107
|
}
|
|
88
108
|
|
|
89
|
-
document.removeEventListener("click", this.boundHandleClickOutside)
|
|
90
109
|
this.focusedIndex = -1
|
|
91
110
|
|
|
92
111
|
this.dispatch("closed")
|
|
93
112
|
}
|
|
94
113
|
|
|
114
|
+
// Called by stimulus-use when clicking outside the element
|
|
115
|
+
clickOutside(event) {
|
|
116
|
+
if (this.isOpen) {
|
|
117
|
+
this.close()
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
95
121
|
select(event) {
|
|
96
122
|
const item = event.currentTarget
|
|
97
123
|
if (item.dataset.disabled !== undefined) return
|
|
@@ -132,12 +158,6 @@ export default class extends Controller {
|
|
|
132
158
|
}
|
|
133
159
|
}
|
|
134
160
|
|
|
135
|
-
handleClickOutside(event) {
|
|
136
|
-
if (!this.element.contains(event.target)) {
|
|
137
|
-
this.close()
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
161
|
handleKeydown(event) {
|
|
142
162
|
if (!this.isOpen) {
|
|
143
163
|
if (event.key === "Enter" || event.key === " " || event.key === "ArrowDown") {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
import { useMatchMedia } from "stimulus-use"
|
|
2
3
|
|
|
3
4
|
// Constants for sidebar dimensions
|
|
4
5
|
const SIDEBAR_COOKIE_NAME = "sidebar:state"
|
|
@@ -7,6 +8,10 @@ const SIDEBAR_WIDTH = "16rem"
|
|
|
7
8
|
const SIDEBAR_WIDTH_MOBILE = "18rem"
|
|
8
9
|
const SIDEBAR_WIDTH_ICON = "3rem"
|
|
9
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Sidebar Controller
|
|
13
|
+
* Uses stimulus-use useMatchMedia for responsive behavior
|
|
14
|
+
*/
|
|
10
15
|
export default class extends Controller {
|
|
11
16
|
static targets = ["sidebar"]
|
|
12
17
|
static values = {
|
|
@@ -26,10 +31,13 @@ export default class extends Controller {
|
|
|
26
31
|
this.handleKeyDown = this.handleKeyDown.bind(this)
|
|
27
32
|
document.addEventListener("keydown", this.handleKeyDown)
|
|
28
33
|
|
|
29
|
-
//
|
|
34
|
+
// Use stimulus-use for responsive media query detection
|
|
30
35
|
this.isMobile = window.innerWidth < 768
|
|
31
|
-
this
|
|
32
|
-
|
|
36
|
+
useMatchMedia(this, {
|
|
37
|
+
mediaQueries: {
|
|
38
|
+
mobile: "(max-width: 767px)"
|
|
39
|
+
}
|
|
40
|
+
})
|
|
33
41
|
|
|
34
42
|
// Initial state sync
|
|
35
43
|
this.syncState()
|
|
@@ -37,7 +45,19 @@ export default class extends Controller {
|
|
|
37
45
|
|
|
38
46
|
disconnect() {
|
|
39
47
|
document.removeEventListener("keydown", this.handleKeyDown)
|
|
40
|
-
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Called by stimulus-use when mobile media query state changes
|
|
51
|
+
mobileChanged({ matches }) {
|
|
52
|
+
const wasMobile = this.isMobile
|
|
53
|
+
this.isMobile = matches
|
|
54
|
+
|
|
55
|
+
// Close mobile sidebar when switching to desktop
|
|
56
|
+
if (wasMobile && !this.isMobile) {
|
|
57
|
+
this.openMobileValue = false
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
this.syncState()
|
|
41
61
|
}
|
|
42
62
|
|
|
43
63
|
handleKeyDown(event) {
|
|
@@ -51,16 +71,6 @@ export default class extends Controller {
|
|
|
51
71
|
}
|
|
52
72
|
}
|
|
53
73
|
|
|
54
|
-
handleResize() {
|
|
55
|
-
const wasMobile = this.isMobile
|
|
56
|
-
this.isMobile = window.innerWidth < 768
|
|
57
|
-
|
|
58
|
-
// Close mobile sidebar when switching to desktop
|
|
59
|
-
if (wasMobile && !this.isMobile) {
|
|
60
|
-
this.openMobileValue = false
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
74
|
toggle() {
|
|
65
75
|
if (this.isMobile) {
|
|
66
76
|
this.openMobileValue = !this.openMobileValue
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
import { positionFloating } from "../utils/floating"
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Tooltip controller for contextual information
|
|
6
|
+
* Uses Floating UI for smart positioning
|
|
5
7
|
*/
|
|
6
8
|
export default class extends Controller {
|
|
7
9
|
static targets = ["trigger", "content"]
|
|
@@ -15,10 +17,25 @@ export default class extends Controller {
|
|
|
15
17
|
connect() {
|
|
16
18
|
this.showTimeout = null
|
|
17
19
|
this.hideTimeout = null
|
|
20
|
+
this.cleanupFloating = null
|
|
18
21
|
}
|
|
19
22
|
|
|
20
23
|
disconnect() {
|
|
21
24
|
this.clearTimeouts()
|
|
25
|
+
this.cleanupPositioning()
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
cleanupPositioning() {
|
|
29
|
+
if (this.cleanupFloating) {
|
|
30
|
+
this.cleanupFloating()
|
|
31
|
+
this.cleanupFloating = null
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
get placement() {
|
|
36
|
+
// Convert side/align to Floating UI placement
|
|
37
|
+
const align = this.alignValue === "center" ? "" : `-${this.alignValue}`
|
|
38
|
+
return `${this.sideValue}${align}`
|
|
22
39
|
}
|
|
23
40
|
|
|
24
41
|
show() {
|
|
@@ -28,7 +45,14 @@ export default class extends Controller {
|
|
|
28
45
|
if (this.hasContentTarget) {
|
|
29
46
|
this.contentTarget.hidden = false
|
|
30
47
|
this.contentTarget.dataset.state = "open"
|
|
31
|
-
|
|
48
|
+
|
|
49
|
+
// Use Floating UI for smart positioning
|
|
50
|
+
if (this.hasTriggerTarget) {
|
|
51
|
+
this.cleanupFloating = positionFloating(this.triggerTarget, this.contentTarget, {
|
|
52
|
+
placement: this.placement,
|
|
53
|
+
offset: 8
|
|
54
|
+
})
|
|
55
|
+
}
|
|
32
56
|
}
|
|
33
57
|
}, this.delayValue)
|
|
34
58
|
}
|
|
@@ -36,6 +60,9 @@ export default class extends Controller {
|
|
|
36
60
|
hide() {
|
|
37
61
|
this.clearTimeouts()
|
|
38
62
|
|
|
63
|
+
// Cleanup Floating UI
|
|
64
|
+
this.cleanupPositioning()
|
|
65
|
+
|
|
39
66
|
this.hideTimeout = setTimeout(() => {
|
|
40
67
|
if (this.hasContentTarget) {
|
|
41
68
|
this.contentTarget.dataset.state = "closed"
|
|
@@ -56,62 +83,4 @@ export default class extends Controller {
|
|
|
56
83
|
this.hideTimeout = null
|
|
57
84
|
}
|
|
58
85
|
}
|
|
59
|
-
|
|
60
|
-
positionTooltip() {
|
|
61
|
-
if (!this.hasContentTarget || !this.hasTriggerTarget) return
|
|
62
|
-
|
|
63
|
-
const trigger = this.triggerTarget.getBoundingClientRect()
|
|
64
|
-
const tooltip = this.contentTarget
|
|
65
|
-
const tooltipRect = tooltip.getBoundingClientRect()
|
|
66
|
-
|
|
67
|
-
// Reset positioning
|
|
68
|
-
tooltip.style.position = "absolute"
|
|
69
|
-
tooltip.style.top = ""
|
|
70
|
-
tooltip.style.bottom = ""
|
|
71
|
-
tooltip.style.left = ""
|
|
72
|
-
tooltip.style.right = ""
|
|
73
|
-
tooltip.style.transform = ""
|
|
74
|
-
|
|
75
|
-
const gap = 8
|
|
76
|
-
|
|
77
|
-
switch (this.sideValue) {
|
|
78
|
-
case "top":
|
|
79
|
-
tooltip.style.bottom = "100%"
|
|
80
|
-
tooltip.style.marginBottom = `${gap}px`
|
|
81
|
-
break
|
|
82
|
-
case "bottom":
|
|
83
|
-
tooltip.style.top = "100%"
|
|
84
|
-
tooltip.style.marginTop = `${gap}px`
|
|
85
|
-
break
|
|
86
|
-
case "left":
|
|
87
|
-
tooltip.style.right = "100%"
|
|
88
|
-
tooltip.style.marginRight = `${gap}px`
|
|
89
|
-
tooltip.style.top = "50%"
|
|
90
|
-
tooltip.style.transform = "translateY(-50%)"
|
|
91
|
-
break
|
|
92
|
-
case "right":
|
|
93
|
-
tooltip.style.left = "100%"
|
|
94
|
-
tooltip.style.marginLeft = `${gap}px`
|
|
95
|
-
tooltip.style.top = "50%"
|
|
96
|
-
tooltip.style.transform = "translateY(-50%)"
|
|
97
|
-
break
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
if (this.sideValue === "top" || this.sideValue === "bottom") {
|
|
101
|
-
switch (this.alignValue) {
|
|
102
|
-
case "start":
|
|
103
|
-
tooltip.style.left = "0"
|
|
104
|
-
break
|
|
105
|
-
case "center":
|
|
106
|
-
tooltip.style.left = "50%"
|
|
107
|
-
tooltip.style.transform = "translateX(-50%)"
|
|
108
|
-
break
|
|
109
|
-
case "end":
|
|
110
|
-
tooltip.style.right = "0"
|
|
111
|
-
break
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
tooltip.dataset.side = this.sideValue
|
|
116
|
-
}
|
|
117
86
|
}
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
// Import all controllers
|
|
17
|
+
import BaseMenuController from "./controllers/base_menu_controller"
|
|
17
18
|
import AccordionController from "./controllers/accordion_controller"
|
|
18
19
|
import AvatarController from "./controllers/avatar_controller"
|
|
19
20
|
import CalendarController from "./controllers/calendar_controller"
|
|
@@ -47,8 +48,12 @@ import TooltipController from "./controllers/tooltip_controller"
|
|
|
47
48
|
import InputOtpController from "./controllers/input_otp_controller"
|
|
48
49
|
import SidebarController from "./controllers/sidebar_controller"
|
|
49
50
|
|
|
51
|
+
// Import floating utility
|
|
52
|
+
import { positionFloating, positionAtPoint } from "./utils/floating"
|
|
53
|
+
|
|
50
54
|
// Export individual controllers
|
|
51
55
|
export {
|
|
56
|
+
BaseMenuController,
|
|
52
57
|
AccordionController,
|
|
53
58
|
AvatarController,
|
|
54
59
|
CalendarController,
|
|
@@ -80,7 +85,10 @@ export {
|
|
|
80
85
|
ToggleController,
|
|
81
86
|
ToggleGroupController,
|
|
82
87
|
TooltipController,
|
|
83
|
-
SidebarController
|
|
88
|
+
SidebarController,
|
|
89
|
+
// Utilities
|
|
90
|
+
positionFloating,
|
|
91
|
+
positionAtPoint
|
|
84
92
|
}
|
|
85
93
|
|
|
86
94
|
// Controller definitions for registration
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { computePosition, autoUpdate, flip, shift, offset, size } from "@floating-ui/dom"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Floating UI positioning utility for shadcn-rails components
|
|
5
|
+
*
|
|
6
|
+
* Provides smart positioning for dropdowns, popovers, tooltips, etc.
|
|
7
|
+
* that automatically handles:
|
|
8
|
+
* - Viewport edge detection (flip to opposite side)
|
|
9
|
+
* - Sliding along axis to stay in view (shift)
|
|
10
|
+
* - Consistent spacing (offset)
|
|
11
|
+
* - Dynamic content sizing
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Default middleware configuration
|
|
16
|
+
*/
|
|
17
|
+
const defaultMiddleware = [
|
|
18
|
+
offset(4),
|
|
19
|
+
flip({
|
|
20
|
+
fallbackAxisSideDirection: "start",
|
|
21
|
+
crossAxis: false
|
|
22
|
+
}),
|
|
23
|
+
shift({ padding: 8 })
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Middleware that includes size constraints for dropdowns/selects
|
|
28
|
+
*/
|
|
29
|
+
const sizeMiddleware = (options = {}) => size({
|
|
30
|
+
apply({ availableWidth, availableHeight, elements }) {
|
|
31
|
+
Object.assign(elements.floating.style, {
|
|
32
|
+
maxWidth: `${Math.max(0, availableWidth)}px`,
|
|
33
|
+
maxHeight: options.maxHeight ? `${Math.min(options.maxHeight, availableHeight)}px` : `${Math.max(0, availableHeight - 10)}px`
|
|
34
|
+
})
|
|
35
|
+
},
|
|
36
|
+
padding: 10
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Position a floating element relative to a reference element
|
|
41
|
+
*
|
|
42
|
+
* @param {HTMLElement} reference - The trigger/reference element
|
|
43
|
+
* @param {HTMLElement} floating - The floating content element
|
|
44
|
+
* @param {Object} options - Positioning options
|
|
45
|
+
* @param {string} options.placement - Placement (top, bottom, left, right, with -start/-end variants)
|
|
46
|
+
* @param {number} options.offset - Offset distance in pixels (default: 4)
|
|
47
|
+
* @param {boolean} options.sameWidth - Make floating element same width as reference
|
|
48
|
+
* @param {number} options.maxHeight - Maximum height for the floating element
|
|
49
|
+
* @param {Function} options.onPositioned - Callback after positioning
|
|
50
|
+
* @returns {Function} Cleanup function to stop auto-updates
|
|
51
|
+
*/
|
|
52
|
+
export function positionFloating(reference, floating, options = {}) {
|
|
53
|
+
const {
|
|
54
|
+
placement = "bottom-start",
|
|
55
|
+
offset: offsetValue = 4,
|
|
56
|
+
sameWidth = false,
|
|
57
|
+
maxHeight = null,
|
|
58
|
+
onPositioned = null
|
|
59
|
+
} = options
|
|
60
|
+
|
|
61
|
+
// Build middleware array
|
|
62
|
+
const middleware = [
|
|
63
|
+
offset(offsetValue),
|
|
64
|
+
flip({
|
|
65
|
+
fallbackAxisSideDirection: "start",
|
|
66
|
+
crossAxis: false
|
|
67
|
+
}),
|
|
68
|
+
shift({ padding: 8 })
|
|
69
|
+
]
|
|
70
|
+
|
|
71
|
+
// Add size middleware if needed
|
|
72
|
+
if (maxHeight || sameWidth) {
|
|
73
|
+
middleware.push(size({
|
|
74
|
+
apply({ availableWidth, availableHeight, elements, rects }) {
|
|
75
|
+
const styles = {}
|
|
76
|
+
|
|
77
|
+
if (sameWidth) {
|
|
78
|
+
styles.width = `${rects.reference.width}px`
|
|
79
|
+
styles.minWidth = `${rects.reference.width}px`
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (maxHeight) {
|
|
83
|
+
styles.maxHeight = `${Math.min(maxHeight, availableHeight - 10)}px`
|
|
84
|
+
} else {
|
|
85
|
+
styles.maxHeight = `${Math.max(0, availableHeight - 10)}px`
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
Object.assign(elements.floating.style, styles)
|
|
89
|
+
},
|
|
90
|
+
padding: 10
|
|
91
|
+
}))
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Set up auto-updating position
|
|
95
|
+
const cleanup = autoUpdate(reference, floating, () => {
|
|
96
|
+
computePosition(reference, floating, {
|
|
97
|
+
placement,
|
|
98
|
+
middleware
|
|
99
|
+
}).then(({ x, y, placement: finalPlacement }) => {
|
|
100
|
+
// Apply position
|
|
101
|
+
Object.assign(floating.style, {
|
|
102
|
+
position: "absolute",
|
|
103
|
+
left: `${x}px`,
|
|
104
|
+
top: `${y}px`
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
// Update data-side attribute for animations
|
|
108
|
+
const side = finalPlacement.split("-")[0]
|
|
109
|
+
floating.dataset.side = side
|
|
110
|
+
|
|
111
|
+
// Call callback if provided
|
|
112
|
+
if (onPositioned) {
|
|
113
|
+
onPositioned({ x, y, placement: finalPlacement })
|
|
114
|
+
}
|
|
115
|
+
})
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
return cleanup
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Position a context menu at specific coordinates
|
|
123
|
+
*
|
|
124
|
+
* @param {HTMLElement} floating - The floating content element
|
|
125
|
+
* @param {number} x - X coordinate (clientX from event)
|
|
126
|
+
* @param {number} y - Y coordinate (clientY from event)
|
|
127
|
+
* @param {Object} options - Positioning options
|
|
128
|
+
* @returns {void}
|
|
129
|
+
*/
|
|
130
|
+
export function positionAtPoint(floating, x, y, options = {}) {
|
|
131
|
+
const { maxHeight = null } = options
|
|
132
|
+
|
|
133
|
+
// Create a virtual reference element at the click point
|
|
134
|
+
const virtualRef = {
|
|
135
|
+
getBoundingClientRect() {
|
|
136
|
+
return {
|
|
137
|
+
width: 0,
|
|
138
|
+
height: 0,
|
|
139
|
+
x,
|
|
140
|
+
y,
|
|
141
|
+
top: y,
|
|
142
|
+
left: x,
|
|
143
|
+
right: x,
|
|
144
|
+
bottom: y
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const middleware = [
|
|
150
|
+
offset(4),
|
|
151
|
+
flip(),
|
|
152
|
+
shift({ padding: 8 })
|
|
153
|
+
]
|
|
154
|
+
|
|
155
|
+
if (maxHeight) {
|
|
156
|
+
middleware.push(size({
|
|
157
|
+
apply({ availableHeight, elements }) {
|
|
158
|
+
elements.floating.style.maxHeight = `${Math.min(maxHeight, availableHeight - 10)}px`
|
|
159
|
+
},
|
|
160
|
+
padding: 10
|
|
161
|
+
}))
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
computePosition(virtualRef, floating, {
|
|
165
|
+
placement: "bottom-start",
|
|
166
|
+
middleware
|
|
167
|
+
}).then(({ x: posX, y: posY, placement }) => {
|
|
168
|
+
Object.assign(floating.style, {
|
|
169
|
+
position: "fixed",
|
|
170
|
+
left: `${posX}px`,
|
|
171
|
+
top: `${posY}px`
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
const side = placement.split("-")[0]
|
|
175
|
+
floating.dataset.side = side
|
|
176
|
+
})
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export { computePosition, autoUpdate, flip, shift, offset, size }
|
|
@@ -302,6 +302,29 @@ body {
|
|
|
302
302
|
}
|
|
303
303
|
}
|
|
304
304
|
|
|
305
|
+
/* Tooltip animations - subtle pop effect */
|
|
306
|
+
@keyframes tooltip-in {
|
|
307
|
+
from {
|
|
308
|
+
opacity: 0;
|
|
309
|
+
transform: scale(0.96) translateY(2px);
|
|
310
|
+
}
|
|
311
|
+
to {
|
|
312
|
+
opacity: 1;
|
|
313
|
+
transform: scale(1) translateY(0);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
@keyframes tooltip-out {
|
|
318
|
+
from {
|
|
319
|
+
opacity: 1;
|
|
320
|
+
transform: scale(1) translateY(0);
|
|
321
|
+
}
|
|
322
|
+
to {
|
|
323
|
+
opacity: 0;
|
|
324
|
+
transform: scale(0.96) translateY(2px);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
305
328
|
/* ============================================
|
|
306
329
|
Animation Utility Classes
|
|
307
330
|
============================================ */
|
|
@@ -386,6 +409,15 @@ body {
|
|
|
386
409
|
animation-name: slide-out-to-right;
|
|
387
410
|
}
|
|
388
411
|
|
|
412
|
+
/* Tooltip animation classes */
|
|
413
|
+
.animate-tooltip-in {
|
|
414
|
+
animation: tooltip-in 150ms ease-out both;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
.animate-tooltip-out {
|
|
418
|
+
animation: tooltip-out 100ms ease-in both;
|
|
419
|
+
}
|
|
420
|
+
|
|
389
421
|
.animate-spin {
|
|
390
422
|
animation: spin 1s linear infinite;
|
|
391
423
|
}
|
|
@@ -437,6 +437,18 @@ button[data-state="off"].shadcn-toggle {
|
|
|
437
437
|
animation: fade-out 150ms ease-in, zoom-out 150ms ease-in;
|
|
438
438
|
}
|
|
439
439
|
|
|
440
|
+
/* ============================================
|
|
441
|
+
Context Menu Component
|
|
442
|
+
============================================ */
|
|
443
|
+
|
|
444
|
+
.shadcn-context-menu[data-state="open"] {
|
|
445
|
+
animation: fade-in 100ms ease-out, zoom-in 100ms ease-out;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
.shadcn-context-menu[data-state="closed"] {
|
|
449
|
+
animation: fade-out 100ms ease-in, zoom-out 100ms ease-in;
|
|
450
|
+
}
|
|
451
|
+
|
|
440
452
|
/* ============================================
|
|
441
453
|
Drawer Component
|
|
442
454
|
============================================ */
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
<div class="<%= accordion_classes %>"
|
|
2
|
+
data-controller="shadcn--accordion"
|
|
3
|
+
data-shadcn--accordion-type-value="<%= @type %>"
|
|
4
|
+
data-shadcn--accordion-collapsible-value="<%= @collapsible %>"
|
|
5
|
+
<% if @default_value %>data-shadcn--accordion-default-value="<%= default_value_string %>"<% end %>
|
|
6
|
+
<%= tag_attributes %>>
|
|
7
|
+
<%= accordion_content %>
|
|
8
|
+
</div>
|
|
@@ -37,27 +37,18 @@ module Shadcn
|
|
|
37
37
|
@default_value = default_value
|
|
38
38
|
end
|
|
39
39
|
|
|
40
|
-
def call
|
|
41
|
-
content_tag(:div, accordion_content, accordion_attributes)
|
|
42
|
-
end
|
|
43
|
-
|
|
44
40
|
private
|
|
45
41
|
|
|
42
|
+
def accordion_classes
|
|
43
|
+
class_name
|
|
44
|
+
end
|
|
45
|
+
|
|
46
46
|
def accordion_content
|
|
47
47
|
safe_join([items, content].compact.flatten)
|
|
48
48
|
end
|
|
49
49
|
|
|
50
|
-
def
|
|
51
|
-
|
|
52
|
-
class: class_name,
|
|
53
|
-
"data-controller": "shadcn--accordion",
|
|
54
|
-
"data-shadcn--accordion-type-value": @type.to_s,
|
|
55
|
-
"data-shadcn--accordion-collapsible-value": @collapsible.to_s,
|
|
56
|
-
"data-shadcn--accordion-default-value": Array(@default_value).join(",")
|
|
57
|
-
}
|
|
58
|
-
attrs.merge!(html_options)
|
|
59
|
-
attrs.merge!(build_data)
|
|
60
|
-
attrs.compact
|
|
50
|
+
def default_value_string
|
|
51
|
+
Array(@default_value).join(",")
|
|
61
52
|
end
|
|
62
53
|
end
|
|
63
54
|
end
|
|
@@ -48,28 +48,10 @@ module Shadcn
|
|
|
48
48
|
@variant = variant.to_sym
|
|
49
49
|
end
|
|
50
50
|
|
|
51
|
-
def call
|
|
52
|
-
content_tag(:div, alert_content, alert_attributes)
|
|
53
|
-
end
|
|
54
|
-
|
|
55
51
|
private
|
|
56
52
|
|
|
57
|
-
def alert_content
|
|
58
|
-
safe_join([icon, title, description, content].compact)
|
|
59
|
-
end
|
|
60
|
-
|
|
61
53
|
def alert_classes
|
|
62
54
|
cn(BASE_CLASSES, VARIANTS[@variant], class_name)
|
|
63
55
|
end
|
|
64
|
-
|
|
65
|
-
def alert_attributes
|
|
66
|
-
attrs = {
|
|
67
|
-
class: alert_classes,
|
|
68
|
-
role: "alert"
|
|
69
|
-
}
|
|
70
|
-
attrs.merge!(html_options)
|
|
71
|
-
attrs.merge!(build_data)
|
|
72
|
-
attrs.compact
|
|
73
|
-
end
|
|
74
56
|
end
|
|
75
57
|
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<div class="<%= alert_dialog_classes %>"
|
|
2
|
+
data-controller="<%= alert_dialog_data_attrs[:controller] %>"
|
|
3
|
+
data-shadcn--dialog-open-value="<%= alert_dialog_data_attrs[:"shadcn--dialog-open-value"] %>"
|
|
4
|
+
data-shadcn--dialog-modal-value="<%= alert_dialog_data_attrs[:"shadcn--dialog-modal-value"] %>"
|
|
5
|
+
<%= tag_attributes %>>
|
|
6
|
+
<% if trigger? %>
|
|
7
|
+
<div data-shadcn--dialog-target="trigger" data-action="click->shadcn--dialog#open">
|
|
8
|
+
<%= trigger %>
|
|
9
|
+
</div>
|
|
10
|
+
<% end %>
|
|
11
|
+
<%= body if body? %>
|
|
12
|
+
</div>
|