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,332 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Shadcn
5
+ # サイドバー(10-roadmap Phase 4 の重量級)。開閉状態は Provider の
6
+ # data-state(open/closed)で持ち、Trigger/Rail がトグルする。
7
+ # upstream のモバイルSheet展開・cookie永続化は本gemでは扱わない
8
+ # (ホストが data-state をサーバ側で出力すれば良い — 05 §6 のキャッシュ方針)
9
+ # JS無効時フォールバック: Readable(初期状態はSSRされた data-state のまま)
10
+ class Sidebar < BaseComponent
11
+ CONTROLLER = "shadcn--sidebar"
12
+
13
+ sig { params(state: T.any(Symbol, String), args: T::Hash[Symbol, T.untyped]).void.checked(:never) }
14
+ def initialize(state: self.class.property_default(:state), **args)
15
+ @state = T.let(normalize_property(:state, state), String)
16
+ super(**args)
17
+ end
18
+
19
+ # 契約スロット構成: sidebar(状態root) > sidebar-gap / sidebar-container > sidebar-inner
20
+ sig { override.returns(String) }
21
+ def call
22
+ content_tag(:div, **html_attributes) do
23
+ safe_join([gap, container])
24
+ end
25
+ end
26
+
27
+ sig { override.returns(T::Hash[Symbol, T.untyped]) }
28
+ def html_attributes
29
+ attributes = @html_args.merge(class: self.class.classes(extra: @user_class))
30
+ data = T.cast(attributes[:data], T.nilable(T::Hash[Symbol, T.untyped])) || {}
31
+ attributes[:data] = { slot: "sidebar", state: @state }.merge(data)
32
+ attributes
33
+ end
34
+
35
+ private
36
+
37
+ sig { returns(String) }
38
+ def gap
39
+ content_tag(:div, class: slot_class("sidebar-gap"), data: { slot: "sidebar-gap" }) { "".html_safe }
40
+ end
41
+
42
+ sig { returns(String) }
43
+ def container
44
+ content_tag(:div, class: slot_class("sidebar-container"), data: { slot: "sidebar-container" }) do
45
+ content_tag(:div, class: slot_class("sidebar-inner"), data: { slot: "sidebar-inner" }) { content }
46
+ end
47
+ end
48
+
49
+ sig { params(name: String).returns(T.nilable(String)) }
50
+ def slot_class(name)
51
+ T.cast(contract_slot(name).dig(:static_attributes, :class), T.nilable(String))
52
+ end
53
+
54
+ class Provider < BaseComponent
55
+ # div(ラッパー)。data-controller を持つ状態の持ち主
56
+ sig { params(state: T.any(Symbol, String), args: T::Hash[Symbol, T.untyped]).void.checked(:never) }
57
+ def initialize(state: self.class.property_default(:state), **args)
58
+ @state = T.let(normalize_property(:state, state), String)
59
+ super(**args)
60
+ end
61
+
62
+ sig { override.returns(T::Hash[Symbol, T.untyped]) }
63
+ def html_attributes
64
+ attributes = @html_args.merge(class: self.class.classes(extra: @user_class))
65
+ data = T.cast(attributes[:data], T.nilable(T::Hash[Symbol, T.untyped])) || {}
66
+ attributes[:data] = { slot: "sidebar-wrapper", controller: Sidebar::CONTROLLER, state: @state }.merge(data)
67
+ attributes
68
+ end
69
+ end
70
+
71
+ class Trigger < BaseComponent
72
+ sig { override.returns(String) }
73
+ def default_tag
74
+ "button"
75
+ end
76
+
77
+ sig { override.returns(T::Hash[Symbol, T.untyped]) }
78
+ def html_attributes
79
+ attributes = super
80
+ attributes[:type] = "button" unless attributes.key?(:type)
81
+ merge_nested(attributes, :aria, { expanded: "true", label: "サイドバーの切替" })
82
+ merge_nested(attributes, :data, { action: "#{Sidebar::CONTROLLER}#toggle" })
83
+ attributes
84
+ end
85
+
86
+ sig { override.returns(String) }
87
+ def call
88
+ content_tag(tag, **html_attributes) do
89
+ safe_join([panel_left_icon, content_tag(:span, class: "sr-only") { "サイドバーの切替" }])
90
+ end
91
+ end
92
+
93
+ private
94
+
95
+ sig { returns(String) }
96
+ def panel_left_icon
97
+ content_tag(
98
+ :svg,
99
+ xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 24 24", fill: "none",
100
+ stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round",
101
+ width: "16", height: "16", class: "size-4",
102
+ aria: { hidden: "true" }
103
+ ) { raw(%(<rect width="18" height="18" x="3" y="4" rx="2"/><path d="M9 4v16"/>)) }
104
+ end
105
+ end
106
+
107
+ class Rail < BaseComponent
108
+ # 折り返し境界のつまみ(クリックでトグル)
109
+ sig { override.returns(String) }
110
+ def default_tag
111
+ "button"
112
+ end
113
+
114
+ sig { override.returns(T::Hash[Symbol, T.untyped]) }
115
+ def html_attributes
116
+ attributes = super
117
+ attributes[:type] = "button" unless attributes.key?(:type)
118
+ attributes[:tabindex] = "-1"
119
+ merge_nested(attributes, :aria, { label: "サイドバーの切替" })
120
+ merge_nested(attributes, :data, { action: "#{Sidebar::CONTROLLER}#toggle" })
121
+ attributes
122
+ end
123
+ end
124
+
125
+ class Inset < BaseComponent
126
+ # main(本体コンテンツ領域)
127
+ sig { override.returns(String) }
128
+ def default_tag
129
+ "main"
130
+ end
131
+ end
132
+
133
+ class Input < BaseComponent
134
+ # upstream は Input(別アイテム)に静的クラスを足す(contains で検証)
135
+ sig { override.returns(String) }
136
+ def call
137
+ # NOTE: `**@html_args, data: {...}` は Ruby 4 でリテラルキーがスプラット側を
138
+ # 黙って上書きするため、merge + 深部マージで利用者指定を保持する
139
+ attributes = @html_args.merge(class: self.class.classes(extra: @user_class))
140
+ merge_nested(attributes, :data, { slot: "sidebar-input", sidebar: "input" })
141
+ render(::Shadcn::Input.new(**attributes)) { content }
142
+ end
143
+ end
144
+
145
+ class Header < BaseComponent
146
+ # div
147
+ end
148
+
149
+ class Footer < BaseComponent
150
+ # div
151
+ end
152
+
153
+ class Separator < BaseComponent
154
+ # upstream は Separator(別アイテム)に静的クラスを足す
155
+ sig { override.returns(String) }
156
+ def call
157
+ attributes = @html_args.merge(class: self.class.classes(extra: @user_class))
158
+ merge_nested(attributes, :data, { slot: "sidebar-separator", sidebar: "separator" })
159
+ render(::Shadcn::Separator.new(**attributes)) { content }
160
+ end
161
+ end
162
+
163
+ class Content < BaseComponent
164
+ # div
165
+ end
166
+
167
+ class Group < BaseComponent
168
+ # div
169
+ end
170
+
171
+ class GroupLabel < BaseComponent
172
+ # div
173
+ end
174
+
175
+ class GroupAction < BaseComponent
176
+ # button相当(利用者が tag: a 等を渡せる)
177
+ sig { override.returns(String) }
178
+ def default_tag
179
+ "button"
180
+ end
181
+ end
182
+
183
+ class GroupContent < BaseComponent
184
+ # div
185
+ end
186
+
187
+ class Menu < BaseComponent
188
+ # ul
189
+ sig { override.returns(String) }
190
+ def default_tag
191
+ "ul"
192
+ end
193
+ end
194
+
195
+ class MenuItem < BaseComponent
196
+ # li
197
+ sig { override.returns(String) }
198
+ def default_tag
199
+ "li"
200
+ end
201
+ end
202
+
203
+ class MenuButton < BaseComponent
204
+ # variant / size は base-nova では data-variant / data-size 属性
205
+ # (クラスは data-[size=...]: 等のCSS variantで追従する)
206
+ sig do
207
+ params(
208
+ variant: T.any(Symbol, String),
209
+ size: T.any(Symbol, String),
210
+ active: T::Boolean,
211
+ args: T::Hash[Symbol, T.untyped]
212
+ ).void.checked(:never)
213
+ end
214
+ def initialize(variant: ShadcnViewComponents::Contracts::Sidebar::MenuButton::DEFAULTS.fetch(:variant),
215
+ size: ShadcnViewComponents::Contracts::Sidebar::MenuButton::DEFAULTS.fetch(:size),
216
+ active: false, **args)
217
+ @variant = T.let(normalize_option(:variant, variant), Symbol)
218
+ @size = T.let(normalize_option(:size, size), Symbol)
219
+ @active = active
220
+ super(**args)
221
+ end
222
+
223
+ sig { override.returns(T::Hash[Symbol, VariantOption]) }
224
+ def variant_options
225
+ { variant: @variant, size: @size }
226
+ end
227
+
228
+ sig { override.returns(T::Hash[Symbol, T.untyped]) }
229
+ def html_attributes
230
+ attributes = super
231
+ attributes[:type] = "button" unless attributes.key?(:type) || tag == "a"
232
+ # 契約スロットの静的属性 data-sidebar="menu-button"
233
+ merge_nested(attributes, :data, { sidebar: "menu-button", active: @active.to_s })
234
+ attributes
235
+ end
236
+ end
237
+
238
+ class MenuAction < BaseComponent
239
+ # button
240
+ sig { override.returns(String) }
241
+ def default_tag
242
+ "button"
243
+ end
244
+ end
245
+
246
+ class MenuBadge < BaseComponent
247
+ # div
248
+ end
249
+
250
+ class MenuSkeleton < BaseComponent
251
+ # upstream は Skeleton(別アイテム)を内側に描く
252
+ sig { params(show_icon: T::Boolean, args: T::Hash[Symbol, T.untyped]).void.checked(:never) }
253
+ def initialize(show_icon: true, **args)
254
+ @show_icon = show_icon
255
+ super(**args)
256
+ end
257
+
258
+ sig { override.returns(String) }
259
+ def call
260
+ content_tag(tag, **html_attributes) do
261
+ safe_join([icon_skeleton, text_skeleton])
262
+ end
263
+ end
264
+
265
+ private
266
+
267
+ # Skeleton(別アイテム)のクラスは Classes.resolve で引き、構造は自前で描く
268
+ sig { returns(String) }
269
+ def icon_skeleton
270
+ return "" unless @show_icon
271
+
272
+ skeleton_div(extra: "size-4 rounded-md")
273
+ end
274
+
275
+ sig { returns(String) }
276
+ def text_skeleton
277
+ skeleton_div(extra: "h-4 flex-1")
278
+ end
279
+
280
+ sig { params(extra: String).returns(String) }
281
+ def skeleton_div(extra:)
282
+ klass = ShadcnViewComponents::Classes.resolve(:skeleton, extra: extra)
283
+ content_tag(:div, class: klass, data: { slot: "skeleton" }) { "".html_safe }
284
+ end
285
+ end
286
+
287
+ class MenuSub < BaseComponent
288
+ # ul
289
+ sig { override.returns(String) }
290
+ def default_tag
291
+ "ul"
292
+ end
293
+ end
294
+
295
+ class MenuSubItem < BaseComponent
296
+ # li
297
+ sig { override.returns(String) }
298
+ def default_tag
299
+ "li"
300
+ end
301
+ end
302
+
303
+ class MenuSubButton < BaseComponent
304
+ # a相当(tag: で差し替え可)。isActive / size は base-nova では data-active /
305
+ # data-size 属性で表現される(クラスは data-[size=...]: のCSS variantで追従)
306
+ sig do
307
+ params(
308
+ size: T.any(Symbol, String),
309
+ active: T::Boolean,
310
+ args: T::Hash[Symbol, T.untyped]
311
+ ).void.checked(:never)
312
+ end
313
+ def initialize(size: self.class.property_default(:size), active: false, **args)
314
+ @size = T.let(normalize_property(:size, size), String)
315
+ @active = active
316
+ super(**args)
317
+ end
318
+
319
+ sig { override.returns(String) }
320
+ def default_tag
321
+ "a"
322
+ end
323
+
324
+ sig { override.returns(T::Hash[Symbol, T.untyped]) }
325
+ def html_attributes
326
+ attributes = super
327
+ merge_nested(attributes, :data, { active: @active.to_s, size: @size })
328
+ attributes
329
+ end
330
+ end
331
+ end
332
+ end
@@ -0,0 +1,13 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Shadcn
5
+ class Skeleton < BaseComponent
6
+ # BaseComponent の rest-kwargs initialize を再宣言する(srb の静的モデルが
7
+ # 継承した **args を誤バインドするため — 他コンポーネントと同じ定形)
8
+ sig { params(args: T::Hash[Symbol, T.untyped]).void.checked(:never) }
9
+ def initialize(**args)
10
+ super
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,279 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Shadcn
5
+ # スライダー(ネイティブ input[type=range] を用いる — 05 §3 ネイティブ最優先)。
6
+ # class / style / tag はupstreamのRootに、それ以外の利用者HTML属性はフォームコントロールの
7
+ # inputへ渡す。track / range / thumbはinputの現在値を表す装飾で、状態源にはしない。
8
+ class Slider < BaseComponent
9
+ # ブラウザがrange inputへ適用する初期値の制約と、装飾位置の算出を同じ値から行う。
10
+ class InitialState
11
+ extend T::Sig
12
+
13
+ sig do
14
+ params(
15
+ min: T.untyped,
16
+ max: T.untyped,
17
+ step: T.untyped,
18
+ value: T.untyped,
19
+ orientation: T.untyped
20
+ ).void
21
+ end
22
+ def initialize(min:, max:, step:, value:, orientation:)
23
+ @min = T.let(normalize_number(:min, min), T.any(Integer, Float))
24
+ @max = T.let(normalize_number(:max, max), T.any(Integer, Float))
25
+ @step = T.let(normalize_number(:step, step), T.any(Integer, Float))
26
+ @value = T.let(normalize_value(value), T.nilable(T.any(Integer, Float)))
27
+ @orientation = T.let(normalize_orientation(orientation), String)
28
+ validate!
29
+ end
30
+
31
+ sig { returns(T::Hash[Symbol, T.untyped]) }
32
+ def input_attributes
33
+ attributes = T.let({ min: @min, max: @max, step: @step }, T::Hash[Symbol, T.untyped])
34
+ attributes[:value] = @value if @value
35
+ attributes
36
+ end
37
+
38
+ sig { returns(String) }
39
+ attr_reader :orientation
40
+
41
+ sig { returns(T::Boolean) }
42
+ def vertical?
43
+ @orientation == "vertical"
44
+ end
45
+
46
+ sig { returns(String) }
47
+ def range_style
48
+ if vertical?
49
+ "position: absolute; bottom: 0; height: #{percentage}%"
50
+ else
51
+ "position: absolute; left: 0; width: #{percentage}%"
52
+ end
53
+ end
54
+
55
+ sig { returns(String) }
56
+ def thumb_style
57
+ if vertical?
58
+ "position: absolute; bottom: #{percentage}%; left: 50%; transform: translate(-50%, 50%)"
59
+ else
60
+ "position: absolute; left: #{percentage}%; top: 50%; transform: translate(-50%, -50%)"
61
+ end
62
+ end
63
+
64
+ private
65
+
66
+ sig { params(property: Symbol, value: T.untyped).returns(T.any(Integer, Float)) }
67
+ def normalize_number(property, value)
68
+ T.cast(normalize(property, value), T.any(Integer, Float))
69
+ end
70
+
71
+ sig { params(value: T.untyped).returns(T.nilable(T.any(Integer, Float))) }
72
+ def normalize_value(value)
73
+ T.cast(normalize(:value, value), T.nilable(T.any(Integer, Float)))
74
+ end
75
+
76
+ sig { params(value: T.untyped).returns(String) }
77
+ def normalize_orientation(value)
78
+ T.cast(normalize(:orientation, value), String)
79
+ end
80
+
81
+ sig { params(property: Symbol, value: T.untyped).returns(T.untyped) }
82
+ def normalize(property, value)
83
+ ShadcnViewComponents::PropertyContracts.normalize(
84
+ component: :slider,
85
+ owner: "Shadcn::Slider",
86
+ property:,
87
+ value:
88
+ )
89
+ end
90
+
91
+ sig { returns(String) }
92
+ def percentage
93
+ result = ((effective_value - decimal(@min)) / (decimal(@max) - decimal(@min))) * 100
94
+ format("%.12g", result.to_f)
95
+ end
96
+
97
+ sig { returns(Rational) }
98
+ def effective_value
99
+ return decimal(@value) if @value
100
+
101
+ minimum = decimal(@min)
102
+ step = decimal(@step)
103
+ midpoint = minimum + ((decimal(@max) - minimum) / 2)
104
+ position = (midpoint - minimum) / step
105
+ step_count = position.floor
106
+ step_count += 1 if (position - step_count) * 2 >= 1
107
+ minimum + (step * step_count)
108
+ end
109
+
110
+ sig { params(value: T.any(Integer, Float)).returns(Rational) }
111
+ def decimal(value)
112
+ Rational(value.to_s)
113
+ end
114
+
115
+ sig { void }
116
+ def validate!
117
+ raise ArgumentError, "Shadcn::Slider max must be greater than min, got min=#{@min.inspect}, max=#{@max.inspect}" unless @min < @max
118
+ if @value && !@value.between?(@min, @max)
119
+ raise ArgumentError,
120
+ "Shadcn::Slider value must be between min and max, got value=#{@value.inspect}, " \
121
+ "min=#{@min.inspect}, max=#{@max.inspect}"
122
+ end
123
+
124
+ validate_step_alignment!
125
+ end
126
+
127
+ sig { void }
128
+ def validate_step_alignment!
129
+ return unless @value
130
+
131
+ steps = (decimal(@value) - decimal(@min)) / decimal(@step)
132
+ return if steps.denominator == 1
133
+
134
+ raise ArgumentError,
135
+ "Shadcn::Slider value must align with step from min, got value=#{@value.inspect}, " \
136
+ "min=#{@min.inspect}, step=#{@step.inspect}"
137
+ end
138
+ end
139
+
140
+ CONTROLLER = "shadcn--slider"
141
+ INPUT_CLASS = "peer absolute inset-0 z-10 size-full cursor-pointer opacity-0"
142
+ INPUT_ACTION = T.let("input->#{CONTROLLER}#sync change->#{CONTROLLER}#sync".freeze, String)
143
+ THUMB_INPUT_STATE_CLASS = "peer-hover:ring-3 peer-focus-visible:ring-3 peer-active:ring-3 " \
144
+ "peer-disabled:pointer-events-none peer-disabled:opacity-50"
145
+
146
+ private_constant :InitialState, :INPUT_CLASS, :INPUT_ACTION, :THUMB_INPUT_STATE_CLASS
147
+
148
+ sig do
149
+ params(
150
+ min: T.any(Integer, Float, String),
151
+ max: T.any(Integer, Float, String),
152
+ step: T.any(Integer, Float, String),
153
+ value: T.nilable(T.any(Integer, Float, String)),
154
+ orientation: T.any(Symbol, String),
155
+ args: T::Hash[Symbol, T.untyped]
156
+ ).void.checked(:never)
157
+ end
158
+ def initialize(min: self.class.property_default(:min), # rubocop:disable Metrics/ParameterLists
159
+ max: self.class.property_default(:max),
160
+ step: self.class.property_default(:step),
161
+ value: self.class.property_default(:value),
162
+ orientation: self.class.property_default(:orientation), **args)
163
+ @initial_state = T.let(InitialState.new(min:, max:, step:, value:, orientation:), InitialState)
164
+
165
+ # SliderのclassName / tagはupstream RootのAPI。それ以外は実際にフォーカス・
166
+ # 送信されるinputのHTML属性として扱い、wrapperへ誤送しない。
167
+ root_args = T.let({}, T::Hash[Symbol, T.untyped])
168
+ %i[class style tag].each { |key| root_args[key] = args.delete(key) if args.key?(key) }
169
+ @input_args = T.let(args, T::Hash[Symbol, T.untyped])
170
+ super(**root_args)
171
+ end
172
+
173
+ sig { override.returns(String) }
174
+ def call
175
+ content_tag(tag, **html_attributes) do
176
+ content_tag(:div, class: control_class, data: part_data) do
177
+ # peer variantでnative inputのhover/focus/active状態をthumbへ渡すため、
178
+ # inputはthumbより前の兄弟に置く。
179
+ safe_join([track, range_input, thumb])
180
+ end
181
+ end
182
+ end
183
+
184
+ sig { override.returns(T::Hash[Symbol, T.untyped]) }
185
+ def html_attributes
186
+ super.tap do |attributes|
187
+ merge_nested(attributes, :data, part_data.merge(controller: CONTROLLER))
188
+ end
189
+ end
190
+
191
+ private
192
+
193
+ sig { returns(String) }
194
+ def track
195
+ content_tag(:div, class: slot_class("slider-track"), data: part_data(slot: "slider-track")) do
196
+ content_tag(
197
+ :div,
198
+ class: slot_class("slider-range"),
199
+ style: @initial_state.range_style,
200
+ data: part_data(slot: "slider-range")
201
+ ) { "".html_safe }
202
+ end
203
+ end
204
+
205
+ sig { returns(String) }
206
+ def thumb
207
+ content_tag(
208
+ :span,
209
+ class: thumb_class,
210
+ style: @initial_state.thumb_style,
211
+ aria: { hidden: "true" },
212
+ data: part_data(slot: "slider-thumb")
213
+ ) { "".html_safe }
214
+ end
215
+
216
+ sig { returns(String) }
217
+ def range_input
218
+ attributes = T.let(
219
+ @input_args.dup,
220
+ T::Hash[Symbol, T.untyped]
221
+ )
222
+ attributes[:type] = "range"
223
+ attributes[:class] = INPUT_CLASS
224
+ attributes.merge!(@initial_state.input_attributes)
225
+
226
+ merge_nested(attributes, :aria, { orientation: @initial_state.orientation })
227
+ merge_input_data(attributes)
228
+ merge_style(attributes, { writing_mode: "vertical-lr", direction: "rtl" }) if @initial_state.vertical?
229
+ void_tag("input", **attributes)
230
+ end
231
+
232
+ sig { params(attributes: T::Hash[Symbol, T.untyped]).void }
233
+ def merge_input_data(attributes)
234
+ user_data = T.cast(attributes[:data], T.nilable(T::Hash[Symbol, T.untyped])) || {}
235
+ user_action = user_data[:action]
236
+ attributes[:data] = user_data.merge(
237
+ slot: "slider-input",
238
+ action: [INPUT_ACTION, user_action].compact.join(" ")
239
+ )
240
+ end
241
+
242
+ sig { params(slot: T.nilable(String)).returns(T::Hash[Symbol, T.untyped]) }
243
+ def part_data(slot: nil)
244
+ data = T.let({ orientation: @initial_state.orientation }, T::Hash[Symbol, T.untyped])
245
+ data[:slot] = slot if slot
246
+ data[:disabled] = "" if disabled?
247
+ data
248
+ end
249
+
250
+ sig { returns(T::Boolean) }
251
+ def disabled?
252
+ !@input_args[:disabled].nil? && @input_args[:disabled] != false
253
+ end
254
+
255
+ sig { params(name: String).returns(String) }
256
+ def slot_class(name)
257
+ slot = contract_slot(name)
258
+ raise KeyError, "Slider contract is missing #{name}" if slot.empty?
259
+
260
+ static_attributes = T.cast(slot.fetch(:static_attributes), T::Hash[Symbol, T.untyped])
261
+ T.cast(static_attributes.fetch(:class), String)
262
+ end
263
+
264
+ sig { returns(String) }
265
+ def thumb_class
266
+ ShadcnViewComponents::Classes::MERGER.merge("#{slot_class('slider-thumb')} #{THUMB_INPUT_STATE_CLASS}")
267
+ end
268
+
269
+ sig { returns(String) }
270
+ def control_class
271
+ slot = T.cast(contract.const_get(:SLOTS), T::Array[T::Hash[Symbol, T.untyped]])
272
+ .find { |candidate| candidate[:tag] == "SliderPrimitive.Control" }
273
+ raise KeyError, "Slider contract is missing SliderPrimitive.Control" unless slot
274
+
275
+ static_attributes = T.cast(slot.fetch(:static_attributes), T::Hash[Symbol, T.untyped])
276
+ T.cast(static_attributes.fetch(:class), String)
277
+ end
278
+ end
279
+ end
@@ -0,0 +1,30 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Shadcn
5
+ # トースト(Phase 4)。upstream は sonner ライブラリを使うが、本gemは
6
+ # Stimulus + CustomEvent で同等の通知領域を提供する:
7
+ # dispatchEvent(new CustomEvent("shadcn:toast", { detail: { title: "保存しました" } }))
8
+ # JS無効時フォールバック: Graceful(通知はJS依存だが、領域自体は空で害がない)
9
+ # Sonner自体は名前空間であり、描画にはToasterを使う。
10
+ module Sonner
11
+ class Toaster < BaseComponent
12
+ CONTROLLER = "shadcn--toast"
13
+
14
+ sig { override.returns(String) }
15
+ def default_tag
16
+ "section"
17
+ end
18
+
19
+ sig { override.returns(T::Hash[Symbol, T.untyped]) }
20
+ def html_attributes
21
+ static_class = T.cast(contract_slot("").dig(:static_attributes, :class), T.nilable(String))
22
+ attributes = @html_args.merge(class: [static_class, @user_class].compact.join(" "))
23
+ data = T.cast(attributes[:data], T.nilable(T::Hash[Symbol, T.untyped])) || {}
24
+ attributes[:data] = { controller: CONTROLLER }.merge(data)
25
+ merge_nested(attributes, :aria, { live: "polite" })
26
+ attributes
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,32 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Shadcn
5
+ class Spinner < BaseComponent
6
+ # 契約タグは lucide の Loader2Icon。lucide と同等の loader-circle パスを持つ
7
+ # インラインSVGとして描画する(アイコン系は契約に data-slot を持たない)
8
+ sig { override.returns(String) }
9
+ def default_tag
10
+ "svg"
11
+ end
12
+
13
+ sig { override.returns(String) }
14
+ def call
15
+ content_tag(
16
+ :svg,
17
+ xmlns: "http://www.w3.org/2000/svg",
18
+ viewBox: "0 0 24 24",
19
+ width: "16",
20
+ height: "16",
21
+ fill: "none",
22
+ stroke: "currentColor",
23
+ "stroke-width": "2",
24
+ "stroke-linecap": "round",
25
+ "stroke-linejoin": "round",
26
+ **html_attributes
27
+ ) do
28
+ raw(%(<path d="M21 12a9 9 0 1 1-9-9"/>))
29
+ end
30
+ end
31
+ end
32
+ end