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,78 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Shadcn
|
|
5
|
+
# 素の select/optgroup/option(JSレス — 05 §3)。ラッパーdiv + select + chevron。
|
|
6
|
+
# 利用者属性(name/id/required 等)はすべて実際の <select> に渡す
|
|
7
|
+
# (ラッパーは gem 内部要素のため name 等が届かないフォーム送信の破壊を防ぐ)
|
|
8
|
+
class NativeSelect < BaseComponent
|
|
9
|
+
sig { params(size: T.any(Symbol, String), args: T::Hash[Symbol, T.untyped]).void.checked(:never) }
|
|
10
|
+
def initialize(size: self.class.property_default(:size), **args)
|
|
11
|
+
@size = T.let(normalize_property(:size, size), String)
|
|
12
|
+
super(**args)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
sig { override.returns(String) }
|
|
16
|
+
def call
|
|
17
|
+
# base-nova ではユーザークラス(className)はラッパーへ流れる
|
|
18
|
+
content_tag(:div, class: wrapper_class, data: { slot: "native-select-wrapper", size: @size }) do
|
|
19
|
+
safe_join([select_element, chevron_icon])
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
# 選択肢は OptGroup / Option コンポーネントで content に渡す
|
|
26
|
+
sig { returns(String) }
|
|
27
|
+
def select_element
|
|
28
|
+
attributes = @html_args.merge(class: select_class)
|
|
29
|
+
merge_nested(attributes, :data, { slot: "native-select", size: @size })
|
|
30
|
+
content_tag(:select, **attributes) { content }
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# ラッパーのクラスは契約combination(group/native-select ...)
|
|
34
|
+
sig { returns(String) }
|
|
35
|
+
def wrapper_class
|
|
36
|
+
ShadcnViewComponents::Classes.resolve(:native_select, extra: @user_class.to_s)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# select 要素のクラスは契約スロットの静的クラス
|
|
40
|
+
sig { returns(T.nilable(String)) }
|
|
41
|
+
def select_class
|
|
42
|
+
T.cast(contract_slot("native-select").dig(:static_attributes, :class), T.nilable(String))
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
sig { returns(String) }
|
|
46
|
+
def chevron_icon
|
|
47
|
+
content_tag(
|
|
48
|
+
:svg,
|
|
49
|
+
xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 24 24", fill: "none",
|
|
50
|
+
stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round",
|
|
51
|
+
width: "16", height: "16",
|
|
52
|
+
class: T.cast(contract_slot("native-select-icon").dig(:static_attributes, :class), T.nilable(String)),
|
|
53
|
+
aria: { hidden: "true" }, data: { slot: "native-select-icon" }
|
|
54
|
+
) { raw(%(<path d="m6 9 6 6 6-6"/>)) }
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
class OptGroup < BaseComponent
|
|
58
|
+
# optgroup
|
|
59
|
+
sig { override.returns(String) }
|
|
60
|
+
def default_tag
|
|
61
|
+
"optgroup"
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
class Option < BaseComponent
|
|
66
|
+
# option(内容はテキストのみ)
|
|
67
|
+
sig { override.returns(String) }
|
|
68
|
+
def default_tag
|
|
69
|
+
"option"
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
sig { override.returns(String) }
|
|
73
|
+
def call
|
|
74
|
+
content_tag(tag, **html_attributes) { content.to_s }
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Shadcn
|
|
5
|
+
# ナビゲーションメニュー(10-roadmap Phase 3)。
|
|
6
|
+
# 通常のnav/linkと開閉領域をnavigation_menu_controller(Popover API)で管理する。
|
|
7
|
+
# ARIA menuではないためLinkはネイティブのTab/Enter操作を維持する。
|
|
8
|
+
# JS無効時フォールバック: Readable(リンク自体はSSR済みで辿れる)
|
|
9
|
+
class NavigationMenu < BaseComponent
|
|
10
|
+
CONTROLLER = "shadcn--navigation-menu"
|
|
11
|
+
|
|
12
|
+
sig { override.returns(T::Hash[Symbol, T.untyped]) }
|
|
13
|
+
def contract_data_attributes
|
|
14
|
+
super.merge(controller: CONTROLLER)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
sig { override.returns(String) }
|
|
18
|
+
def default_tag
|
|
19
|
+
"nav"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
class List < BaseComponent
|
|
23
|
+
# ul
|
|
24
|
+
sig { override.returns(String) }
|
|
25
|
+
def default_tag
|
|
26
|
+
"ul"
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
class Item < BaseComponent
|
|
31
|
+
# li
|
|
32
|
+
sig { override.returns(String) }
|
|
33
|
+
def default_tag
|
|
34
|
+
"li"
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
class Trigger < BaseComponent
|
|
39
|
+
sig { override.returns(String) }
|
|
40
|
+
def default_tag
|
|
41
|
+
"button"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
sig { override.returns(T::Hash[Symbol, T.untyped]) }
|
|
45
|
+
def html_attributes
|
|
46
|
+
attributes = super
|
|
47
|
+
attributes[:type] = "button" unless attributes.key?(:type)
|
|
48
|
+
merge_nested(attributes, :aria, { expanded: "false" })
|
|
49
|
+
merge_nested(attributes, :data, { action: "#{CONTROLLER}#toggle" })
|
|
50
|
+
attributes
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# upstream は chevron を含む(静的クラスは契約スロット外)
|
|
54
|
+
sig { override.returns(String) }
|
|
55
|
+
def call
|
|
56
|
+
content_tag(tag, **html_attributes) do
|
|
57
|
+
safe_join([content, chevron_icon])
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
sig { returns(String) }
|
|
64
|
+
def chevron_icon
|
|
65
|
+
content_tag(
|
|
66
|
+
:svg,
|
|
67
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
68
|
+
viewBox: "0 0 24 24",
|
|
69
|
+
fill: "none",
|
|
70
|
+
stroke: "currentColor",
|
|
71
|
+
"stroke-width": "2",
|
|
72
|
+
"stroke-linecap": "round",
|
|
73
|
+
"stroke-linejoin": "round",
|
|
74
|
+
width: "12",
|
|
75
|
+
height: "12",
|
|
76
|
+
class: "relative top-px ml-1 size-3 transition duration-300 group-data-popup-open/navigation-menu-trigger:rotate-180 group-data-open/navigation-menu-trigger:rotate-180",
|
|
77
|
+
aria: { hidden: "true" }
|
|
78
|
+
) do
|
|
79
|
+
raw(%(<path d="m6 9 6 6 6-6"/>))
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
class Link < BaseComponent
|
|
85
|
+
# a
|
|
86
|
+
sig { override.returns(String) }
|
|
87
|
+
def default_tag
|
|
88
|
+
"a"
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
class Content < BaseComponent
|
|
93
|
+
include Shadcn::FloatingPositionOptions
|
|
94
|
+
|
|
95
|
+
FLOATING_POSITION_DEFAULTS = T.let(
|
|
96
|
+
{ side: :bottom, align: :start, side_offset: 8, align_offset: 0, collision_padding: 5 }.freeze,
|
|
97
|
+
T::Hash[Symbol, T.untyped]
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
# Popover API で開閉。中身のリンク構造は利用者が組む。
|
|
101
|
+
sig { override.returns(T::Hash[Symbol, T.untyped]) }
|
|
102
|
+
def html_attributes
|
|
103
|
+
attributes = super
|
|
104
|
+
attributes[:popover] = "auto"
|
|
105
|
+
merge_floating_position_data(attributes, state: "closed")
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
class Indicator < BaseComponent
|
|
110
|
+
# 装飾要素(矢印)。既定では非表示
|
|
111
|
+
sig { override.returns(String) }
|
|
112
|
+
def call
|
|
113
|
+
content_tag(tag, **html_attributes) do
|
|
114
|
+
content_tag(:div, class: "relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm bg-border shadow-md") { "".html_safe }
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# base-nova では Viewport が廃止され Positioner(Portal 由来の位置決め)に置き換わった。
|
|
120
|
+
# 本gemは viewport/positioner なしで Content を直接表示する設計のため、両方を移植しない
|
|
121
|
+
end
|
|
122
|
+
end
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Shadcn
|
|
5
|
+
# リンクベースのページネーション(JSレス — 10-roadmap Phase 2)。
|
|
6
|
+
# JS無効時フォールバック: Graceful(リンク遷移のみで完全動作)
|
|
7
|
+
# Previous/Next は upstream が PaginationLink を内側で描く構成のため、
|
|
8
|
+
# ルートスロットを Link から継承した契約になっている(抽出器の兄弟継承)
|
|
9
|
+
class Pagination < BaseComponent
|
|
10
|
+
# nav。契約の静的属性(role="navigation" / aria-label="pagination")は
|
|
11
|
+
# BaseComponent#static_attribute_defaults が自動で付与する
|
|
12
|
+
|
|
13
|
+
# Previous/Next/Ellipsis 共通のインラインSVG(lucide相当)。
|
|
14
|
+
# コンポーネントクラス外のモジュールからでもヘルパが解決できるよう、必要な
|
|
15
|
+
# ActionViewヘルパを明示的にincludeする(Sorbetの解決対象にもなる)
|
|
16
|
+
module IconHelpers
|
|
17
|
+
extend T::Sig
|
|
18
|
+
include ActionView::Helpers::OutputSafetyHelper
|
|
19
|
+
include ActionView::Helpers::TagHelper
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
sig { params(paths: String).returns(String) }
|
|
24
|
+
def pagination_icon(paths)
|
|
25
|
+
content_tag(
|
|
26
|
+
:svg,
|
|
27
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
28
|
+
viewBox: "0 0 24 24",
|
|
29
|
+
fill: "none",
|
|
30
|
+
stroke: "currentColor",
|
|
31
|
+
"stroke-width": "2",
|
|
32
|
+
"stroke-linecap": "round",
|
|
33
|
+
"stroke-linejoin": "round",
|
|
34
|
+
width: "16",
|
|
35
|
+
height: "16",
|
|
36
|
+
class: "size-4",
|
|
37
|
+
aria: { hidden: "true" }
|
|
38
|
+
) do
|
|
39
|
+
raw(paths)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
class Content < BaseComponent
|
|
45
|
+
# ul
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
class Item < BaseComponent
|
|
49
|
+
# li
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
class Link < BaseComponent
|
|
53
|
+
# base-nova は Button装飾(variant/size)をrender propで<a>へ合成する。
|
|
54
|
+
# 本gemもButton契約からクラスを合成して同等の<a>を描く(契約クラス自体は空)
|
|
55
|
+
sig { override.returns(String) }
|
|
56
|
+
def default_tag
|
|
57
|
+
"a"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
sig do
|
|
61
|
+
params(
|
|
62
|
+
is_active: T.nilable(T.any(Symbol, String, T::Boolean)),
|
|
63
|
+
size: T.any(Symbol, String),
|
|
64
|
+
args: T::Hash[Symbol, T.untyped]
|
|
65
|
+
).void.checked(:never)
|
|
66
|
+
end
|
|
67
|
+
def initialize(is_active: nil, size: :icon, **args)
|
|
68
|
+
@is_active = T.let(is_active.to_s == "true", T::Boolean)
|
|
69
|
+
@size = T.let(ShadcnViewComponents::Classes.normalize_option(:button, :size, size), Symbol)
|
|
70
|
+
super(**args)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
sig { returns(T::Boolean) }
|
|
74
|
+
def active?
|
|
75
|
+
@is_active
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
sig { override.returns(T::Hash[Symbol, T.untyped]) }
|
|
79
|
+
def html_attributes
|
|
80
|
+
# NOTE: Hashの値型は不変のため、Classes.resolve の options と同じ型で T.let する
|
|
81
|
+
button_options = T.let({ variant: active? ? :outline : :ghost, size: @size },
|
|
82
|
+
T::Hash[Symbol, T.nilable(T.any(Symbol, String))])
|
|
83
|
+
attributes = @html_args.merge(
|
|
84
|
+
class: ShadcnViewComponents::Classes.resolve(:button, extra: @user_class.to_s, **button_options)
|
|
85
|
+
)
|
|
86
|
+
merge_nested(attributes, :data, { slot: "pagination-link", active: active?.to_s })
|
|
87
|
+
attributes[:aria] = { current: "page" } if active?
|
|
88
|
+
attributes
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
class Previous < BaseComponent
|
|
93
|
+
include IconHelpers
|
|
94
|
+
|
|
95
|
+
# upstream は Link(size="default")を描き、固定の子(chevron + ラベル)を足す
|
|
96
|
+
sig { override.returns(String) }
|
|
97
|
+
def call
|
|
98
|
+
render(Link.new(size: :default, class: self.class.classes(extra: @user_class), **link_attributes)) do
|
|
99
|
+
safe_join([pagination_icon(%(<path d="m15 18-6-6 6-6"/>)), default_label])
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
private
|
|
104
|
+
|
|
105
|
+
sig { returns(T::Hash[Symbol, T.untyped]) }
|
|
106
|
+
def link_attributes
|
|
107
|
+
attributes = @html_args.dup
|
|
108
|
+
aria = T.cast(attributes[:aria], T.nilable(T::Hash[Symbol, T.untyped])) || {}
|
|
109
|
+
attributes[:aria] = { label: "Go to previous page" }.merge(aria)
|
|
110
|
+
attributes
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
sig { returns(String) }
|
|
114
|
+
def default_label
|
|
115
|
+
content.presence || content_tag(:span, class: "hidden sm:block") { "Previous" }
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
class Next < BaseComponent
|
|
120
|
+
include IconHelpers
|
|
121
|
+
|
|
122
|
+
sig { override.returns(String) }
|
|
123
|
+
def call
|
|
124
|
+
render(Link.new(size: :default, class: self.class.classes(extra: @user_class), **link_attributes)) do
|
|
125
|
+
safe_join([default_label, pagination_icon(%(<path d="m9 18 6-6-6-6"/>))])
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
private
|
|
130
|
+
|
|
131
|
+
sig { returns(T::Hash[Symbol, T.untyped]) }
|
|
132
|
+
def link_attributes
|
|
133
|
+
attributes = @html_args.dup
|
|
134
|
+
aria = T.cast(attributes[:aria], T.nilable(T::Hash[Symbol, T.untyped])) || {}
|
|
135
|
+
attributes[:aria] = { label: "Go to next page" }.merge(aria)
|
|
136
|
+
attributes
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
sig { returns(String) }
|
|
140
|
+
def default_label
|
|
141
|
+
content.presence || content_tag(:span, class: "hidden sm:block") { "Next" }
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
class Ellipsis < BaseComponent
|
|
146
|
+
include IconHelpers
|
|
147
|
+
|
|
148
|
+
# span(aria-hidden)。lucide の ellipsis アイコンと sr-only ラベルを含む
|
|
149
|
+
sig { override.returns(String) }
|
|
150
|
+
def call
|
|
151
|
+
content_tag(tag, **html_attributes) do
|
|
152
|
+
safe_join([
|
|
153
|
+
pagination_icon(%(<circle cx="12" cy="12" r="1"/><circle cx="19" cy="12" r="1"/><circle cx="5" cy="12" r="1"/>)),
|
|
154
|
+
content_tag(:span, class: "sr-only") { "More pages" }
|
|
155
|
+
])
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Shadcn
|
|
5
|
+
# Popover API によるポップオーバー(10-roadmap Phase 3「popover = Popover API」)。
|
|
6
|
+
# popover="auto" で軽量な外側クリック解散をネイティブに任せ、コントローラは
|
|
7
|
+
# 開閉同期と位置合わせ(anchor相当)を担う
|
|
8
|
+
# JS無効時フォールバック: Readable(展開はできないが本文はSSR済み)
|
|
9
|
+
class Popover < BaseComponent
|
|
10
|
+
CONTROLLER = "shadcn--popover"
|
|
11
|
+
|
|
12
|
+
sig { override.returns(T::Hash[Symbol, T.untyped]) }
|
|
13
|
+
def contract_data_attributes
|
|
14
|
+
super.merge(controller: CONTROLLER)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
class Trigger < BaseComponent
|
|
18
|
+
include Shadcn::ButtonStyled
|
|
19
|
+
|
|
20
|
+
sig { override.returns(String) }
|
|
21
|
+
def default_tag
|
|
22
|
+
"button"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
sig { override.returns(T::Hash[Symbol, T.untyped]) }
|
|
26
|
+
def html_attributes
|
|
27
|
+
attributes = apply_button_styling(super)
|
|
28
|
+
attributes[:type] = "button" unless attributes.key?(:type)
|
|
29
|
+
merge_nested(attributes, :aria, { expanded: "false", haspopup: "true" })
|
|
30
|
+
merge_nested(attributes, :data, { action: "#{CONTROLLER}#toggle" })
|
|
31
|
+
attributes
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# base-nova では Anchor が廃止されたため非対応(位置決めはTrigger基準)
|
|
36
|
+
|
|
37
|
+
class Content < BaseComponent
|
|
38
|
+
include Shadcn::FloatingPositionOptions
|
|
39
|
+
|
|
40
|
+
FLOATING_POSITION_DEFAULTS = T.let(
|
|
41
|
+
{ side: :bottom, align: :center, side_offset: 4, align_offset: 0, collision_padding: 5 }.freeze,
|
|
42
|
+
T::Hash[Symbol, T.untyped]
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
sig { override.returns(T::Hash[Symbol, T.untyped]) }
|
|
46
|
+
def html_attributes
|
|
47
|
+
attributes = super
|
|
48
|
+
attributes[:popover] = "auto"
|
|
49
|
+
merge_floating_position_data(attributes, state: "closed")
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
class Header < BaseComponent
|
|
54
|
+
# div
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
class Title < BaseComponent
|
|
58
|
+
# div(契約タグは h2 の型注記だが実要素は div)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
class Description < BaseComponent
|
|
62
|
+
sig { override.returns(String) }
|
|
63
|
+
def default_tag
|
|
64
|
+
"p"
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Shadcn
|
|
5
|
+
# Lookbook / ViewComponent プレビュー用の基底クラス。
|
|
6
|
+
#
|
|
7
|
+
# ViewComponent::Preview#render はコンポーネントを描画せず、
|
|
8
|
+
# {args:, block:, component:, locals:, template:} からなる描画予約のディスクリプタを返す。
|
|
9
|
+
# このためブロック内でネストした render の戻り値を組み合わせる記法
|
|
10
|
+
# (safe_join([render(Header.new) { ... }, render(Content.new) { ... }]) 等)では
|
|
11
|
+
# ディスクリプタの inspect 文字列がそのままプレビューに露出してしまう。
|
|
12
|
+
#
|
|
13
|
+
# この基底クラスの render は常に即座にHTML文字列を描画して返すため、
|
|
14
|
+
# 単一ルート・ネスト・兄弟連結のどの記法でもそのまま組み合わせられる。
|
|
15
|
+
# example の戻り値は render_args が Fragment 経由でディスクリプタに正規化する。
|
|
16
|
+
# ホストアプリが独自のプレビューを書く際にも同様に利用できる。
|
|
17
|
+
class PreviewBase < ViewComponent::Preview
|
|
18
|
+
extend T::Sig
|
|
19
|
+
|
|
20
|
+
# 描画済みHTML断片を1つのコンポーネントとして扱うラッパー。
|
|
21
|
+
# ViewComponent::Preview のディスクリプタは最終的にビューの render 経由で
|
|
22
|
+
# 描画されるため、render_in に応答するオブジェクトであればよい
|
|
23
|
+
class Fragment
|
|
24
|
+
extend T::Sig
|
|
25
|
+
|
|
26
|
+
sig { params(html: T.untyped).void }
|
|
27
|
+
def initialize(html:)
|
|
28
|
+
super()
|
|
29
|
+
@html = T.let(html, T.untyped)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# ビューの render はブロック付きで呼ぶが、断片では使わないため受けない
|
|
33
|
+
sig { params(_view_context: T.untyped).returns(T.untyped) }
|
|
34
|
+
def render_in(_view_context)
|
|
35
|
+
@html
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# ViewComponent::Preview.render_args はexampleの戻り値にHashを期待するが、
|
|
40
|
+
# この基底クラスのexampleはHTML文字列(SafeBuffer)を返すため、Fragment で包んで
|
|
41
|
+
# ディスクリプタに正規化する。それ以外は VC の挙動(example検証・params渡し・
|
|
42
|
+
# テンプレートフォールバック・layout)を踏襲する
|
|
43
|
+
sig { params(example: T.any(Symbol, String), params: T.untyped).returns(T.untyped) }
|
|
44
|
+
def self.render_args(example, params: {})
|
|
45
|
+
example = example.to_s
|
|
46
|
+
raise AbstractController::ActionNotFound, "#{example} is not a valid preview example" unless examples.include?(example)
|
|
47
|
+
|
|
48
|
+
result = normalize_render_result(invoke_example(example, params))
|
|
49
|
+
result[:template] = preview_example_template_path(example) if result[:template].nil?
|
|
50
|
+
result.merge(layout: instance_variable_get(:@layout))
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# example を呼び出す。宣言済みの引数名と一致するparamsだけを渡す(VCの挙動と同じ)
|
|
54
|
+
sig { params(example: String, params: T.untyped).returns(T.untyped) }
|
|
55
|
+
def self.invoke_example(example, params)
|
|
56
|
+
example_params_names = instance_method(example).parameters.map(&:last)
|
|
57
|
+
provided_params = params.slice(*example_params_names).to_h.symbolize_keys
|
|
58
|
+
provided_params.empty? ? new.public_send(example) : new.public_send(example, **provided_params)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# VC標準のディスクリプタ Hash はそのまま、それ以外(HTML文字列等)は Fragment で包む
|
|
62
|
+
sig { params(result: T.untyped).returns(T.untyped) }
|
|
63
|
+
def self.normalize_render_result(result)
|
|
64
|
+
return result if result.is_a?(Hash)
|
|
65
|
+
|
|
66
|
+
{
|
|
67
|
+
args: {},
|
|
68
|
+
block: nil,
|
|
69
|
+
component: Fragment.new(html: result),
|
|
70
|
+
locals: {},
|
|
71
|
+
template: "view_components/preview"
|
|
72
|
+
}
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
sig { void }
|
|
76
|
+
def initialize
|
|
77
|
+
super
|
|
78
|
+
@preview_view_context = T.let(nil, T.untyped)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
private
|
|
82
|
+
|
|
83
|
+
# example メソッドからの暗黓スコープでの呼び出しのみを想定するため private。
|
|
84
|
+
# public にすると ViewComponent::Preview.examples(public_instance_methods)に
|
|
85
|
+
# 「render」が載り、この基底クラス自体がプレビュー一覧に出てしまう
|
|
86
|
+
#
|
|
87
|
+
# rest-kwargs を持つメソッドの実行時sig検証は sorbet-runtime の既知の誤バインドがあるため
|
|
88
|
+
# checked(:never) で無効化し、srb tc の静的検査に委譲する(BaseComponent と同じ運用)
|
|
89
|
+
sig do
|
|
90
|
+
params(component: T.untyped, args: T.untyped, block: T.nilable(T.proc.returns(T.untyped)))
|
|
91
|
+
.returns(T.untyped).checked(:never)
|
|
92
|
+
end
|
|
93
|
+
def render(component, **args, &block)
|
|
94
|
+
component.render_in(preview_view_context, **args, &block)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# コンポーネントの即時描画に使う単独のビューコンテキスト。
|
|
98
|
+
# Lookbook のプレビュー出力はexampleの戻り値(HTML文字列)だけで構成されるため、
|
|
99
|
+
# ルートとネストでコンテキストが分かれても出力には影響しない。
|
|
100
|
+
# NOTE: 素の ActionController::Base から作るためURLヘルパー等は使えない。
|
|
101
|
+
# 本gemのコンポーネントは属性の出力のみでURLヘルパーに依存しない
|
|
102
|
+
sig { returns(T.untyped) }
|
|
103
|
+
def preview_view_context
|
|
104
|
+
@preview_view_context ||= ActionController::Base.new.view_context
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Shadcn
|
|
5
|
+
# 進捗バー。role=progressbar と aria 値をネイティブARIAで表現する
|
|
6
|
+
class Progress < BaseComponent
|
|
7
|
+
sig do
|
|
8
|
+
params(
|
|
9
|
+
value: T.nilable(T.any(Integer, Float, String)),
|
|
10
|
+
args: T::Hash[Symbol, T.untyped]
|
|
11
|
+
).void.checked(:never)
|
|
12
|
+
end
|
|
13
|
+
def initialize(value: self.class.property_default(:value), **args)
|
|
14
|
+
@value = T.let(normalize_property(:value, value), T.nilable(T.any(Integer, Float)))
|
|
15
|
+
super(**args)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
sig { override.returns(String) }
|
|
19
|
+
def call
|
|
20
|
+
content_tag(tag, **html_attributes) do
|
|
21
|
+
safe_join([content, progress_track])
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
sig { override.returns(T::Hash[Symbol, T.untyped]) }
|
|
26
|
+
def html_attributes
|
|
27
|
+
attributes = super
|
|
28
|
+
attributes[:role] = "progressbar"
|
|
29
|
+
aria_defaults = T.let({ valuemin: "0", valuemax: "100" }, T::Hash[Symbol, T.untyped])
|
|
30
|
+
aria_defaults[:valuenow] = @value.to_s if @value
|
|
31
|
+
merge_nested(attributes, :aria, aria_defaults)
|
|
32
|
+
attributes
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
sig { returns(String) }
|
|
38
|
+
def progress_track
|
|
39
|
+
content_tag(:div, class: ShadcnViewComponents::Classes.resolve(:progress_track), data: { slot: "progress-track" }) do
|
|
40
|
+
content_tag(
|
|
41
|
+
:div,
|
|
42
|
+
class: ShadcnViewComponents::Classes.resolve(:progress_indicator),
|
|
43
|
+
style: "width: #{@value || 0}%",
|
|
44
|
+
data: { slot: "progress-indicator" }
|
|
45
|
+
) { "".html_safe }
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Shadcn
|
|
5
|
+
class RadioGroup < BaseComponent
|
|
6
|
+
# ネイティブなラジオボタン群(JS無しで動作 — 05-stimulus-hotwire §3)。
|
|
7
|
+
# name は利用者が指定する(素のinputなのでそのまま連携する)
|
|
8
|
+
sig { override.returns(T::Hash[Symbol, T.untyped]) }
|
|
9
|
+
def html_attributes
|
|
10
|
+
attributes = super
|
|
11
|
+
attributes[:role] = "radiogroup" unless @html_args.key?(:role)
|
|
12
|
+
attributes
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
class Item < BaseComponent
|
|
16
|
+
include NativeCheckedState
|
|
17
|
+
|
|
18
|
+
NATIVE_STATE_CLASS = T.let([
|
|
19
|
+
"not-checked:border-input not-checked:bg-transparent dark:not-checked:bg-input/30",
|
|
20
|
+
"checked:border-primary checked:bg-primary checked:text-primary-foreground",
|
|
21
|
+
"group-has-[:focus-visible]/field-label:checked:border-primary dark:checked:bg-primary"
|
|
22
|
+
].join(" ").freeze, String)
|
|
23
|
+
|
|
24
|
+
# 契約タグは RadioGroupPrimitive.Item。ネイティブな input[type=radio] として描く
|
|
25
|
+
sig { override.returns(String) }
|
|
26
|
+
def default_tag
|
|
27
|
+
"input"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
sig { override.returns(String) }
|
|
31
|
+
def call
|
|
32
|
+
content_tag(:span, class: "relative flex w-fit") do
|
|
33
|
+
safe_join([input_element, indicator_element])
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
sig { returns(String) }
|
|
40
|
+
def input_element
|
|
41
|
+
attributes = html_attributes.merge(type: "radio")
|
|
42
|
+
# ネイティブウィジェット描画を消す(dark時の白箱問題 — checkbox.rb と同じ理由)。
|
|
43
|
+
# 印の表示は indicator を peer-checked で出すため peer も付与する
|
|
44
|
+
attributes[:class] = [attributes[:class], "appearance-none peer", NATIVE_STATE_CLASS].compact.join(" ")
|
|
45
|
+
merge_native_checked_state(attributes, checked: native_checked?(@html_args))
|
|
46
|
+
void_tag("input", **attributes)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# 契約のスロット構造(radio-group-indicator)を保つ。普段は非表示で、
|
|
50
|
+
# peer であるinputのチェック状態に連動して出す(upstreamのIndicatorと同じ挙動)
|
|
51
|
+
sig { returns(String) }
|
|
52
|
+
def indicator_element
|
|
53
|
+
indicator_class = T.cast(contract_slot("radio-group-indicator").dig(:static_attributes, :class), T.nilable(String))
|
|
54
|
+
content_tag(
|
|
55
|
+
:span,
|
|
56
|
+
data: { slot: "radio-group-indicator" },
|
|
57
|
+
aria: { hidden: "true" },
|
|
58
|
+
class: [indicator_class, "pointer-events-none absolute inset-0 hidden peer-checked:flex"].compact.join(" ")
|
|
59
|
+
) do
|
|
60
|
+
content_tag(
|
|
61
|
+
:svg,
|
|
62
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
63
|
+
viewBox: "0 0 24 24",
|
|
64
|
+
fill: "none",
|
|
65
|
+
stroke: "none",
|
|
66
|
+
class: "absolute top-1/2 left-1/2 size-2 -translate-x-1/2 -translate-y-1/2 fill-current"
|
|
67
|
+
) do
|
|
68
|
+
raw(%(<circle cx="12" cy="12" r="10"/>))
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|