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,265 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Shadcn
5
+ # 実inputを値・selection・フォーム送信の唯一の情報源にし、Slot群はその派生表示とする。
6
+ # inputMode=numericはソフトウェアキーボードへのhintであり、入力制限は利用者指定patternが担う。
7
+ class InputOTP < BaseComponent
8
+ CONTROLLER = "shadcn--input-otp"
9
+
10
+ module Markup
11
+ CONTAINER_CLASS = "cn-input-otp flex items-center has-disabled:opacity-50"
12
+ CONTAINER_STYLE = "position: relative; cursor: %<cursor>s; user-select: none; " \
13
+ "-webkit-user-select: none; pointer-events: none"
14
+ DISPLAY_STYLE = "display: contents"
15
+ INPUT_ACTION = T.let([
16
+ "input->#{InputOTP::CONTROLLER}#sync",
17
+ "change->#{InputOTP::CONTROLLER}#sync",
18
+ "focus->#{InputOTP::CONTROLLER}#sync",
19
+ "blur->#{InputOTP::CONTROLLER}#sync",
20
+ "select->#{InputOTP::CONTROLLER}#sync",
21
+ "invalid->#{InputOTP::CONTROLLER}#sync",
22
+ "compositionstart->#{InputOTP::CONTROLLER}#compositionStart",
23
+ "compositionend->#{InputOTP::CONTROLLER}#compositionEnd"
24
+ ].join(" ").freeze, String)
25
+ INPUT_STYLE = T.let({
26
+ position: "absolute",
27
+ inset: 0,
28
+ width: "100%",
29
+ height: "100%",
30
+ display: "flex",
31
+ text_align: "left",
32
+ opacity: 1,
33
+ color: "transparent",
34
+ pointer_events: "all",
35
+ background: "transparent",
36
+ caret_color: "transparent",
37
+ border: "0 solid transparent",
38
+ outline: "0 solid transparent",
39
+ box_shadow: "none",
40
+ line_height: 1,
41
+ letter_spacing: "-.5em",
42
+ font_size: "var(--root-height, 16px)",
43
+ font_family: "monospace",
44
+ font_variant_numeric: "tabular-nums"
45
+ }.freeze, T::Hash[Symbol, T.untyped])
46
+ INPUT_OVERLAY_STYLE = "position: absolute; inset: 0; pointer-events: none"
47
+ NOSCRIPT_CSS_FALLBACK = <<~CSS
48
+ [data-input-otp] {
49
+ --nojs-bg: white !important;
50
+ --nojs-fg: black !important;
51
+
52
+ background-color: var(--nojs-bg) !important;
53
+ color: var(--nojs-fg) !important;
54
+ caret-color: var(--nojs-fg) !important;
55
+ letter-spacing: .25em !important;
56
+ text-align: center !important;
57
+ border: 1px solid var(--nojs-fg) !important;
58
+ border-radius: 4px !important;
59
+ width: 100% !important;
60
+ }
61
+ @media (prefers-color-scheme: dark) {
62
+ [data-input-otp] {
63
+ --nojs-bg: black !important;
64
+ --nojs-fg: white !important;
65
+ }
66
+ }
67
+ CSS
68
+ end
69
+
70
+ private_constant :Markup
71
+
72
+ sig do
73
+ params(
74
+ length: T.any(Integer, String),
75
+ value: T.nilable(String),
76
+ container_class: T.nilable(String),
77
+ args: T::Hash[Symbol, T.untyped]
78
+ ).void.checked(:never)
79
+ end
80
+ def initialize(length: self.class.property_default(:length), value: nil, container_class: nil, **args)
81
+ @length = T.let(normalize_property(:length, length), Integer)
82
+ @value = T.let(value&.slice(0, @length), T.nilable(String))
83
+ @container_class = T.let(container_class, T.nilable(String))
84
+ super(**args)
85
+ end
86
+
87
+ sig { override.returns(String) }
88
+ def call
89
+ safe_join([noscript_fallback, input_container])
90
+ end
91
+
92
+ private
93
+
94
+ sig { returns(String) }
95
+ def input_container
96
+ content_tag(
97
+ :div,
98
+ class: container_class,
99
+ style: format(Markup::CONTAINER_STYLE, cursor: disabled? ? "default" : "text"),
100
+ data: { controller: CONTROLLER, input_otp_container: "" }
101
+ ) do
102
+ safe_join([decorative_display, input_overlay])
103
+ end
104
+ end
105
+
106
+ sig { returns(String) }
107
+ def decorative_display
108
+ data = T.let({ input_otp_display: "" }, T::Hash[Symbol, T.untyped])
109
+ data[:disabled] = "" if disabled?
110
+ data[:invalid] = "" if invalid?
111
+
112
+ content_tag(:div, style: Markup::DISPLAY_STYLE, aria: { hidden: "true" }, data:) do
113
+ content? ? content : standard_group
114
+ end
115
+ end
116
+
117
+ sig { returns(String) }
118
+ def standard_group
119
+ render(Group.new) do
120
+ safe_join(Array.new(@length) { |index| standard_slot(index) })
121
+ end
122
+ end
123
+
124
+ sig { params(index: Integer).returns(String) }
125
+ def standard_slot(index)
126
+ data = T.let({}, T::Hash[Symbol, T.untyped])
127
+ data[:disabled] = "" if disabled?
128
+
129
+ render(Slot.new(index:, data:, aria: { invalid: invalid?.to_s })) do
130
+ @value.to_s[index].to_s
131
+ end
132
+ end
133
+
134
+ sig { returns(String) }
135
+ def input_overlay
136
+ content_tag(:div, style: Markup::INPUT_OVERLAY_STYLE) { input_element }
137
+ end
138
+
139
+ sig { returns(String) }
140
+ def input_element
141
+ attributes = html_attributes
142
+ attributes[:type] = "text"
143
+ attributes[:inputmode] = "numeric" unless attributes.key?(:inputmode)
144
+ attributes[:maxlength] = @length
145
+ attributes[:autocomplete] = "one-time-code" unless attributes.key?(:autocomplete)
146
+ attributes[:spellcheck] = false unless attributes.key?(:spellcheck)
147
+ attributes[:value] = @value unless @value.nil?
148
+ merge_style(attributes, Markup::INPUT_STYLE)
149
+ merge_input_data(attributes)
150
+ void_tag("input", **attributes)
151
+ end
152
+
153
+ sig { params(attributes: T::Hash[Symbol, T.untyped]).void }
154
+ def merge_input_data(attributes)
155
+ data = T.cast(attributes[:data], T.nilable(T::Hash[Symbol, T.untyped])) || {}
156
+ user_action = data[:action]
157
+ attributes[:data] = data.merge(
158
+ input_otp: "",
159
+ action: [Markup::INPUT_ACTION, user_action].compact.join(" ")
160
+ )
161
+ end
162
+
163
+ sig { returns(String) }
164
+ def noscript_fallback
165
+ content_tag(:noscript) do
166
+ content_tag(:style) { raw(Markup::NOSCRIPT_CSS_FALLBACK) }
167
+ end
168
+ end
169
+
170
+ sig { returns(String) }
171
+ def container_class
172
+ ShadcnViewComponents::Classes::MERGER.merge(
173
+ [Markup::CONTAINER_CLASS, @container_class].compact.join(" ")
174
+ )
175
+ end
176
+
177
+ sig { returns(T::Boolean) }
178
+ def disabled?
179
+ !@html_args[:disabled].nil? && @html_args[:disabled] != false
180
+ end
181
+
182
+ sig { returns(T::Boolean) }
183
+ def invalid?
184
+ aria = T.cast(@html_args[:aria], T.nilable(T::Hash[Symbol, T.untyped])) || {}
185
+ aria[:invalid] == true || aria[:invalid].to_s == "true"
186
+ end
187
+
188
+ class Group < BaseComponent
189
+ # div
190
+ end
191
+
192
+ class Slot < BaseComponent
193
+ CARET_WRAPPER_CLASS = "pointer-events-none absolute inset-0 flex items-center justify-center"
194
+ CARET_CLASS = "h-4 w-px animate-caret-blink bg-foreground duration-1000"
195
+
196
+ private_constant :CARET_WRAPPER_CLASS, :CARET_CLASS
197
+
198
+ sig { params(index: T.any(Integer, String), args: T::Hash[Symbol, T.untyped]).void.checked(:never) }
199
+ def initialize(index:, **args)
200
+ @index = T.let(normalize_property(:index, index), Integer)
201
+ super(**args)
202
+ end
203
+
204
+ sig { override.returns(String) }
205
+ def call
206
+ content_tag(tag, **html_attributes) do
207
+ safe_join([character, fake_caret])
208
+ end
209
+ end
210
+
211
+ private
212
+
213
+ sig { override.returns(T::Hash[Symbol, T.untyped]) }
214
+ def html_attributes
215
+ super.tap do |attributes|
216
+ data = T.cast(attributes[:data], T::Hash[Symbol, T.untyped])
217
+ data[:index] = @index
218
+ data[:active] = "false" unless data.key?(:active)
219
+ end
220
+ end
221
+
222
+ sig { returns(String) }
223
+ def character
224
+ content_tag(:span, data: { slot: "input-otp-character" }) { content }
225
+ end
226
+
227
+ sig { returns(String) }
228
+ def fake_caret
229
+ content_tag(
230
+ :div,
231
+ class: CARET_WRAPPER_CLASS,
232
+ hidden: true,
233
+ aria: { hidden: "true" },
234
+ data: { slot: "input-otp-caret" }
235
+ ) do
236
+ content_tag(:div, class: CARET_CLASS) { "".html_safe }
237
+ end
238
+ end
239
+ end
240
+
241
+ class Separator < BaseComponent
242
+ sig { override.returns(String) }
243
+ def call
244
+ content_tag(tag, **html_attributes) { minus_icon }
245
+ end
246
+
247
+ private
248
+
249
+ sig { returns(String) }
250
+ def minus_icon
251
+ content_tag(
252
+ :svg,
253
+ xmlns: "http://www.w3.org/2000/svg",
254
+ viewBox: "0 0 24 24",
255
+ fill: "none",
256
+ stroke: "currentColor",
257
+ "stroke-width": "2",
258
+ "stroke-linecap": "round",
259
+ "stroke-linejoin": "round",
260
+ aria: { hidden: "true" }
261
+ ) { raw(%(<path d="M5 12h14"/>)) }
262
+ end
263
+ end
264
+ end
265
+ end
@@ -0,0 +1,108 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Shadcn
5
+ class Item < BaseComponent
6
+ sig do
7
+ params(
8
+ variant: T.any(Symbol, String),
9
+ size: T.any(Symbol, String),
10
+ args: T::Hash[Symbol, T.untyped]
11
+ ).void.checked(:never)
12
+ end
13
+ def initialize(variant: ShadcnViewComponents::Contracts::Item::DEFAULTS.fetch(:variant),
14
+ size: ShadcnViewComponents::Contracts::Item::DEFAULTS.fetch(:size), **args)
15
+ @variant = T.let(normalize_option(:variant, variant), Symbol)
16
+ @size = T.let(normalize_option(:size, size), Symbol)
17
+ super(**args)
18
+ end
19
+
20
+ sig { returns(Symbol) }
21
+ attr_reader :variant
22
+
23
+ sig { returns(Symbol) }
24
+ attr_reader :size
25
+
26
+ sig { override.returns(T::Hash[Symbol, VariantOption]) }
27
+ def variant_options
28
+ { variant: @variant, size: @size }
29
+ end
30
+
31
+ # upstream useRender の state は data-variant / data-size として DOM に現れる
32
+ # (ItemGroup の has-data-[size=sm]:gap-2.5 等がこれを参照する)
33
+ sig { override.returns(T::Hash[Symbol, T.untyped]) }
34
+ def contract_data_attributes
35
+ super.merge(variant: @variant, size: @size)
36
+ end
37
+
38
+ class Media < BaseComponent
39
+ sig do
40
+ params(
41
+ variant: T.any(Symbol, String),
42
+ args: T::Hash[Symbol, T.untyped]
43
+ ).void.checked(:never)
44
+ end
45
+ def initialize(variant: ShadcnViewComponents::Contracts::Item::Media::DEFAULTS.fetch(:variant), **args)
46
+ @variant = T.let(normalize_option(:variant, variant), Symbol)
47
+ super(**args)
48
+ end
49
+
50
+ sig { returns(Symbol) }
51
+ attr_reader :variant
52
+
53
+ sig { override.returns(T::Hash[Symbol, VariantOption]) }
54
+ def variant_options
55
+ { variant: @variant }
56
+ end
57
+
58
+ sig { override.returns(T::Hash[Symbol, T.untyped]) }
59
+ def contract_data_attributes
60
+ super.merge(variant: @variant)
61
+ end
62
+ end
63
+
64
+ # upstream は Separator(別アイテム)をラップして "my-0" を重ねる構成。
65
+ # 抽出器は単一アイテム内のクラスしか事前解決しないため、item側の契約には
66
+ # "my-0" のみが記録される。描画は upstream のDOMと同じく Separator のクラス群 +
67
+ # "my-0" とし、クラス検証は contains モードで行う(spec/conformance/allowances.yml 参照)
68
+ class Separator < BaseComponent
69
+ sig { override.returns(String) }
70
+ def call
71
+ content_tag(
72
+ :div,
73
+ role: "separator",
74
+ data: { slot: contract_root_slot[:name], orientation: "horizontal" },
75
+ aria: { orientation: "horizontal" },
76
+ class: separator_class
77
+ ) { content }
78
+ end
79
+
80
+ private
81
+
82
+ sig { returns(String) }
83
+ def separator_class
84
+ # base-nova の item-separator は Separator(別アイテム)のクラスの上に
85
+ # item 側の my-2 を合成する(item契約には my-2 のみ記録される)。
86
+ # h-px / bg-border は Separator 契約の data-horizontal: バリアントが担当し、
87
+ # これが無いと separator の高さが 0 になる
88
+ ShadcnViewComponents::Classes::MERGER.merge(
89
+ "#{ShadcnViewComponents::Classes.resolve(:separator)} #{self.class.classes}"
90
+ )
91
+ end
92
+ end
93
+
94
+ class Content < BaseComponent; end
95
+
96
+ class Actions < BaseComponent; end
97
+
98
+ class Group < BaseComponent; end
99
+
100
+ class Title < BaseComponent; end
101
+
102
+ class Description < BaseComponent; end
103
+
104
+ class Header < BaseComponent; end
105
+
106
+ class Footer < BaseComponent; end
107
+ end
108
+ end
@@ -0,0 +1,9 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Shadcn
5
+ class Kbd < BaseComponent
6
+ class Group < BaseComponent
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,12 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Shadcn
5
+ class Label < BaseComponent
6
+ # 契約タグは LabelPrimitive.Root。ネイティブな label 要素として描画する
7
+ sig { override.returns(String) }
8
+ def default_tag
9
+ "label"
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,37 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Shadcn
5
+ class Marker < BaseComponent
6
+ # 省略時の既定値は契約の defaults から読む(リテラルで二重管理しない)
7
+ sig do
8
+ params(
9
+ variant: T.any(Symbol, String),
10
+ args: T::Hash[Symbol, T.untyped]
11
+ ).void.checked(:never)
12
+ end
13
+ def initialize(variant: ShadcnViewComponents::Contracts::Marker::DEFAULTS.fetch(:variant), **args)
14
+ @variant = T.let(normalize_option(:variant, variant), Symbol)
15
+ super(**args)
16
+ end
17
+
18
+ sig { returns(Symbol) }
19
+ attr_reader :variant
20
+
21
+ sig { override.returns(T::Hash[Symbol, VariantOption]) }
22
+ def variant_options
23
+ { variant: @variant }
24
+ end
25
+
26
+ sig { override.returns(T::Hash[Symbol, T.untyped]) }
27
+ def contract_data_attributes
28
+ super.merge(variant: @variant)
29
+ end
30
+
31
+ class Content < BaseComponent
32
+ end
33
+
34
+ class Icon < BaseComponent
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,110 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Shadcn
5
+ # メニューバー(10-roadmap Phase 3「menubar = 横断」)。
6
+ # 横並びのトリガー群と複数メニューをmenubar_controllerで一対一に管理する。
7
+ # 部品の描画はDropdownMenuを継承するが、状態とキーボード操作は共有しない。
8
+ # JS無効時フォールバック: Readable
9
+ class Menubar < DropdownMenu
10
+ CONTROLLER = "shadcn--menubar"
11
+
12
+ module ControllerIdentifier
13
+ extend T::Sig
14
+
15
+ private
16
+
17
+ sig { returns(String) }
18
+ def controller_identifier
19
+ Menubar::CONTROLLER
20
+ end
21
+ end
22
+
23
+ private_constant :ControllerIdentifier
24
+
25
+ sig { override.returns(T::Hash[Symbol, T.untyped]) }
26
+ def contract_data_attributes
27
+ super.merge(controller: CONTROLLER)
28
+ end
29
+
30
+ sig { override.returns(T::Hash[Symbol, T.untyped]) }
31
+ def html_attributes
32
+ attributes = super
33
+ attributes[:role] = "menubar"
34
+ merge_nested(attributes, :aria, { orientation: "horizontal" })
35
+ attributes
36
+ end
37
+
38
+ class Menu < BaseComponent
39
+ # トリガー+コンテンツのスコープ(div)
40
+ sig { override.returns(T::Hash[Symbol, T.untyped]) }
41
+ def html_attributes
42
+ super.tap { |attributes| attributes[:role] = "none" }
43
+ end
44
+ end
45
+
46
+ class Trigger < DropdownMenu::Trigger
47
+ include ControllerIdentifier
48
+
49
+ sig { override.returns(T::Hash[Symbol, T.untyped]) }
50
+ def html_attributes
51
+ super.tap { |attributes| attributes[:role] = "menuitem" }
52
+ end
53
+ end
54
+
55
+ class Portal < DropdownMenu::Portal
56
+ # 搬送のみの機能要素の実体化
57
+ end
58
+
59
+ class Content < DropdownMenu::Content
60
+ FLOATING_POSITION_DEFAULTS = T.let(
61
+ { side: :bottom, align: :start, side_offset: 8, align_offset: -4, collision_padding: 5 }.freeze,
62
+ T::Hash[Symbol, T.untyped]
63
+ )
64
+
65
+ # popover=auto の ARIA menu
66
+ end
67
+
68
+ class Group < DropdownMenu::Group; end
69
+
70
+ class Label < DropdownMenu::Label; end
71
+
72
+ class Item < DropdownMenu::Item
73
+ include ControllerIdentifier
74
+ end
75
+
76
+ class CheckboxItem < DropdownMenu::CheckboxItem
77
+ include ControllerIdentifier
78
+
79
+ # 契約に indicator スロットが無いため data-slot 無しで描く
80
+ sig { returns(T.nilable(String)) }
81
+ def indicator_slot_name
82
+ nil
83
+ end
84
+ end
85
+
86
+ class RadioGroup < DropdownMenu::RadioGroup; end
87
+
88
+ class RadioItem < DropdownMenu::RadioItem
89
+ include ControllerIdentifier
90
+
91
+ # 契約に indicator スロットが無いため data-slot 無しで描く
92
+ sig { returns(T.nilable(String)) }
93
+ def indicator_slot_name
94
+ nil
95
+ end
96
+ end
97
+
98
+ class Separator < DropdownMenu::Separator; end
99
+
100
+ class Shortcut < DropdownMenu::Shortcut; end
101
+
102
+ class Sub < DropdownMenu::Sub; end
103
+
104
+ class SubTrigger < DropdownMenu::SubTrigger
105
+ include ControllerIdentifier
106
+ end
107
+
108
+ class SubContent < DropdownMenu::SubContent; end
109
+ end
110
+ end
@@ -0,0 +1,29 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Shadcn
5
+ # AIチャット等のメッセージ行(Phase 4)。静的な表示構造
6
+ class Message < BaseComponent
7
+ # div
8
+
9
+ class Group < BaseComponent
10
+ # div
11
+ end
12
+
13
+ class Avatar < BaseComponent
14
+ # div
15
+ end
16
+
17
+ class Content < BaseComponent
18
+ # div
19
+ end
20
+
21
+ class Footer < BaseComponent
22
+ # div
23
+ end
24
+
25
+ class Header < BaseComponent
26
+ # div
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,52 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Shadcn
5
+ # メッセージのスクロール領域(Phase 4)。一番下への追従・ボタンによる
6
+ # スクロールは message_scroller_controller が担う
7
+ # JS無効時フォールバック: Graceful(縦スクロールはネイティブ)
8
+ class MessageScroller < BaseComponent
9
+ CONTROLLER = "shadcn--message-scroller"
10
+
11
+ sig { override.returns(T::Hash[Symbol, T.untyped]) }
12
+ def contract_data_attributes
13
+ super.merge(controller: CONTROLLER)
14
+ end
15
+
16
+ class Provider < BaseComponent
17
+ # 状態のスコープ(div)。子をそのまま並べる
18
+ end
19
+
20
+ class Viewport < BaseComponent
21
+ # div(スクロール対象)
22
+ sig { override.returns(T::Hash[Symbol, T.untyped]) }
23
+ def html_attributes
24
+ super.tap { |attributes| attributes[:tabindex] = "0" }
25
+ end
26
+ end
27
+
28
+ class Content < BaseComponent
29
+ # div
30
+ end
31
+
32
+ class Item < BaseComponent
33
+ # div
34
+ end
35
+
36
+ class Button < BaseComponent
37
+ # 一番下へ戻るボタン
38
+ sig { override.returns(String) }
39
+ def default_tag
40
+ "button"
41
+ end
42
+
43
+ sig { override.returns(T::Hash[Symbol, T.untyped]) }
44
+ def html_attributes
45
+ attributes = super
46
+ attributes[:type] = "button" unless attributes.key?(:type)
47
+ merge_nested(attributes, :data, { action: "#{CONTROLLER}#scrollToBottom" })
48
+ attributes
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,74 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Shadcn
5
+ # Checkbox / Radio / Switch が共有する、ネイティブ input の checked 状態を
6
+ # upstream 契約の data 属性へ投影するための内部実装。
7
+ module NativeCheckedState
8
+ extend T::Sig
9
+
10
+ CONTROLLER = "shadcn--checked-state"
11
+ CHANGE_ACTION = T.let("change->#{CONTROLLER}#sync".freeze, String)
12
+
13
+ private
14
+
15
+ sig { params(attributes: T::Hash[Symbol, T.untyped], checked: T::Boolean).void }
16
+ def merge_native_checked_state(attributes, checked:)
17
+ attributes[:data] = native_checked_data_attributes(attributes[:data], checked:)
18
+ normalize_explicit_aria_checked(attributes, checked:)
19
+ end
20
+
21
+ sig { params(raw_data: T.untyped, checked: T::Boolean).returns(T::Hash[T.untyped, T.untyped]) }
22
+ def native_checked_data_attributes(raw_data, checked:)
23
+ data = T.cast(raw_data, T.nilable(T::Hash[T.untyped, T.untyped])) || {}
24
+ data = data.dup
25
+ user_controller = delete_native_data_value(data, :controller)
26
+ user_action = delete_native_data_value(data, :action)
27
+ delete_native_data_value(data, :checked)
28
+ delete_native_data_value(data, :unchecked)
29
+ data[:controller] = compose_checked_state_tokens(CONTROLLER, user_controller)
30
+ data[:action] = compose_checked_state_tokens(CHANGE_ACTION, user_action)
31
+ data[checked ? :checked : :unchecked] = ""
32
+ data
33
+ end
34
+
35
+ sig { params(data: T::Hash[T.untyped, T.untyped], key: Symbol).returns(T.untyped) }
36
+ def delete_native_data_value(data, key)
37
+ symbol_value = data.delete(key)
38
+ string_value = data.delete(key.to_s)
39
+ symbol_value || string_value
40
+ end
41
+
42
+ sig { params(html_args: T::Hash[Symbol, T.untyped]).returns(T::Boolean) }
43
+ def native_checked?(html_args)
44
+ value = html_args[:checked]
45
+ !value.nil? && value != false
46
+ end
47
+
48
+ sig { params(checked: T::Boolean).returns(T::Hash[Symbol, String]) }
49
+ def native_checked_data(checked:)
50
+ checked ? { checked: "" } : { unchecked: "" }
51
+ end
52
+
53
+ sig { params(tokens: T.untyped).returns(String) }
54
+ def compose_checked_state_tokens(*tokens)
55
+ tokens.compact.flat_map { |token| token.to_s.split }.uniq.join(" ")
56
+ end
57
+
58
+ sig { params(attributes: T::Hash[Symbol, T.untyped], checked: T::Boolean).void }
59
+ def normalize_explicit_aria_checked(attributes, checked:)
60
+ aria = T.cast(attributes[:aria], T.nilable(T::Hash[T.untyped, T.untyped]))
61
+ if aria&.key?(:checked) || aria&.key?("checked")
62
+ normalized = aria.dup
63
+ normalized.delete("checked")
64
+ normalized[:checked] = checked.to_s
65
+ attributes[:aria] = normalized
66
+ end
67
+
68
+ key = :"aria-checked"
69
+ attributes[key] = checked.to_s if attributes.key?(key)
70
+ end
71
+ end
72
+
73
+ private_constant :NativeCheckedState
74
+ end