okonomi_ui_kit 0.1.5 → 0.1.7
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 +4 -4
- data/app/assets/builds/okonomi_ui_kit/application.tailwind.css +313 -4
- data/app/helpers/okonomi_ui_kit/component.rb +80 -0
- data/app/helpers/okonomi_ui_kit/components/alert.rb +9 -0
- data/app/helpers/okonomi_ui_kit/components/badge.rb +31 -0
- data/app/helpers/okonomi_ui_kit/components/button_to.rb +34 -0
- data/app/helpers/okonomi_ui_kit/components/code.rb +73 -0
- data/app/helpers/okonomi_ui_kit/components/link_to.rb +34 -0
- data/app/helpers/okonomi_ui_kit/components/page.rb +247 -0
- data/app/helpers/okonomi_ui_kit/components/table.rb +207 -0
- data/app/helpers/okonomi_ui_kit/components/typography.rb +68 -0
- data/app/helpers/okonomi_ui_kit/config.rb +16 -0
- data/app/helpers/okonomi_ui_kit/theme.rb +55 -3
- data/app/helpers/okonomi_ui_kit/ui_helper.rb +35 -63
- data/app/javascript/okonomi_ui_kit/controllers/modal_controller.js +94 -0
- data/app/views/okonomi/components/alert/_alert.html.erb +3 -0
- data/app/views/okonomi/components/code/_code.html.erb +1 -0
- data/app/views/okonomi/components/page/_page.html.erb +5 -0
- data/app/views/okonomi/components/table/_table.html.erb +3 -0
- data/app/views/okonomi/components/typography/_typography.html.erb +7 -0
- data/app/views/okonomi/modals/_confirmation_modal.html.erb +77 -0
- data/lib/okonomi_ui_kit/engine.rb +0 -3
- data/lib/okonomi_ui_kit/version.rb +1 -1
- metadata +23 -7
- data/app/helpers/okonomi_ui_kit/badge_helper.rb +0 -23
- data/app/helpers/okonomi_ui_kit/page_builder_helper.rb +0 -217
- data/app/helpers/okonomi_ui_kit/table_helper.rb +0 -158
- data/app/views/okonomi/components/_typography.html.erb +0 -7
@@ -0,0 +1,207 @@
|
|
1
|
+
module OkonomiUiKit
|
2
|
+
module Components
|
3
|
+
class Table < OkonomiUiKit::Component
|
4
|
+
def render(options = {}, &block)
|
5
|
+
options = options.with_indifferent_access
|
6
|
+
variant = (options.delete(:variant) || :default).to_sym
|
7
|
+
|
8
|
+
builder = TableBuilder.new(view, theme, self, variant)
|
9
|
+
view.render(template_path, builder: builder, options: options, &block)
|
10
|
+
end
|
11
|
+
|
12
|
+
register_styles :default do
|
13
|
+
{
|
14
|
+
default: {
|
15
|
+
body: {
|
16
|
+
base: "divide-y divide-gray-200 bg-white"
|
17
|
+
},
|
18
|
+
th: {
|
19
|
+
base: "text-sm font-semibold text-gray-900",
|
20
|
+
first: "py-3.5 pr-3",
|
21
|
+
last: "relative py-3.5",
|
22
|
+
middle: "pl-3 pr-3 py-3.5"
|
23
|
+
},
|
24
|
+
td: {
|
25
|
+
base: "text-sm whitespace-nowrap",
|
26
|
+
first: "py-4 pr-3 font-medium text-gray-900",
|
27
|
+
last: "relative py-4 font-medium",
|
28
|
+
middle: "pl-3 pr-3 py-4 text-gray-500"
|
29
|
+
},
|
30
|
+
alignment: {
|
31
|
+
left: "text-left",
|
32
|
+
center: "text-center",
|
33
|
+
right: "text-right"
|
34
|
+
},
|
35
|
+
empty_state: {
|
36
|
+
wrapper: "text-center py-8",
|
37
|
+
icon: "mx-auto h-12 w-12 text-gray-400",
|
38
|
+
title: "mt-2 text-sm font-medium text-gray-900",
|
39
|
+
subtitle: "mt-1 text-sm text-gray-500",
|
40
|
+
cell: "text-center py-8 text-gray-500"
|
41
|
+
}
|
42
|
+
}
|
43
|
+
}
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
class TableBuilder
|
48
|
+
include ActionView::Helpers::TagHelper
|
49
|
+
include ActionView::Helpers::CaptureHelper
|
50
|
+
|
51
|
+
def initialize(template, theme, style_provider, variant = :default)
|
52
|
+
@template = template
|
53
|
+
@theme = theme
|
54
|
+
@style_provider = style_provider
|
55
|
+
@variant = variant
|
56
|
+
@current_row_cells = []
|
57
|
+
@in_header = false
|
58
|
+
@in_body = false
|
59
|
+
end
|
60
|
+
|
61
|
+
def head(&block)
|
62
|
+
@in_header = true
|
63
|
+
@in_body = false
|
64
|
+
result = tag.thead(&block)
|
65
|
+
@in_header = false
|
66
|
+
result
|
67
|
+
end
|
68
|
+
|
69
|
+
def body(&block)
|
70
|
+
@in_header = false
|
71
|
+
@in_body = true
|
72
|
+
result = tag.tbody(class: style(:body, :base), &block)
|
73
|
+
@in_body = false
|
74
|
+
result
|
75
|
+
end
|
76
|
+
|
77
|
+
def tr(&block)
|
78
|
+
@current_row_cells = []
|
79
|
+
|
80
|
+
# Collect all cells first
|
81
|
+
yield if block_given?
|
82
|
+
|
83
|
+
# Now render each cell with proper first/last detection
|
84
|
+
rendered_cells = @current_row_cells.map.with_index do |cell, index|
|
85
|
+
is_first = index == 0
|
86
|
+
is_last = index == @current_row_cells.length - 1
|
87
|
+
|
88
|
+
if cell[:type] == :th
|
89
|
+
render_th(cell, is_first, is_last)
|
90
|
+
else
|
91
|
+
render_td(cell, is_first, is_last)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
result = tag.tr do
|
96
|
+
@template.safe_join(rendered_cells)
|
97
|
+
end
|
98
|
+
|
99
|
+
@current_row_cells = []
|
100
|
+
result
|
101
|
+
end
|
102
|
+
|
103
|
+
def th(scope: "col", align: :left, **options, &block)
|
104
|
+
content = capture(&block) if block_given?
|
105
|
+
|
106
|
+
# Store cell data for later processing in tr
|
107
|
+
cell = { type: :th, scope: scope, align: align, options: options, content: content }
|
108
|
+
@current_row_cells << cell
|
109
|
+
|
110
|
+
# Return empty string for now, actual rendering happens in tr
|
111
|
+
""
|
112
|
+
end
|
113
|
+
|
114
|
+
def td(align: :left, **options, &block)
|
115
|
+
content = capture(&block) if block_given?
|
116
|
+
|
117
|
+
# Store cell data for later processing in tr
|
118
|
+
cell = { type: :td, align: align, options: options, content: content }
|
119
|
+
@current_row_cells << cell
|
120
|
+
|
121
|
+
# Return empty string for now, actual rendering happens in tr
|
122
|
+
""
|
123
|
+
end
|
124
|
+
|
125
|
+
def empty_state(title: "No records found", icon: "heroicons/outline/document", colspan: nil, &block)
|
126
|
+
content = if block_given?
|
127
|
+
capture(&block)
|
128
|
+
else
|
129
|
+
tag.div(class: style(:empty_state, :wrapper)) do
|
130
|
+
icon_content = if @template.respond_to?(:svg_icon)
|
131
|
+
@template.svg_icon(icon, class: style(:empty_state, :icon))
|
132
|
+
else
|
133
|
+
tag.div(class: style(:empty_state, :icon))
|
134
|
+
end
|
135
|
+
|
136
|
+
icon_content + tag.p(title, class: style(:empty_state, :title)) +
|
137
|
+
tag.p("Get started by creating a new record.", class: style(:empty_state, :subtitle))
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
tr do
|
142
|
+
td(colspan: colspan, class: style(:empty_state, :cell)) do
|
143
|
+
content
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
private
|
149
|
+
|
150
|
+
def tag
|
151
|
+
@template.tag
|
152
|
+
end
|
153
|
+
|
154
|
+
def capture(*args, &block)
|
155
|
+
@template.capture(*args, &block)
|
156
|
+
end
|
157
|
+
|
158
|
+
def render_th(cell, is_first, is_last)
|
159
|
+
align_class = style(:alignment, cell[:align]) || style(:alignment, :left)
|
160
|
+
|
161
|
+
position_class = if is_first
|
162
|
+
style(:th, :first)
|
163
|
+
elsif is_last
|
164
|
+
style(:th, :last)
|
165
|
+
else
|
166
|
+
style(:th, :middle)
|
167
|
+
end
|
168
|
+
|
169
|
+
classes = [
|
170
|
+
style(:th, :base),
|
171
|
+
position_class,
|
172
|
+
align_class,
|
173
|
+
cell[:options][:class]
|
174
|
+
].compact.join(' ')
|
175
|
+
|
176
|
+
options = cell[:options].except(:class)
|
177
|
+
tag.th(cell[:content], scope: cell[:scope], class: classes, **options)
|
178
|
+
end
|
179
|
+
|
180
|
+
def render_td(cell, is_first, is_last)
|
181
|
+
align_class = style(:alignment, cell[:align]) || style(:alignment, :left)
|
182
|
+
|
183
|
+
position_class = if is_first
|
184
|
+
style(:td, :first)
|
185
|
+
elsif is_last
|
186
|
+
style(:td, :last)
|
187
|
+
else
|
188
|
+
style(:td, :middle)
|
189
|
+
end
|
190
|
+
|
191
|
+
classes = [
|
192
|
+
style(:td, :base),
|
193
|
+
position_class,
|
194
|
+
align_class,
|
195
|
+
cell[:options][:class]
|
196
|
+
].compact.join(' ')
|
197
|
+
|
198
|
+
options = cell[:options].except(:class)
|
199
|
+
tag.td(cell[:content], class: classes, **options)
|
200
|
+
end
|
201
|
+
|
202
|
+
def style(*keys)
|
203
|
+
@style_provider.style(@variant, *keys)
|
204
|
+
end
|
205
|
+
end
|
206
|
+
end
|
207
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
module OkonomiUiKit
|
2
|
+
module Components
|
3
|
+
class Typography < OkonomiUiKit::Component
|
4
|
+
TYPOGRAPHY_COMPONENTS = {
|
5
|
+
body1: 'p',
|
6
|
+
body2: 'p',
|
7
|
+
h1: 'h1',
|
8
|
+
h2: 'h2',
|
9
|
+
h3: 'h3',
|
10
|
+
h4: 'h4',
|
11
|
+
h5: 'h5',
|
12
|
+
h6: 'h6',
|
13
|
+
}.freeze
|
14
|
+
|
15
|
+
def render(text = nil, options = {}, &block)
|
16
|
+
options, text = text, nil if block_given?
|
17
|
+
options ||= {}
|
18
|
+
options = options.with_indifferent_access
|
19
|
+
|
20
|
+
variant = (options.delete(:variant) || 'body1').to_sym
|
21
|
+
component = (TYPOGRAPHY_COMPONENTS[variant] || 'span').to_s
|
22
|
+
color = (options.delete(:color) || 'default').to_sym
|
23
|
+
|
24
|
+
classes = [
|
25
|
+
style(:variants, variant) || '',
|
26
|
+
style(:colors, color) || '',
|
27
|
+
options.delete(:class) || ''
|
28
|
+
].reject(&:blank?).join(' ')
|
29
|
+
|
30
|
+
view.render(
|
31
|
+
template_path,
|
32
|
+
text: text,
|
33
|
+
options: options,
|
34
|
+
variant: variant,
|
35
|
+
component: component,
|
36
|
+
classes: classes,
|
37
|
+
&block
|
38
|
+
)
|
39
|
+
end
|
40
|
+
|
41
|
+
register_styles :default do
|
42
|
+
{
|
43
|
+
variants: {
|
44
|
+
body1: "text-base font-normal",
|
45
|
+
body2: "text-sm font-normal",
|
46
|
+
h1: "text-3xl font-bold",
|
47
|
+
h2: "text-2xl font-bold",
|
48
|
+
h3: "text-xl font-semibold",
|
49
|
+
h4: "text-lg font-semibold",
|
50
|
+
h5: "text-base font-semibold",
|
51
|
+
h6: "text-sm font-semibold"
|
52
|
+
},
|
53
|
+
colors: {
|
54
|
+
default: "text-default-700",
|
55
|
+
dark: "text-default-900",
|
56
|
+
muted: "text-default-500",
|
57
|
+
primary: "text-primary-600",
|
58
|
+
secondary: "text-secondary-600",
|
59
|
+
success: "text-success-600",
|
60
|
+
danger: "text-danger-600",
|
61
|
+
warning: "text-warning-600",
|
62
|
+
info: "text-info-600"
|
63
|
+
}
|
64
|
+
}
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module OkonomiUiKit
|
2
|
+
class Config
|
3
|
+
def self.register_styles(theme = :default, &block)
|
4
|
+
styles = block.call if block_given?
|
5
|
+
|
6
|
+
raise ArgumentError, "Styles must be a Hash" unless styles.is_a?(Hash)
|
7
|
+
|
8
|
+
styles_registry[theme] ||= {}
|
9
|
+
styles_registry[theme] = styles_registry[theme].deep_merge(styles)
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.styles_registry
|
13
|
+
@styles_registry ||= {}
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -26,9 +26,9 @@ module OkonomiUiKit
|
|
26
26
|
}
|
27
27
|
},
|
28
28
|
link: {
|
29
|
-
root: "hover:cursor-pointer",
|
29
|
+
root: "hover:cursor-pointer text-sm",
|
30
30
|
outlined: {
|
31
|
-
root: "inline-flex border items-center justify-center px-
|
31
|
+
root: "inline-flex border items-center justify-center px-2 py-1 rounded-md font-medium focus:outline-none focus:ring-2 focus:ring-offset-2",
|
32
32
|
colors: {
|
33
33
|
default: "bg-white text-default-700 border-default-700 hover:bg-default-50",
|
34
34
|
primary: "bg-white text-primary-600 border-primary-600 hover:bg-primary-50",
|
@@ -40,7 +40,7 @@ module OkonomiUiKit
|
|
40
40
|
}
|
41
41
|
},
|
42
42
|
contained: {
|
43
|
-
root: "inline-flex border items-center justify-center px-
|
43
|
+
root: "inline-flex border items-center justify-center px-2 py-1 rounded-md font-medium focus:outline-none focus:ring-2 focus:ring-offset-2",
|
44
44
|
colors: {
|
45
45
|
default: "border-default-700 bg-default-600 text-white hover:bg-default-700",
|
46
46
|
primary: "border-primary-700 bg-primary-600 text-white hover:bg-primary-700",
|
@@ -98,6 +98,58 @@ module OkonomiUiKit
|
|
98
98
|
hint: {
|
99
99
|
root: "cursor-pointer text-sm text-gray-400"
|
100
100
|
}
|
101
|
+
},
|
102
|
+
modal: {
|
103
|
+
backdrop: "fixed inset-0 bg-gray-500/75 transition-opacity duration-300 ease-out opacity-0",
|
104
|
+
container: "fixed inset-0 z-10 w-screen overflow-y-auto",
|
105
|
+
wrapper: "flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0",
|
106
|
+
panel: {
|
107
|
+
base: "relative transform overflow-hidden rounded-lg bg-white px-4 pt-5 pb-4 text-left shadow-xl transition-all duration-300 ease-out sm:my-8 sm:w-full sm:p-6 opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95",
|
108
|
+
sizes: {
|
109
|
+
sm: "sm:max-w-sm",
|
110
|
+
md: "sm:max-w-lg",
|
111
|
+
lg: "sm:max-w-2xl",
|
112
|
+
xl: "sm:max-w-4xl"
|
113
|
+
}
|
114
|
+
},
|
115
|
+
close_button: {
|
116
|
+
wrapper: "absolute top-0 right-0 hidden pt-4 pr-4 sm:block",
|
117
|
+
button: "rounded-md bg-white text-gray-400 hover:text-gray-500 focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 focus:outline-none",
|
118
|
+
icon: {
|
119
|
+
file: "heroicons/outline/x-mark",
|
120
|
+
class: "size-6"
|
121
|
+
}
|
122
|
+
},
|
123
|
+
icon: {
|
124
|
+
wrapper: "mx-auto flex size-12 shrink-0 items-center justify-center rounded-full sm:mx-0 sm:size-10",
|
125
|
+
class: "size-6",
|
126
|
+
variants: {
|
127
|
+
warning: {
|
128
|
+
wrapper: "bg-red-100",
|
129
|
+
icon: "text-red-600",
|
130
|
+
file: "heroicons/outline/exclamation-triangle"
|
131
|
+
},
|
132
|
+
info: {
|
133
|
+
wrapper: "bg-blue-100",
|
134
|
+
icon: "text-blue-600",
|
135
|
+
file: "heroicons/outline/information-circle"
|
136
|
+
},
|
137
|
+
success: {
|
138
|
+
wrapper: "bg-green-100",
|
139
|
+
icon: "text-green-600",
|
140
|
+
file: "heroicons/outline/check-circle"
|
141
|
+
}
|
142
|
+
}
|
143
|
+
},
|
144
|
+
content: {
|
145
|
+
wrapper: "sm:flex sm:items-start",
|
146
|
+
text_wrapper: "mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left",
|
147
|
+
title: "text-base font-semibold text-gray-900",
|
148
|
+
message: "mt-2 text-sm text-gray-500"
|
149
|
+
},
|
150
|
+
actions: {
|
151
|
+
wrapper: "mt-5 sm:mt-4 sm:flex sm:flex-row-reverse"
|
152
|
+
}
|
101
153
|
}
|
102
154
|
}
|
103
155
|
}
|
@@ -26,42 +26,6 @@ module OkonomiUiKit
|
|
26
26
|
@_okonomi_ui_kit_theme ||= OkonomiUiKit::Theme::DEFAULT_THEME
|
27
27
|
end
|
28
28
|
|
29
|
-
def link_to(name = nil, options = nil, html_options = nil, &block)
|
30
|
-
html_options, options, name = options, name, block if block_given?
|
31
|
-
|
32
|
-
html_options ||= {}
|
33
|
-
html_options[:class] ||= ''
|
34
|
-
|
35
|
-
variant = (html_options.delete(:variant) || 'text').to_sym
|
36
|
-
color = (html_options.delete(:color) || 'default').to_sym
|
37
|
-
|
38
|
-
html_options[:class] = button_class(variant:, color:, classes: html_options[:class])
|
39
|
-
|
40
|
-
if block_given?
|
41
|
-
@template.link_to(options, html_options, &block)
|
42
|
-
else
|
43
|
-
@template.link_to(name, options, html_options)
|
44
|
-
end
|
45
|
-
end
|
46
|
-
|
47
|
-
def button_to(name = nil, options = nil, html_options = nil, &block)
|
48
|
-
html_options, options, name = options, name, block if block_given?
|
49
|
-
|
50
|
-
html_options ||= {}
|
51
|
-
html_options[:class] ||= ''
|
52
|
-
|
53
|
-
variant = (html_options.delete(:variant) || 'contained').to_sym
|
54
|
-
color = (html_options.delete(:color) || 'default').to_sym
|
55
|
-
|
56
|
-
html_options[:class] = button_class(variant:, color:, classes: html_options[:class])
|
57
|
-
|
58
|
-
if block_given?
|
59
|
-
@template.button_to(options, html_options, &block)
|
60
|
-
else
|
61
|
-
@template.button_to(name, options, html_options)
|
62
|
-
end
|
63
|
-
end
|
64
|
-
|
65
29
|
def button_class(variant: 'contained', color: 'default', classes: '')
|
66
30
|
[
|
67
31
|
get_theme.dig(:components, :link, :root) || '',
|
@@ -71,40 +35,48 @@ module OkonomiUiKit
|
|
71
35
|
].join(' ')
|
72
36
|
end
|
73
37
|
|
74
|
-
def
|
75
|
-
|
38
|
+
def confirmation_modal(title:, message:, confirm_text: "Confirm", cancel_text: "Cancel", variant: :warning, size: :md, **options, &block)
|
39
|
+
modal_options = {
|
40
|
+
title: title,
|
41
|
+
message: message,
|
42
|
+
confirm_text: confirm_text,
|
43
|
+
cancel_text: cancel_text,
|
44
|
+
variant: variant,
|
45
|
+
size: size,
|
46
|
+
has_custom_actions: block_given?,
|
47
|
+
**options
|
48
|
+
}
|
49
|
+
@template.render("okonomi/modals/confirmation_modal", options: modal_options, ui: self, &block)
|
76
50
|
end
|
77
51
|
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
h3: 'h3',
|
84
|
-
h4: 'h4',
|
85
|
-
h5: 'h5',
|
86
|
-
h6: 'h6',
|
87
|
-
}.freeze
|
52
|
+
def modal_data_attributes(options)
|
53
|
+
return "" unless options[:data]
|
54
|
+
|
55
|
+
options[:data].map { |k, v| "data-#{k.to_s.dasherize}=\"#{v}\"" }.join(' ').html_safe
|
56
|
+
end
|
88
57
|
|
89
|
-
def
|
90
|
-
|
91
|
-
|
58
|
+
def modal_panel_class(size)
|
59
|
+
[
|
60
|
+
get_theme.dig(:components, :modal, :panel, :base),
|
61
|
+
get_theme.dig(:components, :modal, :panel, :sizes, size)
|
62
|
+
].compact.join(' ')
|
63
|
+
end
|
92
64
|
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
options.delete(:class) || ''
|
100
|
-
]
|
65
|
+
def modal_icon_wrapper_class(variant)
|
66
|
+
[
|
67
|
+
get_theme.dig(:components, :modal, :icon, :wrapper),
|
68
|
+
get_theme.dig(:components, :modal, :icon, :variants, variant, :wrapper)
|
69
|
+
].compact.join(' ')
|
70
|
+
end
|
101
71
|
|
102
|
-
|
103
|
-
|
72
|
+
def method_missing(method_name, *args, &block)
|
73
|
+
component_name = "OkonomiUiKit::Components::#{method_name.to_s.camelize}"
|
74
|
+
if Object.const_defined?(component_name)
|
75
|
+
return component_name.constantize.new(@template, get_theme).render(*args, &block)
|
104
76
|
else
|
105
|
-
|
77
|
+
super
|
106
78
|
end
|
107
79
|
end
|
108
80
|
end
|
109
81
|
end
|
110
|
-
end
|
82
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
2
|
+
|
3
|
+
export default class extends Controller {
|
4
|
+
static targets = ["container", "backdrop", "panel"]
|
5
|
+
static values = {
|
6
|
+
size: String,
|
7
|
+
autoOpen: Boolean
|
8
|
+
}
|
9
|
+
|
10
|
+
connect() {
|
11
|
+
this.close()
|
12
|
+
if (this.autoOpenValue) {
|
13
|
+
this.open()
|
14
|
+
}
|
15
|
+
|
16
|
+
// Bind escape key handler
|
17
|
+
this.handleEscape = this.handleEscape.bind(this)
|
18
|
+
}
|
19
|
+
|
20
|
+
disconnect() {
|
21
|
+
this.unlockBodyScroll()
|
22
|
+
document.removeEventListener('keydown', this.handleEscape)
|
23
|
+
}
|
24
|
+
|
25
|
+
open() {
|
26
|
+
this.containerTarget.style.display = 'block'
|
27
|
+
this.lockBodyScroll()
|
28
|
+
document.addEventListener('keydown', this.handleEscape)
|
29
|
+
|
30
|
+
// Focus trap - focus first focusable element in modal
|
31
|
+
this.focusFirstElement()
|
32
|
+
|
33
|
+
// Add entrance animations
|
34
|
+
requestAnimationFrame(() => {
|
35
|
+
this.backdropTarget.classList.remove('opacity-0')
|
36
|
+
this.backdropTarget.classList.add('opacity-100')
|
37
|
+
|
38
|
+
this.panelTarget.classList.remove('opacity-0', 'translate-y-4', 'sm:translate-y-0', 'sm:scale-95')
|
39
|
+
this.panelTarget.classList.add('opacity-100', 'translate-y-0', 'sm:scale-100')
|
40
|
+
})
|
41
|
+
}
|
42
|
+
|
43
|
+
close() {
|
44
|
+
// Add exit animations
|
45
|
+
this.backdropTarget.classList.remove('opacity-100')
|
46
|
+
this.backdropTarget.classList.add('opacity-0')
|
47
|
+
|
48
|
+
this.panelTarget.classList.remove('opacity-100', 'translate-y-0', 'sm:scale-100')
|
49
|
+
this.panelTarget.classList.add('opacity-0', 'translate-y-4', 'sm:translate-y-0', 'sm:scale-95')
|
50
|
+
|
51
|
+
// Hide after animation completes
|
52
|
+
setTimeout(() => {
|
53
|
+
this.containerTarget.style.display = 'none'
|
54
|
+
this.unlockBodyScroll()
|
55
|
+
document.removeEventListener('keydown', this.handleEscape)
|
56
|
+
}, 200)
|
57
|
+
}
|
58
|
+
|
59
|
+
confirm() {
|
60
|
+
// Dispatch confirm event for custom handling
|
61
|
+
this.dispatch('confirm', { detail: { modal: this } })
|
62
|
+
this.close()
|
63
|
+
}
|
64
|
+
|
65
|
+
cancel() {
|
66
|
+
// Dispatch cancel event for custom handling
|
67
|
+
this.dispatch('cancel', { detail: { modal: this } })
|
68
|
+
this.close()
|
69
|
+
}
|
70
|
+
|
71
|
+
handleEscape(event) {
|
72
|
+
if (event.key === 'Escape') {
|
73
|
+
this.close()
|
74
|
+
}
|
75
|
+
}
|
76
|
+
|
77
|
+
lockBodyScroll() {
|
78
|
+
document.body.style.overflow = 'hidden'
|
79
|
+
}
|
80
|
+
|
81
|
+
unlockBodyScroll() {
|
82
|
+
document.body.style.overflow = ''
|
83
|
+
}
|
84
|
+
|
85
|
+
focusFirstElement() {
|
86
|
+
const focusableElements = this.panelTarget.querySelectorAll(
|
87
|
+
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
|
88
|
+
)
|
89
|
+
|
90
|
+
if (focusableElements.length > 0) {
|
91
|
+
focusableElements[0].focus()
|
92
|
+
}
|
93
|
+
}
|
94
|
+
}
|
@@ -0,0 +1 @@
|
|
1
|
+
<pre<%= language ? " data-language=\"#{language}\"".html_safe : "" %> class="<%= classes %>"<%= tag.attributes(options) %>><code><%= content %></code></pre>
|