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.
Files changed (169) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +69 -2
  3. data/README.md +102 -1398
  4. data/__mocks__/@floating-ui/dom.js +67 -0
  5. data/app/assets/javascripts/shadcn/controllers/base_menu_controller.js +266 -0
  6. data/app/assets/javascripts/shadcn/controllers/combobox_controller.js +34 -8
  7. data/app/assets/javascripts/shadcn/controllers/command_controller.js +5 -1
  8. data/app/assets/javascripts/shadcn/controllers/context_menu_controller.js +64 -135
  9. data/app/assets/javascripts/shadcn/controllers/dropdown_controller.js +56 -186
  10. data/app/assets/javascripts/shadcn/controllers/hover_card_controller.js +29 -55
  11. data/app/assets/javascripts/shadcn/controllers/menubar_controller.js +10 -7
  12. data/app/assets/javascripts/shadcn/controllers/navigation_menu_controller.js +10 -6
  13. data/app/assets/javascripts/shadcn/controllers/popover_controller.js +35 -60
  14. data/app/assets/javascripts/shadcn/controllers/select_controller.js +37 -17
  15. data/app/assets/javascripts/shadcn/controllers/sidebar_controller.js +24 -14
  16. data/app/assets/javascripts/shadcn/controllers/tooltip_controller.js +28 -59
  17. data/app/assets/javascripts/shadcn/index.js +9 -1
  18. data/app/assets/javascripts/shadcn/utils/floating.js +179 -0
  19. data/app/assets/stylesheets/shadcn/base.css +32 -0
  20. data/app/assets/stylesheets/shadcn/components.css +12 -0
  21. data/app/components/shadcn/accordion_component.html.erb +8 -0
  22. data/app/components/shadcn/accordion_component.rb +6 -15
  23. data/app/components/shadcn/alert_component.html.erb +6 -0
  24. data/app/components/shadcn/alert_component.rb +0 -18
  25. data/app/components/shadcn/alert_dialog_component.html.erb +12 -0
  26. data/app/components/shadcn/alert_dialog_component.rb +7 -27
  27. data/app/components/shadcn/aspect_ratio_component.html.erb +7 -0
  28. data/app/components/shadcn/aspect_ratio_component.rb +4 -19
  29. data/app/components/shadcn/avatar_component.html.erb +20 -0
  30. data/app/components/shadcn/avatar_component.rb +8 -36
  31. data/app/components/shadcn/badge_component.html.erb +1 -0
  32. data/app/components/shadcn/badge_component.rb +0 -11
  33. data/app/components/shadcn/base_component.rb +15 -2
  34. data/app/components/shadcn/breadcrumb_component.html.erb +5 -0
  35. data/app/components/shadcn/breadcrumb_component.rb +6 -16
  36. data/app/components/shadcn/button_component.html.erb +18 -0
  37. data/app/components/shadcn/button_component.rb +1 -41
  38. data/app/components/shadcn/card_component.html.erb +8 -0
  39. data/app/components/shadcn/card_component.rb +2 -6
  40. data/app/components/shadcn/checkbox_component.html.erb +32 -0
  41. data/app/components/shadcn/checkbox_component.rb +4 -43
  42. data/app/components/shadcn/collapsible_component.html.erb +8 -0
  43. data/app/components/shadcn/collapsible_component.rb +6 -15
  44. data/app/components/shadcn/command_list_component.rb +29 -14
  45. data/app/components/shadcn/context_menu_checkbox_item_component.rb +76 -0
  46. data/app/components/shadcn/context_menu_component.html.erb +11 -0
  47. data/app/components/shadcn/context_menu_component.rb +6 -26
  48. data/app/components/shadcn/context_menu_content_component.rb +37 -14
  49. data/app/components/shadcn/context_menu_item_component.rb +3 -2
  50. data/app/components/shadcn/context_menu_radio_group_component.rb +42 -0
  51. data/app/components/shadcn/context_menu_radio_item_component.rb +76 -0
  52. data/app/components/shadcn/dialog_component.html.erb +14 -0
  53. data/app/components/shadcn/dialog_component.rb +8 -29
  54. data/app/components/shadcn/drawer_component.html.erb +12 -0
  55. data/app/components/shadcn/drawer_component.rb +7 -27
  56. data/app/components/shadcn/dropdown_menu_checkbox_item_component.rb +76 -0
  57. data/app/components/shadcn/dropdown_menu_component.html.erb +14 -0
  58. data/app/components/shadcn/dropdown_menu_component.rb +9 -29
  59. data/app/components/shadcn/dropdown_menu_content_component.rb +45 -16
  60. data/app/components/shadcn/dropdown_menu_radio_group_component.rb +42 -0
  61. data/app/components/shadcn/dropdown_menu_radio_item_component.rb +76 -0
  62. data/app/components/shadcn/field_component.rb +7 -8
  63. data/app/components/shadcn/hover_card_component.html.erb +12 -0
  64. data/app/components/shadcn/hover_card_component.rb +7 -26
  65. data/app/components/shadcn/input_component.html.erb +18 -0
  66. data/app/components/shadcn/input_component.rb +2 -27
  67. data/app/components/shadcn/input_otp_component.rb +3 -3
  68. data/app/components/shadcn/kbd_component.html.erb +1 -0
  69. data/app/components/shadcn/kbd_component.rb +3 -10
  70. data/app/components/shadcn/label_component.html.erb +3 -0
  71. data/app/components/shadcn/label_component.rb +2 -18
  72. data/app/components/shadcn/menubar_component.html.erb +6 -0
  73. data/app/components/shadcn/menubar_component.rb +4 -15
  74. data/app/components/shadcn/menubar_content_component.rb +45 -20
  75. data/app/components/shadcn/menubar_sub_content_component.rb +21 -8
  76. data/app/components/shadcn/native_select_component.html.erb +22 -0
  77. data/app/components/shadcn/native_select_component.rb +9 -39
  78. data/app/components/shadcn/navigation_menu_component.html.erb +6 -0
  79. data/app/components/shadcn/navigation_menu_component.rb +4 -15
  80. data/app/components/shadcn/pagination_component.html.erb +5 -0
  81. data/app/components/shadcn/pagination_component.rb +11 -15
  82. data/app/components/shadcn/popover_component.html.erb +15 -0
  83. data/app/components/shadcn/popover_component.rb +10 -30
  84. data/app/components/shadcn/progress_component.html.erb +13 -0
  85. data/app/components/shadcn/progress_component.rb +6 -26
  86. data/app/components/shadcn/radio_group_component.html.erb +8 -0
  87. data/app/components/shadcn/radio_group_component.rb +12 -26
  88. data/app/components/shadcn/radio_group_item_component.rb +32 -6
  89. data/app/components/shadcn/resizable_panel_group_component.rb +27 -16
  90. data/app/components/shadcn/scroll_area_component.html.erb +7 -0
  91. data/app/components/shadcn/scroll_area_component.rb +4 -16
  92. data/app/components/shadcn/select_component.html.erb +46 -0
  93. data/app/components/shadcn/select_component.rb +29 -86
  94. data/app/components/shadcn/separator_component.html.erb +5 -0
  95. data/app/components/shadcn/separator_component.rb +6 -14
  96. data/app/components/shadcn/sheet_component.html.erb +12 -0
  97. data/app/components/shadcn/sheet_component.rb +7 -27
  98. data/app/components/shadcn/sidebar_component.rb +2 -2
  99. data/app/components/shadcn/skeleton_component.html.erb +1 -0
  100. data/app/components/shadcn/skeleton_component.rb +4 -2
  101. data/app/components/shadcn/slider_component.html.erb +12 -0
  102. data/app/components/shadcn/slider_component.rb +2 -21
  103. data/app/components/shadcn/spinner_component.html.erb +18 -0
  104. data/app/components/shadcn/spinner_component.rb +2 -30
  105. data/app/components/shadcn/switch_component.html.erb +72 -0
  106. data/app/components/shadcn/switch_component.rb +4 -82
  107. data/app/components/shadcn/table_component.html.erb +9 -0
  108. data/app/components/shadcn/table_component.rb +2 -10
  109. data/app/components/shadcn/tabs_component.html.erb +8 -0
  110. data/app/components/shadcn/tabs_component.rb +4 -17
  111. data/app/components/shadcn/textarea_component.html.erb +13 -0
  112. data/app/components/shadcn/textarea_component.rb +6 -22
  113. data/app/components/shadcn/toast_component.html.erb +36 -0
  114. data/app/components/shadcn/toast_component.rb +6 -54
  115. data/app/components/shadcn/toggle_component.html.erb +12 -0
  116. data/app/components/shadcn/toggle_component.rb +6 -21
  117. data/app/components/shadcn/toggle_group_component.html.erb +14 -0
  118. data/app/components/shadcn/toggle_group_component.rb +6 -29
  119. data/app/components/shadcn/tooltip_component.html.erb +20 -0
  120. data/app/components/shadcn/tooltip_component.rb +13 -38
  121. data/lib/generators/shadcn/add/USAGE +24 -0
  122. data/lib/generators/shadcn/add/add_generator.rb +279 -0
  123. data/lib/generators/shadcn/install/USAGE +22 -0
  124. data/lib/generators/shadcn/install/install_generator.rb +8 -3
  125. data/lib/generators/shadcn/install/templates/initializer.rb.tt +7 -27
  126. data/lib/generators/shadcn/install/templates/shadcn.yml.tt +15 -31
  127. data/lib/shadcn/rails/version.rb +1 -1
  128. metadata +54 -42
  129. data/.dockerignore +0 -40
  130. data/CLAUDE.md +0 -463
  131. data/PROGRESS.md +0 -485
  132. data/Rakefile +0 -29
  133. data/__tests__/controllers/__snapshots__/calendar_controller.test.js.snap +0 -13
  134. data/__tests__/controllers/__snapshots__/popover_controller.test.js.snap +0 -46
  135. data/__tests__/controllers/__snapshots__/sheet_controller.test.js.snap +0 -111
  136. data/__tests__/controllers/__snapshots__/tabs_controller.test.js.snap +0 -27
  137. data/__tests__/controllers/accordion_controller.test.js +0 -904
  138. data/__tests__/controllers/calendar_controller.test.js +0 -1370
  139. data/__tests__/controllers/carousel_controller.test.js +0 -912
  140. data/__tests__/controllers/checkbox_controller.test.js +0 -454
  141. data/__tests__/controllers/collapsible_controller.test.js +0 -407
  142. data/__tests__/controllers/combobox_controller.test.js +0 -966
  143. data/__tests__/controllers/context_menu_controller.test.js +0 -627
  144. data/__tests__/controllers/date_picker_controller.test.js +0 -636
  145. data/__tests__/controllers/dialog_controller.test.js +0 -878
  146. data/__tests__/controllers/drawer_controller.test.js +0 -995
  147. data/__tests__/controllers/menubar_controller.test.js +0 -736
  148. data/__tests__/controllers/navigation_menu_controller.test.js +0 -598
  149. data/__tests__/controllers/popover_controller.test.js +0 -1007
  150. data/__tests__/controllers/radio_group_controller.test.js +0 -640
  151. data/__tests__/controllers/resizable_controller.test.js +0 -680
  152. data/__tests__/controllers/select_controller.test.js +0 -674
  153. data/__tests__/controllers/sheet_controller.test.js +0 -986
  154. data/__tests__/controllers/slider_controller.test.js +0 -1036
  155. data/__tests__/controllers/switch_controller.test.js +0 -424
  156. data/__tests__/controllers/tabs_controller.test.js +0 -907
  157. data/__tests__/controllers/toggle_group_controller.test.js +0 -839
  158. data/__tests__/controllers/tooltip_controller.test.js +0 -808
  159. data/__tests__/helpers/stimulus-test-helper.js +0 -203
  160. data/babel.config.cjs +0 -5
  161. data/bin/console +0 -11
  162. data/bin/setup +0 -8
  163. data/jest.config.js +0 -19
  164. data/jest.setup.js +0 -8
  165. data/lib/generators/shadcn/component/component_generator.rb +0 -188
  166. data/lib/generators/shadcn/theme/theme_generator.rb +0 -128
  167. data/package-lock.json +0 -7415
  168. data/package.json +0 -68
  169. 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.boundHandleClickOutside = this.handleClickOutside.bind(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
- // Set up mobile detection
34
+ // Use stimulus-use for responsive media query detection
30
35
  this.isMobile = window.innerWidth < 768
31
- this.handleResize = this.handleResize.bind(this)
32
- window.addEventListener("resize", this.handleResize)
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
- window.removeEventListener("resize", this.handleResize)
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
- this.positionTooltip()
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 accordion_attributes
51
- attrs = {
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
@@ -0,0 +1,6 @@
1
+ <div role="alert" class="<%= alert_classes %>" <%= tag_attributes %>>
2
+ <%= icon if icon? %>
3
+ <%= title if title? %>
4
+ <%= description if description? %>
5
+ <%= content %>
6
+ </div>
@@ -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>