@typeroll/mcp-server 0.45.61 → 0.45.62
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.
- package/AGENTS.md +1 -1
- package/dist/bundled-content.js +3 -3
- package/dist/server.js +5 -2
- package/dist/tools/block-types.js +15 -2
- package/dist/tools/page-blocks.js +4 -2
- package/dist/tools/reusable-blocks.js +1 -1
- package/dist/tools/styles.js +14 -5
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/skills/tr-custom-blocks.md +7 -0
- package/skills/tr-new-site.md +6 -0
package/dist/server.js
CHANGED
|
@@ -165,8 +165,11 @@ The full playbook ships with this server — use it:
|
|
|
165
165
|
style through a block's style_id, and create_style for any look that could
|
|
166
166
|
recur. Use one-off block values only for something clearly unique, never
|
|
167
167
|
classed raw HTML. Text must meet WCAG AA contrast. A heading's small label
|
|
168
|
-
is its eyebrow field.
|
|
169
|
-
|
|
168
|
+
is its eyebrow field. A block's sub-elements (links, a hero's heading or
|
|
169
|
+
buttons) are parts (read_block_type): style them site-wide with a style's
|
|
170
|
+
parts or on one block with data.part_styles. For what styles cannot
|
|
171
|
+
express, use page or site custom_css on s-<style> or block custom_class
|
|
172
|
+
selectors, not [data-block].
|
|
170
173
|
7. Reuse: content that must stay identical on many pages is a global block
|
|
171
174
|
(make_block_global; core/global_block references it). A section people
|
|
172
175
|
start from and then adapt is a block template (list_block_templates,
|
|
@@ -39,6 +39,7 @@ const AUTHORING_GUIDE = 'Two kinds of site block type. COMPOSED (preferred): `co
|
|
|
39
39
|
'TEMPLATE (advanced, when existing blocks cannot express the markup): `template` is HTML with {{field}} (escaped), {{{field}}} (raw: richtext fields and derived …_svg / …_html only), {{#field}}…{{/field}} (shown when set), {{^field}}…{{/field}} (shown when empty), {{#each items}}…{{/each}} (per array item; names resolve against the item first; {{@index}}, {{@number}}, {{@first}}, {{@last}}), {{#link field class="…"}}…{{/link}} (wraps the body in a safe <a href rel target> when the link resolves), plus {{children}} / {{slot:NAME}} for containers. ' +
|
|
40
40
|
'Fields: text, textarea, richtext, image, icon (derived {name}_svg), select (options), boolean, number, url, link ({ page_id?, url?, new_tab? } — prefer page_id for a page on the Site, it survives slug changes; resolves to .href, .target, .rel), array (a repeating group: fields, item_label, min_items, max_items; nests 3 levels), object, style … each with name, type, label and optional help, required, default. ' +
|
|
41
41
|
'`styles` is scoped to the block when written: every selector applies inside [data-block="<name>"], :scope is the block element itself, body/html/:root are refused; use the site\'s var(--color-*) and spacing tokens. The response returns `styles_compiled`, the CSS that ships. ' +
|
|
42
|
+
'`parts` names elements of the markup (a title, a button, links) so site styles can restyle them without CSS against the block: [{ name, label, selector, target }]. ' +
|
|
42
43
|
'Unknown properties are errors. Every problem has a JSON-pointer `path` (and a `line` for markup/CSS); a 400 lists them all in `problems`, warnings come back in `warnings`. Call validate_block_type / preview_block_type first, and list_block_type_starters for complete examples. Needs WRITE permission on the Site; editors can author types as well as place them on pages.';
|
|
43
44
|
const definitionShape = {
|
|
44
45
|
label: z.string().optional().describe('Display label in the block picker.'),
|
|
@@ -61,6 +62,18 @@ const definitionShape = {
|
|
|
61
62
|
template: z.string().optional().describe('Markup of a template type (instead of composition); see the section syntax above.'),
|
|
62
63
|
styles: z.string().optional().describe('CSS scoped to the block. Write selectors for the block\'s own markup; :scope is the block element.'),
|
|
63
64
|
script: z.string().optional().describe("Client-side JS shipped with the block (runs on the published site's own origin). Accepted under your API key's authority — the write is audit-logged. Prefer a composed type without script."),
|
|
65
|
+
parts: z.array(z.object({
|
|
66
|
+
name: z.string().describe('Stable name: a-z, 0-9 and _ (max 32), starting with a letter. Never rename a part in use: part_styles and styles refer to it.'),
|
|
67
|
+
label: z.string().describe('Shown in the editor, e.g. "Primary button".'),
|
|
68
|
+
selector: z.string().describe('The element(s), relative to the block root: tags, .classes, [attributes], structural pseudo-classes (:first-child, :nth-child()) and combinators, up to four comma-separated. No braces, at-rules, pseudo-elements or :hover/:focus (hover comes from the style).'),
|
|
69
|
+
target: z.enum(['text', 'heading', 'list', 'button', 'container', 'link', 'image']).describe('The kind of style that fits the part.'),
|
|
70
|
+
style_field: z.string().optional().describe('A `style` field of this type that already chooses the part\'s style per instance; part_styles then skips the part.'),
|
|
71
|
+
color: z.string().optional().describe('Colour token of the part\'s text in the block\'s CSS, for contrast checks (default text).'),
|
|
72
|
+
background: z.string().optional().describe('Colour token behind the part, for contrast checks (default the site background).'),
|
|
73
|
+
background_field: z.string().optional().describe('A `color` field of this type holding the background behind the part.'),
|
|
74
|
+
decorative: z.boolean().optional().describe('The part holds no text (an overlay, a layout row): no text contrast check.'),
|
|
75
|
+
min_render_version: z.number().int().optional(),
|
|
76
|
+
}).passthrough()).optional().describe('Named sub-elements (at most 16) that site styles can reach: site-wide through a style\'s `parts` ("<type id>.<part>"), on one block through its `part_styles` data, a field added automatically when a part has no style_field. Mainly for template types; a warning names a selector class the template never writes. Composed types\' inner blocks keep their own parts. Replaces the whole list on update.'),
|
|
64
77
|
};
|
|
65
78
|
export const blockTypeTools = [
|
|
66
79
|
{
|
|
@@ -128,7 +141,7 @@ export const blockTypeTools = [
|
|
|
128
141
|
},
|
|
129
142
|
{
|
|
130
143
|
name: 'read_block_type',
|
|
131
|
-
description: "Full definition of one block type: the field schema, and its composition (composed types) or template (template types), styles with `styles_compiled` (the scoped CSS that ships), and optional script. Use this when list_block_types' summary isn't enough — before add_block on an unfamiliar block, or before update_block_type.",
|
|
144
|
+
description: "Full definition of one block type: the field schema, its `parts` (named sub-elements such as core/rich_heading's links or core/hero's heading and buttons, each with a target; style one with a style's `parts` site-wide or the block's `data.part_styles`, or the part's own style_field), and its composition (composed types) or template (template types), styles with `styles_compiled` (the scoped CSS that ships), and optional script. Use this when list_block_types' summary isn't enough — before add_block on an unfamiliar block, or before update_block_type.",
|
|
132
145
|
inputSchema: {
|
|
133
146
|
type_id: z.string(),
|
|
134
147
|
version: versionParam,
|
|
@@ -220,7 +233,7 @@ export const blockTypeTools = [
|
|
|
220
233
|
},
|
|
221
234
|
{
|
|
222
235
|
name: 'update_block_type',
|
|
223
|
-
description: "Change a site block type (core/* types are read-only). Needs WRITE permission. schema, composition, template and
|
|
236
|
+
description: "Change a site block type (core/* types are read-only). Needs WRITE permission. schema, composition, template, styles and parts replace wholesale when sent; other properties merge. Changing a type in use: to RENAME a field, send the new schema with `renames` ({ \"title\": \"heading\", \"items.label\": \"text\" } — dotted paths of the OLD names, new name of the field) and the data moves in every block that uses the type (saved pages and drafts, page templates, header/footer/global blocks, block templates, other block types, repeater items), after a revision of each changed page; a composed type's own {{props.…}}/{{item.…}} bindings follow unless you send a composition. Removing or retyping a field that holds data answers 409 with the affected uses until you resend with confirm_data_loss: true (ask the user first). Returns { block_type, warnings, impact: { usages, renamed, removed, retyped } }. The same validation as create_block_type applies.",
|
|
224
237
|
inputSchema: {
|
|
225
238
|
type_id: z.string().describe('Block type id (a site type). Cannot be a core/* block.'),
|
|
226
239
|
...definitionShape,
|
|
@@ -42,6 +42,8 @@ const responseParam = z.enum(['summary', 'tree']).optional().describe('Answer sh
|
|
|
42
42
|
function writeQuery(version, response) {
|
|
43
43
|
return { ...v(version), response: response ?? 'summary' };
|
|
44
44
|
}
|
|
45
|
+
/** Block parts: per-instance part styles in block data. */
|
|
46
|
+
const PART_STYLES = ' `data.part_styles` ({ "<part>": "<style id>" }) gives named parts of the block a style on this block only, replacing a site-wide part style; read_block_type lists the type\'s `parts` and their targets (parts with a style_field, such as core/heading eyebrow → eyebrow_style_id, use that field instead). The map is replaced whole on update; null removes it. A part the block cannot style is 400 `unknown_field` at data.part_styles.<part> with suggestions; a value that is not a style id is `field_type_mismatch`; a style the site lacks (`unknown_style`) or of the wrong kind (`style_target_mismatch`) is refused with the styles that fit, as is text that fails both APCA and WCAG 2 AA on the part\'s background (`part_style_contrast`; an APCA-only miss is a warning).';
|
|
45
47
|
const COMPACT_ANSWER = ' Answers with the changed block, `parent_id`, `slot_index`, `position` and `has_unsaved_changes` (the working copy differs from the saved content); pass `response: "tree"` for the whole tree, or call get_page_blocks.';
|
|
46
48
|
/**
|
|
47
49
|
* Resolve a (target | page_id) pair to a REST path prefix. Pages keep
|
|
@@ -76,7 +78,7 @@ export const pageBlockTools = [
|
|
|
76
78
|
},
|
|
77
79
|
{
|
|
78
80
|
name: 'add_block',
|
|
79
|
-
description: 'Insert a block into a container (page, partial, or page template). BUFFER MODEL: block mutations on pages/partials edit the unsaved DRAFT — commit_working_copy is the explicit save. With no parent_id, inserts at the top level of the container. Otherwise inserts as a child (or into a slot for slot-containers). Position defaults to "end". Slot containers (core/columns, core/tabs — container: "slots"): slot_index picks the slot (0-based; defaults to 0) and works even if the parent instance has no slots array yet; a freshly added slot container gets its slots initialised to the type\'s arity. You can also pass the whole subtree inline via block.slots: [[...],[...]]. Pass `target: { kind: "partial", id: "header" }` to drop a block into the header on every page, etc. `page_id` is the legacy shorthand. A `data` key that no field of the block\'s type declares (read_block_type) is rejected with 400 `unknown_field`, for the block and every child; nothing is written, and `unknown_fields[].suggestions` says where it belongs (e.g. `item_overrides.<key>` for an item-block field on a repeater or its aliases, or the closest field names).' + COMPACT_ANSWER + ' `added_id` is the new block\'s id.',
|
|
81
|
+
description: 'Insert a block into a container (page, partial, or page template). BUFFER MODEL: block mutations on pages/partials edit the unsaved DRAFT — commit_working_copy is the explicit save. With no parent_id, inserts at the top level of the container. Otherwise inserts as a child (or into a slot for slot-containers). Position defaults to "end". Slot containers (core/columns, core/tabs — container: "slots"): slot_index picks the slot (0-based; defaults to 0) and works even if the parent instance has no slots array yet; a freshly added slot container gets its slots initialised to the type\'s arity. You can also pass the whole subtree inline via block.slots: [[...],[...]]. Pass `target: { kind: "partial", id: "header" }` to drop a block into the header on every page, etc. `page_id` is the legacy shorthand. A `data` key that no field of the block\'s type declares (read_block_type) is rejected with 400 `unknown_field`, for the block and every child; nothing is written, and `unknown_fields[].suggestions` says where it belongs (e.g. `item_overrides.<key>` for an item-block field on a repeater or its aliases, or the closest field names).' + PART_STYLES + COMPACT_ANSWER + ' `added_id` is the new block\'s id.',
|
|
80
82
|
inputSchema: {
|
|
81
83
|
target: targetSchema.optional(),
|
|
82
84
|
page_id: z.string().optional(),
|
|
@@ -100,7 +102,7 @@ export const pageBlockTools = [
|
|
|
100
102
|
},
|
|
101
103
|
{
|
|
102
104
|
name: 'update_block',
|
|
103
|
-
description: 'Update a block\'s data fields (shallow merge), style overrides, or responsive settings. Works on pages, partials, and page templates — use `target` to address non-page containers. Does not modify children or slots; use move/add/remove for structural changes. For appearance, prefer a named style (`data.style_id`, see list_styles) over custom_class or custom_css. From render version 2, custom_class lands on the heading or button link element itself instead of the block wrapper. Send values in the field\'s type (numbers as numbers); a value the block will not render comes back in `warnings` (`field_type_mismatch`) with its path. A `null` value removes that key from data (the field renders its default); use it to clean up a key stored by mistake. A written key that no field of the block\'s type declares is rejected with 400 `unknown_field` and nothing is written; `unknown_fields[].suggestions` says where it belongs: on core/repeater and its aliases (core/testimonials, core/page_list, ...) a field of the item block goes in `item_overrides.<key>`, otherwise the closest field names.' + COMPACT_ANSWER,
|
|
105
|
+
description: 'Update a block\'s data fields (shallow merge), style overrides, or responsive settings. Works on pages, partials, and page templates — use `target` to address non-page containers. Does not modify children or slots; use move/add/remove for structural changes. For appearance, prefer a named style (`data.style_id`, see list_styles) over custom_class or custom_css. From render version 2, custom_class lands on the heading or button link element itself instead of the block wrapper. Send values in the field\'s type (numbers as numbers); a value the block will not render comes back in `warnings` (`field_type_mismatch`) with its path. A `null` value removes that key from data (the field renders its default); use it to clean up a key stored by mistake. A written key that no field of the block\'s type declares is rejected with 400 `unknown_field` and nothing is written; `unknown_fields[].suggestions` says where it belongs: on core/repeater and its aliases (core/testimonials, core/page_list, ...) a field of the item block goes in `item_overrides.<key>`, otherwise the closest field names.' + PART_STYLES + COMPACT_ANSWER,
|
|
104
106
|
inputSchema: {
|
|
105
107
|
target: targetSchema.optional(),
|
|
106
108
|
page_id: z.string().optional(),
|
|
@@ -49,7 +49,7 @@ export const reusableBlockTools = [
|
|
|
49
49
|
},
|
|
50
50
|
{
|
|
51
51
|
name: 'save_block_template',
|
|
52
|
-
description: 'Save a block template: a section starter that editors and agents insert as an independent copy. Pass `from: { page_id, block_id }` to save a block from a page (its draft), or `blocks`. Prefer templates that use named styles (style_id) over one-off values, so copies stay consistent. Templates belong to the site, not a branch.',
|
|
52
|
+
description: 'Save a block template: a section starter that editors and agents insert as an independent copy. Pass `from: { page_id, block_id }` to save a block from a page (its draft), or `blocks`. Prefer templates that use named styles (style_id, part_styles) over one-off values, so copies stay consistent. Templates belong to the site, not a branch.',
|
|
53
53
|
inputSchema: {
|
|
54
54
|
name: z.string(),
|
|
55
55
|
description: z.string().optional().describe('When to use it.'),
|
package/dist/tools/styles.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
// Style library tools. A site's look is a library of NAMED styles
|
|
2
2
|
// (SiteSettings.styles): element roles (body, h1–h6, link) style every
|
|
3
3
|
// matching element, and every other style is chosen per block through the
|
|
4
|
-
// block's `style_id` field.
|
|
4
|
+
// block's `style_id` field. A style can also style a block PART (a named
|
|
5
|
+
// sub-element such as core/rich_heading's links): site-wide through the
|
|
6
|
+
// style's `parts`, on one block through its `part_styles` data. Reuse beats
|
|
7
|
+
// one-off values.
|
|
5
8
|
import { z } from 'zod';
|
|
6
9
|
import { ok, withErrorBoundary, versionParam } from './helpers.js';
|
|
7
10
|
function v(version) {
|
|
@@ -33,6 +36,9 @@ const propsSchema = z.object({
|
|
|
33
36
|
color: color.optional(),
|
|
34
37
|
sides: z.enum(['all', 'top', 'right', 'bottom', 'left', 'block', 'inline']).optional(),
|
|
35
38
|
}).optional(),
|
|
39
|
+
opacity: z.number().min(0).max(1).optional().describe('0 (invisible) to 1 (opaque)'),
|
|
40
|
+
shadow: z.enum(['none', 'sm', 'md', 'lg']).optional().describe('Shadow preset; none removes a shadow the block draws'),
|
|
41
|
+
gap: z.string().optional().describe('Space between the children of a flex or grid element: one CSS length, or row and column'),
|
|
36
42
|
}).describe('Style properties. Mobile-first: `base` applies everywhere, `at.<breakpoint>` from that width up.');
|
|
37
43
|
const atSchema = z.object({
|
|
38
44
|
tablet: propsSchema.optional(),
|
|
@@ -44,13 +50,15 @@ const styleFields = {
|
|
|
44
50
|
name: z.string().describe('Author-facing name, in the site\'s language, e.g. "Överrubrik" or "Hero title".'),
|
|
45
51
|
role: z.enum(['body', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'link', 'lead', 'eyebrow', 'small', 'quote', 'button', 'button_secondary', 'section']).optional()
|
|
46
52
|
.describe('Standard role. body/h1–h6/link style every matching element on the site; the others mark the standard style for that purpose. One style per role.'),
|
|
47
|
-
targets: z.array(z.enum(['text', 'heading', 'list', 'button', 'container'])).optional()
|
|
48
|
-
.describe('
|
|
53
|
+
targets: z.array(z.enum(['text', 'heading', 'list', 'button', 'container', 'link', 'image'])).optional()
|
|
54
|
+
.describe('Kinds of element that can select this style: through a block\'s style_id (text = core/prose, heading = core/heading and core/rich_heading, list, button, container = core/section and core/container) and through a block part with that target (link and image exist for parts, e.g. core/rich_heading.link, core/hero.media).'),
|
|
49
55
|
description: z.string().optional().describe('When to use it; shown to editors and agents.'),
|
|
50
56
|
base: propsSchema.optional(),
|
|
51
57
|
at: atSchema.optional(),
|
|
52
58
|
hover: z.object({ color: color.optional(), background: color.optional(), decoration: z.enum(['none', 'underline']).optional() }).optional(),
|
|
53
59
|
css: z.string().optional().describe('Advanced CSS scoped to the style: bare declarations, or rules where `&` is the style selector (e.g. "&::before { content: \'—\' }"). Prefer structured properties.'),
|
|
60
|
+
parts: z.array(z.string()).optional()
|
|
61
|
+
.describe('Block parts this style applies to SITE-WIDE, on every instance, as "<block type>.<part>" (e.g. "core/rich_heading.link", "core/hero.primary_button"); read_block_type lists a block\'s `parts` with their targets. The style\'s targets must include each part\'s target; a part takes one site-wide style; not for element roles (body, h1–h6, link). Unknown parts are refused with the closest names. Precedence, lowest first: the block\'s own CSS and fields; this site-wide part style, replaced on one block by its `data.part_styles` choice; a style class on the element itself (a heading\'s eyebrow_style_id, a button\'s style_id).'),
|
|
54
62
|
};
|
|
55
63
|
export const styleTools = [
|
|
56
64
|
{
|
|
@@ -61,7 +69,7 @@ export const styleTools = [
|
|
|
61
69
|
},
|
|
62
70
|
{
|
|
63
71
|
name: 'create_style',
|
|
64
|
-
description: 'Create a named style. Do this whenever a look could recur — an eyebrow, a lead paragraph, a card title, a quote, a button variant, a section rhythm — then apply it with the block\'s `style_id` field. Only a clearly unique one-off may use block settings instead. Set values for phones in `base` and grow them in `at.tablet`/`at.desktop`. Text contrast against its background is measured with APCA by use, size and weight (body text Lc 75; secondary/small text Lc 60; headings, eyebrows and buttons Lc 60 from 16px at weight 600+, Lc 45 from 24px bold or 36px; smaller or lighter non-body text Lc 75) and with the WCAG 2 ratio. Text that fails both is refused; text below its APCA level that meets WCAG 2 AA (4.5:1, 3:1 for ≥24px or bold ≥18.7px) is saved and returned in `warnings`. Never light grey on white.',
|
|
72
|
+
description: 'Create a named style. Do this whenever a look could recur — an eyebrow, a lead paragraph, a card title, a quote, a button variant, a section rhythm — then apply it with the block\'s `style_id` field. Only a clearly unique one-off may use block settings instead. Set values for phones in `base` and grow them in `at.tablet`/`at.desktop`. Text contrast against its background is measured with APCA by use, size and weight (body text Lc 75; secondary/small text Lc 60; headings, eyebrows and buttons Lc 60 from 16px at weight 600+, Lc 45 from 24px bold or 36px; smaller or lighter non-body text Lc 75) and with the WCAG 2 ratio. Text that fails both is refused; text below its APCA level that meets WCAG 2 AA (4.5:1, 3:1 for ≥24px or bold ≥18.7px) is saved and returned in `warnings`. Never light grey on white. To restyle a sub-element of a block (rich-heading links, a hero\'s buttons or image), list it in `parts` (site-wide) or choose the style in that block\'s `data.part_styles`.',
|
|
65
73
|
inputSchema: {
|
|
66
74
|
id: z.string().regex(/^[a-z][a-z0-9-]{0,47}$/).describe('Stable slug; the CSS class becomes s-<id>. Cannot change later.'),
|
|
67
75
|
...styleFields,
|
|
@@ -75,7 +83,7 @@ export const styleTools = [
|
|
|
75
83
|
},
|
|
76
84
|
{
|
|
77
85
|
name: 'update_style',
|
|
78
|
-
description: 'Update a style. `base`, `hover` and each breakpoint in `at` merge property by property (pass null to remove one); name, targets, description and
|
|
86
|
+
description: 'Update a style. `base`, `hover` and each breakpoint in `at` merge property by property (pass null to remove one); name, targets, description, css and parts replace (parts: [] removes the style from every part). Every page using the style changes at once — tell the user which pages that affects when it matters.',
|
|
79
87
|
inputSchema: {
|
|
80
88
|
style_id: z.string(),
|
|
81
89
|
name: styleFields.name.optional(),
|
|
@@ -86,6 +94,7 @@ export const styleTools = [
|
|
|
86
94
|
at: z.record(z.unknown()).optional().describe('Per-breakpoint property patches; null removes a breakpoint or property.'),
|
|
87
95
|
hover: z.record(z.unknown()).optional(),
|
|
88
96
|
css: z.string().optional(),
|
|
97
|
+
parts: styleFields.parts,
|
|
89
98
|
version: versionParam,
|
|
90
99
|
},
|
|
91
100
|
handler: withErrorBoundary(async (args, { client, siteId }) => {
|
package/dist/version.js
CHANGED
package/package.json
CHANGED
|
@@ -87,6 +87,13 @@ When the markup cannot be built from blocks, use `template` instead of
|
|
|
87
87
|
`{{@number}}`, `{{@first}}`, `{{@last}}`, and `{{#link field class="x"}}…{{/link}}`.
|
|
88
88
|
A type has a composition or a template, never both.
|
|
89
89
|
|
|
90
|
+
Declare `parts` for the elements of the markup a site may want to restyle:
|
|
91
|
+
`parts: [{ name: "title", label: "Title", selector: ".icon-list-title", target: "heading" }]`.
|
|
92
|
+
Selectors are tags, classes, attributes and structural pseudo-classes, never
|
|
93
|
+
`:hover` or `::before`. Styles then reach the part site-wide
|
|
94
|
+
(`parts: ["icon_list.title"]` on the style) or on one block
|
|
95
|
+
(`data.part_styles: { title: "card-title" }`, a field added automatically).
|
|
96
|
+
|
|
90
97
|
## Changing a type in use
|
|
91
98
|
|
|
92
99
|
- Rename a field with `renames: { "old": "new" }` (`"items.old": "new"` inside a
|
package/skills/tr-new-site.md
CHANGED
|
@@ -150,6 +150,12 @@ Set the site's look as named styles before building any page:
|
|
|
150
150
|
Pages then choose styles with each block's `style_id` field. A per-block
|
|
151
151
|
font size, colour or class is only for something clearly unique.
|
|
152
152
|
|
|
153
|
+
To change a sub-element a block styles itself (the underline on formatted
|
|
154
|
+
heading links, a hero's heading or buttons), use its parts:
|
|
155
|
+
`read_block_type` lists them. A style with `parts: ["core/rich_heading.link"]`
|
|
156
|
+
applies to that part on every block; `data.part_styles` on one block replaces
|
|
157
|
+
it there.
|
|
158
|
+
|
|
153
159
|
### 3. Header + footer partials
|
|
154
160
|
|
|
155
161
|
**Start from the native preset — don't hand-roll navigation.** Read
|