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,276 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Shadcn
5
+ # 月表示カレンダー(10-roadmap Phase 4 の個別評価ルート)。
6
+ # upstream は react-day-picker を使うが、本gemは依存せず月テーブルを
7
+ # サーバ側で描く(見出し・曜日・日付ボタン)。年月の移動は GET リンクの
8
+ # サーバラウンドトリップで行い、JSは表示中のgrid内のフォーカス移動だけを補助する。
9
+ # JS無効時も月移動と日付ボタンは利用できる(05 §5 Graceful)
10
+ #
11
+ # 契約クラスは lib/shadcn_view_components/contracts/calendar.rb に
12
+ # 手動で保守する(静的抽出の対象外のため再生成されない点に注意)
13
+ class Calendar < BaseComponent
14
+ CONTROLLER = "shadcn--calendar"
15
+ START_OF_WEEK = 0 # 日曜始まり
16
+ WEEKDAY_LABELS = %w[日 月 火 水 木 金 土].freeze
17
+
18
+ sig do
19
+ params(
20
+ month: Date,
21
+ selected: T.nilable(Date),
22
+ month_path: T.nilable(String),
23
+ args: T::Hash[Symbol, T.untyped]
24
+ ).void.checked(:never)
25
+ end
26
+ def initialize(month: Date.current.beginning_of_month, selected: nil, month_path: nil, **args)
27
+ assign_accessibility_root_id(args, prefix: "calendar")
28
+ @root_id = T.let(args.fetch(:id).to_s, String)
29
+ @month = month
30
+ @selected = selected
31
+ # 年月移動リンクの送り先(例: pages_calendar_path)。未指定の時はリンクを出さない
32
+ @month_path = month_path
33
+ super(**args)
34
+ end
35
+
36
+ sig { override.returns(String) }
37
+ def call
38
+ content_tag(:div, **html_attributes) do
39
+ # ナビはキャプション行に絶対配置で重なる(upstream rdp と同じ構造)
40
+ navigation = Navigation.new(month: @month, caption_id: caption_id, month_path: @month_path)
41
+ safe_join([render(navigation), month_grid])
42
+ end
43
+ end
44
+
45
+ # キャプション行 + 前後月ボタン群。ボタン群はキャプション行の上に絶対配置する
46
+ # (month_path が与えられたときのみリンクになる)
47
+ class Navigation < BaseComponent
48
+ sig do
49
+ params(
50
+ month: Date,
51
+ caption_id: String,
52
+ month_path: T.nilable(String),
53
+ args: T::Hash[Symbol, T.untyped]
54
+ ).void.checked(:never)
55
+ end
56
+ def initialize(month:, caption_id:, month_path: nil, **args)
57
+ @month = month
58
+ @month_path = month_path
59
+ @caption_id = T.let(caption_id, String)
60
+ super(**args)
61
+ end
62
+
63
+ sig { override.returns(String) }
64
+ def call
65
+ safe_join([caption_row, nav_buttons])
66
+ end
67
+
68
+ private
69
+
70
+ # 月タイトルの行(upstream month_caption 相当)
71
+ sig { returns(String) }
72
+ def caption_row
73
+ row_class = "flex h-(--cell-size) w-full items-center justify-center px-(--cell-size) font-medium select-none text-sm"
74
+ content_tag(
75
+ :div,
76
+ id: @caption_id,
77
+ class: row_class,
78
+ data: { slot: "calendar-caption", shadcn_generated_id: "true" }
79
+ ) { "#{@month.year}年#{@month.month}月" }
80
+ end
81
+
82
+ sig { returns(String) }
83
+ def nav_buttons
84
+ content_tag(:div, class: "absolute inset-x-0 top-0 flex w-full items-center justify-between gap-1") do
85
+ safe_join([
86
+ nav_link(direction: :previous, label: "前の月") { chevron(:left) },
87
+ nav_link(direction: :next, label: "次の月") { chevron(:right) }
88
+ ])
89
+ end
90
+ end
91
+
92
+ sig { params(direction: Symbol, label: String, block: T.proc.returns(String)).returns(String) }
93
+ def nav_link(direction:, label:, &block)
94
+ target = direction == :previous ? @month.prev_month : @month.next_month
95
+ content_tag(:a,
96
+ href: "#{@month_path}?month=#{target.strftime('%Y-%m')}",
97
+ class: "inline-flex items-center justify-center rounded-lg size-(--cell-size) p-0 select-none " \
98
+ "hover:bg-muted hover:text-foreground outline-none aria-disabled:opacity-50",
99
+ aria: { label: label }) { block.call }
100
+ end
101
+
102
+ sig { params(direction: Symbol).returns(String) }
103
+ def chevron(direction)
104
+ path = direction == :left ? %(<path d="m15 18-6-6 6-6"/>) : %(<path d="m9 18 6-6-6-6"/>)
105
+ content_tag(
106
+ :svg,
107
+ xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 24 24", fill: "none",
108
+ stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round",
109
+ width: "16", height: "16", class: "size-4",
110
+ aria: { hidden: "true" }
111
+ ) { raw(path) }
112
+ end
113
+ end
114
+
115
+ class DayButton < BaseComponent
116
+ # 1日分のボタン(契約クラスは手動契約モジュールを参照)
117
+ sig do
118
+ params(
119
+ date: Date,
120
+ selected: T::Boolean,
121
+ outside: T::Boolean,
122
+ today: T::Boolean,
123
+ args: T::Hash[Symbol, T.untyped]
124
+ ).void.checked(:never)
125
+ end
126
+ def initialize(date: Date.current, selected: false, outside: false, today: false, **args)
127
+ @date = date
128
+ @selected = selected
129
+ @outside = outside
130
+ @today = today
131
+ super(**args)
132
+ end
133
+
134
+ sig { override.returns(String) }
135
+ def default_tag
136
+ "button"
137
+ end
138
+
139
+ sig { override.returns(T::Hash[Symbol, T.untyped]) }
140
+ def html_attributes
141
+ attributes = super
142
+ attributes[:type] = "button"
143
+ attributes[:tabindex] = "-1" if @outside && !attributes.key?(:tabindex)
144
+ merge_nested(
145
+ attributes,
146
+ :aria,
147
+ { label: full_date_label }.merge(@today ? { current: "date" } : {})
148
+ )
149
+ merge_nested(attributes, :data, {
150
+ day: @date.iso8601,
151
+ selected_single: @selected.to_s,
152
+ today: @today.to_s,
153
+ outside: @outside.to_s
154
+ })
155
+ attributes
156
+ end
157
+
158
+ sig { returns(String) }
159
+ def full_date_label
160
+ "#{@date.year}年#{@date.month}月#{@date.day}日"
161
+ end
162
+
163
+ sig { override.returns(String) }
164
+ def call
165
+ content_tag(tag, **html_attributes) { @date.day.to_s }
166
+ end
167
+ end
168
+
169
+ private
170
+
171
+ sig { returns(T::Hash[Symbol, T.untyped]) }
172
+ def html_attributes
173
+ # flex コンテナ化(契約クラスの冒頭)は重要: block のままだと内側 table
174
+ # (width:100%)の自然幅が利用幅に解決されて w-fit が効かなくなる
175
+ attributes = @html_args.merge(class: self.class.classes(extra: @user_class))
176
+ data = T.cast(attributes[:data], T.nilable(T::Hash[Symbol, T.untyped])) || {}
177
+ attributes[:data] = {
178
+ slot: "calendar",
179
+ controller: CONTROLLER,
180
+ action: "keydown->#{CONTROLLER}#navigate"
181
+ }.merge(data)
182
+ attributes
183
+ end
184
+
185
+ sig { returns(String) }
186
+ def caption_id = "#{@root_id}-caption"
187
+
188
+ sig { returns(String) }
189
+ def grid_id = "#{@root_id}-grid"
190
+
191
+ sig { returns(String) }
192
+ def month_grid
193
+ # 幅は 7×(--cell-size)(= spacing 7 × 7 = w-49)に固定する(upstream rdp の
194
+ # month_grid と同じ 196px)。w-full / w-fit の親子で幅を決めると Chrome の
195
+ # table 自然幅計算が利用幅に解決され、セルが --cell-size より広がり、
196
+ # upstream と寸法が合わなくなる
197
+ content_tag(
198
+ :table,
199
+ id: grid_id,
200
+ role: "grid",
201
+ class: "w-49 border-collapse",
202
+ aria: { labelledby: caption_id },
203
+ data: { slot: "calendar-grid", shadcn_generated_id: "true" }
204
+ ) do
205
+ safe_join([weekday_header, weeks_body])
206
+ end
207
+ end
208
+
209
+ sig { returns(String) }
210
+ def weekday_header
211
+ content_tag(:thead) do
212
+ content_tag(:tr, class: "flex") do
213
+ safe_join(WEEKDAY_LABELS.map do |label|
214
+ content_tag(:th,
215
+ class: "flex-1 rounded-md text-[0.8rem] font-normal text-muted-foreground select-none",
216
+ scope: "col", abbr: label) { label }
217
+ end)
218
+ end
219
+ end
220
+ end
221
+
222
+ sig { returns(String) }
223
+ def weeks_body
224
+ content_tag(:tbody) do
225
+ safe_join(month_weeks.map do |week|
226
+ content_tag(:tr, class: "mt-2 flex w-full") do
227
+ safe_join(week.map { |date| day_cell(date) })
228
+ end
229
+ end)
230
+ end
231
+ end
232
+
233
+ # 月を週(日曜始まり)ごとに分割する。前月末尾・翌月先頭は outside 扱いで補完する
234
+ sig { returns(T::Array[T::Array[Date]]) }
235
+ def month_weeks
236
+ first = @month.beginning_of_month
237
+ last = @month.end_of_month
238
+ start_date = first - ((first.wday - START_OF_WEEK) % 7)
239
+ end_date = last + ((START_OF_WEEK - 1 - last.wday) % 7)
240
+ (start_date..end_date).each_slice(7).to_a
241
+ end
242
+
243
+ sig { params(date: Date).returns(String) }
244
+ def day_cell(date)
245
+ today = date == Date.current
246
+ outside = date.month != @month.month
247
+ selected = date == @selected
248
+ # w-full を付けない: td が table 幅の 100% を要求すると 7列分の割合指定で
249
+ # table 幅が利用幅まで引き延ばされてセルが --cell-size より広くなる
250
+ cell_class = +"group/day relative aspect-square h-full p-0 text-center select-none " \
251
+ "[&:last-child[data-selected=true]_button]:rounded-r-(--cell-radius) " \
252
+ "[&:first-child[data-selected=true]_button]:rounded-l-(--cell-radius)"
253
+ cell_class << " rounded-(--cell-radius) bg-muted text-foreground data-[selected=true]:rounded-none" if today
254
+ cell_class << " text-muted-foreground aria-selected:text-muted-foreground" if outside
255
+
256
+ data = { selected: selected.to_s, today: today.to_s, outside: outside.to_s }
257
+ content_tag(:td, role: "gridcell", class: cell_class, aria: { selected: selected.to_s }, data: data) do
258
+ render(DayButton.new(
259
+ date: date,
260
+ selected: selected,
261
+ outside: outside,
262
+ today: today,
263
+ **T.unsafe({ tabindex: date == focus_date ? "0" : "-1" })
264
+ ))
265
+ end
266
+ end
267
+
268
+ sig { returns(Date) }
269
+ def focus_date
270
+ dates = month_weeks.flatten
271
+ [@selected, Date.current, @month.beginning_of_month].compact.find { |date| dates.include?(date) } || @month
272
+ end
273
+
274
+ private_constant :Navigation
275
+ end
276
+ end
@@ -0,0 +1,18 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Shadcn
5
+ class Card < BaseComponent
6
+ class Header < BaseComponent; end
7
+
8
+ class Title < BaseComponent; end
9
+
10
+ class Description < BaseComponent; end
11
+
12
+ class Action < BaseComponent; end
13
+
14
+ class Content < BaseComponent; end
15
+
16
+ class Footer < BaseComponent; end
17
+ end
18
+ end
@@ -0,0 +1,236 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Shadcn
5
+ # scroll-snap 相当の縦横スクロール + ナビ補助(10-roadmap Phase 2)。
6
+ # upstream は embla-carousel を使うが、本gemは生のスクロール操作で再実装する
7
+ # (契約クラス・data-slot は upstream 由来のまま)
8
+ # JS無効時フォールバック: JS必須(スライド自体はSSR済みで読めるが、ナビは動作しない — 05 §5)
9
+ class Carousel < BaseComponent
10
+ CONTROLLER = "shadcn--carousel"
11
+ KEYDOWN_ACTION = T.let("keydown->#{CONTROLLER}#navigate".freeze, String)
12
+ ROOT_CLASS = "group/carousel"
13
+
14
+ private_constant :KEYDOWN_ACTION, :ROOT_CLASS
15
+
16
+ sig do
17
+ params(
18
+ orientation: T.any(Symbol, String),
19
+ direction: T.any(Symbol, String),
20
+ args: T::Hash[Symbol, T.untyped]
21
+ ).void.checked(:never)
22
+ end
23
+ def initialize(orientation: self.class.property_default(:orientation),
24
+ direction: self.class.property_default(:direction), **args)
25
+ assign_accessibility_root_id(args, prefix: "carousel")
26
+ @orientation = T.let(normalize_property(:orientation, orientation), String)
27
+ @direction = T.let(normalize_property(:direction, direction), String)
28
+ super(**args)
29
+ end
30
+
31
+ # upstream のJSXルートは Context.Provider ラッパーで、実要素はその内側の div。
32
+ # 契約上のルートスロット(carousel)から静的属性(role 等)を引く
33
+ sig { override.returns(T::Hash[Symbol, T.untyped]) }
34
+ def contract_root_slot
35
+ contract_slot("carousel")
36
+ end
37
+
38
+ sig { override.returns(T::Hash[Symbol, T.untyped]) }
39
+ def html_attributes
40
+ attributes = super
41
+ data = T.cast(attributes[:data], T.nilable(T::Hash[Symbol, T.untyped])) || {}
42
+ attributes[:data] = data.merge(
43
+ controller: compose_tokens(CONTROLLER, data[:controller]),
44
+ action: compose_tokens(KEYDOWN_ACTION, data[:action]),
45
+ orientation: @orientation,
46
+ direction: @direction
47
+ )
48
+ attributes[:dir] = @direction
49
+ attributes[:tabindex] = 0 unless attributes.key?(:tabindex)
50
+ attributes
51
+ end
52
+
53
+ sig { override.returns(String) }
54
+ def resolved_class
55
+ self.class.classes(extra: [ROOT_CLASS, @user_class].compact.join(" "))
56
+ end
57
+
58
+ class Content < BaseComponent
59
+ ORIENTATION_CLASS = "group-data-vertical/carousel:ml-0 group-data-vertical/carousel:-mt-4 " \
60
+ "group-data-vertical/carousel:flex-col"
61
+
62
+ private_constant :ORIENTATION_CLASS
63
+
64
+ # upstream は外側に overflow-hidden の viewport、内側に契約クラスを持つ
65
+ # 二重構造(accordion-content と同じ split 構造)
66
+ sig { override.returns(String) }
67
+ def call
68
+ attributes = @html_args.merge(class: root_static_class)
69
+ merge_nested(attributes, :data, { slot: contract_root_slot[:name] })
70
+ content_tag(tag, **attributes) do
71
+ extra = [ORIENTATION_CLASS, @user_class].compact.join(" ")
72
+ content_tag(:div, class: self.class.classes(extra:)) { content }
73
+ end
74
+ end
75
+
76
+ private
77
+
78
+ sig { returns(T.nilable(String)) }
79
+ def root_static_class
80
+ T.cast(contract_root_slot.dig(:static_attributes, :class), T.nilable(String))
81
+ end
82
+ end
83
+
84
+ class Item < BaseComponent
85
+ ORIENTATION_CLASS = "group-data-vertical/carousel:pl-0 group-data-vertical/carousel:pt-4"
86
+
87
+ private_constant :ORIENTATION_CLASS
88
+
89
+ # div(role="group" aria-roledescription="slide")。契約静的属性は自動付与
90
+ sig { override.returns(String) }
91
+ def resolved_class
92
+ self.class.classes(extra: [ORIENTATION_CLASS, @user_class].compact.join(" "))
93
+ end
94
+ end
95
+
96
+ # Previous/Next の共通部分: upstream は Button(outline/icon)に静的クラスを足して構成する
97
+ class Navigation < BaseComponent
98
+ extend T::Helpers
99
+
100
+ abstract!
101
+
102
+ ORIENTATION_CLASS = "group-data-vertical/carousel:inset-y-auto " \
103
+ "group-data-vertical/carousel:left-1/2 group-data-vertical/carousel:my-0 " \
104
+ "group-data-vertical/carousel:-translate-x-1/2 group-data-vertical/carousel:rotate-90"
105
+ RTL_ICON_CLASS = "group-[[data-orientation=horizontal][data-direction=rtl]]/carousel:rotate-180"
106
+
107
+ private_constant :ORIENTATION_CLASS, :RTL_ICON_CLASS
108
+
109
+ private
110
+
111
+ # Button へ渡す引数(バリアント既定 + 契約静的クラス + ナビ用data属性)。
112
+ # 利用者指定(variant 等の上書き)を優先する
113
+ sig { returns(T::Hash[Symbol, T.untyped]) }
114
+ def button_attributes
115
+ attributes = {
116
+ variant: :outline,
117
+ size: :"icon-sm",
118
+ type: "button",
119
+ disabled: true
120
+ }.merge(@html_args)
121
+ attributes[:class] = navigation_class
122
+ merge_navigation_data(attributes)
123
+ attributes
124
+ end
125
+
126
+ sig { returns(String) }
127
+ def navigation_class
128
+ extra = [ORIENTATION_CLASS, edge_orientation_class, @user_class].compact.join(" ")
129
+ self.class.classes(extra:)
130
+ end
131
+
132
+ sig { params(attributes: T::Hash[Symbol, T.untyped]).void }
133
+ def merge_navigation_data(attributes)
134
+ user_data = T.cast(attributes[:data], T.nilable(T::Hash[Symbol, T.untyped])) || {}
135
+ attributes[:data] = user_data.merge(
136
+ slot: self.class.contract.const_get(:ROOT_SLOT),
137
+ action: compose_tokens(navigation_action, user_data[:action])
138
+ )
139
+ end
140
+
141
+ # action のみ書き、controller は付けない: ルート(div[data-controller])の
142
+ # インスタンスへイベントを委任する(ボタン自前のインスタンスは viewport を持たない)
143
+ sig { returns(String) }
144
+ def navigation_action
145
+ "#{CONTROLLER}#scroll#{direction.camelize}"
146
+ end
147
+
148
+ sig { abstract.returns(String) }
149
+ def direction; end
150
+
151
+ sig { abstract.returns(String) }
152
+ def edge_orientation_class; end
153
+
154
+ sig { returns(String) }
155
+ def screen_reader_label
156
+ content_tag(:span, class: "sr-only") { "#{direction.camelize} slide" }
157
+ end
158
+
159
+ sig { params(paths: String).returns(String) }
160
+ def icon_svg(paths)
161
+ content_tag(
162
+ :svg,
163
+ xmlns: "http://www.w3.org/2000/svg",
164
+ viewBox: "0 0 24 24",
165
+ fill: "none",
166
+ stroke: "currentColor",
167
+ "stroke-width": "2",
168
+ "stroke-linecap": "round",
169
+ "stroke-linejoin": "round",
170
+ width: "16",
171
+ height: "16",
172
+ class: "size-4 #{RTL_ICON_CLASS}",
173
+ aria: { hidden: "true" }
174
+ ) do
175
+ raw(paths)
176
+ end
177
+ end
178
+
179
+ sig { params(required: String, supplied: T.untyped).returns(String) }
180
+ def compose_tokens(required, supplied)
181
+ ([required] + supplied.to_s.split).uniq.join(" ")
182
+ end
183
+ end
184
+
185
+ class Previous < Navigation
186
+ sig { override.returns(String) }
187
+ def call
188
+ render(Button.new(**button_attributes)) do
189
+ safe_join([icon_svg(%(<path d="m15 18-6-6 6-6"/>)), screen_reader_label])
190
+ end
191
+ end
192
+
193
+ private
194
+
195
+ sig { override.returns(String) }
196
+ def direction
197
+ "previous"
198
+ end
199
+
200
+ sig { override.returns(String) }
201
+ def edge_orientation_class
202
+ "group-data-vertical/carousel:-top-12"
203
+ end
204
+ end
205
+
206
+ class Next < Navigation
207
+ sig { override.returns(String) }
208
+ def call
209
+ render(Button.new(**button_attributes)) do
210
+ safe_join([icon_svg(%(<path d="m9 18 6-6-6-6"/>)), screen_reader_label])
211
+ end
212
+ end
213
+
214
+ private
215
+
216
+ sig { override.returns(String) }
217
+ def direction
218
+ "next"
219
+ end
220
+
221
+ sig { override.returns(String) }
222
+ def edge_orientation_class
223
+ "group-data-vertical/carousel:right-auto group-data-vertical/carousel:-bottom-12"
224
+ end
225
+ end
226
+
227
+ private
228
+
229
+ sig { params(required: String, supplied: T.untyped).returns(String) }
230
+ def compose_tokens(required, supplied)
231
+ ([required] + supplied.to_s.split).uniq.join(" ")
232
+ end
233
+
234
+ private_constant :Navigation
235
+ end
236
+ end
@@ -0,0 +1,57 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Shadcn
5
+ # グラフ(Phase 4)。upstream は recharts を使うが、本gemは
6
+ # 「容器と凡例・ツールチップの構造」のみを提供し、描画はホストが
7
+ # 好きなライブラリ(Chart.js 等)で行う設計(10-roadmap Phase 4 の個別評価)
8
+ # JS無効時フォールバック: Graceful(容器・凡例はSSR済み)
9
+ # Chart自体は名前空間であり、描画には配下のクラスを使う。
10
+ module Chart
11
+ class Container < BaseComponent
12
+ # 契約スロット構成: 名前無しラッパー > chart(描画領域)
13
+ sig { override.returns(String) }
14
+ def call
15
+ content_tag(:div, **html_attributes) do
16
+ content_tag(:div, class: self.class.classes(extra: @user_class), data: { slot: "chart" }) { content }
17
+ end
18
+ end
19
+
20
+ sig { override.returns(T::Hash[Symbol, T.untyped]) }
21
+ def html_attributes
22
+ attributes = @html_args.dup
23
+ attributes[:class] = T.cast(contract_slot("").dig(:static_attributes, :class), T.nilable(String))
24
+ attributes
25
+ end
26
+ end
27
+
28
+ class TooltipContent < BaseComponent
29
+ # ツールチップ本体(契約クラスのみ。中身は利用者が組む)
30
+ sig { override.returns(String) }
31
+ def call
32
+ content_tag(tag, **html_attributes) { content }
33
+ end
34
+ end
35
+
36
+ class LegendContent < BaseComponent
37
+ # 凡例本体(契約クラスのみ)
38
+ sig { override.returns(String) }
39
+ def call
40
+ content_tag(tag, **html_attributes) { content }
41
+ end
42
+ end
43
+
44
+ class Style < BaseComponent
45
+ # グラフ用のCSSを <style> として出力する(upstream と同じ役割)
46
+ sig { override.returns(String) }
47
+ def default_tag
48
+ "style"
49
+ end
50
+
51
+ sig { override.returns(String) }
52
+ def call
53
+ content_tag(:style) { content.to_s }
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,64 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Shadcn
5
+ class Checkbox < BaseComponent
6
+ include NativeCheckedState
7
+
8
+ NATIVE_STATE_CLASS = T.let([
9
+ "not-checked:border-input not-checked:bg-transparent dark:not-checked:bg-input/30",
10
+ "checked:border-primary checked:bg-primary checked:text-primary-foreground",
11
+ "group-has-[:focus-visible]/field-label:checked:border-primary dark:checked:bg-primary"
12
+ ].join(" ").freeze, String)
13
+
14
+ # 契約タグは CheckboxPrimitive.Root。ネイティブな input[type=checkbox] として描く
15
+ # (JS無しで動作 — 05-stimulus-hotwire §3「checkbox = input + CSS」)。
16
+ # base-nova の契約クラスは data-checked / data-unchecked(属性の存在)を参照する。
17
+ # checked 属性(利用者指定)の有無から導出して素のinputでも発火させる
18
+ sig { override.returns(String) }
19
+ def default_tag
20
+ "input"
21
+ end
22
+
23
+ sig { override.returns(String) }
24
+ def call
25
+ content_tag(:span, class: "relative flex w-fit") do
26
+ safe_join([input_element, indicator_element])
27
+ end
28
+ end
29
+
30
+ private
31
+
32
+ sig { returns(String) }
33
+ def input_element
34
+ attributes = html_attributes.merge(type: "checkbox")
35
+ # ネイティブウィジェット描画(appearance:auto)はCSSの背景の上にOS標準の箱を
36
+ # 塗る(dark時は白箱として視覚差になる)ため消す
37
+ attributes[:class] = [attributes[:class], "appearance-none", NATIVE_STATE_CLASS].compact.join(" ")
38
+ merge_native_checked_state(attributes, checked: native_checked?(@html_args))
39
+ void_tag("input", **attributes)
40
+ end
41
+
42
+ # 契約のスロット構造(checkbox-indicator)を保つ。普段は非表示で、
43
+ # peer であるinputのチェック状態に連動して出す(upstreamのIndicatorと同じ挙動)
44
+ sig { returns(String) }
45
+ def indicator_element
46
+ contract_class = T.cast(contract_slot("checkbox-indicator").dig(:static_attributes, :class), T.nilable(String))
47
+ content_tag(:span, data: { slot: "checkbox-indicator" }, aria: { hidden: "true" }, class: [contract_class, "pointer-events-none absolute inset-0 hidden peer-checked:grid"].compact.join(" ")) do
48
+ content_tag(
49
+ :svg,
50
+ xmlns: "http://www.w3.org/2000/svg",
51
+ viewBox: "0 0 24 24",
52
+ fill: "none",
53
+ stroke: "currentColor",
54
+ "stroke-width": "2",
55
+ "stroke-linecap": "round",
56
+ "stroke-linejoin": "round",
57
+ class: "size-3.5"
58
+ ) do
59
+ raw(%(<path d="M20 6 9 17l-5-5"/>))
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,25 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Shadcn
5
+ class Collapsible < BaseComponent
6
+ # ネイティブな details(JS無しで開閉可 — 05-stimulus-hotwire §3)
7
+ # JS無効時フォールバック: Graceful(ネイティブ要素で成立)
8
+ sig { override.returns(String) }
9
+ def default_tag
10
+ "details"
11
+ end
12
+
13
+ class Trigger < BaseComponent
14
+ # JS無しで開閉するため trigger は summary 要素
15
+ sig { override.returns(String) }
16
+ def default_tag
17
+ "summary"
18
+ end
19
+ end
20
+
21
+ class Content < BaseComponent
22
+ # details の子として開閉時に表示される。契約クラスはそのまま適用
23
+ end
24
+ end
25
+ end