From e1665f348913a143732e9cfbb35a2e405f26cca1 Mon Sep 17 00:00:00 2001 From: sngmin Date: Wed, 22 Apr 2026 19:18:32 +0900 Subject: [PATCH 1/4] feat(blocks): add Tab block to Block Kit --- packages/blocks/src/blocks/tab.tsx | 36 ++++++++++++++++++++++++++++++ packages/blocks/src/builders.ts | 15 +++++++++++++ packages/blocks/src/index.ts | 2 ++ packages/blocks/src/renderer.tsx | 3 +++ packages/blocks/src/types.ts | 14 +++++++++++- 5 files changed, 69 insertions(+), 1 deletion(-) create mode 100644 packages/blocks/src/blocks/tab.tsx diff --git a/packages/blocks/src/blocks/tab.tsx b/packages/blocks/src/blocks/tab.tsx new file mode 100644 index 000000000..6729423ee --- /dev/null +++ b/packages/blocks/src/blocks/tab.tsx @@ -0,0 +1,36 @@ +import { useState } from "react"; +import { BlockRenderer } from "../renderer.js"; +import type { BlockInteraction, TabBlock } from "../types.js"; + +export function TabBlockComponent({ + block, + onAction, + }: { + block: TabBlock; + onAction: (interaction: BlockInteraction) => void; +}) { + const [activeTab, setActiveTab] = useState(block.default_tab ?? 0); + + return ( +
+
+ {block.panels.map((panel, i) => ( + + ))} +
+
+ +
+
+ ); +} \ No newline at end of file diff --git a/packages/blocks/src/builders.ts b/packages/blocks/src/builders.ts index 6255563ea..daac16bb0 100644 --- a/packages/blocks/src/builders.ts +++ b/packages/blocks/src/builders.ts @@ -31,6 +31,8 @@ import type { TableColumn, TextInputElement, ToggleElement, + TabBlock, + TabPanel } from "./types.js"; // ── Block Builders ─────────────────────────────────────────────────────────── @@ -397,6 +399,18 @@ function codeBlock(opts: { }; } +function tabBlock(panels: TabPanel[], opts?: { + defaultTab?: number; + blockId?: string +}): TabBlock { + return { + type: "tab", + panels, + ...(opts?.defaultTab !== undefined && { default_tab: opts.defaultTab }), + ...(opts?.blockId !== undefined && { block_id: opts.blockId }), + }; +} + // ── Exports ────────────────────────────────────────────────────────────────── export const blocks = { @@ -416,6 +430,7 @@ export const blocks = { banner: bannerBlock, meter, code: codeBlock, + tab: tabBlock, }; export const elements = { diff --git a/packages/blocks/src/index.ts b/packages/blocks/src/index.ts index 932c4b326..91e5ab6ca 100644 --- a/packages/blocks/src/index.ts +++ b/packages/blocks/src/index.ts @@ -33,6 +33,7 @@ export type { ChartConfig, TimeseriesChartConfig, CustomChartConfig, + TabPanel, // Blocks HeaderBlock, SectionBlock, @@ -47,6 +48,7 @@ export type { ColumnsBlock, ChartBlock, CodeBlock, + TabBlock, BannerBlock, MeterBlock, Block, diff --git a/packages/blocks/src/renderer.tsx b/packages/blocks/src/renderer.tsx index 476aa34d3..8bdf03c3c 100644 --- a/packages/blocks/src/renderer.tsx +++ b/packages/blocks/src/renderer.tsx @@ -13,6 +13,7 @@ import { MeterBlockComponent } from "./blocks/meter.js"; import { SectionBlockComponent } from "./blocks/section.js"; import { StatsBlockComponent } from "./blocks/stats.js"; import { TableBlockComponent } from "./blocks/table.js"; +import { TabBlockComponent } from "./blocks/tab.js"; import type { Block, BlockInteraction } from "./types.js"; function renderBlock( @@ -50,6 +51,8 @@ function renderBlock( return ; case "code": return ; + case "tab": + return ; default: { const _exhaustive: never = block; return null; diff --git a/packages/blocks/src/types.ts b/packages/blocks/src/types.ts index a003814c2..47aaefdec 100644 --- a/packages/blocks/src/types.ts +++ b/packages/blocks/src/types.ts @@ -281,6 +281,17 @@ export interface CodeBlock extends BlockBase { language?: "ts" | "tsx" | "jsonc" | "bash" | "css"; } +export interface TabPanel { + label: string; + blocks: Block[]; +} + +export interface TabBlock extends BlockBase { + type: "tab"; + panels: TabPanel[]; + default_tab?: number; +} + export type Block = | HeaderBlock | SectionBlock @@ -296,7 +307,8 @@ export type Block = | ChartBlock | BannerBlock | MeterBlock - | CodeBlock; + | CodeBlock + | TabBlock; // ── Interactions ───────────────────────────────────────────────────────────── From 8c4e603727c8db1c8266bca367aaf690e62fe096 Mon Sep 17 00:00:00 2001 From: sngmin Date: Wed, 22 Apr 2026 19:21:11 +0900 Subject: [PATCH 2/4] test(blocks): add Tab block renderer tests --- packages/blocks/tests/renderer.test.tsx | 46 +++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/packages/blocks/tests/renderer.test.tsx b/packages/blocks/tests/renderer.test.tsx index dd02cce65..cc8edccf4 100644 --- a/packages/blocks/tests/renderer.test.tsx +++ b/packages/blocks/tests/renderer.test.tsx @@ -432,6 +432,52 @@ describe("BlockRenderer", () => { expect(screen.getByText("Right")).toBeTruthy(); }); + it("tab block renders panel labels and shows first panel by default", () => { + renderBlocks([ + { + type: "tab", + panels: [ + { label: "General", blocks: [{ type: "header", text: "General Settings" }] }, + { label: "Advanced", blocks: [{ type: "header", text: "Advanced Settings" }] }, + ], + }, + ]); + expect(screen.getByText("General")).toBeTruthy(); + expect(screen.getByText("Advanced")).toBeTruthy(); + expect(screen.getByText("General Settings")).toBeTruthy(); + expect(screen.queryByText("Advanced Settings")).toBeNull(); + }); + + it("tab block switches panel on tab click", () => { + renderBlocks([ + { + type: "tab", + panels: [ + { label: "General", blocks: [{ type: "header", text: "General Settings" }] }, + { label: "Advanced", blocks: [{ type: "header", text: "Advanced Settings" }] }, + ], + }, + ]); + fireEvent.click(screen.getByText("Advanced")); + expect(screen.queryByText("General Settings")).toBeNull(); + expect(screen.getByText("Advanced Settings")).toBeTruthy(); + }); + + it("tab block respects default_tab", () => { + renderBlocks([ + { + type: "tab", + default_tab: 1, + panels: [ + { label: "General", blocks: [{ type: "header", text: "General Settings" }] }, + { label: "Advanced", blocks: [{ type: "header", text: "Advanced Settings" }] }, + ], + }, + ]); + expect(screen.queryByText("General Settings")).toBeNull(); + expect(screen.getByText("Advanced Settings")).toBeTruthy(); + }); + it("button click fires onAction with block_action", () => { const onAction = vi.fn(); renderBlocks( From 76e5b145ec68675c44f6df1f9457ed166f311b74 Mon Sep 17 00:00:00 2001 From: sngmin Date: Wed, 22 Apr 2026 19:26:19 +0900 Subject: [PATCH 3/4] chore: add changeset for Tab block --- .changeset/lazy-shrimps-stand.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/lazy-shrimps-stand.md diff --git a/.changeset/lazy-shrimps-stand.md b/.changeset/lazy-shrimps-stand.md new file mode 100644 index 000000000..e5715a16a --- /dev/null +++ b/.changeset/lazy-shrimps-stand.md @@ -0,0 +1,5 @@ +--- +"@emdash-cms/blocks": minor +--- + +Add Tab block to Block Kit for tabbed panel layouts From cd7ea875e79c2c024c40bb699bfb0e53cad630ba Mon Sep 17 00:00:00 2001 From: sngmin Date: Wed, 22 Apr 2026 19:26:51 +0900 Subject: [PATCH 4/4] style: apply formatter to Tab block files --- packages/blocks/src/blocks/tab.tsx | 9 +++++---- packages/blocks/src/builders.ts | 13 ++++++++----- packages/blocks/src/renderer.tsx | 2 +- 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/packages/blocks/src/blocks/tab.tsx b/packages/blocks/src/blocks/tab.tsx index 6729423ee..60d3110f2 100644 --- a/packages/blocks/src/blocks/tab.tsx +++ b/packages/blocks/src/blocks/tab.tsx @@ -1,11 +1,12 @@ import { useState } from "react"; + import { BlockRenderer } from "../renderer.js"; import type { BlockInteraction, TabBlock } from "../types.js"; export function TabBlockComponent({ - block, - onAction, - }: { + block, + onAction, +}: { block: TabBlock; onAction: (interaction: BlockInteraction) => void; }) { @@ -33,4 +34,4 @@ export function TabBlockComponent({ ); -} \ No newline at end of file +} diff --git a/packages/blocks/src/builders.ts b/packages/blocks/src/builders.ts index daac16bb0..db5afbc27 100644 --- a/packages/blocks/src/builders.ts +++ b/packages/blocks/src/builders.ts @@ -32,7 +32,7 @@ import type { TextInputElement, ToggleElement, TabBlock, - TabPanel + TabPanel, } from "./types.js"; // ── Block Builders ─────────────────────────────────────────────────────────── @@ -399,10 +399,13 @@ function codeBlock(opts: { }; } -function tabBlock(panels: TabPanel[], opts?: { - defaultTab?: number; - blockId?: string -}): TabBlock { +function tabBlock( + panels: TabPanel[], + opts?: { + defaultTab?: number; + blockId?: string; + }, +): TabBlock { return { type: "tab", panels, diff --git a/packages/blocks/src/renderer.tsx b/packages/blocks/src/renderer.tsx index 8bdf03c3c..3b28e1ef9 100644 --- a/packages/blocks/src/renderer.tsx +++ b/packages/blocks/src/renderer.tsx @@ -12,8 +12,8 @@ import { ImageBlockComponent } from "./blocks/image.js"; import { MeterBlockComponent } from "./blocks/meter.js"; import { SectionBlockComponent } from "./blocks/section.js"; import { StatsBlockComponent } from "./blocks/stats.js"; -import { TableBlockComponent } from "./blocks/table.js"; import { TabBlockComponent } from "./blocks/tab.js"; +import { TableBlockComponent } from "./blocks/table.js"; import type { Block, BlockInteraction } from "./types.js"; function renderBlock(