@brilo/base-blocks

中文文档
0.1.77发布于 2026/11/04TypeScriptCommonJS近 30 天下载 2576

Shared Gutenberg block factory library. Tenants call factory functions, register the result, and own only block.json + icon files. This file is the only doc — when it disagrees with src/blocks/*, the code wins.

原始使用文档
文档目录14 节
  1. Install (tenant)
  2. Commands
  3. Structure
  4. Block files
  5. Naming
  6. Imports
  7. Types
  8. Options pattern
  9. Generic factories
  10. Edit component
  11. Shared components and hooks
  12. New block checklist
  13. Tenant usage
  14. Styles (tenant)

@brilo/base-blocks

Shared Gutenberg block factory library. Tenants call factory functions, register the result, and own only block.json + icon files. This file is the only doc — when it disagrees with src/blocks/*, the code wins.

Install (tenant)

Published as @brilo/base-blocks npm package — normal bun install in tenant pulls it.

For local dev against uncommitted base-blocks changes, use bun link:

# in base-blocks
bun run build
bun link

# in tenant gutenberg dir
bun link @brilo/base-blocks

Re-run bun run build in base-blocks after any change — link points at build/, not src/.

Commands

bun run build      # biome check + tsc → build/
bun run check      # biome check --write + tsc --noEmit
bun run format     # biome (TS) + prettier (SCSS)

Structure

src/
  blocks/
    <domain>/     one per content type, mirrors frontoffice twig components 1:1:
                  anchor/, compare/, contact/, faq/, gallery/, hero/, person/,
                  signpost/, step/, testimonial/, usp/ … each with <domain>-group/
                  and/or <domain>-item/
    core/         atomic, no inner blocks: basic-button · basic-heading · basic-overline
    groups/       generic (non-domain) layout groups: banner-group · basic-header-group ·
                  button-group · content-group · text-and-media-group
    sections/     top-level page sections: base-section
    layout/       aside/content layout blocks
    form/         form-group · form-field
    link/ · map/ · newsletter/ · post/ · responsive-grid/
  components/   IconControl · ImagePicker · ItemIconControl · PaletteColorControl
  hooks/        useGroupBlock · useTenantColorPalette
  utils/        createEditAdapter
  constants.ts  shared HEADING_LEVELS · HEADING_STYLES · CONTENT_SIZES
  editor.scss   SCSS barrel — @forward all block + component + global styles
  globals.scss  global editor utility classes (flex, gap-*, u-grid-*, …)
  index.ts      JS/TS barrel
  types.ts      shared types (BriloBlockType, SelectOption, Icon, OptionArray, …)

Don't invent new top-level folders: a block mirroring a frontoffice component goes in its domain folder, an atomic one in core/, a generic wrapper with no content domain in groups/.

Suffix tells the job, same as frontoffice:

suffixjob
-groupInnerBlocks container. Layout/columns/direction controls. Restricts children via allowedBlocks. Never renders entity markup.
-itemSingle entity block inside a matching group (parent: ['brilo/xxx-group']). Never iterates.
-sectionTop-level page section, wraps groups (sections/base-section).
-form / -fieldForm container / single field.

Block files

blocks/<domain>/<block-name>/
├── factory.tsx   required
├── edit.tsx      required
├── types.ts      required
├── index.ts      required
├── constants.ts  only if the block has option arrays
├── icon.tsx      only if the block needs a custom icon
└── editor.scss   only if the block needs editor styles
fileexportsrule
factory.tsxcreateXxxBlock(options) → BlockConfiguration<XxxAttributes>Only place the WP attributes schema is defined. save: () => null (atomic/item) or () => <InnerBlocks.Content /> (group/section). Re-exports XxxAttributes/XxxOptions.
edit.tsxcreateXxxEdit(options) → React componentNo inline arrays — pull from constants.ts. No module-level __().
types.tsXxxAttributes, XxxOptionsXxxAttributes extends Record<string, unknown>, XxxOptions extends BriloBlockType.
constants.tsoption arrays, DEFAULT_*as const. Translated labels live here, not in edit.tsx.
icon.tsxicon (JSX <svg>)Otherwise the factory uses a WP icon slug string (e.g. 'admin-links').
index.tsbarrel + xxxDefaultsxxxDefaults is defined here, not in factory.tsx. Constants not re-exported stay block-private.
editor.scsseditor-only styles@forward it from src/editor.scss. No frontend styles.

Naming

thingpatternexample
factorycreateXxxBlockcreateBasicButtonBlock
edit factorycreateXxxEditcreateBasicButtonEdit
attributes typeXxxAttributesBasicButtonAttributes
options typeXxxOptionsBasicButtonOptions
defaults objectxxxDefaultsbasicButtonDefaults

Constants are SCREAMING_SNAKE. DEFAULT_ prefix only for a fallback of a tenant-overridable option; fixed sets (HEADING_LEVELS, CHIP_POSITIONS) have no prefix.

Imports

tsconfig.json maps ~/* → ./src/*. Shared code is always ~/types, ~/constants, ~/hooks/..., ~/components/... — never ../../../. Block-local code is ./constants, ./types. Name conflict → import alias (import {HEADING_LEVELS as SHARED_HEADING_LEVELS} from '~/constants').

Types

BriloBlockType (from ~/types) is the shared base of every XxxOptions: name, title?, description?, category?, allowedBlocks?, template?, blockClassName?, containerClassName?, icon?, parent?. SelectOption ({value, label}) also lives in ~/types — never redeclare it in a block.

Attribute unions are derived from as const arrays, never written by hand:

// constants.ts
export const MEDIA_POSITIONS = [
  {value: 'left', label: __('Vlevo', 'brilo-blocks')},
  {value: 'right', label: __('Vpravo', 'brilo-blocks')},
] as const
export const HEADING_LEVELS = [1, 2, 3] as const

// types.ts
export type MediaPosition = (typeof MEDIA_POSITIONS)[number]['value'] // 'left' | 'right'
export type HeadingLevel = (typeof HEADING_LEVELS)[number] // 1 | 2 | 3

Use the derived type for the attribute too (headingLevel: HeadingLevel, not number), so casts like tagName={`h${headingLevel}` as `h${HeadingLevel}`} follow the constant. A block that allows only a subset of heading levels defines its own HEADING_LEVELS; otherwise re-export the shared one from ~/constants.

Options pattern

Every tenant-overridable value has a DEFAULT_* in constants.ts, resolved once with ??. That resolved value — never the raw default — feeds both the attribute enum and the edit UI, or tenant overrides get silently dropped:

// ✓ reflects tenant override
const variants = options.variants ?? DEFAULT_VARIANTS
attributes: {variant: {type: 'string', default: variants[0].value, enum: variants.map((v) => v.value)}}

// ✗ ignores tenant override
attributes: {variant: {enum: DEFAULT_VARIANTS.map((v) => v.value)}}

Mandatory options (like name) have no default and are required in the interface.

Generic factories

A factory whose options contain overridable enum arrays is generic, so TypeScript narrows attribute literals to exactly what the tenant passed. Only overridable arrays become type params — fixed sets stay hardcoded. usp/usp-group/factory.tsx is the fullest example.

// factory.tsx
export function createBasicButtonBlock<
  TVariants extends OptionArray = typeof DEFAULT_VARIANTS,
  TSizes extends OptionArray = typeof DEFAULT_SIZES,
>(options: BasicButtonOptions<TVariants, TSizes>): BlockConfiguration<BasicButtonAttributes<TVariants[number]['value'], TSizes[number]['value']>>

// edit.tsx stays non-generic; factory casts when calling it
edit: createBasicButtonEdit(options as unknown as BasicButtonOptions),

Constraint types from ~/types: OptionArray (ReadonlyArray<{value; label}>), NumberArray, StringArray.

Tenant side — as const is required for literal inference:

createBasicButtonBlock({...basicButtonDefaults}) // variant: 'primary' | 'secondary'

const MY_VARIANTS = [
  {value: 'primary', label: 'Primární'},
  {value: 'brand', label: 'Brand'},
] as const
createBasicButtonBlock({...basicButtonDefaults, variants: MY_VARIANTS}) // variant: 'primary' | 'brand'

Edit component

Group blocks use useGroupBlock (wraps useBlockProps + useInnerBlocksProps, accepts templateLock, orientation, layout, renderAppender); item/atomic blocks use plain useBlockProps.

const {blockProps, innerBlocksProps} = useGroupBlock({
  blockClassName: options.blockClassName ?? 'g-usp',
  containerClassName: 'g-usp__list',
  allowedBlocks: options.allowedBlocks,
  template: options.template,
})

return (
  <div {...blockProps}>
    <div {...innerBlocksProps} />
  </div>
)

Classes always via clsx, BEM, same root class as the twig component it renders into (g- group, i- item, base- section) so editor.scss and theme SCSS agree:

const blockProps = useBlockProps({
  className: clsx(options.blockClassName ?? 'i-button', `--variant-${variant}`, outline && '--outline'),
})

All user-visible strings: __('…', 'brilo-blocks').

Shared components and hooks

Use these instead of reimplementing the UI inside a block:

  • PaletteColorControl — labelled tenant-palette colour picker (label, help?, id?, value, onChange, enableAlpha?, clearable?). Takes and returns '' for "no colour". Use for every colour attribute instead of BaseControl + ColorPalette + useTenantColorPalette.
  • IconControl — grid of preselected SVG icons.
  • ItemIconControl + ItemIconPreview — icon control with the "preselected vs. media" toggle (anchor-item, usp-item).
  • ImagePickerControl (sidebar upload/select) + ImagePickerBody (canvas preview), useMediaUrl(id).
  • useTenantColorPalette — tenant paletteColors + resolveHex (for ContrastChecker).

New block checklist

  1. Pick domain folder + suffix (see Structure).
  2. types.ts: XxxAttributes, XxxOptions.
  3. constants.ts if any option arrays.
  4. factory.tsx: resolve options with ??, build attributes, return BlockConfiguration.
  5. edit.tsx: createXxxEdit(options), controls in InspectorControls → PanelBody.
  6. icon.tsx only if needed.
  7. index.ts barrel + xxxDefaults.
  8. editor.scss, @forward from src/editor.scss.
  9. Export from src/index.ts: export type {XxxAttributes, XxxOptions} + export {createXxxBlock, xxxDefaults}.
  10. bun run build, then register in the tenant (below).

Minimal templates:

// factory.tsx
import type {BlockConfiguration} from '@wordpress/blocks'
import {createXxxEdit} from './edit'
import type {XxxAttributes, XxxOptions} from './types'

export type {XxxAttributes, XxxOptions}

export function createXxxBlock(options: XxxOptions): BlockConfiguration<XxxAttributes> {
  return {
    name: options.name,
    title: options.title ?? 'TODO',
    description: options.description,
    category: options.category ?? 'common',
    ...(options.parent && {parent: options.parent}),
    supports: {html: false, customClassName: false},
    attributes: {},
    edit: createXxxEdit(options),
    save: () => null,
  } as unknown as BlockConfiguration<XxxAttributes>
}
// index.ts
export type {XxxAttributes, XxxOptions} from './types'
export {createXxxBlock} from './factory'
export {createXxxEdit} from './edit'

import type {XxxOptions} from './types'

export const xxxDefaults: XxxOptions = {
  name: 'brilo/xxx',
  parent: ['brilo/xxx-group'],
}

Tenant usage

import {createUspItemBlock, uspItemDefaults} from "@brilo/base-blocks"
import {registerBlockType} from "@wordpress/blocks"
import metadata from "./block.json"
import {ICONS, ICONS_SRC} from "./icons"

const config = createUspItemBlock({
  ...uspItemDefaults,
  name: metadata.name,
  parent: metadata.parent as string[],
  icons: ICONS,
  iconsSrc: ICONS_SRC,
})

registerBlockType(config.name as string, config)

Every tenant block dir needs a block.json next to index.tsx: build metadata ($schema, apiVersion, name, editorScript, editorStyle) plus title, description, parent/allowedBlocks, attributes (with enum for option sets) and an example, mirrored from the factory. The block catalog reads them via /wp/v2/block-types/brilo.

Blocks that support it take extraAttributes (tenant attributes on top of base ones) and extraControls (extra Inspector controls below the base ones); createBaseSectionBlock also takes exclude to drop base attributes:

type ExtraAttrs = {overlap: boolean}

const config = createBaseSectionBlock<ExtraAttrs>({
  name: "my-tenant/base-section",
  allowedBlocks: ["my-tenant/basic-header-group", "my-tenant/usp-group"],
  blockClassName: "s-base",
  containerClassName: "s-base__inner",
  extraAttributes: {overlap: {type: "boolean", default: false}},
  extraControls: ({attributes, setAttributes}) => (
    <ToggleControl
      label={__("Překrytí sekce", "brilo-blocks")}
      checked={attributes.overlap}
      onChange={(value) => setAttributes({overlap: value})}
    />
  ),
  // exclude: ["background"],
})

Styles (tenant)

In tenant index.scss:

@use "@brilo/base-blocks/src/editor";  /* block styles, mostly in @layer base */
@import "@brilo/gutenberg-toolkit/build/index.css";

@layer base, tenant;  /* tenant layer always wins */

Override base styles in tenant SCSS with @layer tenant { } — works only for files whose styles sit in @layer base. Some block editor.scss files are still unlayered: they rely on beating unlayered WP editor defaults (h*/p/hr margins and font sizes, .wp-block margins) and on overriding .i-item-icon. Layered CSS loses to any unlayered CSS, so override those unlayered too. Moving them into the layer needs a per-file check in the editor.

版本与发布记录

@brilo/base-blocks 共发布 74 个版本,首次发布于 2026/06/16;过去 12 个月发布了 74 个版本。

版本发布日期距今
0.1.77latest2026/11/044 天前
0.1.762026/11/044 天前
0.1.752026/10/309 天前
0.1.742026/10/309 天前
0.1.732026/10/309 天前
0.1.712026/10/309 天前

关于 @brilo/base-blocks 的常见问题

@brilo/base-blocks 是什么?

Shared Gutenberg block factory library. Tenants call factory functions, register the result, and own only block.json + icon files. This file is the only doc — when it disagrees with src/blocks/*, the code wins.

如何安装 @brilo/base-blocks?

在项目目录运行 npm install @brilo/base-blocks,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:react(^18.3.0)、@wordpress/url(^5.0.0)、@wordpress/data(^11.0.0)、@wordpress/date(^6.0.0)、@wordpress/i18n(^7.0.0)、@wordpress/icons(^19.0.0)、@wordpress/blocks(^17.0.0)、@wordpress/element(^10.0.0)、@wordpress/api-fetch(^8.0.0)、@wordpress/dataviews(^21.0.0)、@wordpress/components(^43.0.0)、@wordpress/block-editor(^19.0.0)、@wordpress/html-entities(^5.0.0)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。

@brilo/base-blocks 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@brilo/base-blocks 支持 ESM 还是 CommonJS?

以 CommonJS 形式发布,可以直接 require();在 ES Module 项目中也可以用 import 引入(Node.js 会把 module.exports 作为默认导出)。

@brilo/base-blocks 可以商用吗?

@brilo/base-blocks 使用 未声明 许可。package.json 没有声明许可证。没有许可证的代码默认保留全部权利,商用前请联系作者或查看仓库中的 LICENSE 文件。以上为概括,不构成法律意见,请以许可原文为准。

@brilo/base-blocks 的下载量有多少?

根据 npm 官方下载统计,@brilo/base-blocks 在 2026/10/09 – 2026/11/07 这 30 天内被下载 2,576 次,日均约 86 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@brilo/base-blocks 还在维护吗?

最新版本 0.1.77 发布于 2026/11/04(4 天前);至今共发布 74 个版本,其中过去 12 个月发布了 74 个;首次发布于 2026/06/16。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。