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,40 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Shadcn
|
|
5
|
+
class Empty < BaseComponent
|
|
6
|
+
class Header < BaseComponent; end
|
|
7
|
+
|
|
8
|
+
class Title < BaseComponent; end
|
|
9
|
+
|
|
10
|
+
class Description < BaseComponent; end
|
|
11
|
+
|
|
12
|
+
class Content < BaseComponent; end
|
|
13
|
+
|
|
14
|
+
class Media < BaseComponent
|
|
15
|
+
sig do
|
|
16
|
+
params(
|
|
17
|
+
variant: T.any(Symbol, String),
|
|
18
|
+
args: T::Hash[Symbol, T.untyped]
|
|
19
|
+
).void.checked(:never)
|
|
20
|
+
end
|
|
21
|
+
def initialize(variant: ShadcnViewComponents::Contracts::Empty::Media::DEFAULTS.fetch(:variant), **args)
|
|
22
|
+
@variant = T.let(normalize_option(:variant, variant), Symbol)
|
|
23
|
+
super(**args)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
sig { returns(Symbol) }
|
|
27
|
+
attr_reader :variant
|
|
28
|
+
|
|
29
|
+
sig { override.returns(T::Hash[Symbol, VariantOption]) }
|
|
30
|
+
def variant_options
|
|
31
|
+
{ variant: @variant }
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
sig { override.returns(T::Hash[Symbol, T.untyped]) }
|
|
35
|
+
def contract_data_attributes
|
|
36
|
+
super.merge(variant: @variant)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Shadcn
|
|
5
|
+
# フィールドコンテナ(Phase 4)。orientation(vertical/horizontal/responsive)の
|
|
6
|
+
# cva バリアントを持つ。フォーム組み立て(upstream docs/forms ガイド)の基盤で、
|
|
7
|
+
# Shadcn::Form::Item もこの構造の上に成り立つ
|
|
8
|
+
class Field < BaseComponent
|
|
9
|
+
sig { params(orientation: T.any(Symbol, String), args: T::Hash[Symbol, T.untyped]).void.checked(:never) }
|
|
10
|
+
def initialize(orientation: ShadcnViewComponents::Contracts::Field::DEFAULTS.fetch(:orientation), **args)
|
|
11
|
+
@orientation = T.let(normalize_option(:orientation, orientation), Symbol)
|
|
12
|
+
super(**args)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
sig { override.returns(T::Hash[Symbol, VariantOption]) }
|
|
16
|
+
def variant_options
|
|
17
|
+
{ orientation: @orientation }
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
sig { override.returns(T::Hash[Symbol, T.untyped]) }
|
|
21
|
+
def html_attributes
|
|
22
|
+
attributes = super
|
|
23
|
+
# upstream は data-orientation={orientation} を常に出力する
|
|
24
|
+
merge_nested(attributes, :data, { orientation: @orientation.to_s })
|
|
25
|
+
attributes
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
class Content < BaseComponent
|
|
29
|
+
# div
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
class Description < BaseComponent
|
|
33
|
+
sig { override.returns(String) }
|
|
34
|
+
def default_tag
|
|
35
|
+
"p"
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
class Error < BaseComponent
|
|
40
|
+
# div(契約静的クラスは自動付与)。upstream と同じく「本文が無ければ描かない」。
|
|
41
|
+
# なお upstream の errors: 配列→ul リスト化は本クラスでは提供しない
|
|
42
|
+
# (upstream の ul は data-slot を持たず field 契約に取込めないため。
|
|
43
|
+
# 文字列配列を受けたい場合は Shadcn::Form::Error を使う)
|
|
44
|
+
sig { override.returns(String) }
|
|
45
|
+
def call
|
|
46
|
+
body = content.to_s
|
|
47
|
+
return "" if body.empty?
|
|
48
|
+
|
|
49
|
+
content_tag(tag, **html_attributes) { body }
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
class Group < BaseComponent
|
|
54
|
+
# div
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
class Label < BaseComponent
|
|
58
|
+
sig { override.returns(String) }
|
|
59
|
+
def default_tag
|
|
60
|
+
"label"
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# upstream の FieldLabel は <Label> プリミティブを描く: ラベル契約のクラスを
|
|
64
|
+
# 土台に field 側のクラスを重ねる(cn(labelVariants(), fieldLabelClasses) と同じ順)。
|
|
65
|
+
# 描画は label 契約のクラス群を含むため、適合試験は allowances の
|
|
66
|
+
# class_mode: contains で緩和する
|
|
67
|
+
sig { override.returns(T::Hash[Symbol, T.untyped]) }
|
|
68
|
+
def html_attributes
|
|
69
|
+
attributes = super
|
|
70
|
+
attributes[:class] = ShadcnViewComponents::Classes.resolve(:label, extra: attributes[:class].to_s)
|
|
71
|
+
attributes
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
class Legend < BaseComponent
|
|
76
|
+
sig { override.returns(String) }
|
|
77
|
+
def default_tag
|
|
78
|
+
"legend"
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
class Separator < BaseComponent
|
|
83
|
+
# 契約スロット構成: field-separator > field-separator-content(span)
|
|
84
|
+
sig { override.returns(String) }
|
|
85
|
+
def call
|
|
86
|
+
content_tag(tag, **html_attributes) do
|
|
87
|
+
content_tag(:span,
|
|
88
|
+
class: T.cast(contract_slot("field-separator-content").dig(:static_attributes, :class), T.nilable(String)),
|
|
89
|
+
data: { slot: "field-separator-content" }) { content }
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
class Set < BaseComponent
|
|
95
|
+
# fieldset
|
|
96
|
+
sig { override.returns(String) }
|
|
97
|
+
def default_tag
|
|
98
|
+
"fieldset"
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
class Title < BaseComponent
|
|
103
|
+
# div(契約の data-slot は field-label を共有する)
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Shadcn
|
|
5
|
+
# 浮動要素の希望配置を、各コンポーネントで共通の data 属性へ変換する。
|
|
6
|
+
# 実際の配置結果に使う data-side / data-align とは属性を分け、衝突回避後の
|
|
7
|
+
# 値が次回表示時の希望配置へ混入しないようにする。
|
|
8
|
+
module FloatingPositionOptions
|
|
9
|
+
extend T::Sig
|
|
10
|
+
|
|
11
|
+
SIDES = %w[top right bottom left inline-start inline-end].freeze
|
|
12
|
+
ALIGNS = %w[start center end].freeze
|
|
13
|
+
INTEGER_PATTERN = /\A[+-]?\d+\z/
|
|
14
|
+
NUMBER_PATTERN = /\A[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?\z/i
|
|
15
|
+
|
|
16
|
+
class Values < T::Struct
|
|
17
|
+
const :side, String
|
|
18
|
+
const :align, String
|
|
19
|
+
const :side_offset, T.any(Integer, Float)
|
|
20
|
+
const :align_offset, T.any(Integer, Float)
|
|
21
|
+
const :collision_padding, T.any(Integer, Float)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
sig { params(args: T::Hash[Symbol, T.untyped]).void.checked(:never) }
|
|
25
|
+
def initialize(**args)
|
|
26
|
+
defaults = T.cast(T.unsafe(self).class.const_get(:FLOATING_POSITION_DEFAULTS), T::Hash[Symbol, T.untyped])
|
|
27
|
+
@floating_position = T.let(normalize_position_options(args, defaults), Values)
|
|
28
|
+
super
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
sig { params(attributes: T::Hash[Symbol, T.untyped], defaults: T::Hash[Symbol, T.untyped]).returns(T::Hash[Symbol, T.untyped]) }
|
|
34
|
+
def merge_floating_position_data(attributes, defaults = {})
|
|
35
|
+
user_data = T.cast(attributes[:data], T.nilable(T::Hash[Symbol, T.untyped])) || {}
|
|
36
|
+
attributes[:data] = defaults.merge(floating_position_data).merge(user_data)
|
|
37
|
+
attributes
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
sig { returns(T::Hash[Symbol, T.untyped]) }
|
|
41
|
+
def floating_position_data
|
|
42
|
+
{
|
|
43
|
+
position_side: @floating_position.side,
|
|
44
|
+
position_align: @floating_position.align,
|
|
45
|
+
position_side_offset: @floating_position.side_offset,
|
|
46
|
+
position_align_offset: @floating_position.align_offset,
|
|
47
|
+
position_collision_padding: @floating_position.collision_padding
|
|
48
|
+
}
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
sig do
|
|
52
|
+
params(
|
|
53
|
+
args: T::Hash[Symbol, T.untyped],
|
|
54
|
+
defaults: T::Hash[Symbol, T.untyped]
|
|
55
|
+
).returns(Values)
|
|
56
|
+
end
|
|
57
|
+
def normalize_position_options(args, defaults)
|
|
58
|
+
Values.new(
|
|
59
|
+
side: normalize_position_choice(:side, extract_position_option(args, defaults, :side), SIDES),
|
|
60
|
+
align: normalize_position_choice(:align, extract_position_option(args, defaults, :align), ALIGNS),
|
|
61
|
+
side_offset: normalize_position_number(:side_offset, extract_position_option(args, defaults, :side_offset)),
|
|
62
|
+
align_offset: normalize_position_number(:align_offset, extract_position_option(args, defaults, :align_offset)),
|
|
63
|
+
collision_padding: normalize_position_number(
|
|
64
|
+
:collision_padding,
|
|
65
|
+
extract_position_option(args, defaults, :collision_padding),
|
|
66
|
+
non_negative: true
|
|
67
|
+
)
|
|
68
|
+
)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
sig do
|
|
72
|
+
params(
|
|
73
|
+
args: T::Hash[Symbol, T.untyped],
|
|
74
|
+
defaults: T::Hash[Symbol, T.untyped],
|
|
75
|
+
name: Symbol
|
|
76
|
+
).returns(T.untyped)
|
|
77
|
+
end
|
|
78
|
+
def extract_position_option(args, defaults, name)
|
|
79
|
+
return args.delete(name) if args.key?(name)
|
|
80
|
+
|
|
81
|
+
defaults.fetch(name)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
sig do
|
|
85
|
+
params(
|
|
86
|
+
name: Symbol,
|
|
87
|
+
value: T.untyped,
|
|
88
|
+
allowed: T::Array[String]
|
|
89
|
+
).returns(String)
|
|
90
|
+
end
|
|
91
|
+
def normalize_position_choice(name, value, allowed)
|
|
92
|
+
normalized = value.to_s
|
|
93
|
+
return normalized if (value.is_a?(String) || value.is_a?(Symbol)) && allowed.include?(normalized)
|
|
94
|
+
|
|
95
|
+
Kernel.raise ArgumentError,
|
|
96
|
+
"unknown floating position #{name} #{value.inspect} (valid: #{allowed.join(', ')})"
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
sig do
|
|
100
|
+
params(
|
|
101
|
+
name: Symbol,
|
|
102
|
+
value: T.untyped,
|
|
103
|
+
non_negative: T::Boolean
|
|
104
|
+
).returns(T.any(Integer, Float))
|
|
105
|
+
end
|
|
106
|
+
def normalize_position_number(name, value, non_negative: false)
|
|
107
|
+
number = parse_position_number(value)
|
|
108
|
+
invalid_position_number!(name, value, non_negative) unless number
|
|
109
|
+
invalid_position_number!(name, value, non_negative) unless number.to_f.finite?
|
|
110
|
+
invalid_position_number!(name, value, non_negative) if non_negative && number.negative?
|
|
111
|
+
|
|
112
|
+
number
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
sig { params(value: T.untyped).returns(T.nilable(T.any(Integer, Float))) }
|
|
116
|
+
def parse_position_number(value)
|
|
117
|
+
return value if value.is_a?(Integer) || value.is_a?(Float)
|
|
118
|
+
return unless value.is_a?(String)
|
|
119
|
+
|
|
120
|
+
normalized = value.strip
|
|
121
|
+
return normalized.to_i if INTEGER_PATTERN.match?(normalized)
|
|
122
|
+
|
|
123
|
+
normalized.to_f if NUMBER_PATTERN.match?(normalized)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
sig { params(name: Symbol, value: T.untyped, non_negative: T::Boolean).returns(T.noreturn) }
|
|
127
|
+
def invalid_position_number!(name, value, non_negative)
|
|
128
|
+
requirement = non_negative ? "a finite non-negative number" : "a finite number"
|
|
129
|
+
Kernel.raise ArgumentError, "floating position #{name} must be #{requirement}, got #{value.inspect}"
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Shadcn
|
|
5
|
+
# Railsフォーム統合層。upstream(base-nova)は form アイテムを廃止し、公式 docs/forms
|
|
6
|
+
# ガイドは Field プリミティブ + data-invalid / aria-invalid の手動ワイヤリングへ移行した
|
|
7
|
+
# (Issue #3)。本gemもそれに倣い、Field の上に最小2部品で構成する:
|
|
8
|
+
#
|
|
9
|
+
# <%= form_with model: user do |f| %>
|
|
10
|
+
# <%= render Shadcn::Form::Item.new(invalid: user.errors[:email].any?) do %>
|
|
11
|
+
# <%= render Shadcn::Field::Label.new(for: "user-email") { "メールアドレス" } %>
|
|
12
|
+
# <%= render Shadcn::Input.new(id: "user-email",
|
|
13
|
+
# aria: { invalid: user.errors[:email].any? }) %>
|
|
14
|
+
# <%= render Shadcn::Field::Description.new { "ログインに使うアドレスです" } %>
|
|
15
|
+
# <%= render Shadcn::Form::Error.new(errors: user.errors.full_messages_for(:email)) %>
|
|
16
|
+
# <% end %>
|
|
17
|
+
# <% end %>
|
|
18
|
+
#
|
|
19
|
+
# ラベル・説明には Shadcn::Field::Label / Shadcn::Field::Description を直接使う。
|
|
20
|
+
# Form::Item は Field と同じ DOM 構造(data-slot="field")を描くため、Field 部品を
|
|
21
|
+
# そのまま内側に置ける。JS無効時フォールバック: Graceful(表示は全てサーバ側で完結する)
|
|
22
|
+
# aria-invalid / aria-describedby の紐付けは利用者(またはフォームビルダ拡張)が
|
|
23
|
+
# 持つ — upstream の useFormField 相当の暗黙接続はRailsの流儀に無い
|
|
24
|
+
# Form自体は名前空間であり、描画にはItemまたはErrorを使う。
|
|
25
|
+
module Form
|
|
26
|
+
# upstream docs/forms の <Field data-invalid={...}> 相当。DOM構造・クラスは
|
|
27
|
+
# Field 契約と同一(契約側で fieldVariants を事前解決済み)。
|
|
28
|
+
# data-slot を "field" に保つのは Field 部品の group/field セレクタ互換のため
|
|
29
|
+
class Item < BaseComponent
|
|
30
|
+
sig do
|
|
31
|
+
params(invalid: T::Boolean,
|
|
32
|
+
orientation: T.any(Symbol, String),
|
|
33
|
+
args: T::Hash[Symbol, T.untyped]).void.checked(:never)
|
|
34
|
+
end
|
|
35
|
+
def initialize(invalid: false, orientation: ShadcnViewComponents::Contracts::Form::Item::DEFAULTS.fetch(:orientation), **args)
|
|
36
|
+
@invalid = invalid
|
|
37
|
+
@orientation = T.let(normalize_option(:orientation, orientation), Symbol)
|
|
38
|
+
super(**args)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
sig { override.returns(T::Hash[Symbol, VariantOption]) }
|
|
42
|
+
def variant_options
|
|
43
|
+
{ orientation: @orientation }
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
sig { override.returns(T::Hash[Symbol, T.untyped]) }
|
|
47
|
+
def html_attributes
|
|
48
|
+
attributes = super
|
|
49
|
+
# upstream は boolean prop を常に "true"/"false" として出力する
|
|
50
|
+
merge_nested(attributes, :data, { invalid: @invalid.to_s, orientation: @orientation.to_s })
|
|
51
|
+
attributes
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# upstream の FieldError セマンティクス(upstream準拠、入力はRails流の文字列配列):
|
|
56
|
+
# 本文 > errors(重複除去) の優先で、1件は平文・複数は ul リスト・無ければ非描画。
|
|
57
|
+
# Rails では model.errors.full_messages_for(:attr) をそのまま渡せる
|
|
58
|
+
class Error < BaseComponent
|
|
59
|
+
sig do
|
|
60
|
+
params(message: T.nilable(String),
|
|
61
|
+
errors: T.nilable(T::Array[T.nilable(String)]),
|
|
62
|
+
args: T::Hash[Symbol, T.untyped]).void.checked(:never)
|
|
63
|
+
end
|
|
64
|
+
def initialize(message: nil, errors: nil, **args)
|
|
65
|
+
@message = message
|
|
66
|
+
@errors = T.let(errors, T.nilable(T::Array[T.nilable(String)]))
|
|
67
|
+
super(**args)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
sig { override.returns(String) }
|
|
71
|
+
def call
|
|
72
|
+
body = content.to_s
|
|
73
|
+
messages = unique_messages
|
|
74
|
+
return "" if body.empty? && messages.empty?
|
|
75
|
+
|
|
76
|
+
content_tag(tag, **html_attributes) do
|
|
77
|
+
if body.present?
|
|
78
|
+
body
|
|
79
|
+
elsif messages.length == 1
|
|
80
|
+
messages.first.to_s
|
|
81
|
+
else
|
|
82
|
+
error_list(messages)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
private
|
|
88
|
+
|
|
89
|
+
sig { returns(T::Array[String]) }
|
|
90
|
+
def unique_messages
|
|
91
|
+
([@message].compact + (@errors || []).compact).uniq
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# 複数件は ul リスト。リストのクラスは契約に記録済み
|
|
95
|
+
# (upstream の ul は data-slot を持たないため、名前無しスロットとして契約化した)
|
|
96
|
+
sig { params(messages: T::Array[String]).returns(String) }
|
|
97
|
+
def error_list(messages)
|
|
98
|
+
list_class = T.cast(contract_slot("").dig(:static_attributes, :class), T.nilable(String))
|
|
99
|
+
content_tag(:ul, class: list_class, data: { slot: "form-error-list" }) do
|
|
100
|
+
safe_join(messages.map { |message| content_tag(:li) { message.to_s } })
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Shadcn
|
|
5
|
+
# hover intent 付きのホバーカード(05 §3「hover-card = Popover API + hover intent」)。
|
|
6
|
+
# JS無効時フォールバック: Readable(内容はSSR済みで読める。展開はJS依存)
|
|
7
|
+
class HoverCard < BaseComponent
|
|
8
|
+
CONTROLLER = "shadcn--hover-card"
|
|
9
|
+
|
|
10
|
+
sig { override.returns(T::Hash[Symbol, T.untyped]) }
|
|
11
|
+
def contract_data_attributes
|
|
12
|
+
super.merge(controller: CONTROLLER)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
class Trigger < BaseComponent
|
|
16
|
+
# 契約タグは HoverCardPrimitive.Trigger(HTML名ではない)。upstream の慣行は
|
|
17
|
+
# <a>(href を渡した場合)なので、href の有無でリンクかボタンかに切り替える
|
|
18
|
+
sig { override.returns(String) }
|
|
19
|
+
def default_tag
|
|
20
|
+
@html_args.key?(:href) ? "a" : "button"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
sig { override.returns(T::Hash[Symbol, T.untyped]) }
|
|
24
|
+
def html_attributes
|
|
25
|
+
attributes = super
|
|
26
|
+
merge_nested(attributes, :data, {
|
|
27
|
+
action: "mouseenter->#{CONTROLLER}#show mouseleave->#{CONTROLLER}#hide focus->#{CONTROLLER}#show blur->#{CONTROLLER}#hide"
|
|
28
|
+
})
|
|
29
|
+
attributes
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
class Content < BaseComponent
|
|
34
|
+
include Shadcn::FloatingPositionOptions
|
|
35
|
+
|
|
36
|
+
FLOATING_POSITION_DEFAULTS = T.let(
|
|
37
|
+
{ side: :bottom, align: :center, side_offset: 4, align_offset: 4, collision_padding: 5 }.freeze,
|
|
38
|
+
T::Hash[Symbol, T.untyped]
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
# 契約スロット構成: hover-card-portal(ラッパー) > hover-card-content。
|
|
42
|
+
# 表示切替は hidden 属性 + data-state で行う
|
|
43
|
+
sig { override.returns(String) }
|
|
44
|
+
def call
|
|
45
|
+
content_tag(:div, data: { slot: "hover-card-portal" }) do
|
|
46
|
+
content_tag(:div, **content_attributes) { content }
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
sig { returns(T::Hash[Symbol, T.untyped]) }
|
|
53
|
+
def content_attributes
|
|
54
|
+
attributes = @html_args.merge(class: self.class.classes(extra: @user_class))
|
|
55
|
+
merge_floating_position_data(
|
|
56
|
+
attributes,
|
|
57
|
+
slot: "hover-card-content",
|
|
58
|
+
state: "closed",
|
|
59
|
+
action: "mouseenter->#{CONTROLLER}#show mouseleave->#{CONTROLLER}#hide"
|
|
60
|
+
)
|
|
61
|
+
attributes[:hidden] = true
|
|
62
|
+
attributes
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Shadcn
|
|
5
|
+
class Input < BaseComponent
|
|
6
|
+
# input タグ・クラス・data-slot は契約由来。type 等の属性はパススルー。
|
|
7
|
+
# base-nova の契約タグは InputPrimitive のため HTML input を明示する
|
|
8
|
+
sig { override.returns(String) }
|
|
9
|
+
def default_tag
|
|
10
|
+
"input"
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Shadcn
|
|
5
|
+
class InputGroup < BaseComponent
|
|
6
|
+
# upstream は Button を asChild で重ねる。本gemでは Button 描画結果に
|
|
7
|
+
# data-slot を差し替える(Phase 3 の AlertDialogAction と同じ構成)。
|
|
8
|
+
# NOTE: Shadcn::Button の sidecar テンプレートと名前が衝突するため専用ファイルに置く
|
|
9
|
+
class Button < BaseComponent
|
|
10
|
+
sig do
|
|
11
|
+
params(
|
|
12
|
+
variant: T.any(Symbol, String),
|
|
13
|
+
size: T.any(Symbol, String),
|
|
14
|
+
args: T::Hash[Symbol, T.untyped]
|
|
15
|
+
).void.checked(:never)
|
|
16
|
+
end
|
|
17
|
+
def initialize(variant: :ghost, size: "xs", **args)
|
|
18
|
+
@variant = T.let(
|
|
19
|
+
ShadcnViewComponents::Classes.normalize_option(:button, :variant, variant),
|
|
20
|
+
Symbol
|
|
21
|
+
)
|
|
22
|
+
@size = T.let(normalize_option(:size, size), Symbol)
|
|
23
|
+
super(**args)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
sig { override.returns(T::Hash[Symbol, VariantOption]) }
|
|
27
|
+
def variant_options
|
|
28
|
+
{ size: @size }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# 契約は Button(ba_buttonVariants) との合成結果を事前解決済みのため、
|
|
32
|
+
# ここではその最終クラスをそのまま描く(upstream の DOM と同一)
|
|
33
|
+
sig { override.returns(String) }
|
|
34
|
+
def default_tag
|
|
35
|
+
"button"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
sig { override.returns(T::Hash[Symbol, T.untyped]) }
|
|
39
|
+
def html_attributes
|
|
40
|
+
attributes = super
|
|
41
|
+
attributes[:type] = "button" unless attributes.key?(:type)
|
|
42
|
+
data = T.cast(attributes[:data], T.nilable(T::Hash[Symbol, T.untyped])) || {}
|
|
43
|
+
attributes[:data] = { size: @size }.merge(data)
|
|
44
|
+
attributes
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Shadcn
|
|
5
|
+
# 入力要素の前後にボタンやテキストを並べるコンテナ(Phase 4)
|
|
6
|
+
class InputGroup < BaseComponent
|
|
7
|
+
# div(契約静的クラスは自動付与)
|
|
8
|
+
|
|
9
|
+
class Addon < BaseComponent
|
|
10
|
+
sig { params(align: T.any(Symbol, String), args: T::Hash[Symbol, T.untyped]).void.checked(:never) }
|
|
11
|
+
def initialize(align: ShadcnViewComponents::Contracts::InputGroup::Addon::DEFAULTS.fetch(:align), **args)
|
|
12
|
+
@align = T.let(normalize_option(:align, align), Symbol)
|
|
13
|
+
super(**args)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
sig { override.returns(T::Hash[Symbol, VariantOption]) }
|
|
17
|
+
def variant_options
|
|
18
|
+
{ align: @align }
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
sig { override.returns(T::Hash[Symbol, T.untyped]) }
|
|
22
|
+
def contract_data_attributes
|
|
23
|
+
super.merge(align: @align)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
class Input < BaseComponent
|
|
28
|
+
# upstream は Input(別アイテム)に上書きクラスを足す
|
|
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: "input-group-control" })
|
|
33
|
+
render(::Shadcn::Input.new(**attributes)) { content }
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
class Textarea < BaseComponent
|
|
38
|
+
# upstream は Textarea(別アイテム)に上書きクラスを足す
|
|
39
|
+
sig { override.returns(String) }
|
|
40
|
+
def call
|
|
41
|
+
attributes = @html_args.merge(class: self.class.classes(extra: @user_class))
|
|
42
|
+
merge_nested(attributes, :data, { slot: "input-group-control" })
|
|
43
|
+
render(::Shadcn::Textarea.new(**attributes)) { content }
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
class Text < BaseComponent
|
|
48
|
+
# span(契約クラスのみ。data-slot は契約上ルートに無い)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|