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
|
@@ -1,19 +1,23 @@
|
|
|
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
|
* Select controller for custom select dropdowns
|
|
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", "input", "item", "display", "checkIcon"]
|
|
10
11
|
static values = {
|
|
11
|
-
value: String
|
|
12
|
+
value: String,
|
|
13
|
+
placement: { type: String, default: "bottom-start" },
|
|
14
|
+
sameWidth: { type: Boolean, default: true }
|
|
12
15
|
}
|
|
13
16
|
|
|
14
17
|
connect() {
|
|
15
18
|
this.isOpen = false
|
|
16
19
|
this.focusedIndex = -1
|
|
20
|
+
this.cleanupFloating = null
|
|
17
21
|
|
|
18
22
|
// Use stimulus-use for click outside detection
|
|
19
23
|
useClickOutside(this)
|
|
@@ -26,6 +30,14 @@ export default class extends Controller {
|
|
|
26
30
|
|
|
27
31
|
disconnect() {
|
|
28
32
|
this.close()
|
|
33
|
+
this.cleanupPositioning()
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
cleanupPositioning() {
|
|
37
|
+
if (this.cleanupFloating) {
|
|
38
|
+
this.cleanupFloating()
|
|
39
|
+
this.cleanupFloating = null
|
|
40
|
+
}
|
|
29
41
|
}
|
|
30
42
|
|
|
31
43
|
toggle(event) {
|
|
@@ -42,15 +54,18 @@ export default class extends Controller {
|
|
|
42
54
|
|
|
43
55
|
this.isOpen = true
|
|
44
56
|
|
|
45
|
-
// Set trigger width as CSS variable for dropdown sizing
|
|
46
|
-
if (this.hasTriggerTarget && this.hasContentTarget) {
|
|
47
|
-
const triggerWidth = this.triggerTarget.offsetWidth
|
|
48
|
-
this.contentTarget.style.setProperty('--radix-select-trigger-width', `${triggerWidth}px`)
|
|
49
|
-
}
|
|
50
|
-
|
|
51
57
|
if (this.hasContentTarget) {
|
|
52
58
|
this.contentTarget.hidden = false
|
|
53
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
|
+
}
|
|
54
69
|
}
|
|
55
70
|
|
|
56
71
|
if (this.hasTriggerTarget) {
|
|
@@ -75,6 +90,9 @@ export default class extends Controller {
|
|
|
75
90
|
|
|
76
91
|
this.isOpen = false
|
|
77
92
|
|
|
93
|
+
// Cleanup Floating UI auto-update
|
|
94
|
+
this.cleanupPositioning()
|
|
95
|
+
|
|
78
96
|
if (this.hasContentTarget) {
|
|
79
97
|
this.contentTarget.dataset.state = "closed"
|
|
80
98
|
setTimeout(() => {
|
|
@@ -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
|
}
|
|
@@ -48,6 +48,9 @@ import TooltipController from "./controllers/tooltip_controller"
|
|
|
48
48
|
import InputOtpController from "./controllers/input_otp_controller"
|
|
49
49
|
import SidebarController from "./controllers/sidebar_controller"
|
|
50
50
|
|
|
51
|
+
// Import floating utility
|
|
52
|
+
import { positionFloating, positionAtPoint } from "./utils/floating"
|
|
53
|
+
|
|
51
54
|
// Export individual controllers
|
|
52
55
|
export {
|
|
53
56
|
BaseMenuController,
|
|
@@ -82,7 +85,10 @@ export {
|
|
|
82
85
|
ToggleController,
|
|
83
86
|
ToggleGroupController,
|
|
84
87
|
TooltipController,
|
|
85
|
-
SidebarController
|
|
88
|
+
SidebarController,
|
|
89
|
+
// Utilities
|
|
90
|
+
positionFloating,
|
|
91
|
+
positionAtPoint
|
|
86
92
|
}
|
|
87
93
|
|
|
88
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
|
}
|
|
@@ -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>
|
|
@@ -34,38 +34,18 @@ module Shadcn
|
|
|
34
34
|
@open = open
|
|
35
35
|
end
|
|
36
36
|
|
|
37
|
-
def call
|
|
38
|
-
content_tag(:div, dialog_content, dialog_attributes)
|
|
39
|
-
end
|
|
40
|
-
|
|
41
37
|
private
|
|
42
38
|
|
|
43
|
-
def
|
|
44
|
-
|
|
45
|
-
trigger_wrapper,
|
|
46
|
-
body
|
|
47
|
-
].compact)
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
def trigger_wrapper
|
|
51
|
-
return unless trigger
|
|
52
|
-
|
|
53
|
-
content_tag(:div, trigger, {
|
|
54
|
-
"data-shadcn--dialog-target": "trigger",
|
|
55
|
-
"data-action": "click->shadcn--dialog#open"
|
|
56
|
-
})
|
|
39
|
+
def alert_dialog_classes
|
|
40
|
+
class_name
|
|
57
41
|
end
|
|
58
42
|
|
|
59
|
-
def
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
"
|
|
63
|
-
"
|
|
64
|
-
"data-shadcn--dialog-modal-value": "true"
|
|
43
|
+
def alert_dialog_data_attrs
|
|
44
|
+
{
|
|
45
|
+
controller: "shadcn--dialog",
|
|
46
|
+
"shadcn--dialog-open-value": @open.to_s,
|
|
47
|
+
"shadcn--dialog-modal-value": "true"
|
|
65
48
|
}
|
|
66
|
-
attrs.merge!(html_options)
|
|
67
|
-
attrs.merge!(build_data)
|
|
68
|
-
attrs.compact
|
|
69
49
|
end
|
|
70
50
|
end
|
|
71
51
|
end
|
|
@@ -21,29 +21,14 @@ module Shadcn
|
|
|
21
21
|
@ratio = ratio.to_f
|
|
22
22
|
end
|
|
23
23
|
|
|
24
|
-
def call
|
|
25
|
-
content_tag(:div, wrapper_attributes) do
|
|
26
|
-
content_tag(:div, content, inner_attributes)
|
|
27
|
-
end
|
|
28
|
-
end
|
|
29
|
-
|
|
30
24
|
private
|
|
31
25
|
|
|
32
|
-
def
|
|
33
|
-
|
|
34
|
-
class: cn("relative w-full", class_name),
|
|
35
|
-
style: "padding-bottom: #{(1.0 / @ratio) * 100}%;"
|
|
36
|
-
}
|
|
37
|
-
attrs.merge!(html_options)
|
|
38
|
-
attrs.merge!(build_data)
|
|
39
|
-
attrs.compact
|
|
26
|
+
def wrapper_classes
|
|
27
|
+
cn("relative w-full", class_name)
|
|
40
28
|
end
|
|
41
29
|
|
|
42
|
-
def
|
|
43
|
-
{
|
|
44
|
-
class: "absolute inset-0",
|
|
45
|
-
style: "position: absolute; top: 0; right: 0; bottom: 0; left: 0;"
|
|
46
|
-
}
|
|
30
|
+
def wrapper_style
|
|
31
|
+
"padding-bottom: #{(1.0 / @ratio) * 100}%;"
|
|
47
32
|
end
|
|
48
33
|
end
|
|
49
34
|
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
<span class="<%= avatar_classes %>" <%= tag_attributes %>>
|
|
2
|
+
<% if has_image? %>
|
|
3
|
+
<span class="contents" data-controller="shadcn--avatar">
|
|
4
|
+
<img src="<%= @src %>"
|
|
5
|
+
alt="<%= @alt %>"
|
|
6
|
+
class="<%= IMAGE_CLASSES %>"
|
|
7
|
+
data-shadcn--avatar-target="image"
|
|
8
|
+
data-action="error->shadcn--avatar#handleError" />
|
|
9
|
+
<span class="<%= FALLBACK_CLASSES %> hidden" data-shadcn--avatar-target="fallback">
|
|
10
|
+
<%= fallback_text %>
|
|
11
|
+
</span>
|
|
12
|
+
</span>
|
|
13
|
+
<% elsif has_fallback_slot? %>
|
|
14
|
+
<%= fallback %>
|
|
15
|
+
<% else %>
|
|
16
|
+
<span class="<%= FALLBACK_CLASSES %>">
|
|
17
|
+
<%= fallback_text %>
|
|
18
|
+
</span>
|
|
19
|
+
<% end %>
|
|
20
|
+
</span>
|