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,78 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Shadcn
5
+ # 素の select/optgroup/option(JSレス — 05 §3)。ラッパーdiv + select + chevron。
6
+ # 利用者属性(name/id/required 等)はすべて実際の <select> に渡す
7
+ # (ラッパーは gem 内部要素のため name 等が届かないフォーム送信の破壊を防ぐ)
8
+ class NativeSelect < BaseComponent
9
+ sig { params(size: T.any(Symbol, String), args: T::Hash[Symbol, T.untyped]).void.checked(:never) }
10
+ def initialize(size: self.class.property_default(:size), **args)
11
+ @size = T.let(normalize_property(:size, size), String)
12
+ super(**args)
13
+ end
14
+
15
+ sig { override.returns(String) }
16
+ def call
17
+ # base-nova ではユーザークラス(className)はラッパーへ流れる
18
+ content_tag(:div, class: wrapper_class, data: { slot: "native-select-wrapper", size: @size }) do
19
+ safe_join([select_element, chevron_icon])
20
+ end
21
+ end
22
+
23
+ private
24
+
25
+ # 選択肢は OptGroup / Option コンポーネントで content に渡す
26
+ sig { returns(String) }
27
+ def select_element
28
+ attributes = @html_args.merge(class: select_class)
29
+ merge_nested(attributes, :data, { slot: "native-select", size: @size })
30
+ content_tag(:select, **attributes) { content }
31
+ end
32
+
33
+ # ラッパーのクラスは契約combination(group/native-select ...)
34
+ sig { returns(String) }
35
+ def wrapper_class
36
+ ShadcnViewComponents::Classes.resolve(:native_select, extra: @user_class.to_s)
37
+ end
38
+
39
+ # select 要素のクラスは契約スロットの静的クラス
40
+ sig { returns(T.nilable(String)) }
41
+ def select_class
42
+ T.cast(contract_slot("native-select").dig(:static_attributes, :class), T.nilable(String))
43
+ end
44
+
45
+ sig { returns(String) }
46
+ def chevron_icon
47
+ content_tag(
48
+ :svg,
49
+ xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 24 24", fill: "none",
50
+ stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round",
51
+ width: "16", height: "16",
52
+ class: T.cast(contract_slot("native-select-icon").dig(:static_attributes, :class), T.nilable(String)),
53
+ aria: { hidden: "true" }, data: { slot: "native-select-icon" }
54
+ ) { raw(%(<path d="m6 9 6 6 6-6"/>)) }
55
+ end
56
+
57
+ class OptGroup < BaseComponent
58
+ # optgroup
59
+ sig { override.returns(String) }
60
+ def default_tag
61
+ "optgroup"
62
+ end
63
+ end
64
+
65
+ class Option < BaseComponent
66
+ # option(内容はテキストのみ)
67
+ sig { override.returns(String) }
68
+ def default_tag
69
+ "option"
70
+ end
71
+
72
+ sig { override.returns(String) }
73
+ def call
74
+ content_tag(tag, **html_attributes) { content.to_s }
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,122 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Shadcn
5
+ # ナビゲーションメニュー(10-roadmap Phase 3)。
6
+ # 通常のnav/linkと開閉領域をnavigation_menu_controller(Popover API)で管理する。
7
+ # ARIA menuではないためLinkはネイティブのTab/Enter操作を維持する。
8
+ # JS無効時フォールバック: Readable(リンク自体はSSR済みで辿れる)
9
+ class NavigationMenu < BaseComponent
10
+ CONTROLLER = "shadcn--navigation-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
+ sig { override.returns(String) }
18
+ def default_tag
19
+ "nav"
20
+ end
21
+
22
+ class List < BaseComponent
23
+ # ul
24
+ sig { override.returns(String) }
25
+ def default_tag
26
+ "ul"
27
+ end
28
+ end
29
+
30
+ class Item < BaseComponent
31
+ # li
32
+ sig { override.returns(String) }
33
+ def default_tag
34
+ "li"
35
+ end
36
+ end
37
+
38
+ class Trigger < BaseComponent
39
+ sig { override.returns(String) }
40
+ def default_tag
41
+ "button"
42
+ end
43
+
44
+ sig { override.returns(T::Hash[Symbol, T.untyped]) }
45
+ def html_attributes
46
+ attributes = super
47
+ attributes[:type] = "button" unless attributes.key?(:type)
48
+ merge_nested(attributes, :aria, { expanded: "false" })
49
+ merge_nested(attributes, :data, { action: "#{CONTROLLER}#toggle" })
50
+ attributes
51
+ end
52
+
53
+ # upstream は chevron を含む(静的クラスは契約スロット外)
54
+ sig { override.returns(String) }
55
+ def call
56
+ content_tag(tag, **html_attributes) do
57
+ safe_join([content, chevron_icon])
58
+ end
59
+ end
60
+
61
+ private
62
+
63
+ sig { returns(String) }
64
+ def chevron_icon
65
+ content_tag(
66
+ :svg,
67
+ xmlns: "http://www.w3.org/2000/svg",
68
+ viewBox: "0 0 24 24",
69
+ fill: "none",
70
+ stroke: "currentColor",
71
+ "stroke-width": "2",
72
+ "stroke-linecap": "round",
73
+ "stroke-linejoin": "round",
74
+ width: "12",
75
+ height: "12",
76
+ class: "relative top-px ml-1 size-3 transition duration-300 group-data-popup-open/navigation-menu-trigger:rotate-180 group-data-open/navigation-menu-trigger:rotate-180",
77
+ aria: { hidden: "true" }
78
+ ) do
79
+ raw(%(<path d="m6 9 6 6 6-6"/>))
80
+ end
81
+ end
82
+ end
83
+
84
+ class Link < BaseComponent
85
+ # a
86
+ sig { override.returns(String) }
87
+ def default_tag
88
+ "a"
89
+ end
90
+ end
91
+
92
+ class Content < BaseComponent
93
+ include Shadcn::FloatingPositionOptions
94
+
95
+ FLOATING_POSITION_DEFAULTS = T.let(
96
+ { side: :bottom, align: :start, side_offset: 8, align_offset: 0, collision_padding: 5 }.freeze,
97
+ T::Hash[Symbol, T.untyped]
98
+ )
99
+
100
+ # Popover API で開閉。中身のリンク構造は利用者が組む。
101
+ sig { override.returns(T::Hash[Symbol, T.untyped]) }
102
+ def html_attributes
103
+ attributes = super
104
+ attributes[:popover] = "auto"
105
+ merge_floating_position_data(attributes, state: "closed")
106
+ end
107
+ end
108
+
109
+ class Indicator < BaseComponent
110
+ # 装飾要素(矢印)。既定では非表示
111
+ sig { override.returns(String) }
112
+ def call
113
+ content_tag(tag, **html_attributes) do
114
+ content_tag(:div, class: "relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm bg-border shadow-md") { "".html_safe }
115
+ end
116
+ end
117
+ end
118
+
119
+ # base-nova では Viewport が廃止され Positioner(Portal 由来の位置決め)に置き換わった。
120
+ # 本gemは viewport/positioner なしで Content を直接表示する設計のため、両方を移植しない
121
+ end
122
+ end
@@ -0,0 +1,160 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Shadcn
5
+ # リンクベースのページネーション(JSレス — 10-roadmap Phase 2)。
6
+ # JS無効時フォールバック: Graceful(リンク遷移のみで完全動作)
7
+ # Previous/Next は upstream が PaginationLink を内側で描く構成のため、
8
+ # ルートスロットを Link から継承した契約になっている(抽出器の兄弟継承)
9
+ class Pagination < BaseComponent
10
+ # nav。契約の静的属性(role="navigation" / aria-label="pagination")は
11
+ # BaseComponent#static_attribute_defaults が自動で付与する
12
+
13
+ # Previous/Next/Ellipsis 共通のインラインSVG(lucide相当)。
14
+ # コンポーネントクラス外のモジュールからでもヘルパが解決できるよう、必要な
15
+ # ActionViewヘルパを明示的にincludeする(Sorbetの解決対象にもなる)
16
+ module IconHelpers
17
+ extend T::Sig
18
+ include ActionView::Helpers::OutputSafetyHelper
19
+ include ActionView::Helpers::TagHelper
20
+
21
+ private
22
+
23
+ sig { params(paths: String).returns(String) }
24
+ def pagination_icon(paths)
25
+ content_tag(
26
+ :svg,
27
+ xmlns: "http://www.w3.org/2000/svg",
28
+ viewBox: "0 0 24 24",
29
+ fill: "none",
30
+ stroke: "currentColor",
31
+ "stroke-width": "2",
32
+ "stroke-linecap": "round",
33
+ "stroke-linejoin": "round",
34
+ width: "16",
35
+ height: "16",
36
+ class: "size-4",
37
+ aria: { hidden: "true" }
38
+ ) do
39
+ raw(paths)
40
+ end
41
+ end
42
+ end
43
+
44
+ class Content < BaseComponent
45
+ # ul
46
+ end
47
+
48
+ class Item < BaseComponent
49
+ # li
50
+ end
51
+
52
+ class Link < BaseComponent
53
+ # base-nova は Button装飾(variant/size)をrender propで<a>へ合成する。
54
+ # 本gemもButton契約からクラスを合成して同等の<a>を描く(契約クラス自体は空)
55
+ sig { override.returns(String) }
56
+ def default_tag
57
+ "a"
58
+ end
59
+
60
+ sig do
61
+ params(
62
+ is_active: T.nilable(T.any(Symbol, String, T::Boolean)),
63
+ size: T.any(Symbol, String),
64
+ args: T::Hash[Symbol, T.untyped]
65
+ ).void.checked(:never)
66
+ end
67
+ def initialize(is_active: nil, size: :icon, **args)
68
+ @is_active = T.let(is_active.to_s == "true", T::Boolean)
69
+ @size = T.let(ShadcnViewComponents::Classes.normalize_option(:button, :size, size), Symbol)
70
+ super(**args)
71
+ end
72
+
73
+ sig { returns(T::Boolean) }
74
+ def active?
75
+ @is_active
76
+ end
77
+
78
+ sig { override.returns(T::Hash[Symbol, T.untyped]) }
79
+ def html_attributes
80
+ # NOTE: Hashの値型は不変のため、Classes.resolve の options と同じ型で T.let する
81
+ button_options = T.let({ variant: active? ? :outline : :ghost, size: @size },
82
+ T::Hash[Symbol, T.nilable(T.any(Symbol, String))])
83
+ attributes = @html_args.merge(
84
+ class: ShadcnViewComponents::Classes.resolve(:button, extra: @user_class.to_s, **button_options)
85
+ )
86
+ merge_nested(attributes, :data, { slot: "pagination-link", active: active?.to_s })
87
+ attributes[:aria] = { current: "page" } if active?
88
+ attributes
89
+ end
90
+ end
91
+
92
+ class Previous < BaseComponent
93
+ include IconHelpers
94
+
95
+ # upstream は Link(size="default")を描き、固定の子(chevron + ラベル)を足す
96
+ sig { override.returns(String) }
97
+ def call
98
+ render(Link.new(size: :default, class: self.class.classes(extra: @user_class), **link_attributes)) do
99
+ safe_join([pagination_icon(%(<path d="m15 18-6-6 6-6"/>)), default_label])
100
+ end
101
+ end
102
+
103
+ private
104
+
105
+ sig { returns(T::Hash[Symbol, T.untyped]) }
106
+ def link_attributes
107
+ attributes = @html_args.dup
108
+ aria = T.cast(attributes[:aria], T.nilable(T::Hash[Symbol, T.untyped])) || {}
109
+ attributes[:aria] = { label: "Go to previous page" }.merge(aria)
110
+ attributes
111
+ end
112
+
113
+ sig { returns(String) }
114
+ def default_label
115
+ content.presence || content_tag(:span, class: "hidden sm:block") { "Previous" }
116
+ end
117
+ end
118
+
119
+ class Next < BaseComponent
120
+ include IconHelpers
121
+
122
+ sig { override.returns(String) }
123
+ def call
124
+ render(Link.new(size: :default, class: self.class.classes(extra: @user_class), **link_attributes)) do
125
+ safe_join([default_label, pagination_icon(%(<path d="m9 18 6-6-6-6"/>))])
126
+ end
127
+ end
128
+
129
+ private
130
+
131
+ sig { returns(T::Hash[Symbol, T.untyped]) }
132
+ def link_attributes
133
+ attributes = @html_args.dup
134
+ aria = T.cast(attributes[:aria], T.nilable(T::Hash[Symbol, T.untyped])) || {}
135
+ attributes[:aria] = { label: "Go to next page" }.merge(aria)
136
+ attributes
137
+ end
138
+
139
+ sig { returns(String) }
140
+ def default_label
141
+ content.presence || content_tag(:span, class: "hidden sm:block") { "Next" }
142
+ end
143
+ end
144
+
145
+ class Ellipsis < BaseComponent
146
+ include IconHelpers
147
+
148
+ # span(aria-hidden)。lucide の ellipsis アイコンと sr-only ラベルを含む
149
+ sig { override.returns(String) }
150
+ def call
151
+ content_tag(tag, **html_attributes) do
152
+ safe_join([
153
+ pagination_icon(%(<circle cx="12" cy="12" r="1"/><circle cx="19" cy="12" r="1"/><circle cx="5" cy="12" r="1"/>)),
154
+ content_tag(:span, class: "sr-only") { "More pages" }
155
+ ])
156
+ end
157
+ end
158
+ end
159
+ end
160
+ end
@@ -0,0 +1,68 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Shadcn
5
+ # Popover API によるポップオーバー(10-roadmap Phase 3「popover = Popover API」)。
6
+ # popover="auto" で軽量な外側クリック解散をネイティブに任せ、コントローラは
7
+ # 開閉同期と位置合わせ(anchor相当)を担う
8
+ # JS無効時フォールバック: Readable(展開はできないが本文はSSR済み)
9
+ class Popover < BaseComponent
10
+ CONTROLLER = "shadcn--popover"
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, { expanded: "false", haspopup: "true" })
30
+ merge_nested(attributes, :data, { action: "#{CONTROLLER}#toggle" })
31
+ attributes
32
+ end
33
+ end
34
+
35
+ # base-nova では Anchor が廃止されたため非対応(位置決めはTrigger基準)
36
+
37
+ class Content < BaseComponent
38
+ include Shadcn::FloatingPositionOptions
39
+
40
+ FLOATING_POSITION_DEFAULTS = T.let(
41
+ { side: :bottom, align: :center, side_offset: 4, align_offset: 0, collision_padding: 5 }.freeze,
42
+ T::Hash[Symbol, T.untyped]
43
+ )
44
+
45
+ sig { override.returns(T::Hash[Symbol, T.untyped]) }
46
+ def html_attributes
47
+ attributes = super
48
+ attributes[:popover] = "auto"
49
+ merge_floating_position_data(attributes, state: "closed")
50
+ end
51
+ end
52
+
53
+ class Header < BaseComponent
54
+ # div
55
+ end
56
+
57
+ class Title < BaseComponent
58
+ # div(契約タグは h2 の型注記だが実要素は div)
59
+ end
60
+
61
+ class Description < BaseComponent
62
+ sig { override.returns(String) }
63
+ def default_tag
64
+ "p"
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,107 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Shadcn
5
+ # Lookbook / ViewComponent プレビュー用の基底クラス。
6
+ #
7
+ # ViewComponent::Preview#render はコンポーネントを描画せず、
8
+ # {args:, block:, component:, locals:, template:} からなる描画予約のディスクリプタを返す。
9
+ # このためブロック内でネストした render の戻り値を組み合わせる記法
10
+ # (safe_join([render(Header.new) { ... }, render(Content.new) { ... }]) 等)では
11
+ # ディスクリプタの inspect 文字列がそのままプレビューに露出してしまう。
12
+ #
13
+ # この基底クラスの render は常に即座にHTML文字列を描画して返すため、
14
+ # 単一ルート・ネスト・兄弟連結のどの記法でもそのまま組み合わせられる。
15
+ # example の戻り値は render_args が Fragment 経由でディスクリプタに正規化する。
16
+ # ホストアプリが独自のプレビューを書く際にも同様に利用できる。
17
+ class PreviewBase < ViewComponent::Preview
18
+ extend T::Sig
19
+
20
+ # 描画済みHTML断片を1つのコンポーネントとして扱うラッパー。
21
+ # ViewComponent::Preview のディスクリプタは最終的にビューの render 経由で
22
+ # 描画されるため、render_in に応答するオブジェクトであればよい
23
+ class Fragment
24
+ extend T::Sig
25
+
26
+ sig { params(html: T.untyped).void }
27
+ def initialize(html:)
28
+ super()
29
+ @html = T.let(html, T.untyped)
30
+ end
31
+
32
+ # ビューの render はブロック付きで呼ぶが、断片では使わないため受けない
33
+ sig { params(_view_context: T.untyped).returns(T.untyped) }
34
+ def render_in(_view_context)
35
+ @html
36
+ end
37
+ end
38
+
39
+ # ViewComponent::Preview.render_args はexampleの戻り値にHashを期待するが、
40
+ # この基底クラスのexampleはHTML文字列(SafeBuffer)を返すため、Fragment で包んで
41
+ # ディスクリプタに正規化する。それ以外は VC の挙動(example検証・params渡し・
42
+ # テンプレートフォールバック・layout)を踏襲する
43
+ sig { params(example: T.any(Symbol, String), params: T.untyped).returns(T.untyped) }
44
+ def self.render_args(example, params: {})
45
+ example = example.to_s
46
+ raise AbstractController::ActionNotFound, "#{example} is not a valid preview example" unless examples.include?(example)
47
+
48
+ result = normalize_render_result(invoke_example(example, params))
49
+ result[:template] = preview_example_template_path(example) if result[:template].nil?
50
+ result.merge(layout: instance_variable_get(:@layout))
51
+ end
52
+
53
+ # example を呼び出す。宣言済みの引数名と一致するparamsだけを渡す(VCの挙動と同じ)
54
+ sig { params(example: String, params: T.untyped).returns(T.untyped) }
55
+ def self.invoke_example(example, params)
56
+ example_params_names = instance_method(example).parameters.map(&:last)
57
+ provided_params = params.slice(*example_params_names).to_h.symbolize_keys
58
+ provided_params.empty? ? new.public_send(example) : new.public_send(example, **provided_params)
59
+ end
60
+
61
+ # VC標準のディスクリプタ Hash はそのまま、それ以外(HTML文字列等)は Fragment で包む
62
+ sig { params(result: T.untyped).returns(T.untyped) }
63
+ def self.normalize_render_result(result)
64
+ return result if result.is_a?(Hash)
65
+
66
+ {
67
+ args: {},
68
+ block: nil,
69
+ component: Fragment.new(html: result),
70
+ locals: {},
71
+ template: "view_components/preview"
72
+ }
73
+ end
74
+
75
+ sig { void }
76
+ def initialize
77
+ super
78
+ @preview_view_context = T.let(nil, T.untyped)
79
+ end
80
+
81
+ private
82
+
83
+ # example メソッドからの暗黓スコープでの呼び出しのみを想定するため private。
84
+ # public にすると ViewComponent::Preview.examples(public_instance_methods)に
85
+ # 「render」が載り、この基底クラス自体がプレビュー一覧に出てしまう
86
+ #
87
+ # rest-kwargs を持つメソッドの実行時sig検証は sorbet-runtime の既知の誤バインドがあるため
88
+ # checked(:never) で無効化し、srb tc の静的検査に委譲する(BaseComponent と同じ運用)
89
+ sig do
90
+ params(component: T.untyped, args: T.untyped, block: T.nilable(T.proc.returns(T.untyped)))
91
+ .returns(T.untyped).checked(:never)
92
+ end
93
+ def render(component, **args, &block)
94
+ component.render_in(preview_view_context, **args, &block)
95
+ end
96
+
97
+ # コンポーネントの即時描画に使う単独のビューコンテキスト。
98
+ # Lookbook のプレビュー出力はexampleの戻り値(HTML文字列)だけで構成されるため、
99
+ # ルートとネストでコンテキストが分かれても出力には影響しない。
100
+ # NOTE: 素の ActionController::Base から作るためURLヘルパー等は使えない。
101
+ # 本gemのコンポーネントは属性の出力のみでURLヘルパーに依存しない
102
+ sig { returns(T.untyped) }
103
+ def preview_view_context
104
+ @preview_view_context ||= ActionController::Base.new.view_context
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,49 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Shadcn
5
+ # 進捗バー。role=progressbar と aria 値をネイティブARIAで表現する
6
+ class Progress < BaseComponent
7
+ sig do
8
+ params(
9
+ value: T.nilable(T.any(Integer, Float, String)),
10
+ args: T::Hash[Symbol, T.untyped]
11
+ ).void.checked(:never)
12
+ end
13
+ def initialize(value: self.class.property_default(:value), **args)
14
+ @value = T.let(normalize_property(:value, value), T.nilable(T.any(Integer, Float)))
15
+ super(**args)
16
+ end
17
+
18
+ sig { override.returns(String) }
19
+ def call
20
+ content_tag(tag, **html_attributes) do
21
+ safe_join([content, progress_track])
22
+ end
23
+ end
24
+
25
+ sig { override.returns(T::Hash[Symbol, T.untyped]) }
26
+ def html_attributes
27
+ attributes = super
28
+ attributes[:role] = "progressbar"
29
+ aria_defaults = T.let({ valuemin: "0", valuemax: "100" }, T::Hash[Symbol, T.untyped])
30
+ aria_defaults[:valuenow] = @value.to_s if @value
31
+ merge_nested(attributes, :aria, aria_defaults)
32
+ attributes
33
+ end
34
+
35
+ private
36
+
37
+ sig { returns(String) }
38
+ def progress_track
39
+ content_tag(:div, class: ShadcnViewComponents::Classes.resolve(:progress_track), data: { slot: "progress-track" }) do
40
+ content_tag(
41
+ :div,
42
+ class: ShadcnViewComponents::Classes.resolve(:progress_indicator),
43
+ style: "width: #{@value || 0}%",
44
+ data: { slot: "progress-indicator" }
45
+ ) { "".html_safe }
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,74 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Shadcn
5
+ class RadioGroup < BaseComponent
6
+ # ネイティブなラジオボタン群(JS無しで動作 — 05-stimulus-hotwire §3)。
7
+ # name は利用者が指定する(素のinputなのでそのまま連携する)
8
+ sig { override.returns(T::Hash[Symbol, T.untyped]) }
9
+ def html_attributes
10
+ attributes = super
11
+ attributes[:role] = "radiogroup" unless @html_args.key?(:role)
12
+ attributes
13
+ end
14
+
15
+ class Item < BaseComponent
16
+ include NativeCheckedState
17
+
18
+ NATIVE_STATE_CLASS = T.let([
19
+ "not-checked:border-input not-checked:bg-transparent dark:not-checked:bg-input/30",
20
+ "checked:border-primary checked:bg-primary checked:text-primary-foreground",
21
+ "group-has-[:focus-visible]/field-label:checked:border-primary dark:checked:bg-primary"
22
+ ].join(" ").freeze, String)
23
+
24
+ # 契約タグは RadioGroupPrimitive.Item。ネイティブな input[type=radio] として描く
25
+ sig { override.returns(String) }
26
+ def default_tag
27
+ "input"
28
+ end
29
+
30
+ sig { override.returns(String) }
31
+ def call
32
+ content_tag(:span, class: "relative flex w-fit") do
33
+ safe_join([input_element, indicator_element])
34
+ end
35
+ end
36
+
37
+ private
38
+
39
+ sig { returns(String) }
40
+ def input_element
41
+ attributes = html_attributes.merge(type: "radio")
42
+ # ネイティブウィジェット描画を消す(dark時の白箱問題 — checkbox.rb と同じ理由)。
43
+ # 印の表示は indicator を peer-checked で出すため peer も付与する
44
+ attributes[:class] = [attributes[:class], "appearance-none peer", NATIVE_STATE_CLASS].compact.join(" ")
45
+ merge_native_checked_state(attributes, checked: native_checked?(@html_args))
46
+ void_tag("input", **attributes)
47
+ end
48
+
49
+ # 契約のスロット構造(radio-group-indicator)を保つ。普段は非表示で、
50
+ # peer であるinputのチェック状態に連動して出す(upstreamのIndicatorと同じ挙動)
51
+ sig { returns(String) }
52
+ def indicator_element
53
+ indicator_class = T.cast(contract_slot("radio-group-indicator").dig(:static_attributes, :class), T.nilable(String))
54
+ content_tag(
55
+ :span,
56
+ data: { slot: "radio-group-indicator" },
57
+ aria: { hidden: "true" },
58
+ class: [indicator_class, "pointer-events-none absolute inset-0 hidden peer-checked:flex"].compact.join(" ")
59
+ ) do
60
+ content_tag(
61
+ :svg,
62
+ xmlns: "http://www.w3.org/2000/svg",
63
+ viewBox: "0 0 24 24",
64
+ fill: "none",
65
+ stroke: "none",
66
+ class: "absolute top-1/2 left-1/2 size-2 -translate-x-1/2 -translate-y-1/2 fill-current"
67
+ ) do
68
+ raw(%(<circle cx="12" cy="12" r="10"/>))
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end