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,239 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "securerandom"
5
+
6
+ module Shadcn
7
+ # 共通基底(04-component-conventions §5-6)。属性マージ・クラス解決・tag差し替えを担う。
8
+ # 利用者が直接参照しない private 扱いのクラス。
9
+ class BaseComponent < ViewComponent::Base
10
+ extend T::Sig
11
+
12
+ VariantOption = T.type_alias { T.nilable(T.any(Symbol, String)) }
13
+
14
+ # HTMLのvoid要素。閉じタグを付けてはならない
15
+ VOID_ELEMENTS = %w[area base br col embed hr img input link meta source track wbr].freeze
16
+
17
+ # ViewComponent::Base は initialize を持たず、ActionView::Base の initialize は
18
+ # 互換性のない引数を取るため super を呼ばない(ViewComponentの標準的な慣行)。
19
+ # この件は .rubocop.yml の Lint/MissingSuper Exclude にも反映している。
20
+ # なお rest-kwargs を持つメソッドの実行時sig検証は sorbet-runtime の既知の誤バインドがあるため
21
+ # checked(:never) で無効化し、srb tc の静的検査に委ねる(module_function 経由の
22
+ # ShadcnViewComponents::Classes は実行時検証が正しく働くので検証はそこで行う)
23
+ sig { params(args: T::Hash[Symbol, T.untyped]).void.checked(:never) }
24
+ def initialize(**args)
25
+ raw_tag = args.delete(:tag)
26
+ raw_class = args.delete(:class)
27
+ @tag = T.let(raw_tag&.to_s, T.nilable(String))
28
+ @user_class = T.let(raw_class&.to_s, T.nilable(String))
29
+ @html_args = T.let(args, T::Hash[Symbol, T.untyped])
30
+ end
31
+
32
+ class << self
33
+ extend T::Sig
34
+
35
+ # 契約モジュールへのパス(例: Shadcn::Button → "button"、Shadcn::Card::Header → "card/header")
36
+ sig { returns(String) }
37
+ def contract_path
38
+ to_s.delete_prefix("Shadcn::").underscore
39
+ end
40
+
41
+ sig { returns(T::Module[T.anything]) }
42
+ def contract
43
+ ShadcnViewComponents::Classes.contract_for(contract_path.to_sym)
44
+ end
45
+
46
+ # クラス文字列だけを提供するモジュール関数(04 §8)。
47
+ # ホストが自前ERBに段階的に導入するための抜け道。
48
+ # checked(:never) の理由は initialize のコメント参照(rest-kwargsの実行時検証問題)
49
+ sig { params(extra: T.nilable(String), options: T::Hash[Symbol, VariantOption]).returns(String).checked(:never) }
50
+ def classes(extra: nil, **options)
51
+ ShadcnViewComponents::Classes.resolve(contract_path.to_sym, extra: extra, **options)
52
+ end
53
+
54
+ # CVAバリアント以外の意味的な公開prop契約を返す。
55
+ sig { params(prop: Symbol).returns(T::Hash[Symbol, T.untyped]) }
56
+ def property_contract(prop)
57
+ ShadcnViewComponents::PropertyContracts.fetch(contract_path.to_sym, prop)
58
+ end
59
+
60
+ sig { params(prop: Symbol).returns(T.untyped) }
61
+ def property_default(prop)
62
+ ShadcnViewComponents::PropertyContracts.default(contract_path.to_sym, prop)
63
+ end
64
+ end
65
+
66
+ # 構造を持たない単一要素コンポーネントの既定レンダリング。
67
+ # ERBテンプレートを置かない限りこれが使われる(クラス文字列を持たない — 04 §5)。
68
+ # 構造を持つコンポーネント(Buttonのdata属性、Tableのラッパー等)はcallを上書きするか
69
+ # sidecarテンプレートを置く
70
+ sig { returns(String) }
71
+ def call
72
+ return open_tag if void_element?
73
+
74
+ content_tag(tag, **html_attributes) { content }
75
+ end
76
+
77
+ sig { returns(T::Boolean) }
78
+ def void_element?
79
+ VOID_ELEMENTS.include?(tag)
80
+ end
81
+
82
+ private
83
+
84
+ # 複合コンポーネントのARIA参照で使うルートIDを、ViewComponentの
85
+ # インスタンス生成時に一度だけ決める。利用者指定IDはそのまま優先する。
86
+ # 自動生成IDの印は、fragment cache済みの同じHTMLが同一documentへ複数回
87
+ # 挿入された場合にだけStimulus側で安全に再採番するために使う。
88
+ sig { params(args: T::Hash[Symbol, T.untyped], prefix: String).void }
89
+ def assign_accessibility_root_id(args, prefix:)
90
+ supplied_id = args[:id]
91
+ return unless supplied_id.nil? || supplied_id.to_s.empty?
92
+
93
+ args[:id] = "shadcn-#{prefix}-#{SecureRandom.hex(12)}"
94
+ data = T.cast(args[:data], T.nilable(T::Hash[Symbol, T.untyped])) || {}
95
+ args[:data] = data.merge(shadcn_generated_root_id: "true")
96
+ end
97
+
98
+ # void要素は閉じタグ無しで出力する。
99
+ # NOTE: content_tag はブロック付き呼び出しでのみ第二引数ハッシュが属性扱いになるため
100
+ # 空ブロックを渡してから閉じタグを取り除く
101
+ sig { returns(String) }
102
+ def open_tag
103
+ void_tag(tag, **html_attributes)
104
+ end
105
+
106
+ # 指定タグの void 要素(input/br/hr 等)を閉じタグ無しで出力する統一ヘルパ。
107
+ # 手書きの input 描画で使う(閉じタグ付き <input></input> はHTML仕様違反のため)
108
+ # attrs を T.untyped にするのは srb の rest-kwargs モデル対策(呼び出し側の
109
+ # 任意キーワードを attrs に誤バインドする既知の挙動のため)
110
+ sig { params(name: String, attrs: T.untyped).returns(String) }
111
+ def void_tag(name, **attrs)
112
+ content_tag(name, **attrs) { "".html_safe }
113
+ .then { |markup| markup.sub(%r{></#{Regexp.escape(name)}>\z}, ">") }
114
+ .then(&:html_safe)
115
+ end
116
+
117
+ # asChild代替(04 §3.3): 同じクラス・スロット構造で要素名だけ変える。
118
+ # 契約タグがPrimitives由来(AvatarPrimitive.Root 等)でHTML要素名ではない場合は
119
+ # この既定タグへフォールバックする(各コンポーネントが上書きしてよい)
120
+ sig { returns(String) }
121
+ def tag
122
+ return @tag if @tag
123
+
124
+ contract_tag = T.cast(contract_root_slot[:tag], T.nilable(String))
125
+ return contract_tag if contract_tag&.match?(/\A[a-z][a-z0-9-]*\z/)
126
+
127
+ default_tag
128
+ end
129
+
130
+ # 契約タグがHTML要素名ではないときの描画タグ
131
+ sig { returns(String) }
132
+ def default_tag
133
+ "div"
134
+ end
135
+
136
+ # バリアント値の正規化 + fail-fast検証。契約に存在しない値は ArgumentError
137
+ sig { params(prop: Symbol, value: T.untyped).returns(Symbol) }
138
+ def normalize_option(prop, value)
139
+ ShadcnViewComponents::Classes.normalize_option(self.class.contract_path.to_sym, prop, value)
140
+ end
141
+
142
+ # data属性・ARIA・CSS値へ出す意味的なpropを、宣言済み契約で正規化する。
143
+ sig { params(prop: Symbol, value: T.untyped).returns(T.untyped) }
144
+ def normalize_property(prop, value)
145
+ ShadcnViewComponents::PropertyContracts.normalize(
146
+ component: self.class.contract_path.to_sym,
147
+ owner: self.class.to_s,
148
+ property: prop,
149
+ value:
150
+ )
151
+ end
152
+
153
+ # コンポーネント固有のバリアント組み合わせ(サブクラスが上書きする)
154
+ sig { returns(T::Hash[Symbol, VariantOption]) }
155
+ def variant_options
156
+ {}
157
+ end
158
+
159
+ # 契約由来の data 属性。ルートの data-slot は必ず出力する(04 §4.2)。
160
+ # ルートがラッパー等で data-slot を持たない契約では、契約クラスが属する要素の
161
+ # スロット(CLASSES_SLOT)に出す。どちらも無い契約(spinner等)では出さない
162
+ sig { returns(T::Hash[Symbol, T.untyped]) }
163
+ def contract_data_attributes
164
+ root_slot = T.cast(contract_root_slot[:name], T.nilable(String)).to_s
165
+ root_slot = T.cast(contract.const_get(:CLASSES_SLOT), T.nilable(String)).to_s if root_slot.empty? && contract.const_defined?(:CLASSES_SLOT)
166
+ root_slot.empty? ? {} : { slot: root_slot }
167
+ end
168
+
169
+ sig { returns(T::Hash[Symbol, T.untyped]) }
170
+ def html_attributes
171
+ attributes = static_attribute_defaults.merge(@html_args)
172
+ attributes[:class] = resolved_class
173
+ merge_nested(attributes, :data, contract_data_attributes)
174
+ merge_nested(attributes, :aria, {})
175
+ attributes
176
+ end
177
+
178
+ sig { returns(T::Module[T.anything]) }
179
+ def contract
180
+ self.class.contract
181
+ end
182
+
183
+ sig { returns(T::Hash[Symbol, T.untyped]) }
184
+ def contract_root_slot
185
+ root_slot_name = T.cast(contract.const_get(:ROOT_SLOT), String)
186
+ contract_slot(root_slot_name)
187
+ end
188
+
189
+ # 契約のスロット定義を data-slot 名で引く
190
+ sig { params(slot_name: String).returns(T::Hash[Symbol, T.untyped]) }
191
+ def contract_slot(slot_name)
192
+ slots = T.cast(contract.const_get(:SLOTS), T::Array[T::Hash[Symbol, T.untyped]])
193
+ slots.find { |slot| slot[:name] == slot_name } || {}
194
+ end
195
+
196
+ # 契約の static_attributes を既定値として採用(利用者指定が優先 — 04 §6)。
197
+ # cn を経ない静的 className(table のラッパー等)は class キーとして現れるため
198
+ # resolved_class で統合する
199
+ sig { returns(T::Hash[Symbol, T.untyped]) }
200
+ def static_attribute_defaults
201
+ T.cast(contract_root_slot[:static_attributes], T::Hash[Symbol, T.untyped])
202
+ .transform_keys(&:to_sym)
203
+ end
204
+
205
+ sig { returns(String) }
206
+ def resolved_class
207
+ static_class = T.cast(static_attribute_defaults[:class], T.nilable(String))
208
+ extra = [static_class, @user_class].compact.join(" ")
209
+ extra = nil if extra.empty?
210
+ self.class.classes(extra: extra, **variant_options)
211
+ end
212
+
213
+ # style は利用者が文字列・ハッシュどちらで渡しても壊れないよう結合する。
214
+ # 出力は常に文字列とする(ハッシュのまま渡すとレンダラの整形に依存するため)。
215
+ # gem 側の宣言を先頭に付け、CSSの後勝ちに従い利用者が上書きできる。
216
+ # key は Rails の style ハッシュと同じ規約でアンダースコアをハイフンへ変換する
217
+ # (aspect_ratio → aspect-ratio)
218
+ sig { params(attributes: T::Hash[Symbol, T.untyped], defaults: T::Hash[Symbol, T.untyped]).void }
219
+ def merge_style(attributes, defaults)
220
+ user = attributes[:style]
221
+ base = defaults.map { |key, value| "#{key.to_s.dasherize}: #{value}" }.join("; ")
222
+ user_part = if user.is_a?(Hash)
223
+ user.map { |key, value| "#{key.to_s.dasherize}: #{value}" }.join("; ")
224
+ else
225
+ user.to_s
226
+ end
227
+ attributes[:style] = [base, user_part].reject(&:empty?).join("; ")
228
+ end
229
+
230
+ # data: / aria: は深部マージ(契約由来の値と利用者指定が共存する)
231
+ sig { params(attributes: T::Hash[Symbol, T.untyped], key: Symbol, defaults: T::Hash[Symbol, T.untyped]).void }
232
+ def merge_nested(attributes, key, defaults)
233
+ user = T.cast(attributes[key], T.nilable(T::Hash[Symbol, T.untyped]))
234
+ attributes[key] = defaults.merge(user || {})
235
+ end
236
+ end
237
+
238
+ private_constant :BaseComponent
239
+ end
@@ -0,0 +1,70 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Shadcn
5
+ class Breadcrumb < BaseComponent
6
+ # ナビゲーション全体。JS不要・リンクベース(05-stimulus-hotwire §6.4)
7
+ class List < BaseComponent; end
8
+
9
+ class Item < BaseComponent; end
10
+
11
+ class Link < BaseComponent; end
12
+
13
+ class Page < BaseComponent; end
14
+
15
+ # 区切り記号。子コンテンツが無ければ chevron アイコン(lucideと同等のパス)を描画する
16
+ class Separator < BaseComponent
17
+ sig { override.returns(String) }
18
+ def call
19
+ content_tag(tag, **html_attributes) { content.presence || chevron_icon }
20
+ end
21
+
22
+ private
23
+
24
+ sig { returns(String) }
25
+ def chevron_icon
26
+ content_tag(
27
+ :svg,
28
+ xmlns: "http://www.w3.org/2000/svg",
29
+ viewBox: "0 0 24 24",
30
+ fill: "none",
31
+ stroke: "currentColor",
32
+ "stroke-width": "2",
33
+ "stroke-linecap": "round",
34
+ "stroke-linejoin": "round",
35
+ width: "14",
36
+ height: "14"
37
+ ) do
38
+ raw(%(<path d="m9 18 6-6-6-6"/>))
39
+ end
40
+ end
41
+ end
42
+
43
+ # 省略記号(…)。lucide と同等のパス + スクリーンリーダー向けラベル
44
+ class Ellipsis < BaseComponent
45
+ sig { override.returns(String) }
46
+ def call
47
+ content_tag(tag, **html_attributes) do
48
+ safe_join([
49
+ content_tag(
50
+ :svg,
51
+ xmlns: "http://www.w3.org/2000/svg",
52
+ viewBox: "0 0 24 24",
53
+ fill: "none",
54
+ stroke: "currentColor",
55
+ "stroke-width": "2",
56
+ "stroke-linecap": "round",
57
+ "stroke-linejoin": "round",
58
+ width: "16",
59
+ height: "16",
60
+ class: "size-4"
61
+ ) do
62
+ raw(%(<circle cx="12" cy="12" r="1"/><circle cx="19" cy="12" r="1"/><circle cx="5" cy="12" r="1"/>))
63
+ end,
64
+ content_tag(:span, "More", class: "sr-only")
65
+ ])
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,48 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Shadcn
5
+ # チャットの吹き出し(Phase 4)
6
+ class Bubble < BaseComponent
7
+ sig { params(variant: T.any(Symbol, String), args: T::Hash[Symbol, T.untyped]).void.checked(:never) }
8
+ def initialize(variant: ShadcnViewComponents::Contracts::Bubble::DEFAULTS.fetch(:variant), **args)
9
+ @variant = T.let(normalize_option(:variant, variant), Symbol)
10
+ super(**args)
11
+ end
12
+
13
+ sig { override.returns(T::Hash[Symbol, VariantOption]) }
14
+ def variant_options
15
+ { variant: @variant }
16
+ end
17
+
18
+ class Group < BaseComponent
19
+ # div
20
+ end
21
+
22
+ class Content < BaseComponent
23
+ # div
24
+ end
25
+
26
+ class Reactions < BaseComponent
27
+ # リアクション一覧(align/side)
28
+ sig do
29
+ params(
30
+ align: T.any(Symbol, String),
31
+ side: T.any(Symbol, String),
32
+ args: T::Hash[Symbol, T.untyped]
33
+ ).void.checked(:never)
34
+ end
35
+ def initialize(align: ShadcnViewComponents::Contracts::Bubble::Reactions::DEFAULTS.fetch(:align),
36
+ side: ShadcnViewComponents::Contracts::Bubble::Reactions::DEFAULTS.fetch(:side), **args)
37
+ @align = T.let(normalize_option(:align, align), Symbol)
38
+ @side = T.let(normalize_option(:side, side), Symbol)
39
+ super(**args)
40
+ end
41
+
42
+ sig { override.returns(T::Hash[Symbol, VariantOption]) }
43
+ def variant_options
44
+ { align: @align, side: @side }
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1 @@
1
+ <%= content_tag(tag, **html_attributes) { content } %>
@@ -0,0 +1,41 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Shadcn
5
+ class Button < BaseComponent
6
+ # 省略時の既定値は契約の defaults から読む(リテラルで二重管理しない)。
7
+ # checked(:never): rest-kwargs の実行時sig検証は sorbet-runtime の誤バインドがあるため
8
+ # 無効化し、srb tc の静的検査に委ねる(BaseComponent#initialize のコメント参照)
9
+ sig { override.returns(String) }
10
+ def default_tag
11
+ "button"
12
+ end
13
+
14
+ sig do
15
+ params(
16
+ variant: T.any(Symbol, String),
17
+ size: T.any(Symbol, String),
18
+ args: T::Hash[Symbol, T.untyped]
19
+ ).void.checked(:never)
20
+ end
21
+ def initialize(variant: ShadcnViewComponents::Contracts::Button::DEFAULTS.fetch(:variant),
22
+ size: ShadcnViewComponents::Contracts::Button::DEFAULTS.fetch(:size), **args)
23
+ @variant = T.let(normalize_option(:variant, variant), Symbol)
24
+ @size = T.let(normalize_option(:size, size), Symbol)
25
+ super(**args)
26
+ end
27
+
28
+ sig { returns(Symbol) }
29
+ attr_reader :variant
30
+
31
+ sig { returns(Symbol) }
32
+ attr_reader :size
33
+
34
+ sig { override.returns(T::Hash[Symbol, VariantOption]) }
35
+ def variant_options
36
+ { variant: @variant, size: @size }
37
+ end
38
+ # base-nova の button は data-variant / data-size をDOMへ出さないため、
39
+ # 動的属性の上書きは行わない(契約どおり)
40
+ end
41
+ end
@@ -0,0 +1,41 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Shadcn
5
+ # ボタンを一括りにするコンテナ(Phase 4)
6
+ class ButtonGroup < BaseComponent
7
+ sig { params(orientation: T.any(Symbol, String), args: T::Hash[Symbol, T.untyped]).void.checked(:never) }
8
+ def initialize(orientation: ShadcnViewComponents::Contracts::ButtonGroup::DEFAULTS.fetch(:orientation), **args)
9
+ @orientation = T.let(normalize_option(:orientation, orientation), Symbol)
10
+ super(**args)
11
+ end
12
+
13
+ sig { override.returns(T::Hash[Symbol, VariantOption]) }
14
+ def variant_options
15
+ { orientation: @orientation }
16
+ end
17
+
18
+ class Separator < BaseComponent
19
+ # div(契約クラスは upstream の separator 上書き分を事前解決したもの)
20
+ sig { params(orientation: T.any(Symbol, String), args: T::Hash[Symbol, T.untyped]).void.checked(:never) }
21
+ def initialize(orientation: :vertical, **args)
22
+ value = orientation.to_s
23
+ raise ArgumentError, "unknown orientation value #{orientation.inspect} (valid: :horizontal, :vertical)" unless %w[horizontal vertical].include?(value)
24
+
25
+ @orientation = T.let(value, String)
26
+ super(**args)
27
+ end
28
+
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: "button-group-separator" })
33
+ render(Shadcn::Separator.new(orientation: @orientation, **attributes))
34
+ end
35
+ end
36
+
37
+ class Text < BaseComponent
38
+ # div
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,51 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Shadcn
5
+ # オーバーレイ系Triggerにボタン装飾を付与する共用モジュール。
6
+ #
7
+ # upstream では <DialogTrigger asChild><Button variant="outline">…</Button></DialogTrigger>
8
+ # のように asChild + Button で装飾するが、本gemのTriggerは常設の <button> 要素のため、
9
+ # variant:/size: キーワードでボタンの契約クラスを合成して同等の見た目を提供する。
10
+ # 指定しない場合は装飾なし(upstream の <XTrigger> 単体と同じ)。
11
+ module ButtonStyled
12
+ extend T::Sig
13
+
14
+ # rest-kwargs を持つメソッドの実行時sig検証は sorbet-runtime の既知の誤バインドが
15
+ # あるため checked(:never) で無効化し、srb tc の静的検査に委譲する(BaseComponent と同じ運用)
16
+ sig do
17
+ params(
18
+ variant: T.nilable(T.any(Symbol, String)),
19
+ size: T.nilable(T.any(Symbol, String)),
20
+ args: T::Hash[Symbol, T.untyped]
21
+ ).void.checked(:never)
22
+ end
23
+ def initialize(variant: nil, size: nil, **args)
24
+ @button_variant = T.let(
25
+ variant.nil? ? nil : ShadcnViewComponents::Classes.normalize_option(:button, :variant, variant),
26
+ T.nilable(Symbol)
27
+ )
28
+ @button_size = T.let(
29
+ size.nil? ? nil : ShadcnViewComponents::Classes.normalize_option(:button, :size, size),
30
+ T.nilable(Symbol)
31
+ )
32
+ super(**args)
33
+ end
34
+
35
+ private
36
+
37
+ # 契約のボタンクラスを属性に合成する。variant:/size: 未指定の時は何もしない
38
+ sig { params(attributes: T::Hash[Symbol, T.untyped]).returns(T::Hash[Symbol, T.untyped]) }
39
+ def apply_button_styling(attributes)
40
+ return attributes if @button_variant.nil? && @button_size.nil?
41
+
42
+ options = { variant: @button_variant, size: @button_size }.compact
43
+ attributes[:class] = ShadcnViewComponents::Classes.resolve(
44
+ :button,
45
+ extra: attributes[:class].to_s,
46
+ **options
47
+ )
48
+ attributes
49
+ end
50
+ end
51
+ end