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