shadcn_view_components 0.2.0

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 (177) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +590 -0
  3. data/LICENSE +49 -0
  4. data/README.md +542 -0
  5. data/app/assets/javascripts/shadcn/aria_relationships.js +88 -0
  6. data/app/assets/javascripts/shadcn/controllers/accordion_controller.js +241 -0
  7. data/app/assets/javascripts/shadcn/controllers/calendar_controller.js +98 -0
  8. data/app/assets/javascripts/shadcn/controllers/carousel_controller.js +305 -0
  9. data/app/assets/javascripts/shadcn/controllers/checked_state_controller.js +121 -0
  10. data/app/assets/javascripts/shadcn/controllers/combobox_controller.js +613 -0
  11. data/app/assets/javascripts/shadcn/controllers/command_controller.js +87 -0
  12. data/app/assets/javascripts/shadcn/controllers/dialog_controller.js +208 -0
  13. data/app/assets/javascripts/shadcn/controllers/hover_card_controller.js +145 -0
  14. data/app/assets/javascripts/shadcn/controllers/input_otp_controller.js +416 -0
  15. data/app/assets/javascripts/shadcn/controllers/menu_controller.js +286 -0
  16. data/app/assets/javascripts/shadcn/controllers/menubar_controller.js +430 -0
  17. data/app/assets/javascripts/shadcn/controllers/message_scroller_controller.js +37 -0
  18. data/app/assets/javascripts/shadcn/controllers/navigation_menu_controller.js +254 -0
  19. data/app/assets/javascripts/shadcn/controllers/popover_controller.js +117 -0
  20. data/app/assets/javascripts/shadcn/controllers/resizable_controller.js +256 -0
  21. data/app/assets/javascripts/shadcn/controllers/select_controller.js +491 -0
  22. data/app/assets/javascripts/shadcn/controllers/sidebar_controller.js +30 -0
  23. data/app/assets/javascripts/shadcn/controllers/slider_controller.js +74 -0
  24. data/app/assets/javascripts/shadcn/controllers/tabs_controller.js +146 -0
  25. data/app/assets/javascripts/shadcn/controllers/toast_controller.js +81 -0
  26. data/app/assets/javascripts/shadcn/controllers/toggle_controller.js +21 -0
  27. data/app/assets/javascripts/shadcn/controllers/toggle_group_controller.js +37 -0
  28. data/app/assets/javascripts/shadcn/controllers/tooltip_controller.js +123 -0
  29. data/app/assets/javascripts/shadcn/floating_position.js +660 -0
  30. data/app/assets/javascripts/shadcn/hide_after_exit.js +53 -0
  31. data/app/assets/javascripts/shadcn/index.js +65 -0
  32. data/app/assets/javascripts/shadcn/menu_popover.js +352 -0
  33. data/app/assets/javascripts/shadcn/package.json +17 -0
  34. data/app/assets/javascripts/shadcn/state_attrs.js +13 -0
  35. data/app/assets/stylesheets/shadcn/shadcn.css +766 -0
  36. data/app/assets/tailwind/shadcn_view_components/engine.css +11 -0
  37. data/app/components/shadcn/accordion.rb +121 -0
  38. data/app/components/shadcn/alert.rb +31 -0
  39. data/app/components/shadcn/alert_dialog.rb +165 -0
  40. data/app/components/shadcn/aspect_ratio.rb +34 -0
  41. data/app/components/shadcn/attachment.rb +86 -0
  42. data/app/components/shadcn/avatar.rb +55 -0
  43. data/app/components/shadcn/badge.rb +32 -0
  44. data/app/components/shadcn/base_component.rb +239 -0
  45. data/app/components/shadcn/breadcrumb.rb +70 -0
  46. data/app/components/shadcn/bubble.rb +48 -0
  47. data/app/components/shadcn/button.html.erb +1 -0
  48. data/app/components/shadcn/button.rb +41 -0
  49. data/app/components/shadcn/button_group.rb +41 -0
  50. data/app/components/shadcn/button_styled.rb +51 -0
  51. data/app/components/shadcn/calendar.rb +276 -0
  52. data/app/components/shadcn/card.rb +18 -0
  53. data/app/components/shadcn/carousel.rb +236 -0
  54. data/app/components/shadcn/chart.rb +57 -0
  55. data/app/components/shadcn/checkbox.rb +64 -0
  56. data/app/components/shadcn/collapsible.rb +25 -0
  57. data/app/components/shadcn/combobox.rb +552 -0
  58. data/app/components/shadcn/command.rb +149 -0
  59. data/app/components/shadcn/context_menu.rb +79 -0
  60. data/app/components/shadcn/dialog.rb +180 -0
  61. data/app/components/shadcn/direction_provider.rb +18 -0
  62. data/app/components/shadcn/drawer.rb +150 -0
  63. data/app/components/shadcn/dropdown_menu.rb +242 -0
  64. data/app/components/shadcn/empty.rb +40 -0
  65. data/app/components/shadcn/field.rb +106 -0
  66. data/app/components/shadcn/floating_position_options.rb +132 -0
  67. data/app/components/shadcn/form.rb +105 -0
  68. data/app/components/shadcn/hover_card.rb +66 -0
  69. data/app/components/shadcn/input.rb +13 -0
  70. data/app/components/shadcn/input_group/button.rb +48 -0
  71. data/app/components/shadcn/input_group.rb +51 -0
  72. data/app/components/shadcn/input_otp.rb +265 -0
  73. data/app/components/shadcn/item.rb +108 -0
  74. data/app/components/shadcn/kbd.rb +9 -0
  75. data/app/components/shadcn/label.rb +12 -0
  76. data/app/components/shadcn/marker.rb +37 -0
  77. data/app/components/shadcn/menubar.rb +110 -0
  78. data/app/components/shadcn/message.rb +29 -0
  79. data/app/components/shadcn/message_scroller.rb +52 -0
  80. data/app/components/shadcn/native_checked_state.rb +74 -0
  81. data/app/components/shadcn/native_select.rb +78 -0
  82. data/app/components/shadcn/navigation_menu.rb +122 -0
  83. data/app/components/shadcn/pagination.rb +160 -0
  84. data/app/components/shadcn/popover.rb +68 -0
  85. data/app/components/shadcn/preview_base.rb +107 -0
  86. data/app/components/shadcn/progress.rb +49 -0
  87. data/app/components/shadcn/radio_group.rb +74 -0
  88. data/app/components/shadcn/resizable.rb +77 -0
  89. data/app/components/shadcn/scroll_area.rb +59 -0
  90. data/app/components/shadcn/select.rb +356 -0
  91. data/app/components/shadcn/separator.rb +42 -0
  92. data/app/components/shadcn/sheet.rb +157 -0
  93. data/app/components/shadcn/sidebar.rb +332 -0
  94. data/app/components/shadcn/skeleton.rb +13 -0
  95. data/app/components/shadcn/slider.rb +279 -0
  96. data/app/components/shadcn/sonner.rb +30 -0
  97. data/app/components/shadcn/spinner.rb +32 -0
  98. data/app/components/shadcn/switch.rb +79 -0
  99. data/app/components/shadcn/table.rb +46 -0
  100. data/app/components/shadcn/tabs.rb +119 -0
  101. data/app/components/shadcn/textarea.rb +7 -0
  102. data/app/components/shadcn/toggle.rb +66 -0
  103. data/app/components/shadcn/toggle_group.rb +127 -0
  104. data/app/components/shadcn/tooltip.rb +74 -0
  105. data/config/importmap.rb +5 -0
  106. data/lib/generators/shadcn_view_components/install_generator.rb +182 -0
  107. data/lib/shadcn_view_components/classes.rb +89 -0
  108. data/lib/shadcn_view_components/component_naming.rb +21 -0
  109. data/lib/shadcn_view_components/contracts/calendar.rb +95 -0
  110. data/lib/shadcn_view_components/engine.rb +56 -0
  111. data/lib/shadcn_view_components/generated/contracts/accordion.rb +112 -0
  112. data/lib/shadcn_view_components/generated/contracts/alert-dialog.rb +300 -0
  113. data/lib/shadcn_view_components/generated/contracts/alert.rb +104 -0
  114. data/lib/shadcn_view_components/generated/contracts/aspect-ratio.rb +34 -0
  115. data/lib/shadcn_view_components/generated/contracts/attachment.rb +224 -0
  116. data/lib/shadcn_view_components/generated/contracts/avatar.rb +149 -0
  117. data/lib/shadcn_view_components/generated/contracts/badge.rb +39 -0
  118. data/lib/shadcn_view_components/generated/contracts/breadcrumb.rb +172 -0
  119. data/lib/shadcn_view_components/generated/contracts/bubble.rb +112 -0
  120. data/lib/shadcn_view_components/generated/contracts/button-group.rb +81 -0
  121. data/lib/shadcn_view_components/generated/contracts/button.rb +81 -0
  122. data/lib/shadcn_view_components/generated/contracts/card.rb +172 -0
  123. data/lib/shadcn_view_components/generated/contracts/carousel.rb +133 -0
  124. data/lib/shadcn_view_components/generated/contracts/chart.rb +109 -0
  125. data/lib/shadcn_view_components/generated/contracts/checkbox.rb +35 -0
  126. data/lib/shadcn_view_components/generated/contracts/collapsible.rb +89 -0
  127. data/lib/shadcn_view_components/generated/contracts/combobox.rb +368 -0
  128. data/lib/shadcn_view_components/generated/contracts/command.rb +222 -0
  129. data/lib/shadcn_view_components/generated/contracts/context-menu.rb +378 -0
  130. data/lib/shadcn_view_components/generated/contracts/dialog.rb +258 -0
  131. data/lib/shadcn_view_components/generated/contracts/direction.rb +34 -0
  132. data/lib/shadcn_view_components/generated/contracts/drawer.rb +280 -0
  133. data/lib/shadcn_view_components/generated/contracts/dropdown-menu.rb +380 -0
  134. data/lib/shadcn_view_components/generated/contracts/empty.rb +150 -0
  135. data/lib/shadcn_view_components/generated/contracts/field.rb +244 -0
  136. data/lib/shadcn_view_components/generated/contracts/form.rb +62 -0
  137. data/lib/shadcn_view_components/generated/contracts/hover-card.rb +90 -0
  138. data/lib/shadcn_view_components/generated/contracts/input-group.rb +155 -0
  139. data/lib/shadcn_view_components/generated/contracts/input-otp.rb +106 -0
  140. data/lib/shadcn_view_components/generated/contracts/input.rb +34 -0
  141. data/lib/shadcn_view_components/generated/contracts/item.rb +251 -0
  142. data/lib/shadcn_view_components/generated/contracts/kbd.rb +57 -0
  143. data/lib/shadcn_view_components/generated/contracts/label.rb +34 -0
  144. data/lib/shadcn_view_components/generated/contracts/marker.rb +82 -0
  145. data/lib/shadcn_view_components/generated/contracts/menubar.rb +394 -0
  146. data/lib/shadcn_view_components/generated/contracts/message-scroller.rb +149 -0
  147. data/lib/shadcn_view_components/generated/contracts/message.rb +149 -0
  148. data/lib/shadcn_view_components/generated/contracts/native-select.rb +82 -0
  149. data/lib/shadcn_view_components/generated/contracts/navigation-menu.rb +195 -0
  150. data/lib/shadcn_view_components/generated/contracts/pagination.rb +176 -0
  151. data/lib/shadcn_view_components/generated/contracts/popover.rb +159 -0
  152. data/lib/shadcn_view_components/generated/contracts/progress.rb +126 -0
  153. data/lib/shadcn_view_components/generated/contracts/radio-group.rb +58 -0
  154. data/lib/shadcn_view_components/generated/contracts/resizable.rb +85 -0
  155. data/lib/shadcn_view_components/generated/contracts/scroll-area.rb +59 -0
  156. data/lib/shadcn_view_components/generated/contracts/select.rb +245 -0
  157. data/lib/shadcn_view_components/generated/contracts/separator.rb +34 -0
  158. data/lib/shadcn_view_components/generated/contracts/sheet.rb +209 -0
  159. data/lib/shadcn_view_components/generated/contracts/sidebar.rb +558 -0
  160. data/lib/shadcn_view_components/generated/contracts/skeleton.rb +34 -0
  161. data/lib/shadcn_view_components/generated/contracts/slider.rb +38 -0
  162. data/lib/shadcn_view_components/generated/contracts/sonner.rb +36 -0
  163. data/lib/shadcn_view_components/generated/contracts/spinner.rb +34 -0
  164. data/lib/shadcn_view_components/generated/contracts/switch.rb +35 -0
  165. data/lib/shadcn_view_components/generated/contracts/table.rb +199 -0
  166. data/lib/shadcn_view_components/generated/contracts/tabs.rb +104 -0
  167. data/lib/shadcn_view_components/generated/contracts/textarea.rb +34 -0
  168. data/lib/shadcn_view_components/generated/contracts/toggle-group.rb +62 -0
  169. data/lib/shadcn_view_components/generated/contracts/toggle.rb +39 -0
  170. data/lib/shadcn_view_components/generated/contracts/tooltip.rb +116 -0
  171. data/lib/shadcn_view_components/numeric_property_validation.rb +124 -0
  172. data/lib/shadcn_view_components/property_contract_definitions.rb +92 -0
  173. data/lib/shadcn_view_components/property_contracts.rb +42 -0
  174. data/lib/shadcn_view_components/property_validation.rb +55 -0
  175. data/lib/shadcn_view_components/version.rb +6 -0
  176. data/lib/shadcn_view_components.rb +21 -0
  177. metadata +446 -0
@@ -0,0 +1,40 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Shadcn
5
+ class Empty < BaseComponent
6
+ class Header < BaseComponent; end
7
+
8
+ class Title < BaseComponent; end
9
+
10
+ class Description < BaseComponent; end
11
+
12
+ class Content < BaseComponent; end
13
+
14
+ class Media < BaseComponent
15
+ sig do
16
+ params(
17
+ variant: T.any(Symbol, String),
18
+ args: T::Hash[Symbol, T.untyped]
19
+ ).void.checked(:never)
20
+ end
21
+ def initialize(variant: ShadcnViewComponents::Contracts::Empty::Media::DEFAULTS.fetch(:variant), **args)
22
+ @variant = T.let(normalize_option(:variant, variant), Symbol)
23
+ super(**args)
24
+ end
25
+
26
+ sig { returns(Symbol) }
27
+ attr_reader :variant
28
+
29
+ sig { override.returns(T::Hash[Symbol, VariantOption]) }
30
+ def variant_options
31
+ { variant: @variant }
32
+ end
33
+
34
+ sig { override.returns(T::Hash[Symbol, T.untyped]) }
35
+ def contract_data_attributes
36
+ super.merge(variant: @variant)
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,106 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Shadcn
5
+ # フィールドコンテナ(Phase 4)。orientation(vertical/horizontal/responsive)の
6
+ # cva バリアントを持つ。フォーム組み立て(upstream docs/forms ガイド)の基盤で、
7
+ # Shadcn::Form::Item もこの構造の上に成り立つ
8
+ class Field < BaseComponent
9
+ sig { params(orientation: T.any(Symbol, String), args: T::Hash[Symbol, T.untyped]).void.checked(:never) }
10
+ def initialize(orientation: ShadcnViewComponents::Contracts::Field::DEFAULTS.fetch(:orientation), **args)
11
+ @orientation = T.let(normalize_option(:orientation, orientation), Symbol)
12
+ super(**args)
13
+ end
14
+
15
+ sig { override.returns(T::Hash[Symbol, VariantOption]) }
16
+ def variant_options
17
+ { orientation: @orientation }
18
+ end
19
+
20
+ sig { override.returns(T::Hash[Symbol, T.untyped]) }
21
+ def html_attributes
22
+ attributes = super
23
+ # upstream は data-orientation={orientation} を常に出力する
24
+ merge_nested(attributes, :data, { orientation: @orientation.to_s })
25
+ attributes
26
+ end
27
+
28
+ class Content < BaseComponent
29
+ # div
30
+ end
31
+
32
+ class Description < BaseComponent
33
+ sig { override.returns(String) }
34
+ def default_tag
35
+ "p"
36
+ end
37
+ end
38
+
39
+ class Error < BaseComponent
40
+ # div(契約静的クラスは自動付与)。upstream と同じく「本文が無ければ描かない」。
41
+ # なお upstream の errors: 配列→ul リスト化は本クラスでは提供しない
42
+ # (upstream の ul は data-slot を持たず field 契約に取込めないため。
43
+ # 文字列配列を受けたい場合は Shadcn::Form::Error を使う)
44
+ sig { override.returns(String) }
45
+ def call
46
+ body = content.to_s
47
+ return "" if body.empty?
48
+
49
+ content_tag(tag, **html_attributes) { body }
50
+ end
51
+ end
52
+
53
+ class Group < BaseComponent
54
+ # div
55
+ end
56
+
57
+ class Label < BaseComponent
58
+ sig { override.returns(String) }
59
+ def default_tag
60
+ "label"
61
+ end
62
+
63
+ # upstream の FieldLabel は <Label> プリミティブを描く: ラベル契約のクラスを
64
+ # 土台に field 側のクラスを重ねる(cn(labelVariants(), fieldLabelClasses) と同じ順)。
65
+ # 描画は label 契約のクラス群を含むため、適合試験は allowances の
66
+ # class_mode: contains で緩和する
67
+ sig { override.returns(T::Hash[Symbol, T.untyped]) }
68
+ def html_attributes
69
+ attributes = super
70
+ attributes[:class] = ShadcnViewComponents::Classes.resolve(:label, extra: attributes[:class].to_s)
71
+ attributes
72
+ end
73
+ end
74
+
75
+ class Legend < BaseComponent
76
+ sig { override.returns(String) }
77
+ def default_tag
78
+ "legend"
79
+ end
80
+ end
81
+
82
+ class Separator < BaseComponent
83
+ # 契約スロット構成: field-separator > field-separator-content(span)
84
+ sig { override.returns(String) }
85
+ def call
86
+ content_tag(tag, **html_attributes) do
87
+ content_tag(:span,
88
+ class: T.cast(contract_slot("field-separator-content").dig(:static_attributes, :class), T.nilable(String)),
89
+ data: { slot: "field-separator-content" }) { content }
90
+ end
91
+ end
92
+ end
93
+
94
+ class Set < BaseComponent
95
+ # fieldset
96
+ sig { override.returns(String) }
97
+ def default_tag
98
+ "fieldset"
99
+ end
100
+ end
101
+
102
+ class Title < BaseComponent
103
+ # div(契約の data-slot は field-label を共有する)
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,132 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Shadcn
5
+ # 浮動要素の希望配置を、各コンポーネントで共通の data 属性へ変換する。
6
+ # 実際の配置結果に使う data-side / data-align とは属性を分け、衝突回避後の
7
+ # 値が次回表示時の希望配置へ混入しないようにする。
8
+ module FloatingPositionOptions
9
+ extend T::Sig
10
+
11
+ SIDES = %w[top right bottom left inline-start inline-end].freeze
12
+ ALIGNS = %w[start center end].freeze
13
+ INTEGER_PATTERN = /\A[+-]?\d+\z/
14
+ NUMBER_PATTERN = /\A[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?\z/i
15
+
16
+ class Values < T::Struct
17
+ const :side, String
18
+ const :align, String
19
+ const :side_offset, T.any(Integer, Float)
20
+ const :align_offset, T.any(Integer, Float)
21
+ const :collision_padding, T.any(Integer, Float)
22
+ end
23
+
24
+ sig { params(args: T::Hash[Symbol, T.untyped]).void.checked(:never) }
25
+ def initialize(**args)
26
+ defaults = T.cast(T.unsafe(self).class.const_get(:FLOATING_POSITION_DEFAULTS), T::Hash[Symbol, T.untyped])
27
+ @floating_position = T.let(normalize_position_options(args, defaults), Values)
28
+ super
29
+ end
30
+
31
+ private
32
+
33
+ sig { params(attributes: T::Hash[Symbol, T.untyped], defaults: T::Hash[Symbol, T.untyped]).returns(T::Hash[Symbol, T.untyped]) }
34
+ def merge_floating_position_data(attributes, defaults = {})
35
+ user_data = T.cast(attributes[:data], T.nilable(T::Hash[Symbol, T.untyped])) || {}
36
+ attributes[:data] = defaults.merge(floating_position_data).merge(user_data)
37
+ attributes
38
+ end
39
+
40
+ sig { returns(T::Hash[Symbol, T.untyped]) }
41
+ def floating_position_data
42
+ {
43
+ position_side: @floating_position.side,
44
+ position_align: @floating_position.align,
45
+ position_side_offset: @floating_position.side_offset,
46
+ position_align_offset: @floating_position.align_offset,
47
+ position_collision_padding: @floating_position.collision_padding
48
+ }
49
+ end
50
+
51
+ sig do
52
+ params(
53
+ args: T::Hash[Symbol, T.untyped],
54
+ defaults: T::Hash[Symbol, T.untyped]
55
+ ).returns(Values)
56
+ end
57
+ def normalize_position_options(args, defaults)
58
+ Values.new(
59
+ side: normalize_position_choice(:side, extract_position_option(args, defaults, :side), SIDES),
60
+ align: normalize_position_choice(:align, extract_position_option(args, defaults, :align), ALIGNS),
61
+ side_offset: normalize_position_number(:side_offset, extract_position_option(args, defaults, :side_offset)),
62
+ align_offset: normalize_position_number(:align_offset, extract_position_option(args, defaults, :align_offset)),
63
+ collision_padding: normalize_position_number(
64
+ :collision_padding,
65
+ extract_position_option(args, defaults, :collision_padding),
66
+ non_negative: true
67
+ )
68
+ )
69
+ end
70
+
71
+ sig do
72
+ params(
73
+ args: T::Hash[Symbol, T.untyped],
74
+ defaults: T::Hash[Symbol, T.untyped],
75
+ name: Symbol
76
+ ).returns(T.untyped)
77
+ end
78
+ def extract_position_option(args, defaults, name)
79
+ return args.delete(name) if args.key?(name)
80
+
81
+ defaults.fetch(name)
82
+ end
83
+
84
+ sig do
85
+ params(
86
+ name: Symbol,
87
+ value: T.untyped,
88
+ allowed: T::Array[String]
89
+ ).returns(String)
90
+ end
91
+ def normalize_position_choice(name, value, allowed)
92
+ normalized = value.to_s
93
+ return normalized if (value.is_a?(String) || value.is_a?(Symbol)) && allowed.include?(normalized)
94
+
95
+ Kernel.raise ArgumentError,
96
+ "unknown floating position #{name} #{value.inspect} (valid: #{allowed.join(', ')})"
97
+ end
98
+
99
+ sig do
100
+ params(
101
+ name: Symbol,
102
+ value: T.untyped,
103
+ non_negative: T::Boolean
104
+ ).returns(T.any(Integer, Float))
105
+ end
106
+ def normalize_position_number(name, value, non_negative: false)
107
+ number = parse_position_number(value)
108
+ invalid_position_number!(name, value, non_negative) unless number
109
+ invalid_position_number!(name, value, non_negative) unless number.to_f.finite?
110
+ invalid_position_number!(name, value, non_negative) if non_negative && number.negative?
111
+
112
+ number
113
+ end
114
+
115
+ sig { params(value: T.untyped).returns(T.nilable(T.any(Integer, Float))) }
116
+ def parse_position_number(value)
117
+ return value if value.is_a?(Integer) || value.is_a?(Float)
118
+ return unless value.is_a?(String)
119
+
120
+ normalized = value.strip
121
+ return normalized.to_i if INTEGER_PATTERN.match?(normalized)
122
+
123
+ normalized.to_f if NUMBER_PATTERN.match?(normalized)
124
+ end
125
+
126
+ sig { params(name: Symbol, value: T.untyped, non_negative: T::Boolean).returns(T.noreturn) }
127
+ def invalid_position_number!(name, value, non_negative)
128
+ requirement = non_negative ? "a finite non-negative number" : "a finite number"
129
+ Kernel.raise ArgumentError, "floating position #{name} must be #{requirement}, got #{value.inspect}"
130
+ end
131
+ end
132
+ end
@@ -0,0 +1,105 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Shadcn
5
+ # Railsフォーム統合層。upstream(base-nova)は form アイテムを廃止し、公式 docs/forms
6
+ # ガイドは Field プリミティブ + data-invalid / aria-invalid の手動ワイヤリングへ移行した
7
+ # (Issue #3)。本gemもそれに倣い、Field の上に最小2部品で構成する:
8
+ #
9
+ # <%= form_with model: user do |f| %>
10
+ # <%= render Shadcn::Form::Item.new(invalid: user.errors[:email].any?) do %>
11
+ # <%= render Shadcn::Field::Label.new(for: "user-email") { "メールアドレス" } %>
12
+ # <%= render Shadcn::Input.new(id: "user-email",
13
+ # aria: { invalid: user.errors[:email].any? }) %>
14
+ # <%= render Shadcn::Field::Description.new { "ログインに使うアドレスです" } %>
15
+ # <%= render Shadcn::Form::Error.new(errors: user.errors.full_messages_for(:email)) %>
16
+ # <% end %>
17
+ # <% end %>
18
+ #
19
+ # ラベル・説明には Shadcn::Field::Label / Shadcn::Field::Description を直接使う。
20
+ # Form::Item は Field と同じ DOM 構造(data-slot="field")を描くため、Field 部品を
21
+ # そのまま内側に置ける。JS無効時フォールバック: Graceful(表示は全てサーバ側で完結する)
22
+ # aria-invalid / aria-describedby の紐付けは利用者(またはフォームビルダ拡張)が
23
+ # 持つ — upstream の useFormField 相当の暗黙接続はRailsの流儀に無い
24
+ # Form自体は名前空間であり、描画にはItemまたはErrorを使う。
25
+ module Form
26
+ # upstream docs/forms の <Field data-invalid={...}> 相当。DOM構造・クラスは
27
+ # Field 契約と同一(契約側で fieldVariants を事前解決済み)。
28
+ # data-slot を "field" に保つのは Field 部品の group/field セレクタ互換のため
29
+ class Item < BaseComponent
30
+ sig do
31
+ params(invalid: T::Boolean,
32
+ orientation: T.any(Symbol, String),
33
+ args: T::Hash[Symbol, T.untyped]).void.checked(:never)
34
+ end
35
+ def initialize(invalid: false, orientation: ShadcnViewComponents::Contracts::Form::Item::DEFAULTS.fetch(:orientation), **args)
36
+ @invalid = invalid
37
+ @orientation = T.let(normalize_option(:orientation, orientation), Symbol)
38
+ super(**args)
39
+ end
40
+
41
+ sig { override.returns(T::Hash[Symbol, VariantOption]) }
42
+ def variant_options
43
+ { orientation: @orientation }
44
+ end
45
+
46
+ sig { override.returns(T::Hash[Symbol, T.untyped]) }
47
+ def html_attributes
48
+ attributes = super
49
+ # upstream は boolean prop を常に "true"/"false" として出力する
50
+ merge_nested(attributes, :data, { invalid: @invalid.to_s, orientation: @orientation.to_s })
51
+ attributes
52
+ end
53
+ end
54
+
55
+ # upstream の FieldError セマンティクス(upstream準拠、入力はRails流の文字列配列):
56
+ # 本文 > errors(重複除去) の優先で、1件は平文・複数は ul リスト・無ければ非描画。
57
+ # Rails では model.errors.full_messages_for(:attr) をそのまま渡せる
58
+ class Error < BaseComponent
59
+ sig do
60
+ params(message: T.nilable(String),
61
+ errors: T.nilable(T::Array[T.nilable(String)]),
62
+ args: T::Hash[Symbol, T.untyped]).void.checked(:never)
63
+ end
64
+ def initialize(message: nil, errors: nil, **args)
65
+ @message = message
66
+ @errors = T.let(errors, T.nilable(T::Array[T.nilable(String)]))
67
+ super(**args)
68
+ end
69
+
70
+ sig { override.returns(String) }
71
+ def call
72
+ body = content.to_s
73
+ messages = unique_messages
74
+ return "" if body.empty? && messages.empty?
75
+
76
+ content_tag(tag, **html_attributes) do
77
+ if body.present?
78
+ body
79
+ elsif messages.length == 1
80
+ messages.first.to_s
81
+ else
82
+ error_list(messages)
83
+ end
84
+ end
85
+ end
86
+
87
+ private
88
+
89
+ sig { returns(T::Array[String]) }
90
+ def unique_messages
91
+ ([@message].compact + (@errors || []).compact).uniq
92
+ end
93
+
94
+ # 複数件は ul リスト。リストのクラスは契約に記録済み
95
+ # (upstream の ul は data-slot を持たないため、名前無しスロットとして契約化した)
96
+ sig { params(messages: T::Array[String]).returns(String) }
97
+ def error_list(messages)
98
+ list_class = T.cast(contract_slot("").dig(:static_attributes, :class), T.nilable(String))
99
+ content_tag(:ul, class: list_class, data: { slot: "form-error-list" }) do
100
+ safe_join(messages.map { |message| content_tag(:li) { message.to_s } })
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,66 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Shadcn
5
+ # hover intent 付きのホバーカード(05 §3「hover-card = Popover API + hover intent」)。
6
+ # JS無効時フォールバック: Readable(内容はSSR済みで読める。展開はJS依存)
7
+ class HoverCard < BaseComponent
8
+ CONTROLLER = "shadcn--hover-card"
9
+
10
+ sig { override.returns(T::Hash[Symbol, T.untyped]) }
11
+ def contract_data_attributes
12
+ super.merge(controller: CONTROLLER)
13
+ end
14
+
15
+ class Trigger < BaseComponent
16
+ # 契約タグは HoverCardPrimitive.Trigger(HTML名ではない)。upstream の慣行は
17
+ # <a>(href を渡した場合)なので、href の有無でリンクかボタンかに切り替える
18
+ sig { override.returns(String) }
19
+ def default_tag
20
+ @html_args.key?(:href) ? "a" : "button"
21
+ end
22
+
23
+ sig { override.returns(T::Hash[Symbol, T.untyped]) }
24
+ def html_attributes
25
+ attributes = super
26
+ merge_nested(attributes, :data, {
27
+ action: "mouseenter->#{CONTROLLER}#show mouseleave->#{CONTROLLER}#hide focus->#{CONTROLLER}#show blur->#{CONTROLLER}#hide"
28
+ })
29
+ attributes
30
+ end
31
+ end
32
+
33
+ class Content < BaseComponent
34
+ include Shadcn::FloatingPositionOptions
35
+
36
+ FLOATING_POSITION_DEFAULTS = T.let(
37
+ { side: :bottom, align: :center, side_offset: 4, align_offset: 4, collision_padding: 5 }.freeze,
38
+ T::Hash[Symbol, T.untyped]
39
+ )
40
+
41
+ # 契約スロット構成: hover-card-portal(ラッパー) > hover-card-content。
42
+ # 表示切替は hidden 属性 + data-state で行う
43
+ sig { override.returns(String) }
44
+ def call
45
+ content_tag(:div, data: { slot: "hover-card-portal" }) do
46
+ content_tag(:div, **content_attributes) { content }
47
+ end
48
+ end
49
+
50
+ private
51
+
52
+ sig { returns(T::Hash[Symbol, T.untyped]) }
53
+ def content_attributes
54
+ attributes = @html_args.merge(class: self.class.classes(extra: @user_class))
55
+ merge_floating_position_data(
56
+ attributes,
57
+ slot: "hover-card-content",
58
+ state: "closed",
59
+ action: "mouseenter->#{CONTROLLER}#show mouseleave->#{CONTROLLER}#hide"
60
+ )
61
+ attributes[:hidden] = true
62
+ attributes
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,13 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Shadcn
5
+ class Input < BaseComponent
6
+ # input タグ・クラス・data-slot は契約由来。type 等の属性はパススルー。
7
+ # base-nova の契約タグは InputPrimitive のため HTML input を明示する
8
+ sig { override.returns(String) }
9
+ def default_tag
10
+ "input"
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,48 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Shadcn
5
+ class InputGroup < BaseComponent
6
+ # upstream は Button を asChild で重ねる。本gemでは Button 描画結果に
7
+ # data-slot を差し替える(Phase 3 の AlertDialogAction と同じ構成)。
8
+ # NOTE: Shadcn::Button の sidecar テンプレートと名前が衝突するため専用ファイルに置く
9
+ class Button < BaseComponent
10
+ sig do
11
+ params(
12
+ variant: T.any(Symbol, String),
13
+ size: T.any(Symbol, String),
14
+ args: T::Hash[Symbol, T.untyped]
15
+ ).void.checked(:never)
16
+ end
17
+ def initialize(variant: :ghost, size: "xs", **args)
18
+ @variant = T.let(
19
+ ShadcnViewComponents::Classes.normalize_option(:button, :variant, variant),
20
+ Symbol
21
+ )
22
+ @size = T.let(normalize_option(:size, size), Symbol)
23
+ super(**args)
24
+ end
25
+
26
+ sig { override.returns(T::Hash[Symbol, VariantOption]) }
27
+ def variant_options
28
+ { size: @size }
29
+ end
30
+
31
+ # 契約は Button(ba_buttonVariants) との合成結果を事前解決済みのため、
32
+ # ここではその最終クラスをそのまま描く(upstream の DOM と同一)
33
+ sig { override.returns(String) }
34
+ def default_tag
35
+ "button"
36
+ end
37
+
38
+ sig { override.returns(T::Hash[Symbol, T.untyped]) }
39
+ def html_attributes
40
+ attributes = super
41
+ attributes[:type] = "button" unless attributes.key?(:type)
42
+ data = T.cast(attributes[:data], T.nilable(T::Hash[Symbol, T.untyped])) || {}
43
+ attributes[:data] = { size: @size }.merge(data)
44
+ attributes
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,51 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Shadcn
5
+ # 入力要素の前後にボタンやテキストを並べるコンテナ(Phase 4)
6
+ class InputGroup < BaseComponent
7
+ # div(契約静的クラスは自動付与)
8
+
9
+ class Addon < BaseComponent
10
+ sig { params(align: T.any(Symbol, String), args: T::Hash[Symbol, T.untyped]).void.checked(:never) }
11
+ def initialize(align: ShadcnViewComponents::Contracts::InputGroup::Addon::DEFAULTS.fetch(:align), **args)
12
+ @align = T.let(normalize_option(:align, align), Symbol)
13
+ super(**args)
14
+ end
15
+
16
+ sig { override.returns(T::Hash[Symbol, VariantOption]) }
17
+ def variant_options
18
+ { align: @align }
19
+ end
20
+
21
+ sig { override.returns(T::Hash[Symbol, T.untyped]) }
22
+ def contract_data_attributes
23
+ super.merge(align: @align)
24
+ end
25
+ end
26
+
27
+ class Input < BaseComponent
28
+ # upstream は Input(別アイテム)に上書きクラスを足す
29
+ sig { override.returns(String) }
30
+ def call
31
+ attributes = @html_args.merge(class: self.class.classes(extra: @user_class))
32
+ merge_nested(attributes, :data, { slot: "input-group-control" })
33
+ render(::Shadcn::Input.new(**attributes)) { content }
34
+ end
35
+ end
36
+
37
+ class Textarea < BaseComponent
38
+ # upstream は Textarea(別アイテム)に上書きクラスを足す
39
+ sig { override.returns(String) }
40
+ def call
41
+ attributes = @html_args.merge(class: self.class.classes(extra: @user_class))
42
+ merge_nested(attributes, :data, { slot: "input-group-control" })
43
+ render(::Shadcn::Textarea.new(**attributes)) { content }
44
+ end
45
+ end
46
+
47
+ class Text < BaseComponent
48
+ # span(契約クラスのみ。data-slot は契約上ルートに無い)
49
+ end
50
+ end
51
+ end