416 lines
9.7 KiB
Markdown
416 lines
9.7 KiB
Markdown
# Block Kit
|
|
|
|
Declarative JSON UI for sandboxed plugin admin pages. The host renders blocks — no plugin JavaScript runs in the browser. Inspired by Slack's Block Kit but not identical — similar concepts and naming, different block/element types and capabilities.
|
|
|
|
Trusted plugins (declared in `astro.config.ts`) can ship custom React components instead. Block Kit is for runtime-installed sandboxed plugins.
|
|
|
|
Block Kit elements are also used for [Portable Text block editing fields](./portable-text-blocks.md). When a plugin declares `fields` on a block type, the editor renders a Block Kit form.
|
|
|
|
## How It Works
|
|
|
|
1. User navigates to plugin admin page
|
|
2. Admin sends `page_load` interaction to plugin's admin route
|
|
3. Plugin returns `BlockResponse` with array of blocks
|
|
4. Admin renders blocks using `BlockRenderer`
|
|
5. User interacts (button click, form submit) → interaction sent back
|
|
6. Plugin returns new blocks
|
|
|
|
```typescript
|
|
routes: {
|
|
admin: {
|
|
handler: async (ctx) => {
|
|
const interaction = await ctx.request.json();
|
|
|
|
if (interaction.type === "page_load") {
|
|
return {
|
|
blocks: [
|
|
{ type: "header", text: "My Plugin Settings" },
|
|
{
|
|
type: "form",
|
|
block_id: "settings",
|
|
fields: [
|
|
{ type: "text_input", action_id: "api_url", label: "API URL" },
|
|
{ type: "toggle", action_id: "enabled", label: "Enabled", initial_value: true },
|
|
],
|
|
submit: { label: "Save", action_id: "save" },
|
|
},
|
|
],
|
|
};
|
|
}
|
|
|
|
if (interaction.type === "form_submit" && interaction.action_id === "save") {
|
|
await ctx.kv.set("settings", interaction.values);
|
|
return {
|
|
blocks: [/* updated blocks */],
|
|
toast: { message: "Settings saved", type: "success" },
|
|
};
|
|
}
|
|
},
|
|
},
|
|
}
|
|
```
|
|
|
|
## Block Types
|
|
|
|
| Type | Description |
|
|
| --------- | --------------------------------------------------- |
|
|
| `header` | Large bold heading |
|
|
| `section` | Text with optional accessory element |
|
|
| `divider` | Horizontal rule |
|
|
| `fields` | Two-column label/value grid |
|
|
| `table` | Data table with formatting, sorting, pagination |
|
|
| `actions` | Horizontal row of buttons and controls |
|
|
| `stats` | Dashboard metric cards with trend indicators |
|
|
| `form` | Input fields with conditional visibility and submit |
|
|
| `image` | Block-level image with caption |
|
|
| `context` | Small muted help text |
|
|
| `columns` | 2-3 column layout with nested blocks |
|
|
| `chart` | Charts (timeseries line/bar, pie, custom ECharts) |
|
|
| `code` | Syntax-highlighted code block |
|
|
| `meter` | Progress/quota meter bar |
|
|
| `banner` | Info, warning, or error inline messages |
|
|
|
|
## Element Types
|
|
|
|
| Type | Description |
|
|
| -------------- | ----------------------------------------------- |
|
|
| `button` | Action button with optional confirmation dialog |
|
|
| `text_input` | Single-line or multiline text input |
|
|
| `number_input` | Numeric input with min/max |
|
|
| `select` | Dropdown select |
|
|
| `toggle` | On/off switch |
|
|
| `secret_input` | Masked input for API keys and tokens |
|
|
| `checkbox` | Multi-select checkboxes |
|
|
| `radio` | Single-select radio buttons |
|
|
| `date_input` | Date picker |
|
|
| `combobox` | Searchable dropdown select |
|
|
|
|
## Block Syntax
|
|
|
|
### Header
|
|
|
|
```json
|
|
{ "type": "header", "text": "Settings" }
|
|
```
|
|
|
|
### Section
|
|
|
|
```json
|
|
{
|
|
"type": "section",
|
|
"text": "Configure your plugin settings below.",
|
|
"accessory": { "type": "button", "text": "Refresh", "action_id": "refresh" }
|
|
}
|
|
```
|
|
|
|
### Divider
|
|
|
|
```json
|
|
{ "type": "divider" }
|
|
```
|
|
|
|
### Fields
|
|
|
|
```json
|
|
{
|
|
"type": "fields",
|
|
"fields": [
|
|
{ "label": "Status", "value": "Active" },
|
|
{ "label": "Last Sync", "value": "2 hours ago" }
|
|
]
|
|
}
|
|
```
|
|
|
|
### Stats
|
|
|
|
```json
|
|
{
|
|
"type": "stats",
|
|
"stats": [
|
|
{ "label": "Total", "value": "1,234", "trend": "+12%", "trend_direction": "up" },
|
|
{ "label": "Active", "value": "567" }
|
|
]
|
|
}
|
|
```
|
|
|
|
### Table
|
|
|
|
```json
|
|
{
|
|
"type": "table",
|
|
"columns": [
|
|
{ "key": "name", "label": "Name" },
|
|
{ "key": "status", "label": "Status" },
|
|
{ "key": "date", "label": "Date" }
|
|
],
|
|
"rows": [{ "name": "Item 1", "status": "Active", "date": "2025-01-01" }]
|
|
}
|
|
```
|
|
|
|
### Actions
|
|
|
|
```json
|
|
{
|
|
"type": "actions",
|
|
"elements": [
|
|
{ "type": "button", "text": "Save", "action_id": "save", "style": "primary" },
|
|
{ "type": "button", "text": "Cancel", "action_id": "cancel" }
|
|
]
|
|
}
|
|
```
|
|
|
|
### Form
|
|
|
|
```json
|
|
{
|
|
"type": "form",
|
|
"block_id": "settings",
|
|
"fields": [
|
|
{ "type": "text_input", "action_id": "name", "label": "Name" },
|
|
{ "type": "number_input", "action_id": "count", "label": "Count", "min": 0, "max": 100 },
|
|
{
|
|
"type": "select",
|
|
"action_id": "theme",
|
|
"label": "Theme",
|
|
"options": [
|
|
{ "label": "Light", "value": "light" },
|
|
{ "label": "Dark", "value": "dark" }
|
|
]
|
|
},
|
|
{ "type": "toggle", "action_id": "enabled", "label": "Enabled", "initial_value": true },
|
|
{ "type": "secret_input", "action_id": "api_key", "label": "API Key" }
|
|
],
|
|
"submit": { "label": "Save", "action_id": "save_settings" }
|
|
}
|
|
```
|
|
|
|
### Columns
|
|
|
|
```json
|
|
{
|
|
"type": "columns",
|
|
"columns": [
|
|
{ "blocks": [{ "type": "header", "text": "Left" }] },
|
|
{ "blocks": [{ "type": "header", "text": "Right" }] }
|
|
]
|
|
}
|
|
```
|
|
|
|
### Chart (Timeseries)
|
|
|
|
```json
|
|
{
|
|
"type": "chart",
|
|
"config": {
|
|
"chart_type": "timeseries",
|
|
"series": [
|
|
{
|
|
"name": "Requests",
|
|
"data": [
|
|
[1709596800000, 42],
|
|
[1709600400000, 67],
|
|
[1709604000000, 53]
|
|
],
|
|
"color": "#086FFF"
|
|
},
|
|
{
|
|
"name": "Errors",
|
|
"data": [
|
|
[1709596800000, 2],
|
|
[1709600400000, 5],
|
|
[1709604000000, 1]
|
|
]
|
|
}
|
|
],
|
|
"x_axis_name": "Time",
|
|
"y_axis_name": "Count",
|
|
"style": "line",
|
|
"gradient": true,
|
|
"height": 300
|
|
}
|
|
}
|
|
```
|
|
|
|
- `series[].data` — array of `[timestamp_ms, value]` tuples
|
|
- `series[].color` — hex color (optional, auto-assigned from Kumo palette)
|
|
- `style` — `"line"` (default) or `"bar"`
|
|
- `gradient` — fill gradient beneath lines (default false)
|
|
- `height` — chart height in pixels (default 350)
|
|
|
|
### Chart (Custom)
|
|
|
|
For pie charts, gauges, or any ECharts visualization:
|
|
|
|
```json
|
|
{
|
|
"type": "chart",
|
|
"config": {
|
|
"chart_type": "custom",
|
|
"options": {
|
|
"series": [
|
|
{
|
|
"type": "pie",
|
|
"data": [
|
|
{ "value": 335, "name": "Published" },
|
|
{ "value": 234, "name": "Draft" },
|
|
{ "value": 120, "name": "Scheduled" }
|
|
]
|
|
}
|
|
]
|
|
},
|
|
"height": 300
|
|
}
|
|
}
|
|
```
|
|
|
|
- `options` — raw ECharts option object passed through to `chart.setOption()`
|
|
|
|
### Code
|
|
|
|
```json
|
|
{
|
|
"type": "code",
|
|
"code": "const greeting = \"Hello!\";\nconsole.log(greeting);",
|
|
"language": "ts"
|
|
}
|
|
```
|
|
|
|
- `language` — `"ts"`, `"tsx"`, `"jsonc"`, `"bash"`, or `"css"` (defaults to `"ts"`)
|
|
|
|
### Meter
|
|
|
|
```json
|
|
{
|
|
"type": "meter",
|
|
"label": "Storage used",
|
|
"value": 65,
|
|
"custom_value": "6.5 GB / 10 GB"
|
|
}
|
|
```
|
|
|
|
- `value` — numeric value (default range 0-100)
|
|
- `max` / `min` — custom range (defaults to 0-100)
|
|
- `custom_value` — display string instead of percentage (e.g. "750 / 1,000")
|
|
|
|
### Banner
|
|
|
|
```json
|
|
{
|
|
"type": "banner",
|
|
"title": "API key invalid",
|
|
"description": "Please check your API key in settings.",
|
|
"variant": "error"
|
|
}
|
|
```
|
|
|
|
- `variant` — `"default"` (info, default), `"alert"` (warning), or `"error"`
|
|
- At least one of `title` or `description` is required
|
|
|
|
## Conditional Fields
|
|
|
|
Show/hide fields based on other field values. Evaluated client-side, no round-trip.
|
|
|
|
```json
|
|
{
|
|
"type": "toggle",
|
|
"action_id": "auth_enabled",
|
|
"label": "Enable Authentication"
|
|
}
|
|
```
|
|
|
|
```json
|
|
{
|
|
"type": "secret_input",
|
|
"action_id": "api_key",
|
|
"label": "API Key",
|
|
"condition": { "field": "auth_enabled", "eq": true }
|
|
}
|
|
```
|
|
|
|
## Builder Helpers
|
|
|
|
`@emdashcms/blocks` provides TypeScript helpers:
|
|
|
|
```typescript
|
|
import { blocks, elements } from "@emdashcms/blocks";
|
|
|
|
const { header, form, section, stats, timeseriesChart, customChart, banner: bannerBlock } = blocks;
|
|
const { textInput, toggle, select, button } = elements;
|
|
|
|
return {
|
|
blocks: [
|
|
header("Settings"),
|
|
form({
|
|
blockId: "settings",
|
|
fields: [
|
|
textInput("site_title", "Site Title", { initialValue: "My Site" }),
|
|
toggle("generate_sitemap", "Generate Sitemap", { initialValue: true }),
|
|
select("robots", "Default Robots", [
|
|
{ label: "Index, Follow", value: "index,follow" },
|
|
{ label: "No Index", value: "noindex,follow" },
|
|
]),
|
|
],
|
|
submit: { label: "Save", actionId: "save" },
|
|
}),
|
|
// Timeseries chart
|
|
timeseriesChart({
|
|
series: [
|
|
{
|
|
name: "Page Views",
|
|
data: [
|
|
[Date.now() - 3600000, 100],
|
|
[Date.now(), 150],
|
|
],
|
|
},
|
|
],
|
|
yAxisName: "Views",
|
|
gradient: true,
|
|
}),
|
|
// Pie chart via custom ECharts options
|
|
customChart({
|
|
options: {
|
|
series: [
|
|
{
|
|
type: "pie",
|
|
data: [
|
|
{ value: 335, name: "Published" },
|
|
{ value: 234, name: "Draft" },
|
|
],
|
|
},
|
|
],
|
|
},
|
|
}),
|
|
],
|
|
};
|
|
```
|
|
|
|
## Button Confirmations
|
|
|
|
```json
|
|
{
|
|
"type": "button",
|
|
"text": "Delete All",
|
|
"action_id": "delete_all",
|
|
"style": "danger",
|
|
"confirm": {
|
|
"title": "Are you sure?",
|
|
"text": "This cannot be undone.",
|
|
"confirm": "Delete",
|
|
"deny": "Cancel"
|
|
}
|
|
}
|
|
```
|
|
|
|
## Toast Responses
|
|
|
|
Return a `toast` alongside blocks to show a notification:
|
|
|
|
```typescript
|
|
return {
|
|
blocks: [
|
|
/* ... */
|
|
],
|
|
toast: { message: "Settings saved", type: "success" }, // "success" | "error" | "info"
|
|
};
|
|
```
|