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