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,79 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Shadcn
5
+ # 右クリックメニュー(10-roadmap Phase 3「context-menu = 同上(右クリック)」)。
6
+ # 実装は DropdownMenu を継承し、差分のみを上書きする:
7
+ # - Trigger は click ではなく contextmenu で showAt を呼ぶ
8
+ # - Content は右クリックイベント列の途中で light dismiss されないよう popover=manual
9
+ # JS無効時フォールバック: Readable
10
+ class ContextMenu < DropdownMenu
11
+ class Trigger < BaseComponent
12
+ # 任意の領域を右クリック対象にできる非button要素。
13
+ sig { override.returns(T::Hash[Symbol, T.untyped]) }
14
+ def html_attributes
15
+ attributes = super
16
+ attributes[:tabindex] = "0" unless attributes.key?(:tabindex)
17
+ merge_nested(attributes, :aria, { haspopup: "menu" })
18
+ merge_nested(attributes, :data, { action: "contextmenu->#{DropdownMenu::CONTROLLER}#showAt" })
19
+ attributes
20
+ end
21
+ end
22
+
23
+ class Portal < DropdownMenu::Portal
24
+ # 搬送のみの機能要素の実体化
25
+ end
26
+
27
+ class Content < DropdownMenu::Content
28
+ FLOATING_POSITION_DEFAULTS = T.let(
29
+ { side: :right, align: :start, side_offset: 0, align_offset: 4, collision_padding: 5 }.freeze,
30
+ T::Hash[Symbol, T.untyped]
31
+ )
32
+
33
+ # 右クリックで開く際、イベント列の途中でlight dismissされないようmanualにする
34
+ sig { override.returns(T::Hash[Symbol, T.untyped]) }
35
+ def html_attributes
36
+ super.tap { |attributes| attributes[:popover] = "manual" }
37
+ end
38
+ end
39
+
40
+ class Group < DropdownMenu::Group; end
41
+
42
+ class Label < DropdownMenu::Label; end
43
+
44
+ class Item < DropdownMenu::Item; end
45
+
46
+ class CheckboxItem < DropdownMenu::CheckboxItem
47
+ # 契約に indicator スロットが無いため data-slot 無しで描く
48
+ sig { returns(T.nilable(String)) }
49
+ def indicator_slot_name
50
+ nil
51
+ end
52
+ end
53
+
54
+ class RadioGroup < DropdownMenu::RadioGroup; end
55
+
56
+ class RadioItem < DropdownMenu::RadioItem
57
+ # 契約に indicator スロットが無いため data-slot 無しで描く
58
+ sig { returns(T.nilable(String)) }
59
+ def indicator_slot_name
60
+ nil
61
+ end
62
+ end
63
+
64
+ class Separator < DropdownMenu::Separator; end
65
+
66
+ class Shortcut < DropdownMenu::Shortcut; end
67
+
68
+ class Sub < DropdownMenu::Sub; end
69
+
70
+ class SubTrigger < DropdownMenu::SubTrigger; end
71
+
72
+ class SubContent < DropdownMenu::SubContent
73
+ FLOATING_POSITION_DEFAULTS = T.let(
74
+ { side: :right, align: :start, side_offset: 0, align_offset: 4, collision_padding: 5 }.freeze,
75
+ T::Hash[Symbol, T.untyped]
76
+ )
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,180 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Shadcn
5
+ # ネイティブ `<dialog>` + showModal によるモーダル(05-stimulus-hotwire §3)。
6
+ # フォーカストラップ・EscClose・背景inert・フォーカス復帰はネイティブが提供する。
7
+ # JS無効時フォールバック: Readable(dialog要素は閉じた状態で出力されるため
8
+ # 内容は取得できる。開く操作にはJSが必要 — 05 §5)
9
+ class Dialog < BaseComponent
10
+ CONTROLLER = "shadcn--dialog"
11
+
12
+ sig { params(args: T::Hash[Symbol, T.untyped]).void.checked(:never) }
13
+ def initialize(**args)
14
+ assign_accessibility_root_id(args, prefix: "dialog")
15
+ super
16
+ end
17
+
18
+ # upstream の Root はDOMを出力しないが、本gemではトリガーとコンテンツを
19
+ # 同一のコントローラスコープに置くためのラッパーとして描く(文書化された構造差)
20
+ sig { override.returns(T::Hash[Symbol, T.untyped]) }
21
+ def contract_data_attributes
22
+ super.merge(controller: CONTROLLER)
23
+ end
24
+
25
+ class Trigger < BaseComponent
26
+ include Shadcn::ButtonStyled
27
+
28
+ sig { override.returns(String) }
29
+ def default_tag
30
+ "button"
31
+ end
32
+
33
+ sig { override.returns(T::Hash[Symbol, T.untyped]) }
34
+ def html_attributes
35
+ attributes = apply_button_styling(super)
36
+ attributes[:type] = "button" unless attributes.key?(:type)
37
+ merge_nested(attributes, :aria, { haspopup: "dialog", expanded: "false" })
38
+ merge_nested(attributes, :data, { action: "#{Dialog::CONTROLLER}#show" })
39
+ attributes
40
+ end
41
+ end
42
+
43
+ class Portal < BaseComponent
44
+ # upstream の Portal は子を body 末尾へ搬送するだけの機能要素。
45
+ # 本gemではラッパー div として実体化する(契約スロットの維持のため)
46
+ end
47
+
48
+ class Close < BaseComponent
49
+ sig { override.returns(String) }
50
+ def default_tag
51
+ "button"
52
+ end
53
+
54
+ sig { override.returns(T::Hash[Symbol, T.untyped]) }
55
+ def html_attributes
56
+ attributes = super
57
+ attributes[:type] = "button" unless attributes.key?(:type)
58
+ merge_nested(attributes, :data, { action: "#{Dialog::CONTROLLER}#close" })
59
+ attributes
60
+ end
61
+ end
62
+
63
+ class Overlay < BaseComponent
64
+ # 契約上は Content の外に置かれる独立エクスポート。ネイティブ運用では
65
+ # dialogの ::backdrop で代替するため、コンテンツ側には含めない
66
+ # (利用者が明示的に描きたい場合に備えて公開する)
67
+ end
68
+
69
+ class Content < BaseComponent
70
+ # 契約スロット構成: 無名ポータルラッパー > dialog-content(dialog要素)
71
+ # + dialog-close(右上クローズ)。オーバーレイは ::backdrop で代替する
72
+ sig { params(show_close_button: T::Boolean, args: T::Hash[Symbol, T.untyped]).void.checked(:never) }
73
+ def initialize(show_close_button: true, **args)
74
+ @show_close_button = show_close_button
75
+ super(**args)
76
+ end
77
+
78
+ sig { override.returns(String) }
79
+ def default_tag
80
+ "div"
81
+ end
82
+
83
+ sig { override.returns(String) }
84
+ def call
85
+ # ポータルラッパーは契約上無名(data-slotを持たない)
86
+ content_tag(:div) { dialog_element }
87
+ end
88
+
89
+ private
90
+
91
+ sig { returns(String) }
92
+ def dialog_element
93
+ content_tag(:dialog, **content_attributes) do
94
+ safe_join([content.presence, close_button].compact)
95
+ end
96
+ end
97
+
98
+ # upstream のクローズボタン(render prop のButton variant="ghost" size="icon-sm"
99
+ # + absolute配置)。契約クラスはButton契約から合成する
100
+ sig { returns(T.nilable(String)) }
101
+ def close_button
102
+ return nil unless @show_close_button
103
+
104
+ content_tag(
105
+ :button,
106
+ type: "button",
107
+ class: close_class,
108
+ data: { slot: "dialog-close", action: "#{Dialog::CONTROLLER}#close" },
109
+ aria: { label: "Close" }
110
+ ) do
111
+ safe_join([close_icon, content_tag(:span, class: "sr-only") { "Close" }])
112
+ end
113
+ end
114
+
115
+ # lucide のXIcon相当
116
+ sig { returns(String) }
117
+ def close_icon
118
+ content_tag(
119
+ :svg,
120
+ xmlns: "http://www.w3.org/2000/svg",
121
+ viewBox: "0 0 24 24",
122
+ fill: "none",
123
+ stroke: "currentColor",
124
+ "stroke-width": "2",
125
+ "stroke-linecap": "round",
126
+ "stroke-linejoin": "round",
127
+ width: "16",
128
+ height: "16",
129
+ class: "size-4",
130
+ aria: { hidden: "true" }
131
+ ) do
132
+ raw(%(<path d="M18 6 6 18"/><path d="m6 6 12 12"/>))
133
+ end
134
+ end
135
+
136
+ sig { returns(T::Hash[Symbol, T.untyped]) }
137
+ def content_attributes
138
+ attributes = @html_args.merge(class: self.class.classes(extra: @user_class))
139
+ data = T.cast(attributes[:data], T.nilable(T::Hash[Symbol, T.untyped])) || {}
140
+ # 開閉状態属性(data-open / data-closed)はコントローラが同期する(SSRでは出さない)
141
+ attributes[:data] = { slot: "dialog-content" }.merge(data)
142
+ aria = T.cast(attributes[:aria], T.nilable(T::Hash[Symbol, T.untyped])) || {}
143
+ attributes[:aria] = { modal: "true" }.merge(aria)
144
+ attributes
145
+ end
146
+
147
+ sig { returns(String) }
148
+ def close_class
149
+ # NOTE: Hashの値型は不変のため、Classes.resolve の options と同じ型で T.let する
150
+ button_options = T.let({ variant: :ghost, size: :"icon-sm" },
151
+ T::Hash[Symbol, T.nilable(T.any(Symbol, String))])
152
+ ShadcnViewComponents::Classes.resolve(:button, extra: "absolute top-2 right-2", **button_options)
153
+ end
154
+ end
155
+
156
+ class Header < BaseComponent
157
+ # div
158
+ end
159
+
160
+ class Footer < BaseComponent
161
+ # div(末尾の任意クローズボタンは利用者が Dialog::Close として置く)
162
+ end
163
+
164
+ class Title < BaseComponent
165
+ # h2(Radix Title の既定)
166
+ sig { override.returns(String) }
167
+ def default_tag
168
+ "h2"
169
+ end
170
+ end
171
+
172
+ class Description < BaseComponent
173
+ # p
174
+ sig { override.returns(String) }
175
+ def default_tag
176
+ "p"
177
+ end
178
+ end
179
+ end
180
+ end
@@ -0,0 +1,18 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Shadcn
5
+ # 書字方向スコープ。div[dir] で括るだけ(Radix DirectionProvider 相当の役割)
6
+ class DirectionProvider < BaseComponent
7
+ sig { params(dir: T.any(Symbol, String), args: T::Hash[Symbol, T.untyped]).void.checked(:never) }
8
+ def initialize(dir: self.class.property_default(:dir), **args)
9
+ @dir = T.let(normalize_property(:dir, dir), String)
10
+ super(**args)
11
+ end
12
+
13
+ sig { override.returns(T::Hash[Symbol, T.untyped]) }
14
+ def html_attributes
15
+ super.tap { |attributes| attributes[:dir] = @dir }
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,150 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Shadcn
5
+ # ボトムシート(ネイティブ <dialog>)。upstream は vaul(ドラッグ操作)を使うが、
6
+ # 本gemは開閉のみをネイティブで提供し、クラス契約は vaul の data 属性付きの
7
+ # まま維持する(ドラッグ方向クラスはホストが data-vaul-drawer-direction を
8
+ # 制御する場合に有効化される)
9
+ # JS無効時フォールバック: Readable
10
+ class Drawer < BaseComponent
11
+ CONTROLLER = "shadcn--dialog"
12
+
13
+ sig { params(args: T::Hash[Symbol, T.untyped]).void.checked(:never) }
14
+ def initialize(**args)
15
+ assign_accessibility_root_id(args, prefix: "drawer")
16
+ super
17
+ end
18
+
19
+ # base-nova では Drawer ルートも data-slot="drawer" を持つ(旧契約には無し)
20
+ sig { override.returns(T::Hash[Symbol, T.untyped]) }
21
+ def contract_data_attributes
22
+ super.merge(slot: "drawer", controller: CONTROLLER)
23
+ end
24
+
25
+ class Trigger < BaseComponent
26
+ include Shadcn::ButtonStyled
27
+
28
+ sig { override.returns(String) }
29
+ def default_tag
30
+ "button"
31
+ end
32
+
33
+ sig { override.returns(T::Hash[Symbol, T.untyped]) }
34
+ def html_attributes
35
+ attributes = apply_button_styling(super)
36
+ attributes[:type] = "button" unless attributes.key?(:type)
37
+ merge_nested(attributes, :aria, { haspopup: "dialog", expanded: "false" })
38
+ merge_nested(attributes, :data, { action: "#{CONTROLLER}#show" })
39
+ attributes
40
+ end
41
+ end
42
+
43
+ class Close < BaseComponent
44
+ sig { override.returns(String) }
45
+ def default_tag
46
+ "button"
47
+ end
48
+
49
+ sig { override.returns(T::Hash[Symbol, T.untyped]) }
50
+ def html_attributes
51
+ attributes = super
52
+ attributes[:type] = "button" unless attributes.key?(:type)
53
+ merge_nested(attributes, :data, { action: "#{CONTROLLER}#close" })
54
+ attributes
55
+ end
56
+ end
57
+
58
+ class Portal < BaseComponent
59
+ # upstream の Portal(子を body 末尾へ搬送する機能要素)の実体化。
60
+ # DrawerContent 内部でも同じラッパーを描く
61
+ end
62
+
63
+ class Overlay < BaseComponent
64
+ # 契約上は独立エクスポート。ネイティブ運用では ::backdrop で代替するため
65
+ # Content 側には含めない(利用者が明示的に描く場合に備えて公開)
66
+ end
67
+
68
+ class Content < BaseComponent
69
+ # 契約スロット構成(base-nova): drawer-portal > drawer-viewport > drawer-popup
70
+ # > drawer-content + ドラッグハンドル。ハンドルは data-slot を持たない装飾要素。
71
+ # viewport / popup をネイティブ <dialog> の内側に置くことで、閉じ状態では
72
+ # UAスタイル(display: none)によりオーバーレイが表示されない(open時はtop-layer)。
73
+ # data-modal は Base UI がモーダル時に付与する属性(本gemは常にモーダル)
74
+ sig { override.returns(String) }
75
+ def call
76
+ content_tag(:div, data: { slot: "drawer-portal" }) do
77
+ content_tag(:dialog, **dialog_attributes) do
78
+ content_tag(:div, class: slot_class("drawer-viewport"), data: { slot: "drawer-viewport", modal: "true" }) do
79
+ content_tag(:div, class: popup_class, data: { slot: "drawer-popup" }) do
80
+ content_tag(:div, **content_attributes) do
81
+ safe_join([drag_handle, content.presence].compact)
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
88
+
89
+ private
90
+
91
+ # 契約クラス(combination)は drawer-popup 要素に属する
92
+ sig { returns(String) }
93
+ def popup_class
94
+ self.class.classes(extra: @user_class)
95
+ end
96
+
97
+ sig { returns(T::Hash[Symbol, T.untyped]) }
98
+ def dialog_attributes
99
+ attributes = @html_args
100
+ data = T.cast(attributes[:data], T.nilable(T::Hash[Symbol, T.untyped])) || {}
101
+ attributes[:data] = { "vaul-drawer-direction": "bottom" }.merge(data)
102
+ aria = T.cast(attributes[:aria], T.nilable(T::Hash[Symbol, T.untyped])) || {}
103
+ attributes[:aria] = { modal: "true" }.merge(aria)
104
+ attributes
105
+ end
106
+
107
+ sig { returns(T::Hash[Symbol, T.untyped]) }
108
+ def content_attributes
109
+ attributes = { class: slot_class("drawer-content") }
110
+ data = { slot: "drawer-content", "vaul-drawer-direction": "bottom" }
111
+ attributes[:data] = data
112
+ attributes
113
+ end
114
+
115
+ # viewport / content のクラスは契約スロットの静的クラス
116
+ sig { params(name: String).returns(T.nilable(String)) }
117
+ def slot_class(name)
118
+ T.cast(contract_slot(name).dig(:static_attributes, :class), T.nilable(String))
119
+ end
120
+
121
+ # upstream のドラッグハンドル(装飾要素)
122
+ sig { returns(String) }
123
+ def drag_handle
124
+ content_tag(:div, class: "mx-auto mt-4 hidden h-2 w-[100px] shrink-0 rounded-full bg-muted") { "".html_safe }
125
+ end
126
+ end
127
+
128
+ class Header < BaseComponent
129
+ # div
130
+ end
131
+
132
+ class Footer < BaseComponent
133
+ # div
134
+ end
135
+
136
+ class Title < BaseComponent
137
+ sig { override.returns(String) }
138
+ def default_tag
139
+ "h2"
140
+ end
141
+ end
142
+
143
+ class Description < BaseComponent
144
+ sig { override.returns(String) }
145
+ def default_tag
146
+ "p"
147
+ end
148
+ end
149
+ end
150
+ end
@@ -0,0 +1,242 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Shadcn
5
+ # ARIA menu パターン + Popover API(10-roadmap Phase 3「dropdown-menu = Popover API +
6
+ # ARIA menu + 共通キーボードlib」)。キーボード操作(矢印・Home/End・Esc・文字検索)は
7
+ # menu_controller が担う
8
+ # JS無効時フォールバック: Readable(項目はSSR済み。開閉はJS依存)
9
+ class DropdownMenu < BaseComponent
10
+ CONTROLLER = "shadcn--menu"
11
+
12
+ sig { override.returns(T::Hash[Symbol, T.untyped]) }
13
+ def contract_data_attributes
14
+ super.merge(controller: CONTROLLER)
15
+ end
16
+
17
+ class Trigger < BaseComponent
18
+ include Shadcn::ButtonStyled
19
+
20
+ sig { override.returns(String) }
21
+ def default_tag
22
+ "button"
23
+ end
24
+
25
+ sig { override.returns(T::Hash[Symbol, T.untyped]) }
26
+ def html_attributes
27
+ attributes = apply_button_styling(super)
28
+ attributes[:type] = "button" unless attributes.key?(:type)
29
+ merge_nested(attributes, :aria, { haspopup: "menu", expanded: "false" })
30
+ merge_nested(attributes, :data, { action: default_action })
31
+ attributes
32
+ end
33
+
34
+ private
35
+
36
+ sig { returns(String) }
37
+ def default_action
38
+ "#{controller_identifier}#toggle"
39
+ end
40
+
41
+ sig { returns(String) }
42
+ def controller_identifier
43
+ DropdownMenu::CONTROLLER
44
+ end
45
+ end
46
+
47
+ class Portal < BaseComponent
48
+ # 搬送のみの機能要素の実体化(Dialog::Portal と同じ)
49
+ end
50
+
51
+ class Content < BaseComponent
52
+ include Shadcn::FloatingPositionOptions
53
+
54
+ FLOATING_POSITION_DEFAULTS = T.let(
55
+ { side: :bottom, align: :start, side_offset: 4, align_offset: 0, collision_padding: 5 }.freeze,
56
+ T::Hash[Symbol, T.untyped]
57
+ )
58
+
59
+ # role=menu + popover=auto。light dismissはネイティブ、状態・位置合わせはコントローラが担う
60
+ sig { override.returns(T::Hash[Symbol, T.untyped]) }
61
+ def html_attributes
62
+ attributes = super
63
+ attributes[:role] = "menu"
64
+ attributes[:popover] = "auto"
65
+ attributes[:tabindex] = "-1"
66
+ merge_floating_position_data(attributes, state: "closed")
67
+ end
68
+ end
69
+
70
+ class Group < BaseComponent
71
+ sig { override.returns(T::Hash[Symbol, T.untyped]) }
72
+ def html_attributes
73
+ super.tap { |attributes| attributes[:role] = "group" }
74
+ end
75
+ end
76
+
77
+ class Label < BaseComponent
78
+ # div
79
+ end
80
+
81
+ class Item < BaseComponent
82
+ # role=menuitem。data-highlighted と tabindex はコントローラが管理する
83
+ sig { override.returns(T::Hash[Symbol, T.untyped]) }
84
+ def html_attributes
85
+ attributes = super
86
+ attributes[:role] = "menuitem"
87
+ attributes[:tabindex] = "-1"
88
+ merge_nested(attributes, :data, { action: default_action })
89
+ attributes
90
+ end
91
+
92
+ private
93
+
94
+ sig { returns(String) }
95
+ def default_action
96
+ "#{controller_identifier}#activate"
97
+ end
98
+
99
+ sig { returns(String) }
100
+ def controller_identifier
101
+ DropdownMenu::CONTROLLER
102
+ end
103
+ end
104
+
105
+ class CheckboxItem < Item
106
+ sig { params(checked: T::Boolean, args: T::Hash[Symbol, T.untyped]).void.checked(:never) }
107
+ def initialize(checked: false, **args)
108
+ @checked = checked
109
+ super(**args)
110
+ end
111
+
112
+ sig { override.returns(T::Hash[Symbol, T.untyped]) }
113
+ def html_attributes
114
+ attributes = super
115
+ attributes[:role] = "menuitemcheckbox"
116
+ merge_nested(attributes, :aria, { checked: @checked.to_s })
117
+ attributes
118
+ end
119
+
120
+ sig { override.returns(String) }
121
+ def call
122
+ content_tag(tag, **html_attributes) do
123
+ safe_join([indicator, content])
124
+ end
125
+ end
126
+
127
+ private
128
+
129
+ # upstream の ItemIndicator + CheckIcon(契約スロット dropdown-menu-checkbox-item-indicator)。
130
+ # indicator スロットを持たない派生(context-menu / menubar)は data-slot 無しで描く
131
+ sig { returns(String) }
132
+ def indicator
133
+ options = { class: indicator_class }
134
+ options[:data] = { slot: indicator_slot_name } if indicator_slot_name
135
+ content_tag(:span, **options) do
136
+ @checked ? check_icon : "".html_safe
137
+ end
138
+ end
139
+
140
+ # 契約に indicator スロットが無い派生では nil を返す
141
+ sig { returns(T.nilable(String)) }
142
+ def indicator_slot_name
143
+ "dropdown-menu-checkbox-item-indicator"
144
+ end
145
+
146
+ sig { returns(T.nilable(String)) }
147
+ def indicator_class
148
+ slot_name = indicator_slot_name
149
+ return nil unless slot_name
150
+
151
+ T.cast(contract_slot(slot_name).dig(:static_attributes, :class), T.nilable(String))
152
+ end
153
+
154
+ sig { returns(String) }
155
+ def check_icon
156
+ content_tag(
157
+ :svg,
158
+ xmlns: "http://www.w3.org/2000/svg",
159
+ viewBox: "0 0 24 24",
160
+ fill: "none",
161
+ stroke: "currentColor",
162
+ "stroke-width": "2",
163
+ "stroke-linecap": "round",
164
+ "stroke-linejoin": "round",
165
+ width: "14",
166
+ height: "14",
167
+ class: "size-3.5"
168
+ ) do
169
+ raw(%(<path d="M20 6 9 17l-5-5"/>))
170
+ end
171
+ end
172
+ end
173
+
174
+ class RadioGroup < BaseComponent
175
+ sig { override.returns(T::Hash[Symbol, T.untyped]) }
176
+ def html_attributes
177
+ super.tap { |attributes| attributes[:role] = "group" }
178
+ end
179
+ end
180
+
181
+ class RadioItem < CheckboxItem
182
+ sig { override.returns(T::Hash[Symbol, T.untyped]) }
183
+ def html_attributes
184
+ attributes = super
185
+ attributes[:role] = "menuitemradio"
186
+ attributes
187
+ end
188
+
189
+ private
190
+
191
+ sig { returns(T.nilable(String)) }
192
+ def indicator_slot_name
193
+ "dropdown-menu-radio-item-indicator"
194
+ end
195
+
196
+ # ラジオはドット表示
197
+ sig { returns(String) }
198
+ def indicator
199
+ content_tag(:span, class: indicator_class, data: { slot: indicator_slot_name }) do
200
+ @checked ? content_tag(:span, class: "size-2 rounded-full bg-current") { "".html_safe } : "".html_safe
201
+ end
202
+ end
203
+ end
204
+
205
+ class Separator < BaseComponent
206
+ # div(契約タグ SeparatorPrimitive は div 相当)
207
+ end
208
+
209
+ class Shortcut < BaseComponent
210
+ # span
211
+ end
212
+
213
+ class Sub < BaseComponent
214
+ # ネストしたサブメニューのスコープ。データ属性のみで構造を持つ
215
+ end
216
+
217
+ class SubTrigger < Item
218
+ sig { override.returns(T::Hash[Symbol, T.untyped]) }
219
+ def html_attributes
220
+ attributes = super
221
+ merge_nested(attributes, :aria, { haspopup: "menu", expanded: "false" })
222
+ attributes
223
+ end
224
+
225
+ private
226
+
227
+ sig { override.returns(String) }
228
+ def default_action
229
+ "click->#{controller_identifier}#toggleSub"
230
+ end
231
+ end
232
+
233
+ class SubContent < Content
234
+ FLOATING_POSITION_DEFAULTS = T.let(
235
+ { side: :right, align: :start, side_offset: 0, align_offset: -3, collision_padding: 5 }.freeze,
236
+ T::Hash[Symbol, T.untyped]
237
+ )
238
+
239
+ # 親Contentと同じ popover=auto。ネストはコントローラが処理する
240
+ end
241
+ end
242
+ end