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.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +76 -16
  3. data/app/assets/builds/tailwind.css +2 -0
  4. data/app/assets/javascripts/tramway/tramway.js +174 -1
  5. data/app/assets/stylesheets/font-awesome.css +2337 -0
  6. data/app/assets/tailwind/application.css +29 -0
  7. data/app/components/tramway/chat_component.html.haml +1 -1
  8. data/app/components/tramway/containers/main_component.rb +1 -3
  9. data/app/components/tramway/containers/narrow_component.rb +5 -4
  10. data/app/components/tramway/form/builder.rb +1 -2
  11. data/app/components/tramway/grid/card_component.html.haml +2 -0
  12. data/app/components/tramway/grid/card_component.rb +37 -0
  13. data/app/components/tramway/grid_component.html.haml +18 -0
  14. data/app/components/tramway/grid_component.rb +76 -0
  15. data/app/components/tramway/native_text_component.rb +1 -2
  16. data/app/components/tramway/nav/item_component.rb +4 -2
  17. data/app/components/tramway/navbar_component.html.haml +104 -110
  18. data/app/components/tramway/navbar_component.rb +84 -2
  19. data/app/components/tramway/table/cell_component.rb +9 -1
  20. data/app/components/tramway/table/header_component.rb +23 -2
  21. data/app/components/tramway/table/row_component.rb +27 -10
  22. data/app/components/tramway/table_component.rb +28 -1
  23. data/app/controllers/tramway/entities_controller.rb +6 -6
  24. data/app/views/tramway/layouts/application.html.haml +8 -6
  25. data/config/routes.rb +1 -2
  26. data/config/tailwind.config.js +57 -0
  27. data/lib/generators/tramway/install/font_awesome_helpers.rb +39 -0
  28. data/lib/generators/tramway/install/install_generator.rb +131 -65
  29. data/lib/generators/tramway/install/layout_helpers.rb +95 -0
  30. data/lib/tramway/engine.rb +12 -0
  31. data/lib/tramway/forms/validations.rb +1 -2
  32. data/lib/tramway/helpers/navbar_helper.rb +3 -1
  33. data/lib/tramway/helpers/table_helper.rb +27 -0
  34. data/lib/tramway/helpers/views_helper.rb +26 -2
  35. data/lib/tramway/version.rb +1 -1
  36. 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: "bg-zinc-950 text-zinc-50 shadow-inner#{options_classes}"
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: 'container p-4 flex align-center justify-center w-full mx-auto ' \
15
- 'shadow-inner rounded-xl bg-zinc-950 text-zinc-50' + options_classes
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,2 @@
1
+ %article{ class: card_classes, role: :gridcell, aria: { label: 'Dashboard card', rowspan: normalized_size.first, colspan: normalized_size.last }, style: card_style, **options.except(:class) }
2
+ = content
@@ -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 ||= 'inline-flex items-center rounded-md px-4 py-3 text-base font-medium text-zinc-100 ' \
10
- 'transition-colors hover:bg-zinc-800 hover:text-zinc-50 sm:px-3 sm:py-2 sm:text-sm'
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
- %nav.bg-zinc-950.text-zinc-50{ class: navbar_classes }
2
- .flex.md:justify-between.w-full.items-center.relative.min-h-8
3
- .block.sm:hidden
4
- %button#mobile-menu-button{ class: mobile_button_classes }
5
- ☰
6
-
7
- - if @title[:text].present? || @left_items.present?
8
- .flex.items-center.absolute.transform{ class: 'left-1/2 -translate-x-1/2 md:left-auto md:translate-x-0' }
9
- - if @title[:text].present?
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-row.items-center.space-x-4.ml-4.hidden.md:flex
28
+ %ul.flex.flex-col.space-y-1.pt-4
15
29
  - @left_items.each do |item|
16
30
  = item
17
31
 
18
- - if @right_items.present?
19
- %ul.items-center.space-x-4.hidden.md:flex
20
- - @right_items.each do |item|
21
- = item
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
- #mobile-menu{ class: mobile_menu_classes }
24
- .flex.flex-1.flex-col
25
- .flex-1
26
- - if @left_items.present?
27
- %ul.flex.flex-col.space-y-1.pt-4
28
- - @left_items.each do |item|
29
- = item
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.flex.flex-col.space-y-1.pt-4
59
+ %ul.items-center.space-x-4.hidden.md:flex
33
60
  - @right_items.each do |item|
34
61
  = item
35
62
 
36
- .pt-6
37
- %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 }
38
- Close
39
-
40
- :javascript
41
- (() => {
42
- const root = document.documentElement;
43
- const MENU_HIDDEN_CLASS = 'hidden';
44
- const MENU_OFFSCREEN_CLASS = '-translate-x-full';
45
- const ROOT_LOCK_CLASS = 'overflow-hidden';
46
-
47
- const closeMenu = (menu, { hideImmediately = false } = {}) => {
48
- if (!menu) {
49
- root.classList.remove(ROOT_LOCK_CLASS);
50
- return;
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
- menu.classList.add(MENU_OFFSCREEN_CLASS);
54
- root.classList.remove(ROOT_LOCK_CLASS);
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
- menu.addEventListener(
62
- 'transitionend',
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
- button.dataset.navbarBound = 'true';
88
-
89
- const toggleMenu = () => {
90
- if (menu.classList.contains(MENU_HIDDEN_CLASS)) {
91
- openMenu(menu);
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
- NAVBAR_CLASSES = %w[
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
- NAVBAR_CLASSES.join(' ')
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
- 'div-table-cell hidden px-6 py-4 text-base font-medium text-zinc-100 first:block md:block'
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
- 'div-table-row grid grid-cols-1 gap-4 rounded-t-xl border-b border-zinc-800 bg-zinc-900 text-zinc-400'
37
+ size_classes.fetch(:row)
23
38
  end
24
39
 
25
40
  def header_cell_classes
26
- 'div-table-cell hidden px-6 py-4 first:block md:block'
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