tramway 2.2.7 → 2.3.1

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 (107) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +54 -4
  3. data/app/components/{tailwinds → tramway}/back_button_component.rb +1 -1
  4. data/app/components/{tailwinds → tramway}/badge_component.rb +4 -2
  5. data/app/components/{tailwinds → tramway}/button_component.rb +3 -1
  6. data/app/components/tramway/chat_component.html.haml +77 -0
  7. data/app/components/tramway/chat_component.rb +17 -0
  8. data/app/components/tramway/chats/message_component.html.haml +26 -0
  9. data/app/components/tramway/chats/message_component.rb +58 -0
  10. data/app/components/tramway/chats/messages/container_component.html.haml +9 -0
  11. data/app/components/tramway/chats/messages/container_component.rb +32 -0
  12. data/app/components/tramway/chats/messages/table_component.html.haml +28 -0
  13. data/app/components/tramway/chats/messages/table_component.rb +12 -0
  14. data/app/components/tramway/chats.rb +7 -0
  15. data/app/components/{tailwinds/base_component.rb → tramway/colors_methods.rb} +3 -3
  16. data/app/components/{tailwinds → tramway}/containers/main_component.rb +1 -1
  17. data/app/components/{tailwinds → tramway}/containers/narrow_component.rb +1 -1
  18. data/app/components/{tailwinds → tramway}/flash_component.rb +5 -3
  19. data/app/components/{tailwinds → tramway}/form/builder.rb +8 -7
  20. data/app/components/{tailwinds → tramway}/form/checkbox_component.html.haml +1 -1
  21. data/app/components/{tailwinds → tramway}/form/checkbox_component.rb +1 -1
  22. data/app/components/{tailwinds → tramway}/form/date_field_component.html.haml +1 -1
  23. data/app/components/{tailwinds → tramway}/form/date_field_component.rb +1 -1
  24. data/app/components/{tailwinds → tramway}/form/datetime_field_component.html.haml +1 -1
  25. data/app/components/{tailwinds → tramway}/form/datetime_field_component.rb +1 -1
  26. data/app/components/{tailwinds → tramway}/form/file_field_component.rb +1 -1
  27. data/app/components/{tailwinds → tramway}/form/label_component.rb +1 -1
  28. data/app/components/{tailwinds → tramway}/form/multiselect/caret_component.rb +1 -1
  29. data/app/components/{tailwinds → tramway}/form/multiselect/dropdown_container_component.rb +1 -1
  30. data/app/components/{tailwinds → tramway}/form/multiselect/item_container_component.rb +1 -1
  31. data/app/components/{tailwinds → tramway}/form/multiselect/select_as_input_component.rb +1 -1
  32. data/app/components/{tailwinds → tramway}/form/multiselect/selected_item_template_component.rb +1 -1
  33. data/app/components/{tailwinds → tramway}/form/multiselect_component.html.haml +3 -3
  34. data/app/components/{tailwinds → tramway}/form/multiselect_component.rb +5 -5
  35. data/app/components/{tailwinds → tramway}/form/number_field_component.html.haml +1 -1
  36. data/app/components/{tailwinds → tramway}/form/number_field_component.rb +1 -1
  37. data/app/components/{tailwinds → tramway}/form/select_component.html.haml +1 -1
  38. data/app/components/{tailwinds → tramway}/form/select_component.rb +1 -1
  39. data/app/components/{tailwinds → tramway}/form/text_area_component.html.haml +1 -1
  40. data/app/components/{tailwinds → tramway}/form/text_area_component.rb +1 -1
  41. data/app/components/{tailwinds → tramway}/form/text_field_component.html.haml +1 -1
  42. data/app/components/{tailwinds → tramway}/form/text_field_component.rb +1 -1
  43. data/app/components/tramway/native_text_component.html.haml +2 -0
  44. data/app/components/tramway/native_text_component.rb +148 -0
  45. data/app/components/{tailwinds → tramway}/nav/item/button_component.rb +2 -2
  46. data/app/components/{tailwinds → tramway}/nav/item/link_component.rb +2 -2
  47. data/app/components/{tailwinds → tramway}/nav/item_component.rb +1 -1
  48. data/app/components/{tailwinds → tramway}/navbar_component.rb +1 -1
  49. data/app/components/{tailwinds → tramway}/pagination/base.rb +1 -1
  50. data/app/components/{tailwinds → tramway}/pagination/first_page_component.rb +2 -2
  51. data/app/components/{tailwinds → tramway}/pagination/gap_component.rb +1 -1
  52. data/app/components/{tailwinds → tramway}/pagination/last_page_component.rb +2 -2
  53. data/app/components/{tailwinds → tramway}/pagination/next_page_component.rb +2 -2
  54. data/app/components/{tailwinds → tramway}/pagination/page_component.rb +2 -2
  55. data/app/components/{tailwinds → tramway}/pagination/prev_page_component.rb +2 -2
  56. data/app/components/{tailwinds → tramway}/table/cell_component.rb +1 -1
  57. data/app/components/{tailwinds → tramway}/table/header_component.rb +1 -1
  58. data/app/components/{tailwinds → tramway}/table/row/preview_component.rb +1 -1
  59. data/app/components/{tailwinds → tramway}/table/row_component.rb +1 -1
  60. data/app/components/{tailwinds → tramway}/table_component.html.haml +1 -1
  61. data/app/components/{tailwinds → tramway}/table_component.rb +1 -1
  62. data/app/components/{tailwinds → tramway}/title_component.rb +1 -1
  63. data/app/views/kaminari/_first_page.html.haml +1 -1
  64. data/app/views/kaminari/_gap.html.haml +1 -1
  65. data/app/views/kaminari/_last_page.html.haml +1 -1
  66. data/app/views/kaminari/_next_page.html.haml +1 -1
  67. data/app/views/kaminari/_page.html.haml +1 -1
  68. data/app/views/kaminari/_prev_page.html.haml +1 -1
  69. data/app/views/tramway/chats/_message.html.haml +4 -0
  70. data/config/locales/en.yml +4 -0
  71. data/config/tailwind.config.js +81 -1
  72. data/docs/AGENTS.md +21 -0
  73. data/lib/tramway/chats/broadcast.rb +19 -0
  74. data/lib/tramway/engine.rb +11 -0
  75. data/lib/tramway/helpers/navbar_helper.rb +1 -1
  76. data/lib/tramway/helpers/views_helper.rb +27 -15
  77. data/lib/tramway/navbar.rb +4 -4
  78. data/lib/tramway/version.rb +1 -1
  79. data/lib/tramway.rb +1 -0
  80. metadata +90 -77
  81. /data/app/components/{tailwinds → tramway}/back_button_component.html.haml +0 -0
  82. /data/app/components/{tailwinds → tramway}/badge_component.html.haml +0 -0
  83. /data/app/components/{tailwinds → tramway}/button_component.html.haml +0 -0
  84. /data/app/components/{tailwinds → tramway}/containers/main_component.html.haml +0 -0
  85. /data/app/components/{tailwinds → tramway}/containers/narrow_component.html.haml +0 -0
  86. /data/app/components/{tailwinds → tramway}/flash_component.html.haml +0 -0
  87. /data/app/components/{tailwinds → tramway}/form/file_field_component.html.haml +0 -0
  88. /data/app/components/{tailwinds → tramway}/form/label_component.html.haml +0 -0
  89. /data/app/components/{tailwinds → tramway}/form/multiselect/caret_component.html.haml +0 -0
  90. /data/app/components/{tailwinds → tramway}/form/multiselect/dropdown_container_component.html.haml +0 -0
  91. /data/app/components/{tailwinds → tramway}/form/multiselect/item_container_component.html.haml +0 -0
  92. /data/app/components/{tailwinds → tramway}/form/multiselect/select_as_input_component.html.haml +0 -0
  93. /data/app/components/{tailwinds → tramway}/form/multiselect/selected_item_template_component.html.haml +0 -0
  94. /data/app/components/{tailwinds → tramway}/nav/item/button_component.html.haml +0 -0
  95. /data/app/components/{tailwinds → tramway}/nav/item/link_component.html.haml +0 -0
  96. /data/app/components/{tailwinds → tramway}/navbar_component.html.haml +0 -0
  97. /data/app/components/{tailwinds → tramway}/pagination/first_page_component.html.haml +0 -0
  98. /data/app/components/{tailwinds → tramway}/pagination/gap_component.html.haml +0 -0
  99. /data/app/components/{tailwinds → tramway}/pagination/last_page_component.html.haml +0 -0
  100. /data/app/components/{tailwinds → tramway}/pagination/next_page_component.html.haml +0 -0
  101. /data/app/components/{tailwinds → tramway}/pagination/page_component.html.haml +0 -0
  102. /data/app/components/{tailwinds → tramway}/pagination/prev_page_component.html.haml +0 -0
  103. /data/app/components/{tailwinds → tramway}/table/cell_component.html.haml +0 -0
  104. /data/app/components/{tailwinds → tramway}/table/header_component.html.haml +0 -0
  105. /data/app/components/{tailwinds → tramway}/table/row/preview_component.html.haml +0 -0
  106. /data/app/components/{tailwinds → tramway}/table/row_component.html.haml +0 -0
  107. /data/app/components/{tailwinds → tramway}/title_component.html.haml +0 -0
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Tailwinds
3
+ module Tramway
4
4
  module Form
5
5
  module Multiselect
6
6
  # Tailwind-styled multi-select field
@@ -1,6 +1,6 @@
1
1
  .mb-4.relative
2
2
  - if @label
3
- = component('tailwinds/form/label', for: @for) do
3
+ = component('tramway/form/label', for: @for) do
4
4
  = @label
5
5
  %div{ role: :combobox, data: multiselect_hash, id: "#{@for}_multiselect" }
6
6
  - classes = "#{size_class(:multiselect_input)} #{select_base_classes}"
@@ -8,6 +8,6 @@
8
8
  .flex.flex-row.flex-nowrap.overflow-x-auto.space-x-1{ data: { "multiselect-target" => "showSelectedArea" } }
9
9
  .flex.flex-col.justify-center
10
10
  .caret-down{ data: { "multiselect-target" => "caretDown" } }
11
- = component 'tailwinds/form/multiselect/caret', size: size, direction: :down
11
+ = component 'tramway/form/multiselect/caret', size: size, direction: :down
12
12
  .caret-up.hidden{ data: { "multiselect-target" => "caretUp" } }
13
- = component 'tailwinds/form/multiselect/caret', size: size, direction: :up
13
+ = component 'tramway/form/multiselect/caret', size: size, direction: :up
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Tailwinds
3
+ module Tramway
4
4
  module Form
5
5
  # Tailwind-styled multi-select field
6
6
  class MultiselectComponent < TailwindComponent
@@ -73,7 +73,7 @@ module Tailwinds
73
73
 
74
74
  def select_as_input
75
75
  component(
76
- 'tailwinds/form/multiselect/select_as_input',
76
+ 'tramway/form/multiselect/select_as_input',
77
77
  options:,
78
78
  attribute:,
79
79
  input:,
@@ -96,15 +96,15 @@ module Tailwinds
96
96
  end
97
97
 
98
98
  def selected_item_template
99
- component('tailwinds/form/multiselect/selected_item_template', size:)
99
+ component('tramway/form/multiselect/selected_item_template', size:)
100
100
  end
101
101
 
102
102
  def dropdown_container
103
- component('tailwinds/form/multiselect/dropdown_container', size:)
103
+ component('tramway/form/multiselect/dropdown_container', size:)
104
104
  end
105
105
 
106
106
  def item_container
107
- component('tailwinds/form/multiselect/item_container', size:)
107
+ component('tramway/form/multiselect/item_container', size:)
108
108
  end
109
109
  end
110
110
  end
@@ -1,6 +1,6 @@
1
1
  .mb-4
2
2
  - if @label
3
- = component('tailwinds/form/label', for: @for) do
3
+ = component('tramway/form/label', for: @for) do
4
4
  = @label
5
5
  - classes = "#{size_class(:text_input)} #{text_input_base_classes}"
6
6
  = @input.call @attribute, **@options.merge(class: classes), value: @value
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Tailwinds
3
+ module Tramway
4
4
  module Form
5
5
  # Tailwind-styled numeric field
6
6
  class NumberFieldComponent < TailwindComponent
@@ -1,6 +1,6 @@
1
1
  .mb-4
2
2
  - if @label
3
- = component('tailwinds/form/label', for: @for) do
3
+ = component('tramway/form/label', for: @for) do
4
4
  = @label
5
5
  - classes = "#{size_class(:select_input)} #{select_base_classes}"
6
6
  = @input.call(@attribute, @collection, { selected: @value }, @options.merge(class: classes))
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Tailwinds
3
+ module Tramway
4
4
  module Form
5
5
  # Tailwind-styled select field
6
6
  class SelectComponent < TailwindComponent
@@ -1,6 +1,6 @@
1
1
  .mb-4
2
2
  - if @label
3
- = component('tailwinds/form/label', for: @for) do
3
+ = component('tramway/form/label', for: @for) do
4
4
  = @label
5
5
  - classes = "#{size_class(:text_input)} #{text_input_base_classes}"
6
6
  = @input.call @attribute, **@options.merge(class: classes), value: @value
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Tailwinds
3
+ module Tramway
4
4
  module Form
5
5
  # Tailwind-styled text field
6
6
  class TextAreaComponent < TailwindComponent
@@ -1,6 +1,6 @@
1
1
  .mb-4
2
2
  - if @label
3
- = component('tailwinds/form/label', for: @for) do
3
+ = component('tramway/form/label', for: @for) do
4
4
  = @label
5
5
  - classes = "#{size_class(:text_input)} #{text_input_base_classes}"
6
6
  = @input.call @attribute, **@options.merge(class: classes), value: @value
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Tailwinds
3
+ module Tramway
4
4
  module Form
5
5
  # Tailwind-styled text field
6
6
  class TextFieldComponent < TailwindComponent
@@ -0,0 +1,2 @@
1
+ %div
2
+ = rendered_html
@@ -0,0 +1,148 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'erb'
4
+
5
+ module Tramway
6
+ # Displays text with size-based utility classes.
7
+ class NativeTextComponent < Tramway::BaseComponent
8
+ URL_REGEX = %r{https?://[^\s<]+}
9
+ MAX_URL_LENGTH = 41
10
+ HEADER_REGEX = /\A(#{Regexp.escape('#')}{1,6})\s+(.+)\z/
11
+ LIST_ITEM_REGEX = /\A[-*]\s+(.+)\z/
12
+ HEADER_CLASSES = {
13
+ 1 => 'text-4xl font-bold leading-tight mt-4 mb-2',
14
+ 2 => 'text-3xl font-bold leading-tight mt-4 mb-2',
15
+ 3 => 'text-2xl font-semibold leading-snug mt-3 mb-2',
16
+ 4 => 'text-xl font-semibold leading-snug mt-3 mb-2',
17
+ 5 => 'text-lg font-semibold leading-snug mt-2 mb-1',
18
+ 6 => 'text-base font-semibold leading-snug mt-2 mb-1'
19
+ }.freeze
20
+
21
+ option :text
22
+ option :size, optional: true, default: -> { :middle }
23
+ option :klass, optional: true, default: -> { '' }
24
+
25
+ def rendered_html
26
+ # Safe because this is an empty static string literal, not user-controlled content.
27
+ return ''.html_safe if text.blank?
28
+
29
+ helpers.safe_join(rendered_blocks)
30
+ end
31
+
32
+ def text_class
33
+ { small: 'text-sm', large: 'text-lg', middle: 'text-base' }.fetch(size, 'md:text-sm lg:text-base') + " #{klass}"
34
+ end
35
+
36
+ private
37
+
38
+ # rubocop:disable Metrics/AbcSize
39
+ # rubocop:disable Metrics/MethodLength
40
+ def rendered_blocks
41
+ blocks = []
42
+ list_items = []
43
+
44
+ text.split("\n").each do |line|
45
+ stripped_line = line.strip
46
+
47
+ if stripped_line.blank?
48
+ flush_list_items(blocks, list_items)
49
+ next
50
+ end
51
+
52
+ header_match = stripped_line.match(HEADER_REGEX)
53
+ if header_match
54
+ flush_list_items(blocks, list_items)
55
+ blocks << helpers.content_tag(
56
+ "h#{header_match[1].length}",
57
+ render_inline_markdown(header_match[2]),
58
+ class: header_class(header_match[1].length)
59
+ )
60
+ next
61
+ end
62
+
63
+ list_match = stripped_line.match(LIST_ITEM_REGEX)
64
+ if list_match
65
+ list_items << helpers.content_tag(:li, render_inline_markdown(list_match[1]),
66
+ class: "#{text_class} marker:hidden")
67
+ next
68
+ end
69
+
70
+ flush_list_items(blocks, list_items)
71
+ blocks << helpers.content_tag(:p, render_inline_markdown(stripped_line), class: "#{text_class} my-1")
72
+ end
73
+
74
+ flush_list_items(blocks, list_items)
75
+ blocks
76
+ end
77
+ # rubocop:enable Metrics/AbcSize
78
+ # rubocop:enable Metrics/MethodLength
79
+
80
+ def flush_list_items(blocks, list_items)
81
+ return if list_items.empty?
82
+
83
+ blocks << helpers.content_tag(:ul, helpers.safe_join(list_items), class: "list-none pl-5 my-2 #{klass}")
84
+ list_items.clear
85
+ end
86
+
87
+ def header_class(level)
88
+ "#{HEADER_CLASSES.fetch(level)} #{klass}".strip
89
+ end
90
+
91
+ def render_inline_markdown(content)
92
+ escaped_content = ERB::Util.html_escape(content)
93
+ with_bold = escaped_content.gsub(/\*\*(.+?)\*\*/, '<strong>\\1</strong>')
94
+ with_italics = with_bold.gsub(/(?<!\*)\*(?!\*)(.+?)(?<!\*)\*(?!\*)/, '<em>\\1</em>')
95
+ with_underscored_italics = with_italics.gsub(/(?<!_)_(?!_)(.+?)(?<!_)_(?!_)/, '<em>\\1</em>')
96
+ # Safe because user input has already been escaped above and only controlled tags are introduced.
97
+ # rubocop:disable Rails/OutputSafety
98
+ linkified(with_underscored_italics).html_safe
99
+ end
100
+
101
+ # rubocop:disable Metrics/MethodLength
102
+ # rubocop:disable Metrics/AbcSize
103
+ def linkified(content)
104
+ fragments = []
105
+ current_index = 0
106
+
107
+ content.to_enum(:scan, URL_REGEX).map do
108
+ match = Regexp.last_match
109
+ matched_url = match[0]
110
+ url, trailing = strip_trailing_punctuation(matched_url)
111
+
112
+ # Safe because this fragment is sliced from `content`, which was already HTML-escaped.
113
+
114
+ fragments << content[current_index...match.begin(0)].html_safe # rubocop:disable Rails/OutputSafety
115
+ fragments << helpers.link_to(
116
+ shorten(url),
117
+ url,
118
+ target: '_blank',
119
+ rel: 'noopener noreferrer',
120
+ class: 'text-blue-400 hover:underline'
121
+ )
122
+ # Safe because `trailing` can only contain stripped URL punctuation (.,!?;:).
123
+ fragments << trailing.html_safe if trailing.present? # rubocop:disable Rails/OutputSafety
124
+ current_index = match.end(0)
125
+ end
126
+
127
+ # Safe because this tail fragment also comes from the already escaped `content` string.
128
+ fragments << content[current_index..].html_safe if current_index < content.length
129
+ # rubocop:enable Rails/OutputSafety
130
+
131
+ helpers.safe_join(fragments)
132
+ end
133
+ # rubocop:enable Metrics/AbcSize
134
+ # rubocop:enable Metrics/MethodLength
135
+
136
+ def strip_trailing_punctuation(url)
137
+ stripped_url = url.sub(/[.,!?;:]+\z/, '')
138
+ trailing = url.delete_prefix(stripped_url)
139
+ [stripped_url, trailing]
140
+ end
141
+
142
+ def shorten(url)
143
+ return url if url.length <= MAX_URL_LENGTH
144
+
145
+ "#{url[0...MAX_URL_LENGTH]}..."
146
+ end
147
+ end
148
+ end
@@ -2,12 +2,12 @@
2
2
 
3
3
  require 'rules/turbo_html_attributes_rules'
4
4
 
5
- module Tailwinds
5
+ module Tramway
6
6
  module Nav
7
7
  module Item
8
8
  # Render button styled with Tailwind using button_to methods
9
9
  #
10
- class ButtonComponent < Tailwinds::Nav::ItemComponent
10
+ class ButtonComponent < Tramway::Nav::ItemComponent
11
11
  def initialize(**options)
12
12
  @href = options[:href]
13
13
  @method = options[:method]
@@ -2,12 +2,12 @@
2
2
 
3
3
  require 'rules/turbo_html_attributes_rules'
4
4
 
5
- module Tailwinds
5
+ module Tramway
6
6
  module Nav
7
7
  module Item
8
8
  # Render button styled with Tailwind using link_to methods
9
9
  #
10
- class LinkComponent < Tailwinds::Nav::ItemComponent
10
+ class LinkComponent < Tramway::Nav::ItemComponent
11
11
  def initialize(**options)
12
12
  @href = options[:href]
13
13
  @options = Rules::TurboHtmlAttributesRules.prepare_turbo_html_attributes(options:)
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Tailwinds
3
+ module Tramway
4
4
  module Nav
5
5
  # Base class for all Nav::ItemComponent classes
6
6
  #
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Tailwinds
3
+ module Tramway
4
4
  # Navbar component
5
5
  class NavbarComponent < TailwindComponent
6
6
  def initialize(**options)
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Tailwinds
3
+ module Tramway
4
4
  module Pagination
5
5
  # Base component for rendering a Kaminari pagination
6
6
  class Base < Tramway::BaseComponent
@@ -1,9 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Tailwinds
3
+ module Tramway
4
4
  module Pagination
5
5
  # Kaminari first page component for rendering a first page button in a pagination
6
- class FirstPageComponent < Tailwinds::Pagination::Base
6
+ class FirstPageComponent < Tramway::Pagination::Base
7
7
  end
8
8
  end
9
9
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Tailwinds
3
+ module Tramway
4
4
  module Pagination
5
5
  # Kaminari gap component for rendering a gap in a pagination
6
6
  class GapComponent < Tramway::BaseComponent
@@ -1,9 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Tailwinds
3
+ module Tramway
4
4
  module Pagination
5
5
  # Kaminari next page component for rendering a last page button in a pagination
6
- class LastPageComponent < Tailwinds::Pagination::Base
6
+ class LastPageComponent < Tramway::Pagination::Base
7
7
  end
8
8
  end
9
9
  end
@@ -1,9 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Tailwinds
3
+ module Tramway
4
4
  module Pagination
5
5
  # Kaminari next page component for rendering a next page button in a pagination
6
- class NextPageComponent < Tailwinds::Pagination::Base
6
+ class NextPageComponent < Tramway::Pagination::Base
7
7
  end
8
8
  end
9
9
  end
@@ -1,9 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Tailwinds
3
+ module Tramway
4
4
  module Pagination
5
5
  # Kaminari page component for rendering a page button in a pagination
6
- class PageComponent < Tailwinds::Pagination::Base
6
+ class PageComponent < Tramway::Pagination::Base
7
7
  option :page
8
8
 
9
9
  def current_page_classes
@@ -1,9 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Tailwinds
3
+ module Tramway
4
4
  module Pagination
5
5
  # Kaminari prev page component for rendering a prev page button in a pagination
6
- class PrevPageComponent < Tailwinds::Pagination::Base
6
+ class PrevPageComponent < Tramway::Pagination::Base
7
7
  end
8
8
  end
9
9
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Tailwinds
3
+ module Tramway
4
4
  module Table
5
5
  # Component for rendering a cell in a table
6
6
  class CellComponent < Tramway::BaseComponent
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Tailwinds
3
+ module Tramway
4
4
  module Table
5
5
  # Component for rendering a header in a table
6
6
  class HeaderComponent < Tramway::BaseComponent
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Tailwinds
3
+ module Tramway
4
4
  module Table
5
5
  module Row
6
6
  # Row component for rendering a row in a table
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Tailwinds
3
+ module Tramway
4
4
  module Table
5
5
  # Component for rendering a row in a table
6
6
  class RowComponent < Tramway::BaseComponent
@@ -1,4 +1,4 @@
1
- = helpers.component 'tailwinds/table/row/preview'
1
+ = helpers.component 'tramway/table/row/preview'
2
2
 
3
3
  - width_class = options[:class]&.include?('w-') ? '' : 'w-full'
4
4
 
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Tailwinds
3
+ module Tramway
4
4
  # Table component for rendering a table
5
5
  class TableComponent < Tramway::BaseComponent
6
6
  option :options, optional: true, default: -> { {} }
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Tailwinds
3
+ module Tramway
4
4
  # Title component
5
5
  class TitleComponent < Tramway::BaseComponent
6
6
  option :text
@@ -1 +1 @@
1
- = component 'tailwinds/pagination/first_page', current_page:, url:, remote:
1
+ = component 'tramway/pagination/first_page', current_page:, url:, remote:
@@ -1 +1 @@
1
- = component 'tailwinds/pagination/gap'
1
+ = component 'tramway/pagination/gap'
@@ -1 +1 @@
1
- = component 'tailwinds/pagination/last_page', current_page:, url:, remote:
1
+ = component 'tramway/pagination/last_page', current_page:, url:, remote:
@@ -1 +1 @@
1
- = component 'tailwinds/pagination/next_page', current_page:, url:, remote:
1
+ = component 'tramway/pagination/next_page', current_page:, url:, remote:
@@ -1 +1 @@
1
- = component 'tailwinds/pagination/page', current_page:, url:, remote:, page:
1
+ = component 'tramway/pagination/page', current_page:, url:, remote:, page:
@@ -1 +1 @@
1
- = component 'tailwinds/pagination/prev_page', current_page:, url:, remote:
1
+ = component 'tramway/pagination/prev_page', current_page:, url:, remote:
@@ -0,0 +1,4 @@
1
+ = component 'tramway/chats/message',
2
+ type:,
3
+ text:,
4
+ sent_at:
@@ -15,3 +15,7 @@ en:
15
15
  created: "The record is created"
16
16
  updated: "The record is updated"
17
17
  deleted: "The record is deleted"
18
+ chat:
19
+ placeholders:
20
+ waiting: "Waiting for an answer..."
21
+ type: "Type your message..."
@@ -27,7 +27,87 @@ module.exports = {
27
27
 
28
28
  // === Entities Index Page ===
29
29
  'md:mt-8',
30
-
30
+
31
+ // === Chat components ===
32
+ 'mt-6',
33
+ 'mt-3',
34
+ 'max-h-full',
35
+ 'rounded-2xl',
36
+ 'rounded-tl-md',
37
+ 'rounded-tr-md',
38
+ 'bg-white',
39
+ 'bg-gray-50',
40
+ 'bg-blue-600',
41
+ 'shadow-sm',
42
+ 'border-t',
43
+ 'border-gray-700',
44
+ 'bg-gray-800',
45
+ 'bg-gray-800/60',
46
+ 'bg-gray-900',
47
+ 'bg-blue-500',
48
+ 'hover:bg-blue-400',
49
+ 'text-gray-100',
50
+ 'text-gray-400',
51
+ 'text-blue-400',
52
+ 'placeholder:text-gray-400',
53
+ 'focus:border-blue-400',
54
+ 'focus:ring-blue-500/30',
55
+ 'ring-gray-700',
56
+ 'h-8',
57
+ 'flex-1',
58
+ 'gap-2',
59
+ 'gap-3',
60
+ 'items-start',
61
+ 'items-end',
62
+ 'max-w-lg',
63
+ 'overflow-hidden',
64
+ 'overflow-y-auto',
65
+ 'my-4',
66
+ 'p-6',
67
+ 'pb-1',
68
+ 'py-3',
69
+ 'ring-1',
70
+ 'space-y-4',
71
+ 'text-right',
72
+ 'text-sm',
73
+ 'text-gray-900',
74
+ 'text-blue-100/90',
75
+ 'text-red-200',
76
+ 'underline',
77
+ 'break-all',
78
+ 'animate-spin',
79
+ 'placeholder:text-gray-400',
80
+ 'focus:border-blue-500',
81
+ 'focus:outline-none',
82
+ 'focus:ring-2',
83
+ 'focus:ring-blue-200',
84
+ 'disabled:bg-gray-100',
85
+ 'hover:bg-blue-500',
86
+ 'bg-blue-600',
87
+ 'bg-gray-800/60',
88
+ 'min-h-0',
89
+ 'min-w-0',
90
+ 'flex-1',
91
+ 'overflow-y-auto',
92
+ 'overflow-hidden',
93
+ 'shrink-0',
94
+ 'md:p-4',
95
+ 'md:p-6',
96
+ 'p-2',
97
+ 'md:gap-1',
98
+ 'md:space-y-4',
99
+ 'space-y-2',
100
+ 'md:pt-2',
101
+ 'md:rounded-xl',
102
+ 'rounded-t-xl',
103
+ 'md:rounded-full',
104
+ 'rounded-bl-2xl',
105
+ 'md:border',
106
+ 'md:gap-1',
107
+ 'hover:underline',
108
+ 'marker:hidden',
109
+ 'list-none',
110
+
31
111
  // === Custom table layout utilities ===
32
112
  'div-table',
33
113
  'div-table-row',
data/docs/AGENTS.md CHANGED
@@ -127,6 +127,15 @@ Use `tramway_form_for` instead `form_with`, `form_for`
127
127
  ### Rule 8
128
128
  Inherit all components from Tramway::BaseComponent
129
129
 
130
+ ### Rule 8.1
131
+ When you need chat UI, use the `tramway_chat` helper. Pass `chat_id`, `messages`, `message_form`, and `send_message_path`.
132
+ Each message must include `:id` and a `:type` of `:sent` or `:received`, and other keys (like `:text`, `:data`, `:sent_at`)
133
+ are forwarded to `tramway/chats/message_component`. Use `message_form: nil` when you only need read-only chat rendering.
134
+
135
+ For live updates to a rendered `tramway_chat`, use `tramway_chat_append_message(chat_id:, message_type:, text:, sent_at:)`.
136
+ This method is included in all controllers and ActiveRecord models. `message_type` must be `:sent` or `:received`, otherwise
137
+ it raises `ArgumentError`. `chat_id` must match the stream id used in `tramway_chat`.
138
+
130
139
  ### Rule 9
131
140
  If page `create` or `update` is configured for an entity, use Tramway Form pattern for forms. Visible fields are configured via `form_fields` method.
132
141
 
@@ -443,6 +452,18 @@ scope :for_role, -> (role) { where role: role }
443
452
  ### Rule 28
444
453
  In case, you need to make a one link in tramway_table on each row. Use `tramway_row href: your_link` instead of putting the like inside a cell.
445
454
 
455
+ ### Rule 29
456
+ Always use tramway decorated objects in views.
457
+
458
+ ### Rule 30
459
+ For Tailwind classes with `/`, `[`, `]` characters use `{ class: 'here is the complicated class' }` in HAML.
460
+
461
+ ### Rule 31
462
+ Always `tramway_decorate` and `tramway_form` for creating these types of objects. Don't use decorator and form classes for this.
463
+
464
+ ### Rule 32
465
+ In Tramway Decorators, use `delegate_attributes` method instead of `delegate :something, to: :object`
466
+
446
467
  ## Controller Patterns
447
468
 
448
469
  - Keep actions short and explicit with guard clauses.