77 * real thing rendered live. The order runs everyday → text → data → media →
88 * interaction → agent, so it reads as a demonstration rather than an inventory.
99 *
10- * Inside the interaction stretch the order is `input` and then `actions`, on the
11- * product owner's instruction and against the order first agreed. With the
12- * button above the fields a reader takes the two examples for one form whose
13- * submit control has slipped to the top; with the fields first the button reads
14- * as what follows them.
10+ * Inside the interaction stretch the order is `actions` and then `input`: the
11+ * order agreed with the product owner, and the one the pinned heading test
12+ * holds.
1513 *
1614 * Page 2 is one situation carried through: booking a meeting. A form — channel,
1715 * time, length, email — a Confirm button, and a confirmation that reports the
6765 * date pickers. A multi-select belongs in an `input` block.
6866 * 5. An `input` block in a *message* only dispatches to a handler when
6967 * `dispatch_action` is true.
70- * 6. Slack fetches image and video URLs at post time; a dead URL refuses the
71- * whole message. The URL used here is verified.
68+ * 6. Slack fetches image, thumbnail and video URLs at post time; a dead URL
69+ * refuses the whole message. Every URL used here is verified reachable .
7270 * 7. `icon_button` accepts only a few icon names; `trash` is verified.
7371 * 8. `plan.title` is a bare string, not a text object — the opposite of every
7472 * other titled block, and contrary to Slack's own reference.
8280 * here lets colour carry meaning.
8381 * 13. A `data_visualization` title is capped at 50 characters, series and
8482 * category labels at 20, and every series' point labels must match
85- * `axis_config.categories` exactly.
83+ * `axis_config.categories` exactly. A `pie` chart is the exception: it
84+ * carries `segments` and no `axis_config`, so there is nothing to match.
8685 * 14. `card` holds a title, a subtitle, a hero image, a body and actions — it
8786 * has no children slot, so it cannot frame arbitrary blocks. `container` is
8887 * the block that groups other blocks, and it is what frames the examples
@@ -127,6 +126,17 @@ const RadioButtons = Slack.Element.RadioButtons as unknown as UntypedNative;
127126/** Verified reachable at post time; Slack refuses the message if a fetch fails. */
128127const IMAGE_URL = "https://picsum.photos/id/1015/800/400" ;
129128
129+ /**
130+ * The video example embeds Blender's "Big Buck Bunny" short film — the clip the
131+ * SDK's own `catalog-fixtures.ts` uses, and the one URL pair proven to survive
132+ * Slack's post-time fetch. Its thumbnail is YouTube's own still for that same
133+ * video, which YouTube's oEmbed response names as the video's `thumbnail_url`,
134+ * so the preview shows the film that plays rather than an unrelated photograph.
135+ * Verified reachable: 200 image/jpeg.
136+ */
137+ const VIDEO_ID = "aqz-KE-bpKQ" ;
138+ const VIDEO_THUMBNAIL_URL = `https://i.ytimg.com/vi/${ VIDEO_ID } /hqdefault.jpg` ;
139+
130140// ── local text helpers ─────────────────────────────────────────────────────
131141
132142/** A `mrkdwn` section. `text`, never children — see constraint 9. */
@@ -232,6 +242,19 @@ const RENEWAL_RATE_BY_MONTH: ReadonlyArray<readonly [string, number]> = [
232242 [ "Dec" , 97 ] ,
233243] ;
234244
245+ /**
246+ * The pie beneath the line, so the example shows Slack drawing more than one
247+ * kind of chart. A pie payload is `segments`, with no `axis_config` — the
248+ * categories rule applies only to bar, area and line. The counts are the ones
249+ * the table example already shows, so the two do not contradict each other, and
250+ * every label is inside the 20-character cap.
251+ */
252+ const RENEWALS_BY_REGION : ReadonlyArray < readonly [ string , number ] > = [
253+ [ "Americas" , 214 ] ,
254+ [ "EMEA" , 128 ] ,
255+ [ "APAC" , 76 ] ,
256+ ] ;
257+
235258const CAROUSEL_CARDS : ReadonlyArray < [ string , string , string ] > = [
236259 [ "card-americas" , "Americas" , "214 renewals · 96% on time" ] ,
237260 [ "card-emea" , "EMEA" , "128 renewals · 91% on time" ] ,
@@ -413,13 +436,15 @@ const EXAMPLES: readonly Example[] = [
413436 } ,
414437 {
415438 emoji : "📈" ,
416- heading : "A trend at a glance " ,
439+ heading : "A trend over time, then a share of the whole " ,
417440 blocks : [ "data_visualization" ] ,
418441 description :
419- "Slack draws the chart itself, so a run of numbers reads as a shape " +
420- "instead of a list." ,
442+ "Slack draws the charts itself, and more than one kind: a line for how " +
443+ "the renewal rate moved through the year, and a pie underneath it for " +
444+ "how this quarter's renewals divide between the regions." ,
421445 specimen : [
422446 < Slack . Block . DataVisualization
447+ key = "trend"
423448 title = "Renewal rate by month"
424449 chart = { {
425450 type : "line" ,
@@ -439,6 +464,20 @@ const EXAMPLES: readonly Example[] = [
439464 } ,
440465 } }
441466 /> ,
467+ // A second `data_visualization`, a sibling of the first and of every
468+ // container — a chart is never a container child (constraint 2). This is
469+ // the block page 1 grew by, from 47 to 48 against the ceiling of 50.
470+ < Slack . Block . DataVisualization
471+ key = "share"
472+ title = "Renewals by region this quarter"
473+ chart = { {
474+ type : "pie" ,
475+ segments : RENEWALS_BY_REGION . map ( ( [ label , value ] ) => ( {
476+ label,
477+ value,
478+ } ) ) ,
479+ } }
480+ /> ,
442481 ] ,
443482 } ,
444483 {
@@ -461,14 +500,15 @@ const EXAMPLES: readonly Example[] = [
461500 blocks : [ "video" ] ,
462501 description :
463502 "A recording plays in the channel itself, so nobody has to open " +
464- "another tab to watch it." ,
503+ "another tab to watch it. The still above it is the video's own frame, " +
504+ "so the preview shows what will play." ,
465505 specimen : [
466506 < Video
467- alt_text = "Recorded walkthrough "
468- title = { Plain ( "Walkthrough · the renewals workflow " ) }
469- title_url = " https://www.youtube.com/watch?v=aqz-KE-bpKQ"
470- thumbnail_url = { IMAGE_URL }
471- video_url = " https://www.youtube.com/embed/aqz-KE-bpKQ"
507+ alt_text = "Still from Blender's Big Buck Bunny short film "
508+ title = { Plain ( "Big Buck Bunny · Blender Foundation short film " ) }
509+ title_url = { ` https://www.youtube.com/watch?v=${ VIDEO_ID } ` }
510+ thumbnail_url = { VIDEO_THUMBNAIL_URL }
511+ video_url = { ` https://www.youtube.com/embed/${ VIDEO_ID } ` }
472512 /> ,
473513 ] ,
474514 } ,
@@ -521,47 +561,19 @@ const EXAMPLES: readonly Example[] = [
521561 /> ,
522562 ] ,
523563 } ,
524- // The fields come before the button on the product owner's instruction — see
525- // the note at the top of the file. Read the other way round, the two examples
526- // look like one form with its submit control above the things it submits.
527- {
528- emoji : "📥" ,
529- heading : "A small input right in the channel" ,
530- blocks : [ "input" ] ,
531- description :
532- "A field in the message itself, so an answer can be given on the spot " +
533- "instead of in a dialog somewhere else." ,
534- specimen : [
535- < Input
536- label = { Plain ( "Teams to include in the review" ) }
537- dispatch_action = { true }
538- element = {
539- < Slack . Element . MultiStaticSelect
540- placeholder = { Plain ( "Pick one or more teams" ) }
541- options = { TEAMS . map ( ( [ label , value ] ) => option ( label , value ) ) }
542- onSelect = { reply ( "input · multi-select" ) }
543- />
544- }
545- /> ,
546- ] ,
547- } ,
548564 {
549565 emoji : "🔘" ,
550566 heading : "Act without leaving Slack" ,
551567 blocks : [ "actions" ] ,
552568 description :
553569 "Buttons, a menu and a date picker sitting under the message, each one " +
554570 "wired to something that actually runs. Try them." ,
571+ // The button is last of the three, so the row reads left to right as choose
572+ // a reviewer, choose a date, then act on both. Elements render in array
573+ // order, so this ordering is the only thing that places it.
555574 specimen : [
556575 < Slack . Block . Actions
557576 elements = { [
558- < Slack . Element . Button
559- key = "approve"
560- style = "primary"
561- text = { Plain ( "Approve renewal" ) }
562- value = "approve_renewal"
563- onClick = { reply ( "actions · button" ) }
564- /> ,
565577 < Slack . Element . StaticSelect
566578 key = "reviewer"
567579 placeholder = { Plain ( "Choose a reviewer" ) }
@@ -574,10 +586,38 @@ const EXAMPLES: readonly Example[] = [
574586 initial_date = "2026-09-12"
575587 onSelect = { reply ( "actions · date picker" ) }
576588 /> ,
589+ < Slack . Element . Button
590+ key = "approve"
591+ style = "primary"
592+ text = { Plain ( "Approve renewal" ) }
593+ value = "approve_renewal"
594+ onClick = { reply ( "actions · button" ) }
595+ /> ,
577596 ] }
578597 /> ,
579598 ] ,
580599 } ,
600+ {
601+ emoji : "📥" ,
602+ heading : "A small input right in the channel" ,
603+ blocks : [ "input" ] ,
604+ description :
605+ "A field in the message itself, so an answer can be given on the spot " +
606+ "instead of in a dialog somewhere else." ,
607+ specimen : [
608+ < Input
609+ label = { Plain ( "Teams to include in the review" ) }
610+ dispatch_action = { true }
611+ element = {
612+ < Slack . Element . MultiStaticSelect
613+ placeholder = { Plain ( "Pick one or more teams" ) }
614+ options = { TEAMS . map ( ( [ label , value ] ) => option ( label , value ) ) }
615+ onSelect = { reply ( "input · multi-select" ) }
616+ />
617+ }
618+ /> ,
619+ ] ,
620+ } ,
581621 {
582622 emoji : "👍" ,
583623 heading : "Give feedback on a result" ,
@@ -996,11 +1036,12 @@ export function BlockCatalog(state: CatalogState = INITIAL_STATE): ChannelNode {
9961036
9971037/**
9981038 * The measured size of page 1 with the navigator on it: the fourteen examples'
999- * 44 blocks plus the rule, the Next button and the page counter. Pinned by a
1039+ * 45 blocks plus the rule, the Next button and the page counter. Pinned by a
10001040 * test against Slack's ceiling of 50, because exceeding it loses the whole
1001- * message rather than the excess blocks.
1041+ * message rather than the excess blocks. Two blocks of headroom is all that is
1042+ * left, so an example that needs a new block has to trade for one.
10021043 */
1003- export const PAGE_1_BLOCK_COUNT = 47 ;
1044+ export const PAGE_1_BLOCK_COUNT = 48 ;
10041045
10051046// Printed at load, not through the logger: it is the one line that answers "is
10061047// the running process the code I just edited?" before anything is triggered —
0 commit comments