tramway 3.1.2.8 → 4.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 +4 -4
- data/README.md +76 -16
- data/app/assets/builds/tailwind.css +2 -0
- data/app/assets/javascripts/tramway/tramway.js +174 -1
- data/app/assets/stylesheets/font-awesome.css +2337 -0
- data/app/assets/tailwind/application.css +29 -0
- data/app/components/tramway/chat_component.html.haml +1 -1
- data/app/components/tramway/containers/main_component.rb +1 -3
- data/app/components/tramway/containers/narrow_component.rb +5 -4
- data/app/components/tramway/form/builder.rb +1 -2
- data/app/components/tramway/grid/card_component.html.haml +2 -0
- data/app/components/tramway/grid/card_component.rb +37 -0
- data/app/components/tramway/grid_component.html.haml +18 -0
- data/app/components/tramway/grid_component.rb +76 -0
- data/app/components/tramway/native_text_component.rb +1 -2
- data/app/components/tramway/nav/item_component.rb +4 -2
- data/app/components/tramway/navbar_component.html.haml +104 -110
- data/app/components/tramway/navbar_component.rb +84 -2
- data/app/components/tramway/table/cell_component.rb +9 -1
- data/app/components/tramway/table/header_component.rb +23 -2
- data/app/components/tramway/table/row_component.rb +27 -10
- data/app/components/tramway/table_component.rb +28 -1
- data/app/controllers/tramway/entities_controller.rb +6 -6
- data/app/views/tramway/layouts/application.html.haml +8 -6
- data/config/routes.rb +1 -2
- data/config/tailwind.config.js +57 -0
- data/lib/generators/tramway/install/font_awesome_helpers.rb +39 -0
- data/lib/generators/tramway/install/install_generator.rb +131 -65
- data/lib/generators/tramway/install/layout_helpers.rb +95 -0
- data/lib/tramway/engine.rb +12 -0
- data/lib/tramway/forms/validations.rb +1 -2
- data/lib/tramway/helpers/navbar_helper.rb +3 -1
- data/lib/tramway/helpers/table_helper.rb +27 -0
- data/lib/tramway/helpers/views_helper.rb +26 -2
- data/lib/tramway/version.rb +1 -1
- metadata +28 -4
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
@import "tailwindcss";
|
|
2
|
+
|
|
3
|
+
@layer utilities {
|
|
4
|
+
.tramway-scrollbar {
|
|
5
|
+
scrollbar-width: thin;
|
|
6
|
+
scrollbar-color: rgba(82, 82, 91, 0.9) rgba(9, 9, 11, 0.92);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.tramway-scrollbar::-webkit-scrollbar {
|
|
10
|
+
width: 0.5rem;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.tramway-scrollbar::-webkit-scrollbar-track {
|
|
14
|
+
background: rgba(9, 9, 11, 0.92);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.tramway-scrollbar::-webkit-scrollbar-thumb {
|
|
18
|
+
border-radius: 9999px;
|
|
19
|
+
background: rgba(82, 82, 91, 0.9);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.tramway-scrollbar::-webkit-scrollbar-thumb:hover {
|
|
23
|
+
background: rgba(113, 113, 122, 0.95);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.tramway-scrollbar::-webkit-scrollbar-corner {
|
|
27
|
+
background: transparent;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
= helpers.turbo_stream_from chat_id, 'messages'
|
|
2
2
|
|
|
3
3
|
#chat.flex.flex-1.h-full.w-full.min-h-0.min-w-0.flex-col
|
|
4
|
-
#messages.flex.flex-col.flex-1.min-h-0.overflow-y-auto.overflow-x-hidden.rounded-xl.rounded-b-none.border.border-zinc-800.p-2.text-zinc-50.space-y-2.md:p-6.md:space-y-4{ data: { scroll_to_bottom_on_update: scroll_to_bottom_on_update }, class: 'bg-zinc-950/80' }
|
|
4
|
+
#messages.flex.flex-col.flex-1.min-h-0.overflow-y-auto.overflow-x-hidden.rounded-xl.rounded-b-none.border.border-zinc-800.p-2.text-zinc-50.space-y-2.md:p-6.md:space-y-4{ data: { scroll_to_bottom_on_update: scroll_to_bottom_on_update }, class: 'bg-zinc-950/80 tramway-scrollbar' }
|
|
5
5
|
- messages.each do |message|
|
|
6
6
|
= component "tramway/chats/message", **message
|
|
7
7
|
- if disabled?
|
|
@@ -7,10 +7,8 @@ module Tramway
|
|
|
7
7
|
option :options, optional: true, default: proc { {} }
|
|
8
8
|
|
|
9
9
|
def container_classes
|
|
10
|
-
options_classes = options[:class] || ''
|
|
11
|
-
|
|
12
10
|
theme_classes(
|
|
13
|
-
classic:
|
|
11
|
+
classic: ['bg-zinc-950 text-zinc-50 shadow-inner', options[:class]].compact.join(' ')
|
|
14
12
|
)
|
|
15
13
|
end
|
|
16
14
|
end
|
|
@@ -8,11 +8,12 @@ module Tramway
|
|
|
8
8
|
option :options, optional: true, default: proc { {} }
|
|
9
9
|
|
|
10
10
|
def container_classes
|
|
11
|
-
options_classes = options[:class] || ''
|
|
12
|
-
|
|
13
11
|
theme_classes(
|
|
14
|
-
classic:
|
|
15
|
-
|
|
12
|
+
classic: [
|
|
13
|
+
'container p-4 flex align-center justify-center w-full mx-auto',
|
|
14
|
+
'shadow-inner rounded-xl bg-zinc-950 text-zinc-50',
|
|
15
|
+
options[:class]
|
|
16
|
+
].compact.join(' ')
|
|
16
17
|
)
|
|
17
18
|
end
|
|
18
19
|
end
|
|
@@ -5,7 +5,7 @@ require 'tramway/utils/field'
|
|
|
5
5
|
module Tramway
|
|
6
6
|
module Form
|
|
7
7
|
# Provides Tailwind-styled forms
|
|
8
|
-
# rubocop:disable Metrics/ClassLength
|
|
8
|
+
# rubocop:disable-next Metrics/ClassLength
|
|
9
9
|
class Builder < Tramway::Views::FormBuilder
|
|
10
10
|
include Tramway::Utils::Field
|
|
11
11
|
include Tramway::ColorsMethods
|
|
@@ -250,6 +250,5 @@ module Tramway
|
|
|
250
250
|
end
|
|
251
251
|
end
|
|
252
252
|
end
|
|
253
|
-
# rubocop:enable Metrics/ClassLength
|
|
254
253
|
end
|
|
255
254
|
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tramway
|
|
4
|
+
module Grid
|
|
5
|
+
# Dashboard-style card for use inside a grid
|
|
6
|
+
class CardComponent < Tramway::BaseComponent
|
|
7
|
+
option :size, optional: true, default: -> { [1, 1] }
|
|
8
|
+
option :options, optional: true, default: -> { {} }
|
|
9
|
+
|
|
10
|
+
def card_classes
|
|
11
|
+
theme_classes(
|
|
12
|
+
classic: [
|
|
13
|
+
'h-full w-full overflow-hidden rounded-2xl border border-zinc-800',
|
|
14
|
+
'bg-zinc-900/80 p-3 shadow-sm backdrop-blur',
|
|
15
|
+
options[:class]
|
|
16
|
+
].compact.join(' ')
|
|
17
|
+
)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def card_style
|
|
21
|
+
"grid-column: span #{normalized_size.last}; grid-row: span #{normalized_size.first};"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def normalized_size
|
|
27
|
+
rows, columns = Array(size)
|
|
28
|
+
|
|
29
|
+
unless rows.to_i.positive? && columns.to_i.positive?
|
|
30
|
+
raise ArgumentError, 'Grid card size must be a two-item array of positive integers.'
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
[rows.to_i, columns.to_i]
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
%div{ class: grid_classes, role: :grid, aria: { label: 'Dashboard grid', rowcount: rows, colcount: columns }, style: grid_shell_style, **options.except(:class) }
|
|
2
|
+
.pointer-events-none.absolute.inset-0.rounded-xl{ aria: { hidden: 'true' } }
|
|
3
|
+
- if outline
|
|
4
|
+
- horizontal_separators.each do |row_index|
|
|
5
|
+
= content_tag :div, nil, class: grid_separator_classes(:horizontal), style: grid_separator_style(:horizontal, row_index)
|
|
6
|
+
|
|
7
|
+
- vertical_separators.each do |column_index|
|
|
8
|
+
= content_tag :div, nil, class: grid_separator_classes(:vertical), style: grid_separator_style(:vertical, column_index)
|
|
9
|
+
|
|
10
|
+
.grid{ style: grid_net_style }
|
|
11
|
+
- grid_cells.each do |row|
|
|
12
|
+
- row.each do |row_index, column_index|
|
|
13
|
+
= content_tag :div, nil,
|
|
14
|
+
class: grid_cell_classes(row_index, column_index),
|
|
15
|
+
style: grid_cell_style
|
|
16
|
+
|
|
17
|
+
.relative.z-10.grid.gap-1{ role: :presentation, style: grid_net_style }
|
|
18
|
+
= content
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tramway
|
|
4
|
+
# Dashboard-style grid container
|
|
5
|
+
class GridComponent < Tramway::BaseComponent
|
|
6
|
+
CELL_SIZE = '20em'
|
|
7
|
+
CELL_GAP = '0.25rem'
|
|
8
|
+
|
|
9
|
+
option :rows
|
|
10
|
+
option :columns
|
|
11
|
+
option :outline, optional: true, default: -> { false }
|
|
12
|
+
option :options, optional: true, default: -> { {} }
|
|
13
|
+
|
|
14
|
+
def grid_classes
|
|
15
|
+
theme_classes(
|
|
16
|
+
classic: [
|
|
17
|
+
'relative overflow-auto rounded-xl bg-zinc-950 shadow-inner',
|
|
18
|
+
options[:class]
|
|
19
|
+
].compact.join(' ')
|
|
20
|
+
)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def grid_shell_style
|
|
24
|
+
return if rows.to_i <= 0 || columns.to_i <= 0
|
|
25
|
+
|
|
26
|
+
"min-width: calc(#{columns.to_i} * #{CELL_SIZE} + #{[columns.to_i - 1, 0].max} * #{CELL_GAP}); " \
|
|
27
|
+
"min-height: calc(#{rows.to_i} * #{CELL_SIZE} + #{[rows.to_i - 1, 0].max} * #{CELL_GAP});"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def grid_net_style
|
|
31
|
+
return if rows.to_i <= 0 || columns.to_i <= 0
|
|
32
|
+
|
|
33
|
+
"grid-template-columns: repeat(#{columns.to_i}, #{CELL_SIZE}); " \
|
|
34
|
+
"grid-template-rows: repeat(#{rows.to_i}, #{CELL_SIZE});"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def grid_cells
|
|
38
|
+
Array.new(rows.to_i) do |row_index|
|
|
39
|
+
Array.new(columns.to_i) do |column_index|
|
|
40
|
+
[row_index, column_index]
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def grid_cell_classes(_row_index, _column_index)
|
|
46
|
+
'box-border'
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def grid_cell_style
|
|
50
|
+
"width: #{CELL_SIZE}; height: #{CELL_SIZE};"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def grid_separator_classes(axis)
|
|
54
|
+
base = axis == :horizontal ? 'absolute left-0 right-0 h-px' : 'absolute top-0 bottom-0 w-px'
|
|
55
|
+
"#{base} bg-zinc-800"
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def grid_separator_style(axis, index)
|
|
59
|
+
position = "calc((#{CELL_SIZE} + #{CELL_GAP}) * #{index + 1} - #{CELL_GAP} / 2)"
|
|
60
|
+
|
|
61
|
+
if axis == :horizontal
|
|
62
|
+
"top: #{position};"
|
|
63
|
+
else
|
|
64
|
+
"left: #{position};"
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def horizontal_separators
|
|
69
|
+
Array.new([rows.to_i - 1, 0].max) { |index| index }
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def vertical_separators
|
|
73
|
+
Array.new([columns.to_i - 1, 0].max) { |index| index }
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -99,7 +99,7 @@ module Tramway
|
|
|
99
99
|
end
|
|
100
100
|
|
|
101
101
|
# rubocop:disable Metrics/MethodLength
|
|
102
|
-
# rubocop:disable Metrics/AbcSize
|
|
102
|
+
# rubocop:disable-next Metrics/AbcSize
|
|
103
103
|
def linkified(content)
|
|
104
104
|
fragments = []
|
|
105
105
|
current_index = 0
|
|
@@ -130,7 +130,6 @@ module Tramway
|
|
|
130
130
|
|
|
131
131
|
helpers.safe_join(fragments)
|
|
132
132
|
end
|
|
133
|
-
# rubocop:enable Metrics/AbcSize
|
|
134
133
|
# rubocop:enable Metrics/MethodLength
|
|
135
134
|
|
|
136
135
|
def strip_trailing_punctuation(url)
|
|
@@ -6,8 +6,10 @@ module Tramway
|
|
|
6
6
|
#
|
|
7
7
|
class ItemComponent < TailwindComponent
|
|
8
8
|
def style
|
|
9
|
-
@style ||=
|
|
10
|
-
|
|
9
|
+
@style ||= [
|
|
10
|
+
'tramway-navbar-item inline-flex items-center rounded-md px-4 py-3 text-base font-medium text-zinc-100',
|
|
11
|
+
'transition-colors hover:bg-zinc-800 hover:text-zinc-50 sm:px-3 sm:py-2 sm:text-sm'
|
|
12
|
+
].join(' ')
|
|
11
13
|
end
|
|
12
14
|
end
|
|
13
15
|
end
|
|
@@ -1,129 +1,123 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
.
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
1
|
+
- if horizontal?
|
|
2
|
+
%nav#desktop-navbar.bg-zinc-950.text-zinc-50{ class: navbar_classes, data: { controller: 'tramway-navbar', direction: direction, expanded: 'true' } }
|
|
3
|
+
.flex.md:justify-between.w-full.items-center.relative.min-h-8
|
|
4
|
+
.block.sm:hidden
|
|
5
|
+
%button#mobile-menu-button{ class: mobile_button_classes }
|
|
6
|
+
☰
|
|
7
|
+
|
|
8
|
+
- if @left_items.present?
|
|
9
|
+
%ul.hidden.md:flex.items-center.space-x-4
|
|
10
|
+
- @left_items.each do |item|
|
|
11
|
+
= item
|
|
12
|
+
|
|
13
|
+
- if @title[:text].present?
|
|
14
|
+
= content_tag :div, class: 'absolute left-1/2 transform -translate-x-1/2 md:left-1/2 md:-translate-x-1/2' do
|
|
10
15
|
= link_to @title[:link] do
|
|
11
16
|
.truncate{ class: title_classes }
|
|
12
17
|
= @title[:text]
|
|
18
|
+
|
|
19
|
+
- if @right_items.present?
|
|
20
|
+
%ul.hidden.md:flex.items-center.space-x-4
|
|
21
|
+
- @right_items.each do |item|
|
|
22
|
+
= item
|
|
23
|
+
|
|
24
|
+
#mobile-menu{ class: mobile_menu_classes }
|
|
25
|
+
.flex.flex-1.flex-col
|
|
26
|
+
.flex-1
|
|
13
27
|
- if @left_items.present?
|
|
14
|
-
%ul.flex
|
|
28
|
+
%ul.flex.flex-col.space-y-1.pt-4
|
|
15
29
|
- @left_items.each do |item|
|
|
16
30
|
= item
|
|
17
31
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
32
|
+
- if @right_items.present?
|
|
33
|
+
%ul.flex.flex-col.space-y-1.pt-4
|
|
34
|
+
- @right_items.each do |item|
|
|
35
|
+
= item
|
|
22
36
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
37
|
+
.pt-6
|
|
38
|
+
%button.w-full.inline-flex.items-center.justify-center.rounded-md.border.border-zinc-700.bg-zinc-900.px-4.py-3.text-sm.font-semibold.text-zinc-50.transition-colors.hover:bg-zinc-800.hover:text-zinc-50.focus:outline-none.focus-visible:ring-2.focus-visible:ring-zinc-400.focus-visible:ring-offset-2.focus-visible:ring-offset-zinc-950#mobile-menu-close-button{ role: :button }
|
|
39
|
+
Close
|
|
40
|
+
- else
|
|
41
|
+
%nav#desktop-navbar{ class: navbar_classes, data: { controller: 'tramway-navbar', direction: direction, expanded: 'true' } }
|
|
42
|
+
.relative{ class: mobile_horizontal_container_classes }
|
|
43
|
+
.block.sm:hidden
|
|
44
|
+
%button#mobile-menu-button{ class: mobile_button_classes }
|
|
45
|
+
☰
|
|
46
|
+
|
|
47
|
+
- if @title[:text].present? || @left_items.present?
|
|
48
|
+
.flex.items-center.absolute.transform{ class: 'left-1/2 -translate-x-1/2' }
|
|
49
|
+
- if @title[:text].present?
|
|
50
|
+
= link_to @title[:link] do
|
|
51
|
+
.truncate{ class: title_classes }
|
|
52
|
+
= @title[:text]
|
|
53
|
+
- if @left_items.present?
|
|
54
|
+
%ul.flex-row.items-center.space-x-4.ml-4.hidden.md:flex
|
|
55
|
+
- @left_items.each do |item|
|
|
56
|
+
= item
|
|
30
57
|
|
|
31
58
|
- if @right_items.present?
|
|
32
|
-
%ul.
|
|
59
|
+
%ul.items-center.space-x-4.hidden.md:flex
|
|
33
60
|
- @right_items.each do |item|
|
|
34
61
|
= item
|
|
35
62
|
|
|
36
|
-
.
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
:
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
63
|
+
.relative{ class: desktop_vertical_container_classes }
|
|
64
|
+
- if @title[:text].present? || @left_items.present? || @right_items.present?
|
|
65
|
+
#desktop-navbar-header{ class: desktop_vertical_header_classes }
|
|
66
|
+
- if @title[:text].present?
|
|
67
|
+
= link_to @title[:link] do
|
|
68
|
+
.truncate{ class: title_classes }
|
|
69
|
+
= @title[:text]
|
|
70
|
+
|
|
71
|
+
- if @left_items.present? || @right_items.present?
|
|
72
|
+
#desktop-navbar-content{ class: desktop_vertical_content_classes }
|
|
73
|
+
- if @left_items.present?
|
|
74
|
+
%ul{ class: desktop_vertical_item_list_classes }
|
|
75
|
+
- @left_items.each do |item|
|
|
76
|
+
= item
|
|
77
|
+
|
|
78
|
+
- if @right_items.present?
|
|
79
|
+
%ul{ class: desktop_vertical_item_list_classes }
|
|
80
|
+
- @right_items.each do |item|
|
|
81
|
+
= item
|
|
82
|
+
|
|
83
|
+
.flex.w-full{ id: 'desktop-navbar-toggle-wrapper', class: desktop_vertical_toggle_wrapper_classes }
|
|
84
|
+
%button#desktop-navbar-toggle-button{ class: desktop_vertical_toggle_button_classes, type: :button, aria: { controls: 'desktop-navbar', expanded: 'true', label: desktop_vertical_toggle_button_label(expanded: true) } }
|
|
85
|
+
%i#desktop-navbar-toggle-icon{ class: desktop_vertical_toggle_icon_classes(expanded: true), aria: { hidden: true } }
|
|
86
|
+
|
|
87
|
+
#mobile-menu{ class: mobile_menu_classes }
|
|
88
|
+
.flex.flex-1.flex-col
|
|
89
|
+
.flex-1
|
|
90
|
+
- if @left_items.present?
|
|
91
|
+
%ul.flex.flex-col.space-y-1.pt-4
|
|
92
|
+
- @left_items.each do |item|
|
|
93
|
+
= item
|
|
94
|
+
|
|
95
|
+
- if @right_items.present?
|
|
96
|
+
%ul.flex.flex-col.space-y-1.pt-4
|
|
97
|
+
- @right_items.each do |item|
|
|
98
|
+
= item
|
|
99
|
+
|
|
100
|
+
.pt-6
|
|
101
|
+
%button.w-full.inline-flex.items-center.justify-center.rounded-md.border.border-zinc-700.bg-zinc-900.px-4.py-3.text-sm.font-semibold.text-zinc-50.transition-colors.hover:bg-zinc-800.hover:text-zinc-50.focus:outline-none.focus-visible:ring-2.focus-visible:ring-zinc-400.focus-visible:ring-offset-2.focus-visible:ring-offset-zinc-950#mobile-menu-close-button{ role: :button }
|
|
102
|
+
Close
|
|
103
|
+
|
|
104
|
+
- if vertical?
|
|
105
|
+
:css
|
|
106
|
+
.tramway-navbar-desktop-vertical .tramway-navbar-item {
|
|
107
|
+
width: 100%;
|
|
51
108
|
}
|
|
52
109
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
if (hideImmediately) {
|
|
57
|
-
menu.classList.add(MENU_HIDDEN_CLASS);
|
|
58
|
-
return;
|
|
110
|
+
.tramway-navbar-desktop-vertical .tramway-navbar-item > a,
|
|
111
|
+
.tramway-navbar-desktop-vertical .tramway-navbar-item > form {
|
|
112
|
+
width: 100%;
|
|
59
113
|
}
|
|
60
114
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
() => menu.classList.add(MENU_HIDDEN_CLASS),
|
|
64
|
-
{ once: true }
|
|
65
|
-
);
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
const openMenu = (menu) => {
|
|
69
|
-
if (!menu) return;
|
|
70
|
-
|
|
71
|
-
menu.classList.remove(MENU_HIDDEN_CLASS);
|
|
72
|
-
requestAnimationFrame(() => {
|
|
73
|
-
menu.classList.remove(MENU_OFFSCREEN_CLASS);
|
|
74
|
-
root.classList.add(ROOT_LOCK_CLASS);
|
|
75
|
-
});
|
|
76
|
-
};
|
|
77
|
-
|
|
78
|
-
const bindNavbar = () => {
|
|
79
|
-
const button = document.getElementById('mobile-menu-button');
|
|
80
|
-
const closeButton = document.getElementById('mobile-menu-close-button');
|
|
81
|
-
const menu = document.getElementById('mobile-menu');
|
|
82
|
-
|
|
83
|
-
if (!button || !closeButton || !menu || button.dataset.navbarBound === 'true') {
|
|
84
|
-
return;
|
|
115
|
+
.tramway-navbar-desktop-vertical .tramway-navbar-item > form {
|
|
116
|
+
display: block;
|
|
85
117
|
}
|
|
86
118
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
return;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
closeMenu(menu);
|
|
96
|
-
};
|
|
97
|
-
|
|
98
|
-
button.addEventListener('click', toggleMenu);
|
|
99
|
-
closeButton.addEventListener('click', () => closeMenu(menu));
|
|
100
|
-
menu.querySelectorAll('a').forEach((link) => {
|
|
101
|
-
link.addEventListener('click', () => closeMenu(menu, { hideImmediately: true }));
|
|
102
|
-
});
|
|
103
|
-
};
|
|
104
|
-
|
|
105
|
-
if (window.__tramwayNavbarGlobalBound !== true) {
|
|
106
|
-
window.__tramwayNavbarGlobalBound = true;
|
|
107
|
-
|
|
108
|
-
document.addEventListener('turbo:before-cache', () => {
|
|
109
|
-
closeMenu(document.getElementById('mobile-menu'), { hideImmediately: true });
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
document.addEventListener('turbo:load', () => {
|
|
113
|
-
const menu = document.getElementById('mobile-menu');
|
|
114
|
-
bindNavbar();
|
|
115
|
-
|
|
116
|
-
if (!menu || menu.classList.contains(MENU_HIDDEN_CLASS)) {
|
|
117
|
-
root.classList.remove(ROOT_LOCK_CLASS);
|
|
118
|
-
}
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
window.addEventListener('resize', () => {
|
|
122
|
-
if (window.innerWidth >= 640) {
|
|
123
|
-
closeMenu(document.getElementById('mobile-menu'), { hideImmediately: true });
|
|
124
|
-
}
|
|
125
|
-
});
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
bindNavbar();
|
|
129
|
-
})();
|
|
119
|
+
.tramway-navbar-desktop-vertical .tramway-navbar-item > a,
|
|
120
|
+
.tramway-navbar-desktop-vertical .tramway-navbar-item > form > button {
|
|
121
|
+
width: 100%;
|
|
122
|
+
justify-content: flex-start;
|
|
123
|
+
}
|
|
@@ -3,10 +3,24 @@
|
|
|
3
3
|
module Tramway
|
|
4
4
|
# Navbar component
|
|
5
5
|
class NavbarComponent < TailwindComponent
|
|
6
|
-
|
|
6
|
+
NAVBAR_BASE_CLASSES = %w[
|
|
7
|
+
bg-zinc-950 text-zinc-50
|
|
8
|
+
].freeze
|
|
9
|
+
|
|
10
|
+
HORIZONTAL_NAVBAR_CLASSES = %w[
|
|
7
11
|
flex items-center justify-between border-zinc-800 px-4 py-3 shadow-sm backdrop-blur sm:px-6
|
|
8
12
|
].freeze
|
|
9
13
|
|
|
14
|
+
VERTICAL_NAVBAR_CLASSES = %w[
|
|
15
|
+
flex flex-col border-zinc-800 px-4 py-3 shadow-sm backdrop-blur sm:px-6
|
|
16
|
+
].freeze
|
|
17
|
+
|
|
18
|
+
VERTICAL_DESKTOP_NAVBAR_CLASSES = %w[
|
|
19
|
+
md:fixed md:left-0 md:top-0 md:z-40 md:h-dvh md:w-72 md:flex-col md:border-r md:border-zinc-800
|
|
20
|
+
md:bg-zinc-950 md:px-4 md:py-3 md:shadow-sm md:backdrop-blur md:overflow-hidden md:transition-[width]
|
|
21
|
+
md:duration-300 md:ease-in-out-strong md:motion-reduce:duration-0
|
|
22
|
+
].freeze
|
|
23
|
+
|
|
10
24
|
MOBILE_BUTTON_CLASSES = %w[
|
|
11
25
|
inline-flex items-center justify-center rounded-md p-2 transition-colors hover:bg-zinc-800 hover:text-zinc-50
|
|
12
26
|
focus:outline-none focus-visible:ring-2 focus-visible:ring-zinc-400 focus-visible:ring-offset-2
|
|
@@ -22,10 +36,15 @@ module Tramway
|
|
|
22
36
|
@title = { text: options[:title], link: options[:title_link] || '/' }
|
|
23
37
|
@left_items = options[:left_items]
|
|
24
38
|
@right_items = options[:right_items]
|
|
39
|
+
@direction = options[:direction].presence&.to_sym || :vertical
|
|
25
40
|
end
|
|
26
41
|
|
|
27
42
|
def navbar_classes
|
|
28
|
-
|
|
43
|
+
classes = NAVBAR_BASE_CLASSES.dup
|
|
44
|
+
classes.concat(HORIZONTAL_NAVBAR_CLASSES) if horizontal?
|
|
45
|
+
classes.concat(VERTICAL_NAVBAR_CLASSES + VERTICAL_DESKTOP_NAVBAR_CLASSES) if vertical?
|
|
46
|
+
|
|
47
|
+
classes.join(' ')
|
|
29
48
|
end
|
|
30
49
|
|
|
31
50
|
def title_classes
|
|
@@ -39,5 +58,68 @@ module Tramway
|
|
|
39
58
|
def mobile_menu_classes
|
|
40
59
|
MOBILE_MENU_CLASSES.join(' ')
|
|
41
60
|
end
|
|
61
|
+
|
|
62
|
+
def mobile_horizontal_container_classes
|
|
63
|
+
"#{HORIZONTAL_NAVBAR_CLASSES.join(' ')} md:hidden"
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def horizontal?
|
|
67
|
+
@direction == :horizontal
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def vertical?
|
|
71
|
+
@direction == :vertical
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def direction
|
|
75
|
+
@direction.to_s
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def desktop_vertical_container_classes
|
|
79
|
+
'hidden md:flex tramway-navbar-desktop-vertical h-full w-full flex-col overflow-hidden'
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def desktop_vertical_header_classes
|
|
83
|
+
[
|
|
84
|
+
'tramway-navbar-desktop-vertical-header flex w-full items-center justify-between overflow-hidden',
|
|
85
|
+
'transition-opacity duration-300 ease-out-strong'
|
|
86
|
+
].join(' ')
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def desktop_vertical_content_classes
|
|
90
|
+
[
|
|
91
|
+
'tramway-navbar-desktop-vertical-content flex w-full flex-1 flex-col gap-3 overflow-y-auto overflow-hidden',
|
|
92
|
+
'transition-opacity duration-300 ease-out-strong'
|
|
93
|
+
].join(' ')
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def desktop_vertical_item_list_classes
|
|
97
|
+
'tramway-navbar-desktop-vertical-list mt-8 flex w-full flex-col gap-1'
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def desktop_vertical_toggle_wrapper_classes(expanded: true)
|
|
101
|
+
base = 'mt-auto flex w-full'
|
|
102
|
+
expanded ? "#{base} justify-end" : "#{base} justify-center"
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def desktop_vertical_toggle_button_classes
|
|
106
|
+
[
|
|
107
|
+
'inline-flex cursor-pointer items-center justify-center rounded-md px-4 py-3 transition-colors',
|
|
108
|
+
'hover:bg-zinc-800 hover:text-zinc-50',
|
|
109
|
+
'focus:outline-none focus-visible:ring-2 focus-visible:ring-zinc-400 focus-visible:ring-offset-2',
|
|
110
|
+
'focus-visible:ring-offset-zinc-950 w-fit'
|
|
111
|
+
].join(' ')
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def desktop_vertical_toggle_button_label(expanded:)
|
|
115
|
+
expanded ? 'Collapse sidebar' : 'Expand sidebar'
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def desktop_vertical_toggle_icon_classes(expanded:)
|
|
119
|
+
classes = %w[fa fa-chevron-left transition-transform duration-200 ease-in-out-strong motion-reduce:duration-0]
|
|
120
|
+
classes << 'rotate-180' unless expanded
|
|
121
|
+
|
|
122
|
+
classes.join(' ')
|
|
123
|
+
end
|
|
42
124
|
end
|
|
43
125
|
end
|
|
@@ -4,10 +4,18 @@ module Tramway
|
|
|
4
4
|
module Table
|
|
5
5
|
# Component for rendering a cell in a table
|
|
6
6
|
class CellComponent < Tramway::BaseComponent
|
|
7
|
+
SIZE_CLASSES = {
|
|
8
|
+
small: 'div-table-cell hidden bg-transparent px-4 py-2 text-sm font-medium text-zinc-100 first:block md:block',
|
|
9
|
+
medium: 'div-table-cell hidden bg-transparent px-6 py-4 text-base font-medium ' \
|
|
10
|
+
'text-zinc-100 first:block md:block',
|
|
11
|
+
large: 'div-table-cell hidden bg-transparent px-6 py-6 text-lg font-medium ' \
|
|
12
|
+
'text-zinc-100 first:block md:block'
|
|
13
|
+
}.freeze
|
|
14
|
+
|
|
7
15
|
option :options, optional: true, default: -> { {} }
|
|
8
16
|
|
|
9
17
|
def cell_classes
|
|
10
|
-
|
|
18
|
+
SIZE_CLASSES.fetch(tramway_table_size) { SIZE_CLASSES[:medium] }
|
|
11
19
|
end
|
|
12
20
|
end
|
|
13
21
|
end
|
|
@@ -6,6 +6,21 @@ module Tramway
|
|
|
6
6
|
class HeaderComponent < Tramway::BaseComponent
|
|
7
7
|
include ContentCells
|
|
8
8
|
|
|
9
|
+
SIZE_CLASSES = {
|
|
10
|
+
small: {
|
|
11
|
+
row: 'div-table-row grid grid-cols-1 gap-2 rounded-t-xl border-b border-zinc-800 bg-zinc-900 text-zinc-400',
|
|
12
|
+
cell: 'div-table-cell hidden px-4 py-2 first:block md:block'
|
|
13
|
+
},
|
|
14
|
+
medium: {
|
|
15
|
+
row: 'div-table-row grid grid-cols-1 gap-4 rounded-t-xl border-b border-zinc-800 bg-zinc-900 text-zinc-400',
|
|
16
|
+
cell: 'div-table-cell hidden px-6 py-4 first:block md:block'
|
|
17
|
+
},
|
|
18
|
+
large: {
|
|
19
|
+
row: 'div-table-row grid grid-cols-1 gap-6 rounded-t-xl border-b border-zinc-800 bg-zinc-900 text-zinc-400',
|
|
20
|
+
cell: 'div-table-cell hidden px-6 py-6 first:block md:block'
|
|
21
|
+
}
|
|
22
|
+
}.freeze
|
|
23
|
+
|
|
9
24
|
option :headers, optional: true, default: -> { [] }
|
|
10
25
|
option :columns, optional: true, default: -> { 3 }
|
|
11
26
|
option :options, optional: true, default: -> { {} }
|
|
@@ -19,11 +34,17 @@ module Tramway
|
|
|
19
34
|
end
|
|
20
35
|
|
|
21
36
|
def header_row_classes
|
|
22
|
-
|
|
37
|
+
size_classes.fetch(:row)
|
|
23
38
|
end
|
|
24
39
|
|
|
25
40
|
def header_cell_classes
|
|
26
|
-
|
|
41
|
+
size_classes.fetch(:cell)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def size_classes
|
|
47
|
+
SIZE_CLASSES.fetch(tramway_table_size) { SIZE_CLASSES[:medium] }
|
|
27
48
|
end
|
|
28
49
|
end
|
|
29
50
|
end
|