slackblocks 3.1.0 → 3.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2854904b3e2e3ecbdf99f451ce5fa572336b2f9ed1946d18b9a5f6089d6ea889
4
- data.tar.gz: 2fc7f9e644c71a5554b4095e2896374a9be3d8cd7366d7c700a9bbf4899fc604
3
+ metadata.gz: 827536ece4c547f433c37b5ea250d8df94e5cc1455675c5c6535bfc43a662f0f
4
+ data.tar.gz: 5a070516e5d8948924d79dd08d779678f3ae7d4af2dbbda540d9e0332df5e44c
5
5
  SHA512:
6
- metadata.gz: 7e63f0c5e302a0ee9841fcd18cf143cb9995ab9a6314063efc5f8111a792423dec1c8dbdec3503602155823b71257d37a856953f973bf1e3d2a5c6a5eddec618
7
- data.tar.gz: 3a30a4e84adb15803679cc90b2a7d1b1fc24d687b6a56e558f5278414067e962bc55782c6efdcb75324192ed5be45e2d113ef7bd129443e6352bdfdf0ba5e7c3
6
+ metadata.gz: fcb284a50cc685907415660a3308f4b7107f086d45dc523fd56c93ea996ae1605f9e8dd853b63cbcbfbdefa257e9af5019e0bd59a4f2c2a5bb8143c9f8a34283
7
+ data.tar.gz: 6fbaaae9df8455182841fa8102310f44e9d377fa48dd2c6218a963764033f1a57026af29142ca05544ae1fbfda802a6631a05153b31b23cd2e3b61c738d5fbce
data/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## [3.2.0] — 2026-10-31
4
+
5
+ - Add native layout blocks, rows and items with typed widths/alignment, closed child and fallback roles, checked nesting and receiving-surface validation (spec 1.5.0).
6
+ - Preserve confirmed top-level message/view block counting, supported runtime minimums, legacy container-only behavior, and existing chart/text limits. No unconfirmed recursive block budget is imposed.
7
+ - Add a runnable ten-language layout recipe and rendered preview; preserve outgoing 3.1.0 documentation.
8
+
3
9
  ## [3.1.0] — 2026-10-28
4
10
 
5
11
  - Coordinate this release with the new Haskell implementation and ten-language documentation. Existing runtime support remains unchanged.
@@ -1,7 +1,7 @@
1
1
  # Generated from spec/limits.json. Do not edit by hand.
2
2
  module Slackblocks
3
3
  module Limits
4
- VALUES = {"action_id.max_length" => 255, "actions.elements.max_items" => 25, "alert.text.max_length" => 200, "block_id.max_length" => 255, "button.accessibility_label.max_length" => 75, "button.agent_prompt.max_length" => 4000, "button.text.max_length" => 75, "button.url.max_length" => 3000, "button.value.max_length" => 2000, "card.actions.max_items" => 3, "card.body.max_length" => 200, "card.subtext.max_length" => 200, "card.subtitle.max_length" => 150, "card.title.max_length" => 150, "carousel.elements.max_items" => 10, "carousel.elements.min_items" => 1, "checkboxes.options.max_items" => 10, "checkboxes.options.min_items" => 1, "confirmation.confirm.max_length" => 30, "confirmation.deny.max_length" => 30, "confirmation.text.max_length" => 300, "confirmation.title.max_length" => 100, "container.child_blocks.max_items" => 10, "container.child_blocks.min_items" => 1, "container.subtitle.max_length" => 150, "container.title.max_length" => 150, "context.elements.max_items" => 10, "context_actions.elements.max_items" => 5, "conversation_filter.include.min_items" => 1, "data_table.cell_text.min_length" => 1, "data_table.columns.max_items" => 20, "data_table.columns.min_items" => 1, "data_table.content.max_length" => 20000, "data_table.page_size.max" => 100, "data_table.page_size.min" => 1, "data_table.row_header_column_index.min" => 0, "data_table.rows.max_items" => 201, "data_table.rows.min_items" => 2, "data_table.total_content.max_length" => 20000, "data_visualization.axis_label.max_length" => 50, "data_visualization.categories.max_items" => 20, "data_visualization.categories.min_items" => 1, "data_visualization.category_label.max_length" => 20, "data_visualization.data.max_items" => 20, "data_visualization.data.min_items" => 1, "data_visualization.point_label.max_length" => 20, "data_visualization.segment.label.max_length" => 20, "data_visualization.segment.value.exclusive_min" => 0, "data_visualization.segments.max_items" => 12, "data_visualization.segments.min_items" => 1, "data_visualization.series.max_items" => 12, "data_visualization.series.min_items" => 1, "data_visualization.series_name.max_length" => 20, "data_visualization.title.max_length" => 50, "date_picker.placeholder.max_length" => 150, "dispatch_action_configuration.trigger_actions_on.max_items" => 2, "dispatch_action_configuration.trigger_actions_on.min_items" => 1, "email_input.placeholder.max_length" => 150, "feedback_button.accessibility_label.max_length" => 75, "feedback_button.text.max_length" => 75, "feedback_button.value.max_length" => 2000, "file_input.max_files.max" => 10, "file_input.max_files.min" => 1, "header.text.max_length" => 150, "icon_button.accessibility_label.max_length" => 75, "icon_button.value.max_length" => 2000, "icon_button.visible_to_user_ids.max_items" => 10, "image.alt_text.max_length" => 2000, "image.image_url.max_length" => 3000, "image.title.max_length" => 2000, "image_element.alt_text.max_length" => 2000, "image_element.image_url.max_length" => 3000, "input.hint.max_length" => 2000, "input.label.max_length" => 2000, "markdown.text.max_length" => 12000, "markdown.total_text.max_length" => 12000, "message.attachments.max_items" => 100, "message.blocks.max_items" => 50, "message.channel.min_length" => 1, "message.data_visualizations.max_items" => 2, "multi_select.max_selected_items.min" => 1, "number_input.placeholder.max_length" => 150, "option.description.max_length" => 75, "option.text.max_length" => 75, "option.url.max_length" => 3000, "option.value.max_length" => 150, "option_group.label.max_length" => 75, "option_group.options.max_items" => 100, "option_group.options.min_items" => 1, "overflow.options.max_items" => 5, "overflow.options.min_items" => 1, "plain_text_input.max_length.max" => 3000, "plain_text_input.max_length.min" => 1, "plain_text_input.min_length.max" => 3000, "plain_text_input.min_length.min" => 0, "plain_text_input.placeholder.max_length" => 150, "plan.tasks.max_items" => 50, "radio_buttons.options.max_items" => 10, "radio_buttons.options.min_items" => 1, "rich_text_input.max_lines.max" => 100, "rich_text_input.max_lines.min" => 1, "rich_text_input.min_lines.max" => 100, "rich_text_input.min_lines.min" => 1, "rich_text_input.placeholder.max_length" => 150, "rich_text_list.border.max" => 1, "rich_text_list.border.min" => 0, "rich_text_list.indent.max" => 8, "rich_text_list.indent.min" => 0, "rich_text_list.offset.min" => 0, "rich_text_preformatted.border.max" => 1, "rich_text_preformatted.border.min" => 0, "rich_text_quote.border.max" => 1, "rich_text_quote.border.min" => 0, "section.fields.item_max_length" => 2000, "section.fields.max_items" => 10, "section.text.max_length" => 3000, "select.min_query_length.min" => 0, "select.option_groups.max_items" => 100, "select.option_groups.min_items" => 1, "select.options.max_items" => 100, "select.options.min_items" => 1, "select.placeholder.max_length" => 150, "table.column_settings.max_items" => 20, "table.columns.max_items" => 20, "table.rows.max_items" => 100, "text.max_length" => 3000, "text.min_length" => 1, "time_picker.placeholder.max_length" => 150, "url_input.placeholder.max_length" => 150, "video.alt_text.max_length" => 2000, "video.author_name.max_length" => 50, "video.description.max_length" => 200, "video.provider_icon_url.max_length" => 2000, "video.provider_name.max_length" => 50, "video.thumbnail_url.max_length" => 3000, "video.title.max_length" => 200, "video.title_url.max_length" => 2000, "video.video_url.max_length" => 3000, "view.blocks.max_items" => 100, "view.callback_id.max_length" => 255, "view.close.max_length" => 24, "view.external_id.max_length" => 255, "view.private_metadata.max_length" => 3000, "view.submit.max_length" => 24, "view.title.max_length" => 24, "workflow_button.accessibility_label.max_length" => 75, "workflow_button.text.max_length" => 75}.freeze
4
+ VALUES = {"action_id.max_length" => 255, "actions.elements.max_items" => 25, "alert.text.max_length" => 200, "block_id.max_length" => 255, "button.accessibility_label.max_length" => 75, "button.agent_prompt.max_length" => 4000, "button.text.max_length" => 75, "button.url.max_length" => 3000, "button.value.max_length" => 2000, "card.actions.max_items" => 3, "card.body.max_length" => 200, "card.subtext.max_length" => 200, "card.subtitle.max_length" => 150, "card.title.max_length" => 150, "carousel.elements.max_items" => 10, "carousel.elements.min_items" => 1, "checkboxes.options.max_items" => 10, "checkboxes.options.min_items" => 1, "confirmation.confirm.max_length" => 30, "confirmation.deny.max_length" => 30, "confirmation.text.max_length" => 300, "confirmation.title.max_length" => 100, "container.child_blocks.max_items" => 10, "container.child_blocks.min_items" => 1, "container.subtitle.max_length" => 150, "container.title.max_length" => 150, "context.elements.max_items" => 10, "context_actions.elements.max_items" => 5, "conversation_filter.include.min_items" => 1, "data_table.cell_text.min_length" => 1, "data_table.columns.max_items" => 20, "data_table.columns.min_items" => 1, "data_table.content.max_length" => 20000, "data_table.page_size.max" => 100, "data_table.page_size.min" => 1, "data_table.row_header_column_index.min" => 0, "data_table.rows.max_items" => 201, "data_table.rows.min_items" => 2, "data_table.total_content.max_length" => 20000, "data_visualization.axis_label.max_length" => 50, "data_visualization.categories.max_items" => 20, "data_visualization.categories.min_items" => 1, "data_visualization.category_label.max_length" => 20, "data_visualization.data.max_items" => 20, "data_visualization.data.min_items" => 1, "data_visualization.point_label.max_length" => 20, "data_visualization.segment.label.max_length" => 20, "data_visualization.segment.value.exclusive_min" => 0, "data_visualization.segments.max_items" => 12, "data_visualization.segments.min_items" => 1, "data_visualization.series.max_items" => 12, "data_visualization.series.min_items" => 1, "data_visualization.series_name.max_length" => 20, "data_visualization.title.max_length" => 50, "date_picker.placeholder.max_length" => 150, "dispatch_action_configuration.trigger_actions_on.max_items" => 2, "dispatch_action_configuration.trigger_actions_on.min_items" => 1, "email_input.placeholder.max_length" => 150, "feedback_button.accessibility_label.max_length" => 75, "feedback_button.text.max_length" => 75, "feedback_button.value.max_length" => 2000, "file_input.max_files.max" => 10, "file_input.max_files.min" => 1, "header.text.max_length" => 150, "icon_button.accessibility_label.max_length" => 75, "icon_button.value.max_length" => 2000, "icon_button.visible_to_user_ids.max_items" => 10, "image.alt_text.max_length" => 2000, "image.image_url.max_length" => 3000, "image.title.max_length" => 2000, "image_element.alt_text.max_length" => 2000, "image_element.image_url.max_length" => 3000, "input.hint.max_length" => 2000, "input.label.max_length" => 2000, "layout.items.max_items" => 3, "layout.items.min_items" => 1, "layout.label.max_length" => 150, "layout.label.min_length" => 1, "layout.rows.max_items" => 5, "layout.rows.min_items" => 1, "markdown.text.max_length" => 12000, "markdown.total_text.max_length" => 12000, "message.attachments.max_items" => 100, "message.blocks.max_items" => 50, "message.channel.min_length" => 1, "message.data_visualizations.max_items" => 2, "multi_select.max_selected_items.min" => 1, "number_input.placeholder.max_length" => 150, "option.description.max_length" => 75, "option.text.max_length" => 75, "option.url.max_length" => 3000, "option.value.max_length" => 150, "option_group.label.max_length" => 75, "option_group.options.max_items" => 100, "option_group.options.min_items" => 1, "overflow.options.max_items" => 5, "overflow.options.min_items" => 1, "plain_text_input.max_length.max" => 3000, "plain_text_input.max_length.min" => 1, "plain_text_input.min_length.max" => 3000, "plain_text_input.min_length.min" => 0, "plain_text_input.placeholder.max_length" => 150, "plan.tasks.max_items" => 50, "radio_buttons.options.max_items" => 10, "radio_buttons.options.min_items" => 1, "rich_text_input.max_lines.max" => 100, "rich_text_input.max_lines.min" => 1, "rich_text_input.min_lines.max" => 100, "rich_text_input.min_lines.min" => 1, "rich_text_input.placeholder.max_length" => 150, "rich_text_list.border.max" => 1, "rich_text_list.border.min" => 0, "rich_text_list.indent.max" => 8, "rich_text_list.indent.min" => 0, "rich_text_list.offset.min" => 0, "rich_text_preformatted.border.max" => 1, "rich_text_preformatted.border.min" => 0, "rich_text_quote.border.max" => 1, "rich_text_quote.border.min" => 0, "section.fields.item_max_length" => 2000, "section.fields.max_items" => 10, "section.text.max_length" => 3000, "select.min_query_length.min" => 0, "select.option_groups.max_items" => 100, "select.option_groups.min_items" => 1, "select.options.max_items" => 100, "select.options.min_items" => 1, "select.placeholder.max_length" => 150, "table.column_settings.max_items" => 20, "table.columns.max_items" => 20, "table.rows.max_items" => 100, "text.max_length" => 3000, "text.min_length" => 1, "time_picker.placeholder.max_length" => 150, "url_input.placeholder.max_length" => 150, "video.alt_text.max_length" => 2000, "video.author_name.max_length" => 50, "video.description.max_length" => 200, "video.provider_icon_url.max_length" => 2000, "video.provider_name.max_length" => 50, "video.thumbnail_url.max_length" => 3000, "video.title.max_length" => 200, "video.title_url.max_length" => 2000, "video.video_url.max_length" => 3000, "view.blocks.max_items" => 100, "view.callback_id.max_length" => 255, "view.close.max_length" => 24, "view.external_id.max_length" => 255, "view.private_metadata.max_length" => 3000, "view.submit.max_length" => 24, "view.title.max_length" => 24, "workflow_button.accessibility_label.max_length" => 75, "workflow_button.text.max_length" => 75}.freeze
5
5
  def self.[](path)
6
6
  VALUES.fetch(path)
7
7
  end
@@ -33,6 +33,12 @@ module Slackblocks
33
33
  module RichTextSectionElement
34
34
  end
35
35
 
36
+ module LayoutChild
37
+ end
38
+
39
+ module LayoutFallback
40
+ end
41
+
36
42
  module AlertLevel
37
43
  DEFAULT = :default
38
44
  INFO = :info
@@ -80,9 +86,28 @@ module Slackblocks
80
86
  EPHEMERAL = :ephemeral
81
87
  end
82
88
 
89
+ module LayoutWidth
90
+ NARROW = :narrow
91
+ STANDARD = :standard
92
+ WIDE = :wide
93
+ end
94
+
95
+ module LayoutAlignment
96
+ START = :start
97
+ CENTER = :center
98
+ END_ = :end
99
+ end
100
+
101
+ module LayoutGridLines
102
+ NONE = :none
103
+ ROWS = :rows
104
+ OUTER = :outer
105
+ end
106
+
83
107
  class ActionsBlock
84
108
  include Value
85
109
  include Block
110
+ include LayoutChild
86
111
  attr_reader :elements, :block_id, :additional_fields
87
112
  end
88
113
  ActionsBlock.const_set(:WIRE_TYPE, "actions")
@@ -93,6 +118,7 @@ module Slackblocks
93
118
  class AlertBlock
94
119
  include Value
95
120
  include Block
121
+ include LayoutChild
96
122
  attr_reader :text, :level, :block_id, :additional_fields
97
123
  end
98
124
  AlertBlock.const_set(:WIRE_TYPE, "alert")
@@ -103,6 +129,7 @@ module Slackblocks
103
129
  class CardBlock
104
130
  include Value
105
131
  include Block
132
+ include LayoutChild
106
133
  attr_reader :hero_image, :icon, :title, :subtitle, :body, :actions, :slack_icon, :subtext, :block_id, :additional_fields
107
134
  end
108
135
  CardBlock.const_set(:WIRE_TYPE, "card")
@@ -113,6 +140,7 @@ module Slackblocks
113
140
  class CarouselBlock
114
141
  include Value
115
142
  include Block
143
+ include LayoutChild
116
144
  attr_reader :elements, :block_id, :additional_fields
117
145
  end
118
146
  CarouselBlock.const_set(:WIRE_TYPE, "carousel")
@@ -123,6 +151,7 @@ module Slackblocks
123
151
  class ContainerBlock
124
152
  include Value
125
153
  include Block
154
+ include LayoutChild
126
155
  attr_reader :child_blocks, :title, :rich_text_title, :subtitle, :width, :icon, :is_collapsible, :default_collapsed, :has_header_divider, :block_id, :additional_fields
127
156
  end
128
157
  ContainerBlock.const_set(:WIRE_TYPE, "container")
@@ -143,6 +172,8 @@ module Slackblocks
143
172
  class ContextBlock
144
173
  include Value
145
174
  include Block
175
+ include LayoutChild
176
+ include LayoutFallback
146
177
  attr_reader :elements, :block_id, :additional_fields
147
178
  end
148
179
  ContextBlock.const_set(:WIRE_TYPE, "context")
@@ -153,6 +184,7 @@ module Slackblocks
153
184
  class DataTableBlock
154
185
  include Value
155
186
  include Block
187
+ include LayoutChild
156
188
  attr_reader :rows, :caption, :page_size, :row_header_column_index, :block_id, :additional_fields
157
189
  end
158
190
  DataTableBlock.const_set(:WIRE_TYPE, "data_table")
@@ -163,6 +195,7 @@ module Slackblocks
163
195
  class DataVisualizationBlock
164
196
  include Value
165
197
  include Block
198
+ include LayoutChild
166
199
  attr_reader :title, :chart, :block_id, :additional_fields
167
200
  end
168
201
  DataVisualizationBlock.const_set(:WIRE_TYPE, "data_visualization")
@@ -173,6 +206,8 @@ module Slackblocks
173
206
  class DividerBlock
174
207
  include Value
175
208
  include Block
209
+ include LayoutChild
210
+ include LayoutFallback
176
211
  attr_reader :block_id, :additional_fields
177
212
  end
178
213
  DividerBlock.const_set(:WIRE_TYPE, "divider")
@@ -183,6 +218,7 @@ module Slackblocks
183
218
  class FileBlock
184
219
  include Value
185
220
  include Block
221
+ include LayoutChild
186
222
  attr_reader :external_id, :source, :block_id, :additional_fields
187
223
  end
188
224
  FileBlock.const_set(:WIRE_TYPE, "file")
@@ -193,6 +229,8 @@ module Slackblocks
193
229
  class HeaderBlock
194
230
  include Value
195
231
  include Block
232
+ include LayoutChild
233
+ include LayoutFallback
196
234
  attr_reader :text, :block_id, :additional_fields
197
235
  end
198
236
  HeaderBlock.const_set(:WIRE_TYPE, "header")
@@ -203,6 +241,8 @@ module Slackblocks
203
241
  class ImageBlock
204
242
  include Value
205
243
  include Block
244
+ include LayoutChild
245
+ include LayoutFallback
206
246
  attr_reader :image_url, :slack_file, :alt_text, :title, :block_id, :additional_fields
207
247
  end
208
248
  ImageBlock.const_set(:WIRE_TYPE, "image")
@@ -213,6 +253,7 @@ module Slackblocks
213
253
  class InputBlock
214
254
  include Value
215
255
  include Block
256
+ include LayoutChild
216
257
  attr_reader :label, :element, :dispatch_action, :block_id, :hint, :optional, :additional_fields
217
258
  end
218
259
  InputBlock.const_set(:WIRE_TYPE, "input")
@@ -244,6 +285,8 @@ module Slackblocks
244
285
  include Value
245
286
  include Block
246
287
  include DataTableCell
288
+ include LayoutChild
289
+ include LayoutFallback
247
290
  include TableCell
248
291
  attr_reader :elements, :block_id, :additional_fields
249
292
  end
@@ -255,6 +298,8 @@ module Slackblocks
255
298
  class SectionBlock
256
299
  include Value
257
300
  include Block
301
+ include LayoutChild
302
+ include LayoutFallback
258
303
  attr_reader :text, :fields, :accessory, :block_id, :additional_fields
259
304
  end
260
305
  SectionBlock.const_set(:WIRE_TYPE, "section")
@@ -265,6 +310,8 @@ module Slackblocks
265
310
  class TableBlock
266
311
  include Value
267
312
  include Block
313
+ include LayoutChild
314
+ include LayoutFallback
268
315
  attr_reader :rows, :column_settings, :block_id, :additional_fields
269
316
  end
270
317
  TableBlock.const_set(:WIRE_TYPE, "table")
@@ -285,6 +332,8 @@ module Slackblocks
285
332
  class VideoBlock
286
333
  include Value
287
334
  include Block
335
+ include LayoutChild
336
+ include LayoutFallback
288
337
  attr_reader :alt_text, :thumbnail_url, :title, :video_url, :block_id, :author_name, :description, :provider_icon_url, :provider_name, :title_url, :additional_fields
289
338
  end
290
339
  VideoBlock.const_set(:WIRE_TYPE, "video")
@@ -976,4 +1025,32 @@ module Slackblocks
976
1025
  WebhookMessage.const_set(:DEFAULTS, Value.deep_freeze({}))
977
1026
  WebhookMessage.singleton_class.prepend(KeywordOnly)
978
1027
 
1028
+ class LayoutBlock
1029
+ include Value
1030
+ include Block
1031
+ attr_reader :label, :rows, :is_label_visible, :grid_lines, :fallback, :block_id, :additional_fields
1032
+ end
1033
+ LayoutBlock.const_set(:WIRE_TYPE, "layout")
1034
+ LayoutBlock.const_set(:FIELD_SPECS, Value.deep_freeze([{"name" => "label", "wire" => "label", "kind" => "string", "target" => nil, "coerce" => nil, "flags" => nil, "limits" => "layout.label", "required" => true}, {"name" => "rows", "wire" => "rows", "kind" => "list", "target" => "LayoutRow", "coerce" => nil, "flags" => nil, "limits" => "layout.rows", "required" => true}, {"name" => "is_label_visible", "wire" => "is_label_visible", "kind" => "boolean", "target" => nil, "coerce" => nil, "flags" => nil, "limits" => nil, "required" => false}, {"name" => "grid_lines", "wire" => "grid_lines", "kind" => "enum", "target" => "LayoutGridLines", "coerce" => nil, "flags" => nil, "limits" => nil, "required" => false}, {"name" => "fallback", "wire" => "fallback", "kind" => "list", "target" => "LayoutFallback", "coerce" => nil, "flags" => nil, "limits" => nil, "required" => false}, {"name" => "block_id", "wire" => "block_id", "kind" => "string", "target" => nil, "coerce" => nil, "flags" => nil, "limits" => "block_id", "required" => false}]))
1035
+ LayoutBlock.const_set(:DEFAULTS, Value.deep_freeze({}))
1036
+ LayoutBlock.singleton_class.prepend(KeywordOnly)
1037
+
1038
+ class LayoutRow
1039
+ include Value
1040
+ attr_reader :items, :additional_fields
1041
+ end
1042
+ LayoutRow.const_set(:WIRE_TYPE, "")
1043
+ LayoutRow.const_set(:FIELD_SPECS, Value.deep_freeze([{"name" => "items", "wire" => "items", "kind" => "list", "target" => "LayoutItem", "coerce" => nil, "flags" => nil, "limits" => "layout.items", "required" => true}]))
1044
+ LayoutRow.const_set(:DEFAULTS, Value.deep_freeze({}))
1045
+ LayoutRow.singleton_class.prepend(KeywordOnly)
1046
+
1047
+ class LayoutItem
1048
+ include Value
1049
+ attr_reader :block, :width, :horizontal_alignment, :additional_fields
1050
+ end
1051
+ LayoutItem.const_set(:WIRE_TYPE, "")
1052
+ LayoutItem.const_set(:FIELD_SPECS, Value.deep_freeze([{"name" => "block", "wire" => "block", "kind" => "object", "target" => "LayoutChild", "coerce" => nil, "flags" => nil, "limits" => nil, "required" => true}, {"name" => "width", "wire" => "width", "kind" => "enum", "target" => "LayoutWidth", "coerce" => nil, "flags" => nil, "limits" => nil, "required" => false}, {"name" => "horizontal_alignment", "wire" => "horizontal_alignment", "kind" => "enum", "target" => "LayoutAlignment", "coerce" => nil, "flags" => nil, "limits" => nil, "required" => false}]))
1053
+ LayoutItem.const_set(:DEFAULTS, Value.deep_freeze({}))
1054
+ LayoutItem.singleton_class.prepend(KeywordOnly)
1055
+
979
1056
  end
@@ -2,6 +2,6 @@
2
2
  module Slackblocks
3
3
  module Vocabulary
4
4
  ICON_NAMES = Value.deep_freeze(["archive", "book", "bookmark", "bot", "bug", "calendar", "call", "caret-left", "caret-right", "check", "clipboard", "code", "comment", "compass", "copy", "cube", "download", "edit", "email", "eye-closed", "eye-open", "file", "flag", "folder", "gear", "globe", "heart", "help", "image", "info", "key", "lightbulb", "link", "map", "mobile", "new-window", "pin", "plus", "refine", "refresh", "rocket", "save", "screen", "share", "sparkle", "star", "star-filled", "tag", "thumbs-down", "thumbs-up", "trash", "upload", "user", "warning"])
5
- SURFACE_BLOCK_TYPES = Value.deep_freeze({"home" => ["actions", "card", "carousel", "container", "context", "data_table", "data_visualization", "divider", "header", "image", "input", "rich_text", "section", "table", "video"], "message" => ["actions", "card", "carousel", "container", "context", "context_actions", "data_table", "data_visualization", "divider", "file", "header", "image", "input", "markdown", "plan", "rich_text", "section", "table", "task_card", "video"], "modal" => ["actions", "alert", "card", "context", "divider", "header", "image", "input", "rich_text", "section", "video"]})
5
+ SURFACE_BLOCK_TYPES = Value.deep_freeze({"home" => ["actions", "card", "carousel", "container", "context", "data_table", "data_visualization", "divider", "header", "image", "input", "layout", "rich_text", "section", "table", "video"], "message" => ["actions", "card", "carousel", "container", "context", "context_actions", "data_table", "data_visualization", "divider", "file", "header", "image", "input", "layout", "markdown", "plan", "rich_text", "section", "table", "task_card", "video"], "modal" => ["actions", "alert", "card", "context", "divider", "header", "image", "input", "layout", "rich_text", "section", "video"]})
6
6
  end
7
7
  end
@@ -73,7 +73,23 @@ module Slackblocks
73
73
  if wire.key?("icon") && wire.key?("slack_icon")
74
74
  fail!("mutually-exclusive", path, "icon and slack_icon cannot both be present")
75
75
  end
76
+ when "LayoutBlock"
77
+ composite_depth(wire, path)
78
+ when "LayoutRow"
79
+ fail!("type-mismatch", path, "a row only supports items") unless (wire.keys - ["items"]).empty?
80
+ items = wire.fetch("items")
81
+ wide = items.count { |item| %w[carousel data_table data_visualization].include?(item.fetch("block")["type"]) }
82
+ fail!("invalid-usage", path, "at most one wide-content block per row") if wide > 1
83
+ total = items.sum { |item| layout_weight(item) }
84
+ items.each do |item|
85
+ if item.fetch("block")["type"] == "data_visualization" && layout_weight(item) * 2 <= total
86
+ fail!("invalid-usage", path, "a data visualization requires more than half the row weight")
87
+ end
88
+ end
89
+ when "LayoutItem"
90
+ fail!("type-mismatch", path, "unsupported layout item property") unless (wire.keys - %w[block width horizontal_alignment]).empty?
76
91
  when "ContainerBlock"
92
+ composite_depth(wire, path)
77
93
  unless wire.key?("title") || wire.key?("rich_text_title")
78
94
  fail!("missing-required", path, "expected title")
79
95
  end
@@ -147,7 +163,7 @@ module Slackblocks
147
163
  message(wire, path)
148
164
  when "ModalView", "HomeTabView"
149
165
  surface(wire.fetch("blocks"), (klass == ModalView) ? "modal" : "home", "#{path}.blocks")
150
- if klass == ModalView && !wire.key?("submit") && wire.fetch("blocks").any? { |block| block["type"] == "input" }
166
+ if klass == ModalView && !wire.key?("submit") && descendants(wire.fetch("blocks")).any? { |block| block["type"] == "input" }
151
167
  fail!("missing-required", "#{path}.submit", "required when modal contains an input")
152
168
  end
153
169
  end
@@ -179,12 +195,40 @@ module Slackblocks
179
195
  left[0] * (rank.zero? ? left[2].ljust(width, "0") <=> right[2].ljust(width, "0") : rank)
180
196
  end
181
197
 
198
+ def self.layout_weight(item)
199
+ {"narrow" => 1, "wide" => 3}.fetch(item["width"], 2)
200
+ end
201
+
202
+ def self.composite_children(block, fallback: true)
203
+ empty = [] # : Array[Hash[String, untyped]]
204
+ case block["type"]
205
+ when "container" then block.fetch("child_blocks")
206
+ when "layout"
207
+ block.fetch("rows").flat_map { |row| row.fetch("items").map { |item| item.fetch("block") } } + (fallback ? block.fetch("fallback", empty) : empty)
208
+ else []
209
+ end
210
+ end
211
+
212
+ def self.composite_depth(block, path, depth = 0, has_layout = false)
213
+ has_layout ||= block["type"] == "layout"
214
+ depth += 1 if %w[layout container].include?(block["type"])
215
+ fail!("invalid-usage", path, "layout/container nesting exceeds two composite levels") if has_layout && depth > 2
216
+ composite_children(block, fallback: false).each { |child| composite_depth(child, path, depth, has_layout) }
217
+ end
218
+
219
+ def self.descendants(blocks)
220
+ blocks.flat_map { |block| [block] + descendants(composite_children(block)) }
221
+ end
222
+
182
223
  def self.surface(blocks, name, path)
224
+ empty = [] # : Array[Hash[String, untyped]]
183
225
  allowed = Vocabulary::SURFACE_BLOCK_TYPES.fetch(name)
184
226
  blocks.each_with_index do |block, index|
185
227
  type = block["type"]
186
228
  fail!("type-mismatch", "#{path}[#{index}].type", "unsupported #{name} block") unless allowed.include?(type)
187
229
  standalone_task(block, "#{path}[#{index}]")
230
+ surface(composite_children(block, fallback: false), name, "#{path}[#{index}].children")
231
+ surface(block.fetch("fallback", empty), name, "#{path}[#{index}].fallback") if type == "layout"
188
232
  end
189
233
  end
190
234
 
@@ -227,8 +271,8 @@ module Slackblocks
227
271
  totals["data_visualization"] += 1
228
272
  elsif node["type"] == "data_table"
229
273
  totals["data_table"] += characters(node.fetch("rows"))
230
- elsif node["type"] == "container"
231
- tally(node["child_blocks"], totals)
274
+ elsif %w[container layout].include?(node["type"])
275
+ tally(composite_children(node), totals)
232
276
  end
233
277
  when Array
234
278
  node.each { |child| tally(child, totals) }
@@ -1,4 +1,4 @@
1
1
  module Slackblocks
2
- VERSION = "3.1.0"
3
- SPEC_VERSION = "1.4.0"
2
+ VERSION = "3.2.0"
3
+ SPEC_VERSION = "1.5.0"
4
4
  end
@@ -33,6 +33,12 @@ module Slackblocks
33
33
  module RichTextSectionElement
34
34
  end
35
35
 
36
+ module LayoutChild
37
+ end
38
+
39
+ module LayoutFallback
40
+ end
41
+
36
42
  module AlertLevel
37
43
  DEFAULT: :default
38
44
  INFO: :info
@@ -80,9 +86,28 @@ module Slackblocks
80
86
  EPHEMERAL: :ephemeral
81
87
  end
82
88
 
89
+ module LayoutWidth
90
+ NARROW: :narrow
91
+ STANDARD: :standard
92
+ WIDE: :wide
93
+ end
94
+
95
+ module LayoutAlignment
96
+ START: :start
97
+ CENTER: :center
98
+ END_: :end
99
+ end
100
+
101
+ module LayoutGridLines
102
+ NONE: :none
103
+ ROWS: :rows
104
+ OUTER: :outer
105
+ end
106
+
83
107
  class ActionsBlock
84
108
  include Value
85
109
  include Block
110
+ include LayoutChild
86
111
  attr_reader elements: Array[Element]
87
112
  attr_reader block_id: (String)?
88
113
  attr_reader additional_fields: Hash[String, untyped]
@@ -94,6 +119,7 @@ module Slackblocks
94
119
  class AlertBlock
95
120
  include Value
96
121
  include Block
122
+ include LayoutChild
97
123
  attr_reader text: Text
98
124
  attr_reader level: (:default | :info | :warning | :error | :success)?
99
125
  attr_reader block_id: (String)?
@@ -106,6 +132,7 @@ module Slackblocks
106
132
  class CardBlock
107
133
  include Value
108
134
  include Block
135
+ include LayoutChild
109
136
  attr_reader hero_image: (ImageElement)?
110
137
  attr_reader icon: (ImageElement)?
111
138
  attr_reader title: (Text)?
@@ -124,6 +151,7 @@ module Slackblocks
124
151
  class CarouselBlock
125
152
  include Value
126
153
  include Block
154
+ include LayoutChild
127
155
  attr_reader elements: Array[CardBlock]
128
156
  attr_reader block_id: (String)?
129
157
  attr_reader additional_fields: Hash[String, untyped]
@@ -135,6 +163,7 @@ module Slackblocks
135
163
  class ContainerBlock
136
164
  include Value
137
165
  include Block
166
+ include LayoutChild
138
167
  attr_reader child_blocks: Array[Block]
139
168
  attr_reader title: (PlainText)?
140
169
  attr_reader rich_text_title: (RichTextBlock)?
@@ -165,6 +194,8 @@ module Slackblocks
165
194
  class ContextBlock
166
195
  include Value
167
196
  include Block
197
+ include LayoutChild
198
+ include LayoutFallback
168
199
  attr_reader elements: Array[ContextElement]
169
200
  attr_reader block_id: (String)?
170
201
  attr_reader additional_fields: Hash[String, untyped]
@@ -176,6 +207,7 @@ module Slackblocks
176
207
  class DataTableBlock
177
208
  include Value
178
209
  include Block
210
+ include LayoutChild
179
211
  attr_reader rows: Array[Array[DataTableCell]]
180
212
  attr_reader caption: String
181
213
  attr_reader page_size: (Integer)?
@@ -190,6 +222,7 @@ module Slackblocks
190
222
  class DataVisualizationBlock
191
223
  include Value
192
224
  include Block
225
+ include LayoutChild
193
226
  attr_reader title: String
194
227
  attr_reader chart: Chart
195
228
  attr_reader block_id: (String)?
@@ -202,6 +235,8 @@ module Slackblocks
202
235
  class DividerBlock
203
236
  include Value
204
237
  include Block
238
+ include LayoutChild
239
+ include LayoutFallback
205
240
  attr_reader block_id: (String)?
206
241
  attr_reader additional_fields: Hash[String, untyped]
207
242
  def initialize: (?block_id: (String)?, ?additional_fields: Hash[String | Symbol, untyped]?) -> void
@@ -212,6 +247,7 @@ module Slackblocks
212
247
  class FileBlock
213
248
  include Value
214
249
  include Block
250
+ include LayoutChild
215
251
  attr_reader external_id: String
216
252
  attr_reader source: (String)?
217
253
  attr_reader block_id: (String)?
@@ -224,6 +260,8 @@ module Slackblocks
224
260
  class HeaderBlock
225
261
  include Value
226
262
  include Block
263
+ include LayoutChild
264
+ include LayoutFallback
227
265
  attr_reader text: PlainText
228
266
  attr_reader block_id: (String)?
229
267
  attr_reader additional_fields: Hash[String, untyped]
@@ -235,6 +273,8 @@ module Slackblocks
235
273
  class ImageBlock
236
274
  include Value
237
275
  include Block
276
+ include LayoutChild
277
+ include LayoutFallback
238
278
  attr_reader image_url: (String)?
239
279
  attr_reader slack_file: (SlackFile)?
240
280
  attr_reader alt_text: String
@@ -249,6 +289,7 @@ module Slackblocks
249
289
  class InputBlock
250
290
  include Value
251
291
  include Block
292
+ include LayoutChild
252
293
  attr_reader label: PlainText
253
294
  attr_reader element: InputElement
254
295
  attr_reader dispatch_action: (bool)?
@@ -288,6 +329,8 @@ module Slackblocks
288
329
  include Value
289
330
  include Block
290
331
  include DataTableCell
332
+ include LayoutChild
333
+ include LayoutFallback
291
334
  include TableCell
292
335
  attr_reader elements: Array[RichTextBlockElement]
293
336
  attr_reader block_id: (String)?
@@ -300,6 +343,8 @@ module Slackblocks
300
343
  class SectionBlock
301
344
  include Value
302
345
  include Block
346
+ include LayoutChild
347
+ include LayoutFallback
303
348
  attr_reader text: (Text)?
304
349
  attr_reader fields: (Array[Text])?
305
350
  attr_reader accessory: (Element)?
@@ -313,6 +358,8 @@ module Slackblocks
313
358
  class TableBlock
314
359
  include Value
315
360
  include Block
361
+ include LayoutChild
362
+ include LayoutFallback
316
363
  attr_reader rows: Array[Array[TableCell]]
317
364
  attr_reader column_settings: (Array[ColumnSettings])?
318
365
  attr_reader block_id: (String)?
@@ -341,6 +388,8 @@ module Slackblocks
341
388
  class VideoBlock
342
389
  include Value
343
390
  include Block
391
+ include LayoutChild
392
+ include LayoutFallback
344
393
  attr_reader alt_text: String
345
394
  attr_reader thumbnail_url: String
346
395
  attr_reader title: PlainText
@@ -1249,6 +1298,41 @@ module Slackblocks
1249
1298
  def self.[]: (?blocks: (Array[Block])?, ?attachments: (Array[Attachment])?, ?text: (String)?, ?response_type: (:in_channel | :ephemeral)?, ?replace_original: (bool)?, ?delete_original: (bool)?, ?unfurl_links: (bool)?, ?unfurl_media: (bool)?, ?metadata: (Hash[String | Symbol, untyped])?, ?additional_fields: Hash[String | Symbol, untyped]?) -> WebhookMessage
1250
1299
  end
1251
1300
 
1301
+ class LayoutBlock
1302
+ include Value
1303
+ include Block
1304
+ attr_reader label: String
1305
+ attr_reader rows: Array[LayoutRow]
1306
+ attr_reader is_label_visible: (bool)?
1307
+ attr_reader grid_lines: (:none | :rows | :outer)?
1308
+ attr_reader fallback: (Array[LayoutFallback])?
1309
+ attr_reader block_id: (String)?
1310
+ attr_reader additional_fields: Hash[String, untyped]
1311
+ def initialize: (label: String, rows: Array[LayoutRow], ?is_label_visible: (bool)?, ?grid_lines: (:none | :rows | :outer)?, ?fallback: (Array[LayoutFallback])?, ?block_id: (String)?, ?additional_fields: Hash[String | Symbol, untyped]?) -> void
1312
+ def self.new: (label: String, rows: Array[LayoutRow], ?is_label_visible: (bool)?, ?grid_lines: (:none | :rows | :outer)?, ?fallback: (Array[LayoutFallback])?, ?block_id: (String)?, ?additional_fields: Hash[String | Symbol, untyped]?) -> LayoutBlock
1313
+ def self.[]: (label: String, rows: Array[LayoutRow], ?is_label_visible: (bool)?, ?grid_lines: (:none | :rows | :outer)?, ?fallback: (Array[LayoutFallback])?, ?block_id: (String)?, ?additional_fields: Hash[String | Symbol, untyped]?) -> LayoutBlock
1314
+ end
1315
+
1316
+ class LayoutRow
1317
+ include Value
1318
+ attr_reader items: Array[LayoutItem]
1319
+ attr_reader additional_fields: Hash[String, untyped]
1320
+ def initialize: (items: Array[LayoutItem], ?additional_fields: Hash[String | Symbol, untyped]?) -> void
1321
+ def self.new: (items: Array[LayoutItem], ?additional_fields: Hash[String | Symbol, untyped]?) -> LayoutRow
1322
+ def self.[]: (items: Array[LayoutItem], ?additional_fields: Hash[String | Symbol, untyped]?) -> LayoutRow
1323
+ end
1324
+
1325
+ class LayoutItem
1326
+ include Value
1327
+ attr_reader block: LayoutChild
1328
+ attr_reader width: (:narrow | :standard | :wide)?
1329
+ attr_reader horizontal_alignment: (:start | :center | :end)?
1330
+ attr_reader additional_fields: Hash[String, untyped]
1331
+ def initialize: (block: LayoutChild, ?width: (:narrow | :standard | :wide)?, ?horizontal_alignment: (:start | :center | :end)?, ?additional_fields: Hash[String | Symbol, untyped]?) -> void
1332
+ def self.new: (block: LayoutChild, ?width: (:narrow | :standard | :wide)?, ?horizontal_alignment: (:start | :center | :end)?, ?additional_fields: Hash[String | Symbol, untyped]?) -> LayoutItem
1333
+ def self.[]: (block: LayoutChild, ?width: (:narrow | :standard | :wide)?, ?horizontal_alignment: (:start | :center | :end)?, ?additional_fields: Hash[String | Symbol, untyped]?) -> LayoutItem
1334
+ end
1335
+
1252
1336
  module StyleFlags
1253
1337
  NAMES: Array[String]
1254
1338
  end
data/sig/slackblocks.rbs CHANGED
@@ -38,6 +38,10 @@ module Slackblocks
38
38
  end
39
39
 
40
40
  module Validator
41
+ def self.layout_weight: (Hash[String, untyped] item) -> Integer
42
+ def self.composite_children: (Hash[String, untyped] block, ?fallback: bool) -> Array[Hash[String, untyped]]
43
+ def self.composite_depth: (Hash[String, untyped] block, String path, ?Integer depth, ?bool has_layout) -> untyped
44
+ def self.descendants: (Array[Hash[String, untyped]] blocks) -> Array[Hash[String, untyped]]
41
45
  def self.validate: (untyped klass, Hash[Symbol, untyped] values) -> untyped
42
46
  def self.limit: (String key) -> Integer
43
47
  def self.fail!: (String category, String path, String message) -> bot
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: slackblocks
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.1.0
4
+ version: 3.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nicholas Lambourne