shadcn-rails 0.2.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +66 -2
  3. data/README.md +21 -8
  4. data/__mocks__/@floating-ui/dom.js +67 -0
  5. data/app/assets/javascripts/shadcn/controllers/combobox_controller.js +23 -2
  6. data/app/assets/javascripts/shadcn/controllers/context_menu_controller.js +4 -31
  7. data/app/assets/javascripts/shadcn/controllers/dropdown_controller.js +32 -41
  8. data/app/assets/javascripts/shadcn/controllers/hover_card_controller.js +29 -55
  9. data/app/assets/javascripts/shadcn/controllers/popover_controller.js +29 -54
  10. data/app/assets/javascripts/shadcn/controllers/select_controller.js +26 -8
  11. data/app/assets/javascripts/shadcn/controllers/tooltip_controller.js +28 -59
  12. data/app/assets/javascripts/shadcn/index.js +7 -1
  13. data/app/assets/javascripts/shadcn/utils/floating.js +179 -0
  14. data/app/assets/stylesheets/shadcn/base.css +32 -0
  15. data/app/components/shadcn/accordion_component.html.erb +8 -0
  16. data/app/components/shadcn/accordion_component.rb +6 -15
  17. data/app/components/shadcn/alert_component.html.erb +6 -0
  18. data/app/components/shadcn/alert_component.rb +0 -18
  19. data/app/components/shadcn/alert_dialog_component.html.erb +12 -0
  20. data/app/components/shadcn/alert_dialog_component.rb +7 -27
  21. data/app/components/shadcn/aspect_ratio_component.html.erb +7 -0
  22. data/app/components/shadcn/aspect_ratio_component.rb +4 -19
  23. data/app/components/shadcn/avatar_component.html.erb +20 -0
  24. data/app/components/shadcn/avatar_component.rb +8 -36
  25. data/app/components/shadcn/badge_component.html.erb +1 -0
  26. data/app/components/shadcn/badge_component.rb +0 -11
  27. data/app/components/shadcn/base_component.rb +15 -2
  28. data/app/components/shadcn/breadcrumb_component.html.erb +5 -0
  29. data/app/components/shadcn/breadcrumb_component.rb +6 -16
  30. data/app/components/shadcn/button_component.html.erb +18 -0
  31. data/app/components/shadcn/button_component.rb +1 -41
  32. data/app/components/shadcn/card_component.html.erb +8 -0
  33. data/app/components/shadcn/card_component.rb +2 -6
  34. data/app/components/shadcn/checkbox_component.html.erb +32 -0
  35. data/app/components/shadcn/checkbox_component.rb +4 -43
  36. data/app/components/shadcn/collapsible_component.html.erb +8 -0
  37. data/app/components/shadcn/collapsible_component.rb +6 -15
  38. data/app/components/shadcn/context_menu_component.html.erb +11 -0
  39. data/app/components/shadcn/context_menu_component.rb +6 -26
  40. data/app/components/shadcn/dialog_component.html.erb +14 -0
  41. data/app/components/shadcn/dialog_component.rb +8 -29
  42. data/app/components/shadcn/drawer_component.html.erb +12 -0
  43. data/app/components/shadcn/drawer_component.rb +7 -27
  44. data/app/components/shadcn/dropdown_menu_component.html.erb +14 -0
  45. data/app/components/shadcn/dropdown_menu_component.rb +9 -29
  46. data/app/components/shadcn/field_component.rb +7 -8
  47. data/app/components/shadcn/hover_card_component.html.erb +12 -0
  48. data/app/components/shadcn/hover_card_component.rb +7 -26
  49. data/app/components/shadcn/input_component.html.erb +18 -0
  50. data/app/components/shadcn/input_component.rb +2 -27
  51. data/app/components/shadcn/input_otp_component.rb +3 -3
  52. data/app/components/shadcn/kbd_component.html.erb +1 -0
  53. data/app/components/shadcn/kbd_component.rb +3 -10
  54. data/app/components/shadcn/label_component.html.erb +3 -0
  55. data/app/components/shadcn/label_component.rb +2 -18
  56. data/app/components/shadcn/menubar_component.html.erb +6 -0
  57. data/app/components/shadcn/menubar_component.rb +4 -15
  58. data/app/components/shadcn/native_select_component.html.erb +22 -0
  59. data/app/components/shadcn/native_select_component.rb +9 -39
  60. data/app/components/shadcn/navigation_menu_component.html.erb +6 -0
  61. data/app/components/shadcn/navigation_menu_component.rb +4 -15
  62. data/app/components/shadcn/pagination_component.html.erb +5 -0
  63. data/app/components/shadcn/pagination_component.rb +11 -15
  64. data/app/components/shadcn/popover_component.html.erb +15 -0
  65. data/app/components/shadcn/popover_component.rb +10 -30
  66. data/app/components/shadcn/progress_component.html.erb +13 -0
  67. data/app/components/shadcn/progress_component.rb +6 -26
  68. data/app/components/shadcn/radio_group_component.html.erb +8 -0
  69. data/app/components/shadcn/radio_group_component.rb +12 -26
  70. data/app/components/shadcn/scroll_area_component.html.erb +7 -0
  71. data/app/components/shadcn/scroll_area_component.rb +4 -16
  72. data/app/components/shadcn/select_component.html.erb +46 -0
  73. data/app/components/shadcn/select_component.rb +6 -80
  74. data/app/components/shadcn/separator_component.html.erb +5 -0
  75. data/app/components/shadcn/separator_component.rb +6 -14
  76. data/app/components/shadcn/sheet_component.html.erb +12 -0
  77. data/app/components/shadcn/sheet_component.rb +7 -27
  78. data/app/components/shadcn/sidebar_component.rb +2 -2
  79. data/app/components/shadcn/skeleton_component.html.erb +1 -0
  80. data/app/components/shadcn/skeleton_component.rb +4 -2
  81. data/app/components/shadcn/slider_component.html.erb +12 -0
  82. data/app/components/shadcn/slider_component.rb +2 -21
  83. data/app/components/shadcn/spinner_component.html.erb +18 -0
  84. data/app/components/shadcn/spinner_component.rb +2 -30
  85. data/app/components/shadcn/switch_component.html.erb +72 -0
  86. data/app/components/shadcn/switch_component.rb +4 -82
  87. data/app/components/shadcn/table_component.html.erb +9 -0
  88. data/app/components/shadcn/table_component.rb +2 -10
  89. data/app/components/shadcn/tabs_component.html.erb +8 -0
  90. data/app/components/shadcn/tabs_component.rb +4 -17
  91. data/app/components/shadcn/textarea_component.html.erb +13 -0
  92. data/app/components/shadcn/textarea_component.rb +6 -22
  93. data/app/components/shadcn/toast_component.html.erb +36 -0
  94. data/app/components/shadcn/toast_component.rb +6 -54
  95. data/app/components/shadcn/toggle_component.html.erb +12 -0
  96. data/app/components/shadcn/toggle_component.rb +6 -21
  97. data/app/components/shadcn/toggle_group_component.html.erb +14 -0
  98. data/app/components/shadcn/toggle_group_component.rb +6 -29
  99. data/app/components/shadcn/tooltip_component.html.erb +20 -0
  100. data/app/components/shadcn/tooltip_component.rb +13 -38
  101. data/lib/generators/shadcn/add/USAGE +24 -0
  102. data/lib/generators/shadcn/add/add_generator.rb +279 -0
  103. data/lib/generators/shadcn/install/USAGE +22 -0
  104. data/lib/generators/shadcn/install/install_generator.rb +8 -3
  105. data/lib/generators/shadcn/install/templates/initializer.rb.tt +7 -27
  106. data/lib/generators/shadcn/install/templates/shadcn.yml.tt +15 -31
  107. data/lib/shadcn/rails/version.rb +1 -1
  108. metadata +47 -45
  109. data/.dockerignore +0 -40
  110. data/CLAUDE.md +0 -612
  111. data/PROGRESS.md +0 -495
  112. data/Rakefile +0 -95
  113. data/__tests__/controllers/__snapshots__/calendar_controller.test.js.snap +0 -13
  114. data/__tests__/controllers/__snapshots__/popover_controller.test.js.snap +0 -46
  115. data/__tests__/controllers/__snapshots__/sheet_controller.test.js.snap +0 -111
  116. data/__tests__/controllers/__snapshots__/tabs_controller.test.js.snap +0 -27
  117. data/__tests__/controllers/accordion_controller.test.js +0 -904
  118. data/__tests__/controllers/calendar_controller.test.js +0 -1370
  119. data/__tests__/controllers/carousel_controller.test.js +0 -912
  120. data/__tests__/controllers/checkbox_controller.test.js +0 -454
  121. data/__tests__/controllers/collapsible_controller.test.js +0 -407
  122. data/__tests__/controllers/combobox_controller.test.js +0 -971
  123. data/__tests__/controllers/context_menu_controller.test.js +0 -905
  124. data/__tests__/controllers/date_picker_controller.test.js +0 -636
  125. data/__tests__/controllers/dialog_controller.test.js +0 -878
  126. data/__tests__/controllers/drawer_controller.test.js +0 -995
  127. data/__tests__/controllers/menubar_controller.test.js +0 -737
  128. data/__tests__/controllers/navigation_menu_controller.test.js +0 -599
  129. data/__tests__/controllers/popover_controller.test.js +0 -982
  130. data/__tests__/controllers/radio_group_controller.test.js +0 -640
  131. data/__tests__/controllers/resizable_controller.test.js +0 -680
  132. data/__tests__/controllers/select_controller.test.js +0 -678
  133. data/__tests__/controllers/sheet_controller.test.js +0 -986
  134. data/__tests__/controllers/slider_controller.test.js +0 -1036
  135. data/__tests__/controllers/switch_controller.test.js +0 -424
  136. data/__tests__/controllers/tabs_controller.test.js +0 -907
  137. data/__tests__/controllers/toggle_group_controller.test.js +0 -839
  138. data/__tests__/controllers/tooltip_controller.test.js +0 -808
  139. data/__tests__/helpers/stimulus-test-helper.js +0 -203
  140. data/babel.config.cjs +0 -5
  141. data/bin/bump +0 -321
  142. data/bin/console +0 -11
  143. data/bin/release +0 -205
  144. data/bin/setup +0 -8
  145. data/bin/test +0 -75
  146. data/jest.config.js +0 -19
  147. data/jest.setup.js +0 -8
  148. data/lib/generators/shadcn/component/component_generator.rb +0 -188
  149. data/lib/generators/shadcn/theme/theme_generator.rb +0 -128
  150. data/package-lock.json +0 -7438
  151. data/package.json +0 -71
  152. data/rollup.config.js +0 -29
@@ -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
- 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
  }
@@ -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 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>
@@ -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 dialog_content
44
- safe_join([
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 dialog_attributes
60
- attrs = {
61
- class: class_name,
62
- "data-controller": "shadcn--dialog",
63
- "data-shadcn--dialog-open-value": @open.to_s,
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
@@ -0,0 +1,7 @@
1
+ <div class="<%= wrapper_classes %>"
2
+ style="<%= wrapper_style %>"
3
+ <%= tag_attributes %>>
4
+ <div class="absolute inset-0" style="position: absolute; top: 0; right: 0; bottom: 0; left: 0;">
5
+ <%= content %>
6
+ </div>
7
+ </div>
@@ -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 wrapper_attributes
33
- attrs = {
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 inner_attributes
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>