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,77 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Shadcn
5
+ # ペインリサイズ。ハンドルのドラッグ/矢印キーで前後パネルの
6
+ # flex-basis を配分する(10-roadmap Phase 3 の「CSS grid」案を
7
+ # flex-basis で実装したもの。契約・parity は flex 前提)
8
+ # JS無効時フォールバック: Graceful(静的レイアウトとして表示される)
9
+ # Resizable自体は名前空間であり、描画には配下のクラスを使う。
10
+ module Resizable
11
+ class PanelGroup < BaseComponent
12
+ # div(水平flex。配置方向は data-orientation のクラス契約に従う)
13
+ CONTROLLER = "shadcn--resizable"
14
+
15
+ sig do
16
+ params(orientation: T.any(Symbol, String), args: T::Hash[Symbol, T.untyped]).void.checked(:never)
17
+ end
18
+ def initialize(orientation: self.class.property_default(:orientation), **args)
19
+ assign_accessibility_root_id(args, prefix: "resizable")
20
+ @orientation = T.let(normalize_property(:orientation, orientation), String)
21
+ super(**args)
22
+ end
23
+
24
+ sig { override.returns(T::Hash[Symbol, T.untyped]) }
25
+ def html_attributes
26
+ attributes = super
27
+ attributes[:role] = "group"
28
+ merge_style(attributes, { display: "flex" })
29
+ merge_nested(attributes, :data, { controller: CONTROLLER, orientation: @orientation })
30
+ attributes
31
+ end
32
+ end
33
+
34
+ class Panel < BaseComponent
35
+ # div。flex-grow の比率は利用者が style で指定する(既定 1)
36
+ sig { override.returns(T::Hash[Symbol, T.untyped]) }
37
+ def html_attributes
38
+ attributes = super
39
+ merge_style(attributes, { "flex-grow": "1", "flex-basis": "0", overflow: "auto",
40
+ "min-width": "0", "min-height": "0" })
41
+ attributes
42
+ end
43
+ end
44
+
45
+ class Handle < BaseComponent
46
+ # ドラッグで前後パネルの幅を配分する(キーボードは矢印キー)。
47
+ # orientation はセパレータ自身の向き(縦積みグループでは horizontal)で、
48
+ # グループの orientation の逆になる
49
+ sig do
50
+ params(orientation: T.any(Symbol, String), args: T::Hash[Symbol, T.untyped]).void.checked(:never)
51
+ end
52
+ def initialize(orientation: self.class.property_default(:orientation), **args)
53
+ @orientation = T.let(normalize_property(:orientation, orientation), String)
54
+ super(**args)
55
+ end
56
+
57
+ sig { override.returns(T::Hash[Symbol, T.untyped]) }
58
+ def html_attributes
59
+ attributes = super
60
+ attributes[:role] = "separator"
61
+ attributes[:tabindex] = "0"
62
+ merge_nested(attributes, :aria, {
63
+ orientation: @orientation,
64
+ valuemin: "10",
65
+ valuemax: "90",
66
+ valuenow: "50"
67
+ })
68
+ merge_style(attributes, { cursor: @orientation == "vertical" ? "col-resize" : "row-resize",
69
+ flex: "0 0 auto" })
70
+ merge_nested(attributes, :data, {
71
+ action: "mousedown->shadcn--resizable#startDrag keydown->shadcn--resizable#nudge"
72
+ })
73
+ attributes
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,59 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Shadcn
5
+ class ScrollArea < BaseComponent
6
+ # 契約タグは ScrollAreaPrimitive.Root/Viewport。ネイティブな CSSスクロールで再現する
7
+ # (05-stimulus-hotwire §3「scroll-area = CSSユーティリティ」)。
8
+ # overflow-auto は本ライブラリの装飾(契約クラスはRadixの構造前提のため)
9
+ sig { override.returns(String) }
10
+ def call
11
+ content_tag(tag, **html_attributes) do
12
+ content_tag(:div, class: viewport_class, data: { slot: "scroll-area-viewport" }) { content }
13
+ end
14
+ end
15
+
16
+ private
17
+
18
+ sig { returns(String) }
19
+ def viewport_class
20
+ contract_class = T.cast(contract_slot("scroll-area-viewport").dig(:static_attributes, :class), T.nilable(String))
21
+ [contract_class, "size-full overflow-auto"].compact.join(" ")
22
+ end
23
+
24
+ class Scrollbar < BaseComponent
25
+ # ネイティブCSSスクロールでは独自スクロールバー部品は不要。契約構造を保つため
26
+ # 描画するが、表示はCSSスクロールバーが担う。
27
+ # base-nova では orientation は契約propではなく data-orientation 属性
28
+ # (クラスは data-horizontal: / data-vertical: のCSS variantで追従する)
29
+ sig do
30
+ params(orientation: T.any(Symbol, String), args: T::Hash[Symbol, T.untyped]).void.checked(:never)
31
+ end
32
+ def initialize(orientation: self.class.property_default(:orientation), **args)
33
+ @orientation = T.let(normalize_property(:orientation, orientation), String)
34
+ super(**args)
35
+ end
36
+
37
+ sig { override.returns(T::Hash[Symbol, T.untyped]) }
38
+ def html_attributes
39
+ attributes = super
40
+ merge_nested(attributes, :data, { orientation: @orientation })
41
+ attributes
42
+ end
43
+
44
+ sig { override.returns(String) }
45
+ def call
46
+ content_tag(tag, **html_attributes) do
47
+ content_tag(:span, data: { slot: "scroll-area-thumb" }, class: thumb_class) { content }
48
+ end
49
+ end
50
+
51
+ private
52
+
53
+ sig { returns(String) }
54
+ def thumb_class
55
+ T.cast(contract_slot("scroll-area-thumb").dig(:static_attributes, :class), T.nilable(String)).to_s
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,356 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Shadcn
5
+ # 単一選択のcombobox + listbox。確定値はルート直下のhidden inputだけに保持し、
6
+ # 表示値・選択ARIA・ハイライトは専用Stimulus controllerがそこから導出する。
7
+ # JS不要、multiple、ネイティブ制約検証が必要な場合はNativeSelectを使う。
8
+ class Select < BaseComponent
9
+ CONTROLLER = "shadcn--select"
10
+
11
+ sig do
12
+ params(
13
+ name: T.nilable(T.any(String, Symbol)),
14
+ default_value: T.nilable(T.any(String, Symbol, Integer, Float)),
15
+ disabled: T::Boolean,
16
+ args: T::Hash[Symbol, T.untyped]
17
+ ).void.checked(:never)
18
+ end
19
+ def initialize(name: nil, default_value: nil, disabled: false, **args)
20
+ assign_accessibility_root_id(args, prefix: "select")
21
+ @name = T.let(name&.to_s, T.nilable(String))
22
+ @default_value = T.let(default_value&.to_s, T.nilable(String))
23
+ @disabled = T.let(disabled, T::Boolean)
24
+ super(**args)
25
+ end
26
+
27
+ # base-novaのSelect rootはDOMやclassを追加しないprimitive aliasで、生成契約も
28
+ # その空の契約面を記録する。Ruby側ではhidden inputとcontrollerの状態ホストを描く。
29
+ sig { override.returns(String) }
30
+ def call
31
+ content_tag(tag, **html_attributes) { safe_join([hidden_input, content]) }
32
+ end
33
+
34
+ private
35
+
36
+ sig { override.returns(T::Hash[Symbol, T.untyped]) }
37
+ def html_attributes
38
+ attributes = super
39
+ data = T.cast(attributes[:data], T.nilable(T::Hash[Symbol, T.untyped])) || {}
40
+ defaults = {
41
+ slot: "select",
42
+ controller: CONTROLLER,
43
+ action: "keydown->#{CONTROLLER}#navigate focusout->#{CONTROLLER}#focusOut"
44
+ }
45
+ defaults[:disabled] = "true" if @disabled
46
+ attributes[:data] = defaults.merge(data)
47
+ attributes
48
+ end
49
+
50
+ sig { returns(String) }
51
+ def hidden_input
52
+ attributes = T.let(
53
+ {
54
+ type: "hidden",
55
+ value: @default_value.to_s,
56
+ data: {
57
+ slot: "select-input",
58
+ value_present: (!@default_value.nil?).to_s
59
+ }
60
+ },
61
+ T::Hash[Symbol, T.untyped]
62
+ )
63
+ attributes[:name] = @name if @name
64
+ attributes[:disabled] = true if @disabled
65
+ void_tag("input", **attributes)
66
+ end
67
+
68
+ class Trigger < BaseComponent
69
+ sig { params(size: T.any(Symbol, String), args: T::Hash[Symbol, T.untyped]).void.checked(:never) }
70
+ def initialize(size: self.class.property_default(:size), **args)
71
+ @size = T.let(normalize_property(:size, size), String)
72
+ super(**args)
73
+ end
74
+
75
+ sig { override.returns(String) }
76
+ def default_tag
77
+ "button"
78
+ end
79
+
80
+ sig { override.returns(String) }
81
+ def call
82
+ content_tag(tag, **html_attributes) { safe_join([content, chevron_icon]) }
83
+ end
84
+
85
+ sig { override.returns(T::Hash[Symbol, T.untyped]) }
86
+ def html_attributes
87
+ attributes = super
88
+ attributes[:type] = "button" unless attributes.key?(:type)
89
+ attributes[:role] = "combobox"
90
+ merge_nested(attributes, :aria, { haspopup: "listbox", expanded: "false" })
91
+ merge_nested(attributes, :data, { size: @size, action: "click->#{CONTROLLER}#toggle" })
92
+ attributes
93
+ end
94
+
95
+ private
96
+
97
+ sig { returns(String) }
98
+ def chevron_icon
99
+ content_tag(
100
+ :svg,
101
+ xmlns: "http://www.w3.org/2000/svg",
102
+ viewBox: "0 0 24 24",
103
+ fill: "none",
104
+ stroke: "currentColor",
105
+ "stroke-width": "2",
106
+ "stroke-linecap": "round",
107
+ "stroke-linejoin": "round",
108
+ class: "pointer-events-none size-4 text-muted-foreground",
109
+ aria: { hidden: "true" }
110
+ ) { raw(%(<path d="m6 9 6 6 6-6"/>)) }
111
+ end
112
+ end
113
+
114
+ class Value < BaseComponent
115
+ sig { params(placeholder: T.nilable(String), args: T::Hash[Symbol, T.untyped]).void.checked(:never) }
116
+ def initialize(placeholder: nil, **args)
117
+ @placeholder = T.let(placeholder, T.nilable(String))
118
+ super(**args)
119
+ end
120
+
121
+ sig { override.returns(String) }
122
+ def call
123
+ label = content.to_s
124
+ label = @placeholder.to_s if label.empty?
125
+ content_tag(tag, **html_attributes) { label }
126
+ end
127
+ end
128
+
129
+ class Content < BaseComponent
130
+ include Shadcn::FloatingPositionOptions
131
+
132
+ FLOATING_POSITION_DEFAULTS = T.let(
133
+ { side: :bottom, align: :center, side_offset: 4, align_offset: 0, collision_padding: 5 }.freeze,
134
+ T::Hash[Symbol, T.untyped]
135
+ )
136
+
137
+ # Positioner相当のラッパー > Popup > Listというupstream構造を保つ。
138
+ sig { override.returns(String) }
139
+ def call
140
+ content_tag(:div, class: "isolate z-50") do
141
+ content_tag(tag, **content_attributes) do
142
+ safe_join([
143
+ render(ScrollUpButton.new),
144
+ content_tag(
145
+ :div,
146
+ role: "listbox",
147
+ tabindex: "-1",
148
+ style: "position: relative; min-height: 0; flex: 1 1 auto; overflow-x: hidden; overflow-y: auto"
149
+ ) { content },
150
+ render(ScrollDownButton.new)
151
+ ])
152
+ end
153
+ end
154
+ end
155
+
156
+ sig { returns(T::Hash[Symbol, T.untyped]) }
157
+ def content_attributes
158
+ attributes = @html_args.merge(class: self.class.classes(extra: @user_class))
159
+ attributes[:role] = "presentation" unless attributes.key?(:role)
160
+ attributes[:popover] = "auto" unless attributes.key?(:popover)
161
+ merge_style(attributes, display: "flex", flex_direction: "column", overflow: "hidden")
162
+ merge_floating_position_data(
163
+ attributes,
164
+ slot: "select-content",
165
+ state: "closed",
166
+ closed: "",
167
+ align_trigger: "true"
168
+ )
169
+ end
170
+ end
171
+
172
+ class Group < BaseComponent
173
+ sig { override.returns(T::Hash[Symbol, T.untyped]) }
174
+ def html_attributes
175
+ super.merge(role: "group")
176
+ end
177
+ end
178
+
179
+ class Item < BaseComponent
180
+ sig do
181
+ params(
182
+ value: T.any(String, Symbol, Integer, Float),
183
+ disabled: T::Boolean,
184
+ args: T::Hash[Symbol, T.untyped]
185
+ ).void.checked(:never)
186
+ end
187
+ def initialize(value:, disabled: false, **args)
188
+ @value = T.let(value.to_s, String)
189
+ @disabled = T.let(disabled, T::Boolean)
190
+ super(**args)
191
+ end
192
+
193
+ sig { override.returns(String) }
194
+ def call
195
+ content_tag(tag, **html_attributes) do
196
+ safe_join([item_text, item_indicator])
197
+ end
198
+ end
199
+
200
+ sig { override.returns(T::Hash[Symbol, T.untyped]) }
201
+ def html_attributes
202
+ attributes = super
203
+ attributes[:role] = "option"
204
+ attributes[:tabindex] = "-1"
205
+ merge_nested(attributes, :aria, { selected: "false" }.merge(@disabled ? { disabled: "true" } : {}))
206
+ item_data = {
207
+ value: @value,
208
+ selected: "false",
209
+ action: "click->#{CONTROLLER}#select mouseenter->#{CONTROLLER}#highlight"
210
+ }
211
+ item_data[:disabled] = "" if @disabled
212
+ merge_nested(attributes, :data, item_data)
213
+ attributes
214
+ end
215
+
216
+ private
217
+
218
+ sig { returns(String) }
219
+ def item_text
220
+ content_tag(:span, class: "flex flex-1 shrink-0 gap-2 whitespace-nowrap") { content }
221
+ end
222
+
223
+ sig { returns(String) }
224
+ def item_indicator
225
+ content_tag(
226
+ :span,
227
+ hidden: true,
228
+ class: "pointer-events-none absolute right-2 flex size-4 items-center justify-center",
229
+ data: { indicator: "" }
230
+ ) { check_icon }
231
+ end
232
+
233
+ sig { returns(String) }
234
+ def check_icon
235
+ content_tag(
236
+ :svg,
237
+ xmlns: "http://www.w3.org/2000/svg",
238
+ viewBox: "0 0 24 24",
239
+ fill: "none",
240
+ stroke: "currentColor",
241
+ "stroke-width": "2",
242
+ "stroke-linecap": "round",
243
+ "stroke-linejoin": "round",
244
+ class: "pointer-events-none",
245
+ aria: { hidden: "true" }
246
+ ) { raw(%(<path d="M20 6 9 17l-5-5"/>)) }
247
+ end
248
+ end
249
+
250
+ class Label < BaseComponent
251
+ # div
252
+ end
253
+
254
+ class Separator < BaseComponent
255
+ sig { override.returns(T::Hash[Symbol, T.untyped]) }
256
+ def html_attributes
257
+ super.tap { |attributes| attributes[:role] = "presentation" unless attributes.key?(:role) }
258
+ end
259
+ end
260
+
261
+ module ChevronIcon
262
+ extend T::Helpers
263
+ extend T::Sig
264
+
265
+ requires_ancestor { BaseComponent }
266
+
267
+ private
268
+
269
+ sig { params(path: String).returns(String) }
270
+ def chevron_icon(path)
271
+ T.unsafe(self).content_tag(
272
+ :svg,
273
+ xmlns: "http://www.w3.org/2000/svg",
274
+ viewBox: "0 0 24 24",
275
+ fill: "none",
276
+ stroke: "currentColor",
277
+ "stroke-width": "2",
278
+ "stroke-linecap": "round",
279
+ "stroke-linejoin": "round",
280
+ aria: { hidden: "true" }
281
+ ) { T.unsafe(self).raw(%(<path d="#{path}"/>)) }
282
+ end
283
+ end
284
+ private_constant :ChevronIcon
285
+
286
+ class ScrollUpButton < BaseComponent
287
+ include ChevronIcon
288
+
289
+ sig { override.returns(String) }
290
+ def call
291
+ content_tag(tag, **html_attributes) { content.presence || chevron_icon("m18 15-6-6-6 6") }
292
+ end
293
+
294
+ sig { override.returns(T::Hash[Symbol, T.untyped]) }
295
+ def html_attributes
296
+ scroll_button_attributes(super, "up")
297
+ end
298
+
299
+ private
300
+
301
+ sig do
302
+ params(attributes: T::Hash[Symbol, T.untyped], direction: String)
303
+ .returns(T::Hash[Symbol, T.untyped])
304
+ end
305
+ def scroll_button_attributes(attributes, direction)
306
+ attributes[:hidden] = true
307
+ merge_nested(attributes, :aria, { hidden: "true" })
308
+ merge_nested(
309
+ attributes,
310
+ :data,
311
+ {
312
+ scroll_direction: direction,
313
+ action: "mouseenter->#{CONTROLLER}#startScroll mouseleave->#{CONTROLLER}#stopScroll"
314
+ }
315
+ )
316
+ merge_style(attributes, position: "absolute")
317
+ attributes
318
+ end
319
+ end
320
+
321
+ class ScrollDownButton < BaseComponent
322
+ include ChevronIcon
323
+
324
+ sig { override.returns(String) }
325
+ def call
326
+ content_tag(tag, **html_attributes) { content.presence || chevron_icon("m6 9 6 6 6-6") }
327
+ end
328
+
329
+ sig { override.returns(T::Hash[Symbol, T.untyped]) }
330
+ def html_attributes
331
+ scroll_button_attributes(super, "down")
332
+ end
333
+
334
+ private
335
+
336
+ sig do
337
+ params(attributes: T::Hash[Symbol, T.untyped], direction: String)
338
+ .returns(T::Hash[Symbol, T.untyped])
339
+ end
340
+ def scroll_button_attributes(attributes, direction)
341
+ attributes[:hidden] = true
342
+ merge_nested(attributes, :aria, { hidden: "true" })
343
+ merge_nested(
344
+ attributes,
345
+ :data,
346
+ {
347
+ scroll_direction: direction,
348
+ action: "mouseenter->#{CONTROLLER}#startScroll mouseleave->#{CONTROLLER}#stopScroll"
349
+ }
350
+ )
351
+ merge_style(attributes, position: "absolute")
352
+ attributes
353
+ end
354
+ end
355
+ end
356
+ end
@@ -0,0 +1,42 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Shadcn
5
+ class Separator < BaseComponent
6
+ # 契約タグは SeparatorPrimitive.Root。ネイティブな div(装飾区切り線)として描画する
7
+ sig { override.returns(String) }
8
+ def default_tag
9
+ "div"
10
+ end
11
+
12
+ # upstream は orientation / decorative を動的属性として持つ(規定 horizontal / true)
13
+ sig do
14
+ params(
15
+ orientation: T.any(Symbol, String),
16
+ args: T::Hash[Symbol, T.untyped]
17
+ ).void.checked(:never)
18
+ end
19
+ def initialize(orientation: self.class.property_default(:orientation), **args)
20
+ @orientation = T.let(normalize_property(:orientation, orientation), String)
21
+ super(**args)
22
+ end
23
+
24
+ sig { returns(String) }
25
+ attr_reader :orientation
26
+
27
+ # ネイティブ要素化に伴う追加属性(05-stimulus-hotwire §3 の方針。
28
+ # 適合試験は spec/conformance/allowances.yml でこれらを明示的に許可する)
29
+ sig { override.returns(T::Hash[Symbol, T.untyped]) }
30
+ def contract_data_attributes
31
+ super.merge(orientation: @orientation)
32
+ end
33
+
34
+ sig { override.returns(T::Hash[Symbol, T.untyped]) }
35
+ def html_attributes
36
+ attributes = super
37
+ merge_nested(attributes, :aria, { orientation: @orientation })
38
+ attributes[:role] = "separator" unless @html_args.key?(:role)
39
+ attributes
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,157 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Shadcn
5
+ # スライドインパネル(ネイティブ <dialog> + スライド変形 — 10-roadmap Phase 3)。
6
+ # JS無効時フォールバック: Readable(dialog と同じ)
7
+ class Sheet < BaseComponent
8
+ CONTROLLER = "shadcn--dialog"
9
+
10
+ sig { params(args: T::Hash[Symbol, T.untyped]).void.checked(:never) }
11
+ def initialize(**args)
12
+ assign_accessibility_root_id(args, prefix: "sheet")
13
+ super
14
+ end
15
+
16
+ sig { override.returns(T::Hash[Symbol, T.untyped]) }
17
+ def contract_data_attributes
18
+ super.merge(controller: CONTROLLER)
19
+ end
20
+
21
+ class Trigger < BaseComponent
22
+ include Shadcn::ButtonStyled
23
+
24
+ sig { override.returns(String) }
25
+ def default_tag
26
+ "button"
27
+ end
28
+
29
+ sig { override.returns(T::Hash[Symbol, T.untyped]) }
30
+ def html_attributes
31
+ attributes = apply_button_styling(super)
32
+ attributes[:type] = "button" unless attributes.key?(:type)
33
+ merge_nested(attributes, :aria, { haspopup: "dialog", expanded: "false" })
34
+ merge_nested(attributes, :data, { action: "#{CONTROLLER}#show" })
35
+ attributes
36
+ end
37
+ end
38
+
39
+ class Close < BaseComponent
40
+ sig { override.returns(String) }
41
+ def default_tag
42
+ "button"
43
+ end
44
+
45
+ sig { override.returns(T::Hash[Symbol, T.untyped]) }
46
+ def html_attributes
47
+ attributes = super
48
+ attributes[:type] = "button" unless attributes.key?(:type)
49
+ merge_nested(attributes, :data, { action: "#{CONTROLLER}#close" })
50
+ attributes
51
+ end
52
+ end
53
+
54
+ class Content < BaseComponent
55
+ # side は base-nova では契約propではなく data-side 属性(クラスは
56
+ # data-[side=...]: のCSS variantで追従する)。4枝のクラスは契約に統合済み
57
+ sig do
58
+ params(
59
+ side: T.any(Symbol, String),
60
+ show_close_button: T::Boolean,
61
+ args: T::Hash[Symbol, T.untyped]
62
+ ).void.checked(:never)
63
+ end
64
+ def initialize(side: self.class.property_default(:side), show_close_button: true, **args)
65
+ @side = T.let(normalize_property(:side, side), String)
66
+ @show_close_button = show_close_button
67
+ super(**args)
68
+ end
69
+
70
+ sig { override.returns(String) }
71
+ def call
72
+ content_tag(:dialog, **content_attributes) do
73
+ safe_join([content.presence, close_button].compact)
74
+ end
75
+ end
76
+
77
+ private
78
+
79
+ sig { returns(T::Hash[Symbol, T.untyped]) }
80
+ def content_attributes
81
+ attributes = @html_args.merge(class: self.class.classes(extra: @user_class))
82
+ data = T.cast(attributes[:data], T.nilable(T::Hash[Symbol, T.untyped])) || {}
83
+ attributes[:data] = { slot: "sheet-content", side: @side }.merge(data)
84
+ aria = T.cast(attributes[:aria], T.nilable(T::Hash[Symbol, T.untyped])) || {}
85
+ attributes[:aria] = { modal: "true" }.merge(aria)
86
+ attributes
87
+ end
88
+
89
+ # upstream のクローズボタン(render prop のButton variant="ghost" size="icon-sm"
90
+ # + absolute配置)。契約クラスはButton契約から合成する
91
+ sig { returns(T.nilable(String)) }
92
+ def close_button
93
+ return nil unless @show_close_button
94
+
95
+ content_tag(
96
+ :button,
97
+ type: "button",
98
+ class: close_class,
99
+ data: { slot: "sheet-close", action: "#{CONTROLLER}#close" },
100
+ aria: { label: "Close" }
101
+ ) do
102
+ safe_join([close_icon, content_tag(:span, class: "sr-only") { "Close" }])
103
+ end
104
+ end
105
+
106
+ sig { returns(String) }
107
+ def close_class
108
+ # NOTE: Hashの値型は不変のため、Classes.resolve の options と同じ型で T.let する
109
+ button_options = T.let({ variant: :ghost, size: :"icon-sm" },
110
+ T::Hash[Symbol, T.nilable(T.any(Symbol, String))])
111
+ ShadcnViewComponents::Classes.resolve(:button, extra: "absolute top-3 right-3", **button_options)
112
+ end
113
+
114
+ sig { returns(String) }
115
+ def close_icon
116
+ content_tag(
117
+ :svg,
118
+ xmlns: "http://www.w3.org/2000/svg",
119
+ viewBox: "0 0 24 24",
120
+ fill: "none",
121
+ stroke: "currentColor",
122
+ "stroke-width": "2",
123
+ "stroke-linecap": "round",
124
+ "stroke-linejoin": "round",
125
+ width: "16",
126
+ height: "16",
127
+ class: "size-4",
128
+ aria: { hidden: "true" }
129
+ ) do
130
+ raw(%(<path d="M18 6 6 18"/><path d="m6 6 12 12"/>))
131
+ end
132
+ end
133
+ end
134
+
135
+ class Header < BaseComponent
136
+ # div
137
+ end
138
+
139
+ class Footer < BaseComponent
140
+ # div
141
+ end
142
+
143
+ class Title < BaseComponent
144
+ sig { override.returns(String) }
145
+ def default_tag
146
+ "h2"
147
+ end
148
+ end
149
+
150
+ class Description < BaseComponent
151
+ sig { override.returns(String) }
152
+ def default_tag
153
+ "p"
154
+ end
155
+ end
156
+ end
157
+ end