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,239 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "securerandom"
|
|
5
|
+
|
|
6
|
+
module Shadcn
|
|
7
|
+
# 共通基底(04-component-conventions §5-6)。属性マージ・クラス解決・tag差し替えを担う。
|
|
8
|
+
# 利用者が直接参照しない private 扱いのクラス。
|
|
9
|
+
class BaseComponent < ViewComponent::Base
|
|
10
|
+
extend T::Sig
|
|
11
|
+
|
|
12
|
+
VariantOption = T.type_alias { T.nilable(T.any(Symbol, String)) }
|
|
13
|
+
|
|
14
|
+
# HTMLのvoid要素。閉じタグを付けてはならない
|
|
15
|
+
VOID_ELEMENTS = %w[area base br col embed hr img input link meta source track wbr].freeze
|
|
16
|
+
|
|
17
|
+
# ViewComponent::Base は initialize を持たず、ActionView::Base の initialize は
|
|
18
|
+
# 互換性のない引数を取るため super を呼ばない(ViewComponentの標準的な慣行)。
|
|
19
|
+
# この件は .rubocop.yml の Lint/MissingSuper Exclude にも反映している。
|
|
20
|
+
# なお rest-kwargs を持つメソッドの実行時sig検証は sorbet-runtime の既知の誤バインドがあるため
|
|
21
|
+
# checked(:never) で無効化し、srb tc の静的検査に委ねる(module_function 経由の
|
|
22
|
+
# ShadcnViewComponents::Classes は実行時検証が正しく働くので検証はそこで行う)
|
|
23
|
+
sig { params(args: T::Hash[Symbol, T.untyped]).void.checked(:never) }
|
|
24
|
+
def initialize(**args)
|
|
25
|
+
raw_tag = args.delete(:tag)
|
|
26
|
+
raw_class = args.delete(:class)
|
|
27
|
+
@tag = T.let(raw_tag&.to_s, T.nilable(String))
|
|
28
|
+
@user_class = T.let(raw_class&.to_s, T.nilable(String))
|
|
29
|
+
@html_args = T.let(args, T::Hash[Symbol, T.untyped])
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
class << self
|
|
33
|
+
extend T::Sig
|
|
34
|
+
|
|
35
|
+
# 契約モジュールへのパス(例: Shadcn::Button → "button"、Shadcn::Card::Header → "card/header")
|
|
36
|
+
sig { returns(String) }
|
|
37
|
+
def contract_path
|
|
38
|
+
to_s.delete_prefix("Shadcn::").underscore
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
sig { returns(T::Module[T.anything]) }
|
|
42
|
+
def contract
|
|
43
|
+
ShadcnViewComponents::Classes.contract_for(contract_path.to_sym)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# クラス文字列だけを提供するモジュール関数(04 §8)。
|
|
47
|
+
# ホストが自前ERBに段階的に導入するための抜け道。
|
|
48
|
+
# checked(:never) の理由は initialize のコメント参照(rest-kwargsの実行時検証問題)
|
|
49
|
+
sig { params(extra: T.nilable(String), options: T::Hash[Symbol, VariantOption]).returns(String).checked(:never) }
|
|
50
|
+
def classes(extra: nil, **options)
|
|
51
|
+
ShadcnViewComponents::Classes.resolve(contract_path.to_sym, extra: extra, **options)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# CVAバリアント以外の意味的な公開prop契約を返す。
|
|
55
|
+
sig { params(prop: Symbol).returns(T::Hash[Symbol, T.untyped]) }
|
|
56
|
+
def property_contract(prop)
|
|
57
|
+
ShadcnViewComponents::PropertyContracts.fetch(contract_path.to_sym, prop)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
sig { params(prop: Symbol).returns(T.untyped) }
|
|
61
|
+
def property_default(prop)
|
|
62
|
+
ShadcnViewComponents::PropertyContracts.default(contract_path.to_sym, prop)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# 構造を持たない単一要素コンポーネントの既定レンダリング。
|
|
67
|
+
# ERBテンプレートを置かない限りこれが使われる(クラス文字列を持たない — 04 §5)。
|
|
68
|
+
# 構造を持つコンポーネント(Buttonのdata属性、Tableのラッパー等)はcallを上書きするか
|
|
69
|
+
# sidecarテンプレートを置く
|
|
70
|
+
sig { returns(String) }
|
|
71
|
+
def call
|
|
72
|
+
return open_tag if void_element?
|
|
73
|
+
|
|
74
|
+
content_tag(tag, **html_attributes) { content }
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
sig { returns(T::Boolean) }
|
|
78
|
+
def void_element?
|
|
79
|
+
VOID_ELEMENTS.include?(tag)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
private
|
|
83
|
+
|
|
84
|
+
# 複合コンポーネントのARIA参照で使うルートIDを、ViewComponentの
|
|
85
|
+
# インスタンス生成時に一度だけ決める。利用者指定IDはそのまま優先する。
|
|
86
|
+
# 自動生成IDの印は、fragment cache済みの同じHTMLが同一documentへ複数回
|
|
87
|
+
# 挿入された場合にだけStimulus側で安全に再採番するために使う。
|
|
88
|
+
sig { params(args: T::Hash[Symbol, T.untyped], prefix: String).void }
|
|
89
|
+
def assign_accessibility_root_id(args, prefix:)
|
|
90
|
+
supplied_id = args[:id]
|
|
91
|
+
return unless supplied_id.nil? || supplied_id.to_s.empty?
|
|
92
|
+
|
|
93
|
+
args[:id] = "shadcn-#{prefix}-#{SecureRandom.hex(12)}"
|
|
94
|
+
data = T.cast(args[:data], T.nilable(T::Hash[Symbol, T.untyped])) || {}
|
|
95
|
+
args[:data] = data.merge(shadcn_generated_root_id: "true")
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# void要素は閉じタグ無しで出力する。
|
|
99
|
+
# NOTE: content_tag はブロック付き呼び出しでのみ第二引数ハッシュが属性扱いになるため
|
|
100
|
+
# 空ブロックを渡してから閉じタグを取り除く
|
|
101
|
+
sig { returns(String) }
|
|
102
|
+
def open_tag
|
|
103
|
+
void_tag(tag, **html_attributes)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# 指定タグの void 要素(input/br/hr 等)を閉じタグ無しで出力する統一ヘルパ。
|
|
107
|
+
# 手書きの input 描画で使う(閉じタグ付き <input></input> はHTML仕様違反のため)
|
|
108
|
+
# attrs を T.untyped にするのは srb の rest-kwargs モデル対策(呼び出し側の
|
|
109
|
+
# 任意キーワードを attrs に誤バインドする既知の挙動のため)
|
|
110
|
+
sig { params(name: String, attrs: T.untyped).returns(String) }
|
|
111
|
+
def void_tag(name, **attrs)
|
|
112
|
+
content_tag(name, **attrs) { "".html_safe }
|
|
113
|
+
.then { |markup| markup.sub(%r{></#{Regexp.escape(name)}>\z}, ">") }
|
|
114
|
+
.then(&:html_safe)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# asChild代替(04 §3.3): 同じクラス・スロット構造で要素名だけ変える。
|
|
118
|
+
# 契約タグがPrimitives由来(AvatarPrimitive.Root 等)でHTML要素名ではない場合は
|
|
119
|
+
# この既定タグへフォールバックする(各コンポーネントが上書きしてよい)
|
|
120
|
+
sig { returns(String) }
|
|
121
|
+
def tag
|
|
122
|
+
return @tag if @tag
|
|
123
|
+
|
|
124
|
+
contract_tag = T.cast(contract_root_slot[:tag], T.nilable(String))
|
|
125
|
+
return contract_tag if contract_tag&.match?(/\A[a-z][a-z0-9-]*\z/)
|
|
126
|
+
|
|
127
|
+
default_tag
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# 契約タグがHTML要素名ではないときの描画タグ
|
|
131
|
+
sig { returns(String) }
|
|
132
|
+
def default_tag
|
|
133
|
+
"div"
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# バリアント値の正規化 + fail-fast検証。契約に存在しない値は ArgumentError
|
|
137
|
+
sig { params(prop: Symbol, value: T.untyped).returns(Symbol) }
|
|
138
|
+
def normalize_option(prop, value)
|
|
139
|
+
ShadcnViewComponents::Classes.normalize_option(self.class.contract_path.to_sym, prop, value)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# data属性・ARIA・CSS値へ出す意味的なpropを、宣言済み契約で正規化する。
|
|
143
|
+
sig { params(prop: Symbol, value: T.untyped).returns(T.untyped) }
|
|
144
|
+
def normalize_property(prop, value)
|
|
145
|
+
ShadcnViewComponents::PropertyContracts.normalize(
|
|
146
|
+
component: self.class.contract_path.to_sym,
|
|
147
|
+
owner: self.class.to_s,
|
|
148
|
+
property: prop,
|
|
149
|
+
value:
|
|
150
|
+
)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# コンポーネント固有のバリアント組み合わせ(サブクラスが上書きする)
|
|
154
|
+
sig { returns(T::Hash[Symbol, VariantOption]) }
|
|
155
|
+
def variant_options
|
|
156
|
+
{}
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# 契約由来の data 属性。ルートの data-slot は必ず出力する(04 §4.2)。
|
|
160
|
+
# ルートがラッパー等で data-slot を持たない契約では、契約クラスが属する要素の
|
|
161
|
+
# スロット(CLASSES_SLOT)に出す。どちらも無い契約(spinner等)では出さない
|
|
162
|
+
sig { returns(T::Hash[Symbol, T.untyped]) }
|
|
163
|
+
def contract_data_attributes
|
|
164
|
+
root_slot = T.cast(contract_root_slot[:name], T.nilable(String)).to_s
|
|
165
|
+
root_slot = T.cast(contract.const_get(:CLASSES_SLOT), T.nilable(String)).to_s if root_slot.empty? && contract.const_defined?(:CLASSES_SLOT)
|
|
166
|
+
root_slot.empty? ? {} : { slot: root_slot }
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
sig { returns(T::Hash[Symbol, T.untyped]) }
|
|
170
|
+
def html_attributes
|
|
171
|
+
attributes = static_attribute_defaults.merge(@html_args)
|
|
172
|
+
attributes[:class] = resolved_class
|
|
173
|
+
merge_nested(attributes, :data, contract_data_attributes)
|
|
174
|
+
merge_nested(attributes, :aria, {})
|
|
175
|
+
attributes
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
sig { returns(T::Module[T.anything]) }
|
|
179
|
+
def contract
|
|
180
|
+
self.class.contract
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
sig { returns(T::Hash[Symbol, T.untyped]) }
|
|
184
|
+
def contract_root_slot
|
|
185
|
+
root_slot_name = T.cast(contract.const_get(:ROOT_SLOT), String)
|
|
186
|
+
contract_slot(root_slot_name)
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# 契約のスロット定義を data-slot 名で引く
|
|
190
|
+
sig { params(slot_name: String).returns(T::Hash[Symbol, T.untyped]) }
|
|
191
|
+
def contract_slot(slot_name)
|
|
192
|
+
slots = T.cast(contract.const_get(:SLOTS), T::Array[T::Hash[Symbol, T.untyped]])
|
|
193
|
+
slots.find { |slot| slot[:name] == slot_name } || {}
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# 契約の static_attributes を既定値として採用(利用者指定が優先 — 04 §6)。
|
|
197
|
+
# cn を経ない静的 className(table のラッパー等)は class キーとして現れるため
|
|
198
|
+
# resolved_class で統合する
|
|
199
|
+
sig { returns(T::Hash[Symbol, T.untyped]) }
|
|
200
|
+
def static_attribute_defaults
|
|
201
|
+
T.cast(contract_root_slot[:static_attributes], T::Hash[Symbol, T.untyped])
|
|
202
|
+
.transform_keys(&:to_sym)
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
sig { returns(String) }
|
|
206
|
+
def resolved_class
|
|
207
|
+
static_class = T.cast(static_attribute_defaults[:class], T.nilable(String))
|
|
208
|
+
extra = [static_class, @user_class].compact.join(" ")
|
|
209
|
+
extra = nil if extra.empty?
|
|
210
|
+
self.class.classes(extra: extra, **variant_options)
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# style は利用者が文字列・ハッシュどちらで渡しても壊れないよう結合する。
|
|
214
|
+
# 出力は常に文字列とする(ハッシュのまま渡すとレンダラの整形に依存するため)。
|
|
215
|
+
# gem 側の宣言を先頭に付け、CSSの後勝ちに従い利用者が上書きできる。
|
|
216
|
+
# key は Rails の style ハッシュと同じ規約でアンダースコアをハイフンへ変換する
|
|
217
|
+
# (aspect_ratio → aspect-ratio)
|
|
218
|
+
sig { params(attributes: T::Hash[Symbol, T.untyped], defaults: T::Hash[Symbol, T.untyped]).void }
|
|
219
|
+
def merge_style(attributes, defaults)
|
|
220
|
+
user = attributes[:style]
|
|
221
|
+
base = defaults.map { |key, value| "#{key.to_s.dasherize}: #{value}" }.join("; ")
|
|
222
|
+
user_part = if user.is_a?(Hash)
|
|
223
|
+
user.map { |key, value| "#{key.to_s.dasherize}: #{value}" }.join("; ")
|
|
224
|
+
else
|
|
225
|
+
user.to_s
|
|
226
|
+
end
|
|
227
|
+
attributes[:style] = [base, user_part].reject(&:empty?).join("; ")
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
# data: / aria: は深部マージ(契約由来の値と利用者指定が共存する)
|
|
231
|
+
sig { params(attributes: T::Hash[Symbol, T.untyped], key: Symbol, defaults: T::Hash[Symbol, T.untyped]).void }
|
|
232
|
+
def merge_nested(attributes, key, defaults)
|
|
233
|
+
user = T.cast(attributes[key], T.nilable(T::Hash[Symbol, T.untyped]))
|
|
234
|
+
attributes[key] = defaults.merge(user || {})
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
private_constant :BaseComponent
|
|
239
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Shadcn
|
|
5
|
+
class Breadcrumb < BaseComponent
|
|
6
|
+
# ナビゲーション全体。JS不要・リンクベース(05-stimulus-hotwire §6.4)
|
|
7
|
+
class List < BaseComponent; end
|
|
8
|
+
|
|
9
|
+
class Item < BaseComponent; end
|
|
10
|
+
|
|
11
|
+
class Link < BaseComponent; end
|
|
12
|
+
|
|
13
|
+
class Page < BaseComponent; end
|
|
14
|
+
|
|
15
|
+
# 区切り記号。子コンテンツが無ければ chevron アイコン(lucideと同等のパス)を描画する
|
|
16
|
+
class Separator < BaseComponent
|
|
17
|
+
sig { override.returns(String) }
|
|
18
|
+
def call
|
|
19
|
+
content_tag(tag, **html_attributes) { content.presence || chevron_icon }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
sig { returns(String) }
|
|
25
|
+
def chevron_icon
|
|
26
|
+
content_tag(
|
|
27
|
+
:svg,
|
|
28
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
29
|
+
viewBox: "0 0 24 24",
|
|
30
|
+
fill: "none",
|
|
31
|
+
stroke: "currentColor",
|
|
32
|
+
"stroke-width": "2",
|
|
33
|
+
"stroke-linecap": "round",
|
|
34
|
+
"stroke-linejoin": "round",
|
|
35
|
+
width: "14",
|
|
36
|
+
height: "14"
|
|
37
|
+
) do
|
|
38
|
+
raw(%(<path d="m9 18 6-6-6-6"/>))
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# 省略記号(…)。lucide と同等のパス + スクリーンリーダー向けラベル
|
|
44
|
+
class Ellipsis < BaseComponent
|
|
45
|
+
sig { override.returns(String) }
|
|
46
|
+
def call
|
|
47
|
+
content_tag(tag, **html_attributes) do
|
|
48
|
+
safe_join([
|
|
49
|
+
content_tag(
|
|
50
|
+
:svg,
|
|
51
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
52
|
+
viewBox: "0 0 24 24",
|
|
53
|
+
fill: "none",
|
|
54
|
+
stroke: "currentColor",
|
|
55
|
+
"stroke-width": "2",
|
|
56
|
+
"stroke-linecap": "round",
|
|
57
|
+
"stroke-linejoin": "round",
|
|
58
|
+
width: "16",
|
|
59
|
+
height: "16",
|
|
60
|
+
class: "size-4"
|
|
61
|
+
) do
|
|
62
|
+
raw(%(<circle cx="12" cy="12" r="1"/><circle cx="19" cy="12" r="1"/><circle cx="5" cy="12" r="1"/>))
|
|
63
|
+
end,
|
|
64
|
+
content_tag(:span, "More", class: "sr-only")
|
|
65
|
+
])
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Shadcn
|
|
5
|
+
# チャットの吹き出し(Phase 4)
|
|
6
|
+
class Bubble < BaseComponent
|
|
7
|
+
sig { params(variant: T.any(Symbol, String), args: T::Hash[Symbol, T.untyped]).void.checked(:never) }
|
|
8
|
+
def initialize(variant: ShadcnViewComponents::Contracts::Bubble::DEFAULTS.fetch(:variant), **args)
|
|
9
|
+
@variant = T.let(normalize_option(:variant, variant), Symbol)
|
|
10
|
+
super(**args)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
sig { override.returns(T::Hash[Symbol, VariantOption]) }
|
|
14
|
+
def variant_options
|
|
15
|
+
{ variant: @variant }
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
class Group < BaseComponent
|
|
19
|
+
# div
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
class Content < BaseComponent
|
|
23
|
+
# div
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
class Reactions < BaseComponent
|
|
27
|
+
# リアクション一覧(align/side)
|
|
28
|
+
sig do
|
|
29
|
+
params(
|
|
30
|
+
align: T.any(Symbol, String),
|
|
31
|
+
side: T.any(Symbol, String),
|
|
32
|
+
args: T::Hash[Symbol, T.untyped]
|
|
33
|
+
).void.checked(:never)
|
|
34
|
+
end
|
|
35
|
+
def initialize(align: ShadcnViewComponents::Contracts::Bubble::Reactions::DEFAULTS.fetch(:align),
|
|
36
|
+
side: ShadcnViewComponents::Contracts::Bubble::Reactions::DEFAULTS.fetch(:side), **args)
|
|
37
|
+
@align = T.let(normalize_option(:align, align), Symbol)
|
|
38
|
+
@side = T.let(normalize_option(:side, side), Symbol)
|
|
39
|
+
super(**args)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
sig { override.returns(T::Hash[Symbol, VariantOption]) }
|
|
43
|
+
def variant_options
|
|
44
|
+
{ align: @align, side: @side }
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<%= content_tag(tag, **html_attributes) { content } %>
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Shadcn
|
|
5
|
+
class Button < BaseComponent
|
|
6
|
+
# 省略時の既定値は契約の defaults から読む(リテラルで二重管理しない)。
|
|
7
|
+
# checked(:never): rest-kwargs の実行時sig検証は sorbet-runtime の誤バインドがあるため
|
|
8
|
+
# 無効化し、srb tc の静的検査に委ねる(BaseComponent#initialize のコメント参照)
|
|
9
|
+
sig { override.returns(String) }
|
|
10
|
+
def default_tag
|
|
11
|
+
"button"
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
sig do
|
|
15
|
+
params(
|
|
16
|
+
variant: T.any(Symbol, String),
|
|
17
|
+
size: T.any(Symbol, String),
|
|
18
|
+
args: T::Hash[Symbol, T.untyped]
|
|
19
|
+
).void.checked(:never)
|
|
20
|
+
end
|
|
21
|
+
def initialize(variant: ShadcnViewComponents::Contracts::Button::DEFAULTS.fetch(:variant),
|
|
22
|
+
size: ShadcnViewComponents::Contracts::Button::DEFAULTS.fetch(:size), **args)
|
|
23
|
+
@variant = T.let(normalize_option(:variant, variant), Symbol)
|
|
24
|
+
@size = T.let(normalize_option(:size, size), Symbol)
|
|
25
|
+
super(**args)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
sig { returns(Symbol) }
|
|
29
|
+
attr_reader :variant
|
|
30
|
+
|
|
31
|
+
sig { returns(Symbol) }
|
|
32
|
+
attr_reader :size
|
|
33
|
+
|
|
34
|
+
sig { override.returns(T::Hash[Symbol, VariantOption]) }
|
|
35
|
+
def variant_options
|
|
36
|
+
{ variant: @variant, size: @size }
|
|
37
|
+
end
|
|
38
|
+
# base-nova の button は data-variant / data-size をDOMへ出さないため、
|
|
39
|
+
# 動的属性の上書きは行わない(契約どおり)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Shadcn
|
|
5
|
+
# ボタンを一括りにするコンテナ(Phase 4)
|
|
6
|
+
class ButtonGroup < BaseComponent
|
|
7
|
+
sig { params(orientation: T.any(Symbol, String), args: T::Hash[Symbol, T.untyped]).void.checked(:never) }
|
|
8
|
+
def initialize(orientation: ShadcnViewComponents::Contracts::ButtonGroup::DEFAULTS.fetch(:orientation), **args)
|
|
9
|
+
@orientation = T.let(normalize_option(:orientation, orientation), Symbol)
|
|
10
|
+
super(**args)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
sig { override.returns(T::Hash[Symbol, VariantOption]) }
|
|
14
|
+
def variant_options
|
|
15
|
+
{ orientation: @orientation }
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
class Separator < BaseComponent
|
|
19
|
+
# div(契約クラスは upstream の separator 上書き分を事前解決したもの)
|
|
20
|
+
sig { params(orientation: T.any(Symbol, String), args: T::Hash[Symbol, T.untyped]).void.checked(:never) }
|
|
21
|
+
def initialize(orientation: :vertical, **args)
|
|
22
|
+
value = orientation.to_s
|
|
23
|
+
raise ArgumentError, "unknown orientation value #{orientation.inspect} (valid: :horizontal, :vertical)" unless %w[horizontal vertical].include?(value)
|
|
24
|
+
|
|
25
|
+
@orientation = T.let(value, String)
|
|
26
|
+
super(**args)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
sig { override.returns(String) }
|
|
30
|
+
def call
|
|
31
|
+
attributes = @html_args.merge(class: self.class.classes(extra: @user_class))
|
|
32
|
+
merge_nested(attributes, :data, { slot: "button-group-separator" })
|
|
33
|
+
render(Shadcn::Separator.new(orientation: @orientation, **attributes))
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
class Text < BaseComponent
|
|
38
|
+
# div
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Shadcn
|
|
5
|
+
# オーバーレイ系Triggerにボタン装飾を付与する共用モジュール。
|
|
6
|
+
#
|
|
7
|
+
# upstream では <DialogTrigger asChild><Button variant="outline">…</Button></DialogTrigger>
|
|
8
|
+
# のように asChild + Button で装飾するが、本gemのTriggerは常設の <button> 要素のため、
|
|
9
|
+
# variant:/size: キーワードでボタンの契約クラスを合成して同等の見た目を提供する。
|
|
10
|
+
# 指定しない場合は装飾なし(upstream の <XTrigger> 単体と同じ)。
|
|
11
|
+
module ButtonStyled
|
|
12
|
+
extend T::Sig
|
|
13
|
+
|
|
14
|
+
# rest-kwargs を持つメソッドの実行時sig検証は sorbet-runtime の既知の誤バインドが
|
|
15
|
+
# あるため checked(:never) で無効化し、srb tc の静的検査に委譲する(BaseComponent と同じ運用)
|
|
16
|
+
sig do
|
|
17
|
+
params(
|
|
18
|
+
variant: T.nilable(T.any(Symbol, String)),
|
|
19
|
+
size: T.nilable(T.any(Symbol, String)),
|
|
20
|
+
args: T::Hash[Symbol, T.untyped]
|
|
21
|
+
).void.checked(:never)
|
|
22
|
+
end
|
|
23
|
+
def initialize(variant: nil, size: nil, **args)
|
|
24
|
+
@button_variant = T.let(
|
|
25
|
+
variant.nil? ? nil : ShadcnViewComponents::Classes.normalize_option(:button, :variant, variant),
|
|
26
|
+
T.nilable(Symbol)
|
|
27
|
+
)
|
|
28
|
+
@button_size = T.let(
|
|
29
|
+
size.nil? ? nil : ShadcnViewComponents::Classes.normalize_option(:button, :size, size),
|
|
30
|
+
T.nilable(Symbol)
|
|
31
|
+
)
|
|
32
|
+
super(**args)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
# 契約のボタンクラスを属性に合成する。variant:/size: 未指定の時は何もしない
|
|
38
|
+
sig { params(attributes: T::Hash[Symbol, T.untyped]).returns(T::Hash[Symbol, T.untyped]) }
|
|
39
|
+
def apply_button_styling(attributes)
|
|
40
|
+
return attributes if @button_variant.nil? && @button_size.nil?
|
|
41
|
+
|
|
42
|
+
options = { variant: @button_variant, size: @button_size }.compact
|
|
43
|
+
attributes[:class] = ShadcnViewComponents::Classes.resolve(
|
|
44
|
+
:button,
|
|
45
|
+
extra: attributes[:class].to_s,
|
|
46
|
+
**options
|
|
47
|
+
)
|
|
48
|
+
attributes
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|