tramway 3.0.3.3 → 3.0.4.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 17fe97e6ab94f2d72d4cb67fad3facfafa3d2e9e046e9a52b84bc376beddd3f2
4
- data.tar.gz: 1d3ba244c662e9facdd4a2f28c63c4a20bfb720295fca5713e7eb633cf1ff696
3
+ metadata.gz: cfd16d191c35f49e31f08328da1a8992ab6f1f08182c8b919355d54ff9951074
4
+ data.tar.gz: d1f8dfe3c21273a746a13750ee89c575cb9c0a065aca4aff0b93010613a381dd
5
5
  SHA512:
6
- metadata.gz: 745193d204d4e871248c8bc7531dd5d74b9c99759cc45d5190ed1f66ffca3b109d82239154aa86cfd70804c15c1683f848b92adbd20a43d3bdddc102a2c9b7d2
7
- data.tar.gz: d14350608083e3dc4ab6d0c4de6bd7e057a45c9d289c0fdff37080e0e365b4d265f734647cfe296afbc1356c1dcf0ea1c7ef3258265206f588b85f3e90f2f6ad
6
+ metadata.gz: f90c814812cd3714bc1f4492ae243a9278dfd49e0cb4d8796597a548fd0c18d9eceab36e4cae21d28f4807dc036f088efe91f555d8623a7c28066422ee6c0b0d
7
+ data.tar.gz: b77c7144dd9fa87c805b4427a7d22f43011dba7da7cdc53eb494aeac47a15b6601bb06f076a2195347f0a22d1a62ec0293eba04ca29c472b81caa62f625229ac
data/README.md CHANGED
@@ -925,17 +925,17 @@ When set to `false`, the text field is disabled and the waiting placeholder is s
925
925
  If you do not want to render the message form, pass `message_form: nil`. When the form is present, `send_message_path` is
926
926
  required and the helper will generate the correct POST form.
927
927
 
928
- To append messages to an already-rendered `tramway_chat` stream, use `tramway_chat_append_message`.
928
+ To append a message to an already-rendered `tramway_chat` stream, use `tramway_chat_append_message`.
929
929
  The method is mixed into controllers and ActiveRecord models by Tramway and expects:
930
930
  - `chat_id:` — the same value used in `tramway_chat chat_id:`
931
- - `message_type:` — only `:sent` or `:received` (raises `ArgumentError` otherwise)
931
+ - `type:` — only `:sent` or `:received` (raises `ArgumentError` otherwise)
932
932
  - `text:` — message content
933
933
  - `sent_at:` — message timestamp
934
934
 
935
935
  ```ruby
936
936
  tramway_chat_append_message(
937
937
  chat_id: 'support-chat',
938
- message_type: :received,
938
+ type: :received,
939
939
  text: 'We got your request',
940
940
  sent_at: Time.current
941
941
  )
@@ -943,6 +943,49 @@ tramway_chat_append_message(
943
943
 
944
944
  It broadcasts with `target: 'messages'` and renders the `tramway/chats/message` partial so the message appears in the live chat.
945
945
 
946
+ To prepend a message to the same stream, use `tramway_chat_prepend_message`. It accepts the same arguments and validation rules,
947
+ but inserts the rendered `tramway/chats/message` partial at the beginning of the `messages` target instead of the end.
948
+
949
+ ```ruby
950
+ tramway_chat_prepend_message(
951
+ chat_id: 'support-chat',
952
+ type: :received,
953
+ text: 'Earlier message from history',
954
+ sent_at: 5.minutes.ago
955
+ )
956
+ ```
957
+
958
+ To append multiple messages at once, use `tramway_chat_append_messages`. The method expects:
959
+ - `chat_id:` — the same value used in `tramway_chat chat_id:`
960
+ - `messages:` — an array of hashes, where each hash includes `type:`, `text:`, and `sent_at:`
961
+
962
+ Each message `type:` must be `:sent` or `:received` or the helper raises `ArgumentError`. It broadcasts with `target:
963
+ 'messages'` and renders the `tramway/chats/messages` partial so the full collection appears in the live chat.
964
+
965
+ ```ruby
966
+ tramway_chat_append_messages(
967
+ chat_id: 'support-chat',
968
+ messages: [
969
+ { type: :received, text: 'First update', sent_at: 2.minutes.ago },
970
+ { type: :sent, text: 'Thanks, checking now', sent_at: 1.minute.ago }
971
+ ]
972
+ )
973
+ ```
974
+
975
+ To prepend multiple messages, use `tramway_chat_prepend_messages`. It accepts the same `chat_id:` and `messages:` arguments
976
+ as `tramway_chat_append_messages`, validates each `type:`, and prepends the rendered `tramway/chats/messages` partial to the
977
+ beginning of the `messages` target.
978
+
979
+ ```ruby
980
+ tramway_chat_prepend_messages(
981
+ chat_id: 'support-chat',
982
+ messages: [
983
+ { type: :received, text: 'Older history item', sent_at: 10.minutes.ago },
984
+ { type: :sent, text: 'Reply from earlier', sent_at: 9.minutes.ago }
985
+ ]
986
+ )
987
+ ```
988
+
946
989
  ### Tramway Table Component
947
990
 
948
991
  Tramway provides a responsive, tailwind-styled table with light and dark themes. Use the `tramway_table`, `tramway_row`, and
@@ -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.p-2.md:p-6.space-y-2.md:space-y-4.md:rounded-xl.rounded-t-xl{ class: 'text-gray-100 bg-gray-800/60' }
4
+ #messages.flex.flex-col.flex-1.min-h-0.overflow-y-auto.overflow-x-hidden.p-2.md:p-6.space-y-2.md:space-y-4.md:rounded-xl.rounded-t-xl{ class: 'text-gray-100 bg-gray-800/60', data: { scroll_to_bottom_on_update: scroll_to_bottom_on_update } }
5
5
  - messages.each do |message|
6
6
  = component "tramway/chats/message", **message
7
7
  - if disabled?
@@ -31,13 +31,23 @@
31
31
  const messages = document.getElementById('messages');
32
32
  if (!messages || messages.dataset.chatScrollInitialized === 'true') return;
33
33
 
34
+ const shouldScrollToBottomOnUpdate = () => {
35
+ return messages.dataset.scrollToBottomOnUpdate !== 'false' &&
36
+ messages.dataset.preserveScroll !== 'true';
37
+ };
38
+
34
39
  const scrollToBottom = () => {
35
40
  messages.scrollTop = messages.scrollHeight;
36
41
  };
37
42
 
43
+ // Always scroll to bottom on initial page load.
38
44
  scrollToBottom();
39
45
 
40
- const observer = new MutationObserver(scrollToBottom);
46
+ const observer = new MutationObserver(() => {
47
+ if (!shouldScrollToBottomOnUpdate()) return;
48
+ scrollToBottom();
49
+ });
50
+
41
51
  observer.observe(messages, { childList: true });
42
52
 
43
53
  messages.dataset.chatScrollInitialized = 'true';
@@ -9,6 +9,7 @@ module Tramway
9
9
  option :options, optional: true, default: -> { {} }
10
10
  option :send_message_path
11
11
  option :send_messages_enabled, optional: true, default: -> { true }
12
+ option :scroll_to_bottom_on_update, optional: true, default: -> { true }
12
13
 
13
14
  def disabled?
14
15
  options[:disabled]
@@ -1,2 +1,2 @@
1
- %main.w-full.min-h-dvh{ class: container_classes }
1
+ %main.w-full.min-h-dvh{ class: container_classes, **options.except(:class) }
2
2
  = content
@@ -4,9 +4,13 @@ module Tramway
4
4
  module Containers
5
5
  # Main container for tailwind-styled layout
6
6
  class MainComponent < Tramway::BaseComponent
7
+ option :options, optional: true, default: proc { {} }
8
+
7
9
  def container_classes
10
+ options_classes = options[:class] || ''
11
+
8
12
  theme_classes(
9
- classic: 'bg-gray-900 text-gray-100 shadow-inner'
13
+ classic: "bg-gray-900 text-gray-100 shadow-inner#{options_classes}"
10
14
  )
11
15
  end
12
16
  end
@@ -1,3 +1,3 @@
1
- %div{ class: container_classes, id:, **options }
1
+ %div{ class: container_classes, id:, **options.except(:class, :id) }
2
2
  .flex.flex-col.justify-center.w-full
3
3
  = content
@@ -8,9 +8,11 @@ module Tramway
8
8
  option :options, optional: true, default: proc { {} }
9
9
 
10
10
  def container_classes
11
+ options_classes = options[:class] || ''
12
+
11
13
  theme_classes(
12
14
  classic: 'container p-4 flex align-center justify-center w-full mx-auto bg-gray-100 text-gray-700 ' \
13
- 'shadow-inner rounded-xl bg-gray-900 text-gray-100'
15
+ 'shadow-inner rounded-xl bg-gray-900 text-gray-100' + options_classes
14
16
  )
15
17
  end
16
18
  end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tramway
4
+ module Table
5
+ # Helpers for extracting and normalizing visible table content cells from HTML fragments.
6
+ module ContentCells
7
+ private
8
+
9
+ def visible_cells_from(content)
10
+ fragment = Nokogiri::HTML.fragment(content)
11
+ parsed_cells = fragment.xpath(
12
+ './*[@class and contains(concat(" ", normalize-space(@class), " "), " div-table-cell ")]'
13
+ )
14
+
15
+ parsed_cells.each { |cell| remove_hidden_class!(cell) }
16
+ end
17
+
18
+ def remove_hidden_class!(node)
19
+ classes = node['class'].to_s.split
20
+ return if classes.empty?
21
+
22
+ node['class'] = classes.reject { |class_name| class_name == 'hidden' }.join(' ')
23
+ end
24
+ end
25
+ end
26
+ end
@@ -1,4 +1,5 @@
1
- %div{ class: "#{header_row_classes} md:grid-cols-#{columns_count}", aria: { label: "Table Header" }, role: "row" }
1
+ - parsed_cells = headers.any? ? nil : visible_cells_from(content)
2
+ %div{ class: "#{header_row_classes} md:grid-cols-#{columns_count(parsed_cells: parsed_cells)}", aria: { label: "Table Header" }, role: "row" }
2
3
  - if headers.any?
3
4
  - headers.each do |header|
4
5
  .div-table-cell{ class: header_cell_classes }
@@ -4,11 +4,17 @@ module Tramway
4
4
  module Table
5
5
  # Component for rendering a header in a table
6
6
  class HeaderComponent < Tramway::BaseComponent
7
+ include ContentCells
8
+
7
9
  option :headers, optional: true, default: -> { [] }
8
10
  option :columns, optional: true, default: -> { 3 }
9
11
 
10
- def columns_count
11
- headers.present? ? headers.size : columns
12
+ def columns_count(content = nil, parsed_cells: nil)
13
+ return headers.size if headers.present?
14
+ return parsed_cells.size if parsed_cells
15
+ return visible_cells_from(content).size if content.present?
16
+
17
+ columns
12
18
  end
13
19
 
14
20
  def header_row_classes
@@ -4,6 +4,8 @@ module Tramway
4
4
  module Table
5
5
  # Component for rendering a row in a table
6
6
  class RowComponent < Tramway::BaseComponent
7
+ include ContentCells
8
+
7
9
  option :cells, optional: true, default: -> { [] }
8
10
  option :href, optional: true
9
11
  option :preview, optional: true, default: -> { true }
@@ -58,22 +60,6 @@ module Tramway
58
60
 
59
61
  private
60
62
 
61
- def visible_cells_from(content)
62
- fragment = Nokogiri::HTML.fragment(content)
63
- parsed_cells = fragment.xpath(
64
- './*[@class and contains(concat(" ", normalize-space(@class), " "), " div-table-cell ")]'
65
- )
66
-
67
- parsed_cells.each { |cell| remove_hidden_class!(cell) }
68
- end
69
-
70
- def remove_hidden_class!(node)
71
- classes = node['class'].to_s.split
72
- return if classes.empty?
73
-
74
- node['class'] = classes.reject { |class_name| class_name == 'hidden' }.join(' ')
75
- end
76
-
77
63
  def ensure_view_context_accessor
78
64
  return if view_context.respond_to?(:tramway_inside_cell=)
79
65
 
@@ -1,4 +1 @@
1
- = component 'tramway/chats/message',
2
- type:,
3
- text:,
4
- sent_at:
1
+ = component 'tramway/chats/message', type:, text:, sent_at:
@@ -0,0 +1,2 @@
1
+ - messages.each do |message|
2
+ = component 'tramway/chats/message', type:, text:, sent_at:
@@ -91,7 +91,7 @@ module Tramway
91
91
  end
92
92
 
93
93
  def agents_template_url
94
- 'https://github.com/Purple-Magic/base_project/blob/main/docs/agents/tramway.md'
94
+ 'https://raw.githubusercontent.com/Purple-Magic/tramway-skill/refs/heads/main/skills/tramway-skill/agents/tramway.md'
95
95
  end
96
96
 
97
97
  def agents_template_body
@@ -14,6 +14,43 @@ module Tramway
14
14
  partial: 'tramway/chats/message',
15
15
  locals: { type:, text:, sent_at: }
16
16
  end
17
+
18
+ def tramway_chat_prepend_message(chat_id:, type:, text:, sent_at:)
19
+ raise ArgumentError, 'message_type must be :sent or :received' unless MESSAGE_TYPES.include?(type.to_sym)
20
+
21
+ Turbo::StreamsChannel.broadcast_prepend_to [chat_id, 'messages'],
22
+ target: 'messages',
23
+ partial: 'tramway/chats/message',
24
+ locals: { type:, text:, sent_at: }
25
+ end
26
+
27
+ def tramway_chat_append_messages(chat_id:, messages:)
28
+ messages.each do |message|
29
+ unless MESSAGE_TYPES.include?(message[:type].to_sym)
30
+ raise ArgumentError,
31
+ 'Each message must have :id and :type keys'
32
+ end
33
+ end
34
+
35
+ Turbo::StreamsChannel.broadcast_append_to [chat_id, 'messages'],
36
+ target: 'messages',
37
+ partial: 'tramway/chats/messages',
38
+ locals: { messages: }
39
+ end
40
+
41
+ def tramway_chat_prepend_messages(chat_id:, messages:)
42
+ messages.each do |message|
43
+ unless MESSAGE_TYPES.include?(message[:type].to_sym)
44
+ raise ArgumentError,
45
+ 'Each message must have :id and :type keys'
46
+ end
47
+ end
48
+
49
+ Turbo::StreamsChannel.broadcast_prepend_to [chat_id, 'messages'],
50
+ target: 'messages',
51
+ partial: 'tramway/chats/messages',
52
+ locals: { messages: }
53
+ end
17
54
  end
18
55
  end
19
56
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Tramway
4
- VERSION = '3.0.3.3'
4
+ VERSION = '3.0.4.1'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tramway
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.0.3.3
4
+ version: 3.0.4.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - kalashnikovisme
@@ -230,6 +230,7 @@ files:
230
230
  - app/components/tramway/pagination/prev_page_component.rb
231
231
  - app/components/tramway/table/cell_component.html.haml
232
232
  - app/components/tramway/table/cell_component.rb
233
+ - app/components/tramway/table/content_cells.rb
233
234
  - app/components/tramway/table/header_component.html.haml
234
235
  - app/components/tramway/table/header_component.rb
235
236
  - app/components/tramway/table/row/preview_component.html.haml
@@ -250,6 +251,7 @@ files:
250
251
  - app/views/kaminari/_paginator.html.haml
251
252
  - app/views/kaminari/_prev_page.html.haml
252
253
  - app/views/tramway/chats/_message.html.haml
254
+ - app/views/tramway/chats/_messages.html.haml
253
255
  - app/views/tramway/entities/_form.html.haml
254
256
  - app/views/tramway/entities/_list.html.haml
255
257
  - app/views/tramway/entities/edit.html.haml