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,552 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Shadcn
|
|
5
|
+
module ComboboxValueNormalization
|
|
6
|
+
extend T::Sig
|
|
7
|
+
|
|
8
|
+
private
|
|
9
|
+
|
|
10
|
+
sig { params(value: T.untyped, attribute: String).returns(String) }
|
|
11
|
+
def normalize_form_value(value, attribute:)
|
|
12
|
+
valid = value.is_a?(String) || value.is_a?(Symbol) || value.is_a?(Integer) || value.is_a?(Float)
|
|
13
|
+
Kernel.raise ArgumentError, "#{attribute} must be a String, Symbol, Integer, or Float" unless valid
|
|
14
|
+
|
|
15
|
+
value.to_s
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
private_constant :ComboboxValueNormalization
|
|
19
|
+
|
|
20
|
+
# 検索表示と確定したフォーム値を分離するコンボボックス。
|
|
21
|
+
# upstream と同じ input-group の見た目を、Input + Popover API リストで構成する。
|
|
22
|
+
# JS無効時フォールバック: JS必須(項目はSSR済みで読める。絞り込みはJS依存)
|
|
23
|
+
class Combobox < BaseComponent
|
|
24
|
+
CONTROLLER = "shadcn--combobox"
|
|
25
|
+
FormValue = T.type_alias { T.any(String, Symbol, Integer, Float) }
|
|
26
|
+
DefaultValue = T.type_alias { T.nilable(T.any(FormValue, T::Array[T.nilable(FormValue)])) }
|
|
27
|
+
FORM_OWNED_ATTRIBUTES = %i[name form required disabled value multiple].freeze
|
|
28
|
+
VISUALLY_HIDDEN_CONTROL_STYLE = "clip-path: inset(50%); overflow: hidden; white-space: nowrap; border: 0; " \
|
|
29
|
+
"padding: 0; width: 1px; height: 1px; margin: -1px; position: absolute"
|
|
30
|
+
|
|
31
|
+
include ComboboxValueNormalization
|
|
32
|
+
|
|
33
|
+
private_constant :FormValue, :DefaultValue, :FORM_OWNED_ATTRIBUTES, :VISUALLY_HIDDEN_CONTROL_STYLE
|
|
34
|
+
|
|
35
|
+
# ネイティブフォームと同じ公開keywordを列挙し、一つのHashへ隠さない。
|
|
36
|
+
sig do
|
|
37
|
+
params(
|
|
38
|
+
name: T.nilable(T.any(String, Symbol)),
|
|
39
|
+
default_value: DefaultValue,
|
|
40
|
+
multiple: T::Boolean,
|
|
41
|
+
disabled: T::Boolean,
|
|
42
|
+
required: T::Boolean,
|
|
43
|
+
form: T.nilable(T.any(String, Symbol)),
|
|
44
|
+
args: T::Hash[Symbol, T.untyped]
|
|
45
|
+
).void.checked(:never)
|
|
46
|
+
end
|
|
47
|
+
def initialize(name: nil, default_value: nil, multiple: false, disabled: false, required: false, form: nil, **args) # rubocop:disable Metrics/ParameterLists
|
|
48
|
+
assign_accessibility_root_id(args, prefix: "combobox")
|
|
49
|
+
@name = T.let(name&.to_s, T.nilable(String))
|
|
50
|
+
@multiple = T.let(multiple, T::Boolean)
|
|
51
|
+
@disabled = T.let(disabled, T::Boolean)
|
|
52
|
+
@required = T.let(required, T::Boolean)
|
|
53
|
+
@form = T.let(form&.to_s, T.nilable(String))
|
|
54
|
+
@default_values = T.let(normalize_default_values(default_value), T::Array[String])
|
|
55
|
+
@root_id = T.let(args[:id].to_s, String)
|
|
56
|
+
super(**args)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# upstream の Combobox ルートは描画物を持たないprimitive aliasで、生成契約も
|
|
60
|
+
# その空の契約面を記録する。本gemでは入力とリストを束ねるラッパーとして描く。
|
|
61
|
+
sig { override.returns(String) }
|
|
62
|
+
def call
|
|
63
|
+
content_tag(:div, **root_attributes) { safe_join([content, form_control, empty_multiple_control]) }
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
private
|
|
67
|
+
|
|
68
|
+
sig { returns(T::Hash[Symbol, T.untyped]) }
|
|
69
|
+
def root_attributes
|
|
70
|
+
attributes = @html_args.merge(class: @user_class, data: root_data_attributes)
|
|
71
|
+
add_disabled_root_attributes(attributes) if @disabled
|
|
72
|
+
attributes
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
sig { returns(T::Hash[Symbol, T.untyped]) }
|
|
76
|
+
def root_data_attributes
|
|
77
|
+
data = T.cast(@html_args[:data], T.nilable(T::Hash[Symbol, T.untyped])) || {}
|
|
78
|
+
{ controller: CONTROLLER }.merge(data)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
sig { params(attributes: T::Hash[Symbol, T.untyped]).void }
|
|
82
|
+
def add_disabled_root_attributes(attributes)
|
|
83
|
+
attributes[:inert] = true unless attributes.key?(:inert)
|
|
84
|
+
aria = T.cast(attributes[:aria], T.nilable(T::Hash[Symbol, T.untyped])) || {}
|
|
85
|
+
attributes[:aria] = { disabled: "true" }.merge(aria)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
sig { params(default_value: DefaultValue).returns(T::Array[String]).checked(:never) }
|
|
89
|
+
def normalize_default_values(default_value)
|
|
90
|
+
values = @multiple ? multiple_default_values(default_value) : single_default_values(default_value)
|
|
91
|
+
values.filter_map do |value|
|
|
92
|
+
next if value.nil?
|
|
93
|
+
|
|
94
|
+
normalized = normalize_form_value(value, attribute: "default_value")
|
|
95
|
+
normalized unless normalized.strip.empty?
|
|
96
|
+
end.uniq
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
sig { params(default_value: DefaultValue).returns(T::Array[T.nilable(FormValue)]).checked(:never) }
|
|
100
|
+
def multiple_default_values(default_value)
|
|
101
|
+
return [] if default_value.nil?
|
|
102
|
+
|
|
103
|
+
raise ArgumentError, "default_value must be an Array when multiple is true" unless default_value.is_a?(Array)
|
|
104
|
+
|
|
105
|
+
default_value
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
sig { params(default_value: DefaultValue).returns(T::Array[T.nilable(FormValue)]).checked(:never) }
|
|
109
|
+
def single_default_values(default_value)
|
|
110
|
+
raise ArgumentError, "default_value must be a scalar when multiple is false" if default_value.is_a?(Array)
|
|
111
|
+
|
|
112
|
+
default_value.nil? ? [] : [default_value]
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
sig { returns(T::Hash[Symbol, T.untyped]) }
|
|
116
|
+
def form_owned_attributes
|
|
117
|
+
attributes = T.let({}, T::Hash[Symbol, T.untyped])
|
|
118
|
+
attributes[:name] = @name if @name
|
|
119
|
+
attributes[:form] = @form if @form
|
|
120
|
+
attributes
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
sig { returns(String) }
|
|
124
|
+
def empty_multiple_control
|
|
125
|
+
return "" unless @multiple
|
|
126
|
+
|
|
127
|
+
attributes = form_owned_attributes.merge(
|
|
128
|
+
type: "hidden",
|
|
129
|
+
value: "",
|
|
130
|
+
disabled: @disabled || @default_values.any?,
|
|
131
|
+
data: { slot: "combobox-empty-form-control" }
|
|
132
|
+
)
|
|
133
|
+
void_tag("input", **attributes)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
sig { returns(String) }
|
|
137
|
+
def form_control
|
|
138
|
+
attributes = form_owned_attributes.merge(
|
|
139
|
+
multiple: @multiple,
|
|
140
|
+
disabled: @disabled,
|
|
141
|
+
required: @required,
|
|
142
|
+
tabindex: "-1",
|
|
143
|
+
style: VISUALLY_HIDDEN_CONTROL_STYLE,
|
|
144
|
+
aria: { hidden: "true" },
|
|
145
|
+
data: { slot: "combobox-form-control", shadcn_generated_id: "true" }
|
|
146
|
+
)
|
|
147
|
+
attributes[:id] = "#{@root_id}-form-control"
|
|
148
|
+
|
|
149
|
+
values = !@multiple && @default_values.empty? ? [""] : @default_values
|
|
150
|
+
content_tag(:select, **attributes) do
|
|
151
|
+
safe_join(values.map { |value| content_tag(:option, value:, selected: true) { value } })
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
class Input < BaseComponent
|
|
156
|
+
# 契約スロット構成: input-group-button(トリガーアイコン)のみ。
|
|
157
|
+
# 検索inputは data-slot を持たない(role=combobox で識別する)
|
|
158
|
+
sig { override.returns(String) }
|
|
159
|
+
def default_tag
|
|
160
|
+
"div"
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
sig { params(args: T::Hash[Symbol, T.untyped]).void.checked(:never) }
|
|
164
|
+
def initialize(**args)
|
|
165
|
+
attribute = FORM_OWNED_ATTRIBUTES.find { |name| args.key?(name) }
|
|
166
|
+
raise ArgumentError, "#{attribute} belongs to Shadcn::Combobox, not its query input" if attribute
|
|
167
|
+
|
|
168
|
+
super
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# upstream は <InputGroup className="w-auto"> でラップするため、
|
|
172
|
+
# ラッパのクラスは input-group の契約クラスを土台に組む。
|
|
173
|
+
# class は InputGroup ラッパー、その他のHTML属性は実際の検索inputへ渡す。
|
|
174
|
+
sig { override.returns(T::Hash[Symbol, T.untyped]) }
|
|
175
|
+
def html_attributes
|
|
176
|
+
{
|
|
177
|
+
class: ShadcnViewComponents::Classes.resolve(:input_group, extra: "w-auto #{@user_class}".strip)
|
|
178
|
+
}
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
sig { override.returns(String) }
|
|
182
|
+
def call
|
|
183
|
+
content_tag(tag, **html_attributes) do
|
|
184
|
+
safe_join([search_input, trigger_addon])
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
private
|
|
189
|
+
|
|
190
|
+
sig { returns(String) }
|
|
191
|
+
def search_input
|
|
192
|
+
# キーボード操作はコントローラのキャプチャリスナーで一元処理する(二重発火防止)。
|
|
193
|
+
# クラスは upstream と同じ構成: Input の契約クラス + input-group-control の上書き
|
|
194
|
+
base = ShadcnViewComponents::Classes.resolve(:input)
|
|
195
|
+
overlay = input_group_combination("Input", {})
|
|
196
|
+
attributes = @html_args.except(:tag, :class)
|
|
197
|
+
attributes[:type] = "search" unless attributes.key?(:type)
|
|
198
|
+
attributes[:class] = ShadcnViewComponents::Classes::MERGER.merge("#{base} #{overlay}")
|
|
199
|
+
attributes[:role] = "combobox"
|
|
200
|
+
merge_nested(
|
|
201
|
+
attributes,
|
|
202
|
+
:aria,
|
|
203
|
+
{ expanded: "false", haspopup: "listbox", autocomplete: "list", label: "候補を検索" }
|
|
204
|
+
)
|
|
205
|
+
merge_nested(attributes, :data, { action: "input->#{Combobox::CONTROLLER}#filter" })
|
|
206
|
+
void_tag("input", **attributes)
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# upstream の <InputGroupAddon align="inline-end">(トリガーを右端に置く)
|
|
210
|
+
sig { returns(String) }
|
|
211
|
+
def trigger_addon
|
|
212
|
+
inline_end = { align: :"inline-end" }
|
|
213
|
+
content_tag(:div, class: input_group_combination("Addon", inline_end), data: { slot: "input-group-addon" }) do
|
|
214
|
+
trigger_button
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# 契約の input-group-button スロット。クラスは upstream と同じ構成で
|
|
219
|
+
# Button(ghost) + input-group-button(icon-xs) + combobox側の上書きを重ねる
|
|
220
|
+
sig { returns(String) }
|
|
221
|
+
def trigger_button
|
|
222
|
+
content_tag(
|
|
223
|
+
:button,
|
|
224
|
+
type: "button",
|
|
225
|
+
class: trigger_class,
|
|
226
|
+
data: { slot: "input-group-button", action: "#{Combobox::CONTROLLER}#toggleList" },
|
|
227
|
+
aria: { label: "選択肢を開く", haspopup: "listbox", expanded: "false" }
|
|
228
|
+
) { chevron_icon }
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
sig { returns(String) }
|
|
232
|
+
def trigger_class
|
|
233
|
+
# NOTE: Hashの値型は不変のため、Classes.resolve の options と同じ型で T.let する
|
|
234
|
+
button_options = T.let({ variant: :ghost }, T::Hash[Symbol, T.nilable(T.any(Symbol, String))])
|
|
235
|
+
button_base = ShadcnViewComponents::Classes.resolve(:button, extra: nil, **button_options)
|
|
236
|
+
overlay = input_group_combination("Button", { size: :"icon-xs" })
|
|
237
|
+
tail = T.cast(contract_slot("input-group-button").dig(:static_attributes, :class), T.nilable(String)).to_s
|
|
238
|
+
ShadcnViewComponents::Classes::MERGER.merge([button_base, overlay, tail].join(" "))
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
# input-group 契約の各スロット組み合わせを引く
|
|
242
|
+
sig { params(export: String, options: T::Hash[Symbol, T.untyped]).returns(String) }
|
|
243
|
+
def input_group_combination(export, options = {})
|
|
244
|
+
contract = ShadcnViewComponents::Contracts::InputGroup.const_get(export)
|
|
245
|
+
defaults = T.cast(contract.const_get(:DEFAULTS), T::Hash[Symbol, Symbol])
|
|
246
|
+
T.cast(T.unsafe(contract).combination(defaults.merge(options)), String)
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
sig { returns(String) }
|
|
250
|
+
def chevron_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
|
+
width: "16",
|
|
261
|
+
height: "16",
|
|
262
|
+
class: "size-4",
|
|
263
|
+
aria: { hidden: "true" }
|
|
264
|
+
) do
|
|
265
|
+
raw(%(<path d="m6 9 6 6 6-6"/>))
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
class Trigger < BaseComponent
|
|
271
|
+
# 開閉アイコン(chevron)。button として描く
|
|
272
|
+
sig { override.returns(String) }
|
|
273
|
+
def default_tag
|
|
274
|
+
"button"
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
sig { override.returns(T::Hash[Symbol, T.untyped]) }
|
|
278
|
+
def html_attributes
|
|
279
|
+
attributes = super
|
|
280
|
+
attributes[:type] = "button" unless attributes.key?(:type)
|
|
281
|
+
merge_nested(attributes, :aria, { label: "選択肢を開く", haspopup: "listbox", expanded: "false" })
|
|
282
|
+
merge_nested(attributes, :data, { action: "#{Combobox::CONTROLLER}#toggleList" })
|
|
283
|
+
attributes
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
sig { override.returns(String) }
|
|
287
|
+
def call
|
|
288
|
+
content_tag(tag, **html_attributes) { chevron_icon }
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
private
|
|
292
|
+
|
|
293
|
+
sig { returns(String) }
|
|
294
|
+
def chevron_icon
|
|
295
|
+
content_tag(
|
|
296
|
+
:svg,
|
|
297
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
298
|
+
viewBox: "0 0 24 24",
|
|
299
|
+
fill: "none",
|
|
300
|
+
stroke: "currentColor",
|
|
301
|
+
"stroke-width": "2",
|
|
302
|
+
"stroke-linecap": "round",
|
|
303
|
+
"stroke-linejoin": "round",
|
|
304
|
+
width: "16",
|
|
305
|
+
height: "16",
|
|
306
|
+
class: "pointer-events-none size-4 text-muted-foreground",
|
|
307
|
+
aria: { hidden: "true" }
|
|
308
|
+
) do
|
|
309
|
+
raw(%(<path d="m6 9 6 6 6-6"/>))
|
|
310
|
+
end
|
|
311
|
+
end
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
class Content < BaseComponent
|
|
315
|
+
# Popover API で開くリストボックス
|
|
316
|
+
sig { override.returns(T::Hash[Symbol, T.untyped]) }
|
|
317
|
+
def html_attributes
|
|
318
|
+
attributes = super
|
|
319
|
+
attributes[:role] = "listbox"
|
|
320
|
+
attributes[:popover] = "manual"
|
|
321
|
+
data = T.cast(attributes[:data], T.nilable(T::Hash[Symbol, T.untyped])) || {}
|
|
322
|
+
attributes[:data] = { state: "closed" }.merge(data)
|
|
323
|
+
attributes
|
|
324
|
+
end
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
class List < BaseComponent
|
|
328
|
+
# div(絞り込み対象の親)
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
class Item < BaseComponent
|
|
332
|
+
include ComboboxValueNormalization
|
|
333
|
+
|
|
334
|
+
# base-nova の combobox-item に indicator スロットは無い(チェックは装飾要素)
|
|
335
|
+
sig do
|
|
336
|
+
params(
|
|
337
|
+
value: FormValue,
|
|
338
|
+
disabled: T::Boolean,
|
|
339
|
+
args: T::Hash[Symbol, T.untyped]
|
|
340
|
+
).void.checked(:never)
|
|
341
|
+
end
|
|
342
|
+
def initialize(value:, disabled: false, **args)
|
|
343
|
+
@value = T.let(normalize_form_value(value, attribute: "value"), String)
|
|
344
|
+
raise ArgumentError, "value must not be blank" if @value.strip.empty?
|
|
345
|
+
|
|
346
|
+
@disabled = T.let(disabled, T::Boolean)
|
|
347
|
+
super(**args)
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
sig { override.returns(T::Hash[Symbol, T.untyped]) }
|
|
351
|
+
def html_attributes
|
|
352
|
+
attributes = super
|
|
353
|
+
attributes[:role] = "option"
|
|
354
|
+
attributes[:tabindex] = "-1"
|
|
355
|
+
merge_nested(
|
|
356
|
+
attributes,
|
|
357
|
+
:aria,
|
|
358
|
+
{ selected: "false" }.merge(@disabled ? { disabled: "true" } : {})
|
|
359
|
+
)
|
|
360
|
+
# クリックでの選択確定。キー操作はコントローラのキャプチャリスナーで
|
|
361
|
+
# 一元処理するため data-action にしない(二重発火防止)
|
|
362
|
+
merge_nested(
|
|
363
|
+
attributes,
|
|
364
|
+
:data,
|
|
365
|
+
{
|
|
366
|
+
value: @value,
|
|
367
|
+
selected: "false",
|
|
368
|
+
action: "click->#{Combobox::CONTROLLER}#select"
|
|
369
|
+
}.merge(@disabled ? { disabled: "" } : {})
|
|
370
|
+
)
|
|
371
|
+
attributes
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
sig { override.returns(String) }
|
|
375
|
+
def call
|
|
376
|
+
content_tag(tag, **html_attributes) do
|
|
377
|
+
safe_join([content, indicator])
|
|
378
|
+
end
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
private
|
|
382
|
+
|
|
383
|
+
sig { returns(String) }
|
|
384
|
+
def indicator
|
|
385
|
+
# 未選択時は HTML の hidden 属性で隠す(upstream の ItemIndicator と同じ挙動)。
|
|
386
|
+
# data-indicator は JS からの選択状態切替用(data-slot ではない)
|
|
387
|
+
content_tag(:span, hidden: true, data: { indicator: "true" }) do
|
|
388
|
+
check_icon
|
|
389
|
+
end
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
sig { returns(String) }
|
|
393
|
+
def check_icon
|
|
394
|
+
content_tag(
|
|
395
|
+
:svg,
|
|
396
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
397
|
+
viewBox: "0 0 24 24",
|
|
398
|
+
fill: "none",
|
|
399
|
+
stroke: "currentColor",
|
|
400
|
+
"stroke-width": "2",
|
|
401
|
+
"stroke-linecap": "round",
|
|
402
|
+
"stroke-linejoin": "round",
|
|
403
|
+
width: "14",
|
|
404
|
+
height: "14",
|
|
405
|
+
class: "size-4"
|
|
406
|
+
) do
|
|
407
|
+
raw(%(<path d="M20 6 9 17l-5-5"/>))
|
|
408
|
+
end
|
|
409
|
+
end
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
class Group < BaseComponent
|
|
413
|
+
# div
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
class Label < BaseComponent
|
|
417
|
+
# div
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
class Empty < BaseComponent
|
|
421
|
+
sig { override.returns(T::Hash[Symbol, T.untyped]) }
|
|
422
|
+
def html_attributes
|
|
423
|
+
super.tap { |attributes| attributes[:hidden] = true }
|
|
424
|
+
end
|
|
425
|
+
end
|
|
426
|
+
|
|
427
|
+
class Separator < BaseComponent
|
|
428
|
+
# div
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
class Collection < BaseComponent
|
|
432
|
+
# 利用者が項目群を括るための汎用div
|
|
433
|
+
end
|
|
434
|
+
|
|
435
|
+
class Value < BaseComponent
|
|
436
|
+
# 選択値の表示用span
|
|
437
|
+
end
|
|
438
|
+
|
|
439
|
+
class Chips < BaseComponent
|
|
440
|
+
sig { override.returns(String) }
|
|
441
|
+
def call
|
|
442
|
+
content_tag(tag, **html_attributes) do
|
|
443
|
+
safe_join([content, chip_template])
|
|
444
|
+
end
|
|
445
|
+
end
|
|
446
|
+
|
|
447
|
+
private
|
|
448
|
+
|
|
449
|
+
sig { returns(String) }
|
|
450
|
+
def chip_template
|
|
451
|
+
content_tag(:template, data: { slot: "combobox-chip-template" }) do
|
|
452
|
+
render(Chip.new(value: "__combobox_chip_template__")) { "__combobox_chip_label__" }
|
|
453
|
+
end
|
|
454
|
+
end
|
|
455
|
+
end
|
|
456
|
+
|
|
457
|
+
class Chip < BaseComponent
|
|
458
|
+
include ComboboxValueNormalization
|
|
459
|
+
|
|
460
|
+
# チップ + 取り除きボタン(契約スロット: combobox-chip / combobox-chip-remove)
|
|
461
|
+
sig do
|
|
462
|
+
params(
|
|
463
|
+
value: FormValue,
|
|
464
|
+
args: T::Hash[Symbol, T.untyped]
|
|
465
|
+
).void.checked(:never)
|
|
466
|
+
end
|
|
467
|
+
def initialize(value:, **args)
|
|
468
|
+
@value = T.let(normalize_form_value(value, attribute: "value"), String)
|
|
469
|
+
raise ArgumentError, "value must not be blank" if @value.strip.empty?
|
|
470
|
+
|
|
471
|
+
super(**args)
|
|
472
|
+
end
|
|
473
|
+
|
|
474
|
+
sig { override.returns(T::Hash[Symbol, T.untyped]) }
|
|
475
|
+
def html_attributes
|
|
476
|
+
super.tap { |attributes| merge_nested(attributes, :data, { value: @value }) }
|
|
477
|
+
end
|
|
478
|
+
|
|
479
|
+
sig { override.returns(String) }
|
|
480
|
+
def call
|
|
481
|
+
content_tag(tag, **html_attributes) do
|
|
482
|
+
safe_join([content_tag(:span, data: { slot: "combobox-chip-label" }) { content }, remove_button])
|
|
483
|
+
end
|
|
484
|
+
end
|
|
485
|
+
|
|
486
|
+
private
|
|
487
|
+
|
|
488
|
+
sig { returns(String) }
|
|
489
|
+
def remove_button
|
|
490
|
+
# upstream は <Button variant="ghost" size="icon-xs"> に
|
|
491
|
+
# combobox 側の上書き(-ml-1 opacity-50 …)を重ねる
|
|
492
|
+
options = T.let({ variant: :ghost, size: :"icon-xs" }, T::Hash[Symbol, T.nilable(T.any(Symbol, String))])
|
|
493
|
+
base = ShadcnViewComponents::Classes.resolve(:button, extra: nil, **options)
|
|
494
|
+
content_tag(
|
|
495
|
+
:button,
|
|
496
|
+
type: "button",
|
|
497
|
+
class: ShadcnViewComponents::Classes::MERGER.merge("#{base} -ml-1 opacity-50 hover:opacity-100"),
|
|
498
|
+
data: { slot: "combobox-chip-remove", action: "#{Combobox::CONTROLLER}#removeChip" },
|
|
499
|
+
aria: { label: "削除" }
|
|
500
|
+
) { remove_icon }
|
|
501
|
+
end
|
|
502
|
+
|
|
503
|
+
sig { returns(String) }
|
|
504
|
+
def remove_icon
|
|
505
|
+
content_tag(
|
|
506
|
+
:svg,
|
|
507
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
508
|
+
viewBox: "0 0 24 24",
|
|
509
|
+
fill: "none",
|
|
510
|
+
stroke: "currentColor",
|
|
511
|
+
"stroke-width": "2",
|
|
512
|
+
"stroke-linecap": "round",
|
|
513
|
+
"stroke-linejoin": "round",
|
|
514
|
+
width: "12",
|
|
515
|
+
height: "12",
|
|
516
|
+
class: "size-3",
|
|
517
|
+
aria: { hidden: "true" }
|
|
518
|
+
) { raw(%(<path d="M18 6 6 18"/><path d="m6 6 12 12"/>)) }
|
|
519
|
+
end
|
|
520
|
+
end
|
|
521
|
+
|
|
522
|
+
class ChipsInput < BaseComponent
|
|
523
|
+
# チップ列の末尾にある検索input
|
|
524
|
+
sig { override.returns(String) }
|
|
525
|
+
def default_tag
|
|
526
|
+
"input"
|
|
527
|
+
end
|
|
528
|
+
|
|
529
|
+
sig { params(args: T::Hash[Symbol, T.untyped]).void.checked(:never) }
|
|
530
|
+
def initialize(**args)
|
|
531
|
+
attribute = FORM_OWNED_ATTRIBUTES.find { |name| args.key?(name) }
|
|
532
|
+
raise ArgumentError, "#{attribute} belongs to Shadcn::Combobox, not its query input" if attribute
|
|
533
|
+
|
|
534
|
+
super
|
|
535
|
+
end
|
|
536
|
+
|
|
537
|
+
sig { override.returns(T::Hash[Symbol, T.untyped]) }
|
|
538
|
+
def html_attributes
|
|
539
|
+
attributes = super
|
|
540
|
+
attributes[:type] = "text" unless attributes.key?(:type)
|
|
541
|
+
attributes[:role] = "combobox"
|
|
542
|
+
merge_nested(
|
|
543
|
+
attributes,
|
|
544
|
+
:aria,
|
|
545
|
+
{ expanded: "false", haspopup: "listbox", autocomplete: "list", label: "候補を検索" }
|
|
546
|
+
)
|
|
547
|
+
merge_nested(attributes, :data, { action: "input->#{Combobox::CONTROLLER}#filter" })
|
|
548
|
+
attributes
|
|
549
|
+
end
|
|
550
|
+
end
|
|
551
|
+
end
|
|
552
|
+
end
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Shadcn
|
|
5
|
+
# コマンドパレット(10-roadmap Phase 3「command = ARIA combobox/listbox + フィルタlib」)。
|
|
6
|
+
# SSR済みリストを入力でフィルタする(command_controller)。情報はJSで構築しない
|
|
7
|
+
# JS無効時フォールバック: Readable(全項目がSSR済み)
|
|
8
|
+
class Command < BaseComponent
|
|
9
|
+
CONTROLLER = "shadcn--command"
|
|
10
|
+
|
|
11
|
+
sig { override.returns(T::Hash[Symbol, T.untyped]) }
|
|
12
|
+
def contract_data_attributes
|
|
13
|
+
super.merge(controller: CONTROLLER)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# upstream は Dialog + Command の合成。本gemでも同じ構成で再利用する
|
|
17
|
+
class Dialog < BaseComponent
|
|
18
|
+
sig { override.returns(String) }
|
|
19
|
+
def call
|
|
20
|
+
# 契約ルートは data-slot を持たない div(className="overflow-hidden p-0")。
|
|
21
|
+
# dialog/command の開閉構造はその内側に置く
|
|
22
|
+
content_tag(:div, class: self.class.classes(extra: @user_class)) do
|
|
23
|
+
render(::Shadcn::Dialog.new) do
|
|
24
|
+
render(::Shadcn::Dialog::Content.new) do
|
|
25
|
+
render(::Shadcn::Command.new) { content }
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
class Input < BaseComponent
|
|
33
|
+
# 契約スロット構成: command-input-wrapper(div) > command-input(input)。
|
|
34
|
+
# upstream はさらに内側を <InputGroup>(別アイテム)でラップし、検索アイコンは
|
|
35
|
+
# <InputGroupAddon align="inline-start"> に置く(そのため input が先・icon が後)
|
|
36
|
+
sig { override.returns(String) }
|
|
37
|
+
def default_tag
|
|
38
|
+
"div"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
sig do
|
|
42
|
+
params(placeholder: T.nilable(String), args: T::Hash[Symbol, T.untyped]).void.checked(:never)
|
|
43
|
+
end
|
|
44
|
+
def initialize(placeholder: nil, **args)
|
|
45
|
+
@placeholder = T.let(placeholder, T.nilable(String))
|
|
46
|
+
super(**args)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
sig { override.returns(String) }
|
|
50
|
+
def call
|
|
51
|
+
content_tag(tag, **html_attributes) do
|
|
52
|
+
render(Shadcn::InputGroup.new(**group_args)) do
|
|
53
|
+
safe_join([input_element, addon_element])
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
# upstream CommandInput の InputGroup への上書き(className)
|
|
61
|
+
sig { returns(T::Hash[Symbol, T.untyped]) }
|
|
62
|
+
def group_args
|
|
63
|
+
{ class: "h-8! rounded-lg! border-input/30 bg-input/30 shadow-none! " \
|
|
64
|
+
"*:data-[slot=input-group-addon]:pl-2!" }
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
sig { returns(String) }
|
|
68
|
+
def input_element
|
|
69
|
+
# キーボード操作はコントローラのキャプチャリスナーで一元処理する(data-action に
|
|
70
|
+
# keydown を置くと二重発火する — 片方のみにバインドする)
|
|
71
|
+
void_tag(
|
|
72
|
+
"input",
|
|
73
|
+
type: "text",
|
|
74
|
+
placeholder: @placeholder,
|
|
75
|
+
autocomplete: "off",
|
|
76
|
+
class: self.class.classes,
|
|
77
|
+
data: { slot: "command-input", action: "input->#{Command::CONTROLLER}#filter" },
|
|
78
|
+
aria: { label: "コマンド検索" }
|
|
79
|
+
)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
sig { returns(String) }
|
|
83
|
+
def addon_element
|
|
84
|
+
render(Shadcn::InputGroup::Addon.new(align: "inline-start")) do
|
|
85
|
+
search_icon
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
sig { returns(String) }
|
|
90
|
+
def search_icon
|
|
91
|
+
content_tag(
|
|
92
|
+
:svg,
|
|
93
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
94
|
+
viewBox: "0 0 24 24",
|
|
95
|
+
fill: "none",
|
|
96
|
+
stroke: "currentColor",
|
|
97
|
+
"stroke-width": "2",
|
|
98
|
+
"stroke-linecap": "round",
|
|
99
|
+
"stroke-linejoin": "round",
|
|
100
|
+
class: "size-4 shrink-0 opacity-50",
|
|
101
|
+
aria: { hidden: "true" }
|
|
102
|
+
) do
|
|
103
|
+
raw(%(<path d="m21 21-4.34-4.34"/><circle cx="11" cy="11" r="8"/>))
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
class List < BaseComponent
|
|
109
|
+
# div(フィルタ対象の親)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
class Empty < BaseComponent
|
|
113
|
+
# フィルタ結果が空のときだけ表示(コントローラが切替)
|
|
114
|
+
sig { override.returns(T::Hash[Symbol, T.untyped]) }
|
|
115
|
+
def html_attributes
|
|
116
|
+
super.tap { |attributes| attributes[:hidden] = true }
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
class Group < BaseComponent
|
|
121
|
+
# div(見出しは利用者が content に含める)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
class Item < BaseComponent
|
|
125
|
+
sig { params(value: T.nilable(String), args: T::Hash[Symbol, T.untyped]).void.checked(:never) }
|
|
126
|
+
def initialize(value: nil, **args)
|
|
127
|
+
@value = value
|
|
128
|
+
super(**args)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
sig { override.returns(T::Hash[Symbol, T.untyped]) }
|
|
132
|
+
def html_attributes
|
|
133
|
+
attributes = super
|
|
134
|
+
attributes[:role] = "option"
|
|
135
|
+
attributes[:tabindex] = "-1"
|
|
136
|
+
merge_nested(attributes, :data, { value: @value, selected: "false" }.compact)
|
|
137
|
+
attributes
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
class Separator < BaseComponent
|
|
142
|
+
# div
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
class Shortcut < BaseComponent
|
|
146
|
+
# span
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|