@comet/agent-features 9.0.0-canary-20260527154746

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.
Files changed (35) hide show
  1. package/LICENSE +24 -0
  2. package/package.json +19 -0
  3. package/rules/coding-guidelines/api-nestjs.instructions.md +107 -0
  4. package/rules/coding-guidelines/cdn.instructions.md +24 -0
  5. package/rules/coding-guidelines/general.instructions.md +30 -0
  6. package/rules/coding-guidelines/git.instructions.md +37 -0
  7. package/rules/coding-guidelines/kubernetes.instructions.md +59 -0
  8. package/rules/coding-guidelines/libraries.instructions.md +34 -0
  9. package/rules/coding-guidelines/naming.instructions.md +39 -0
  10. package/rules/coding-guidelines/postgresql.instructions.md +40 -0
  11. package/rules/coding-guidelines/react.instructions.md +102 -0
  12. package/rules/coding-guidelines/security.instructions.md +44 -0
  13. package/rules/coding-guidelines/styling.instructions.md +50 -0
  14. package/rules/coding-guidelines/typescript.instructions.md +50 -0
  15. package/skills/.gitkeep +0 -0
  16. package/skills/comet-block/SKILL.md +246 -0
  17. package/skills/comet-block/references/admin-patterns.md +192 -0
  18. package/skills/comet-block/references/api-patterns.md +183 -0
  19. package/skills/comet-block/references/block-loader.md +368 -0
  20. package/skills/comet-block/references/block-types.md +210 -0
  21. package/skills/comet-block/references/custom-block-field.md +266 -0
  22. package/skills/comet-block/references/fixtures.md +436 -0
  23. package/skills/comet-block/references/image.md +341 -0
  24. package/skills/comet-block/references/migration.md +597 -0
  25. package/skills/comet-block/references/registration.md +167 -0
  26. package/skills/comet-block/references/response-summary.md +102 -0
  27. package/skills/comet-block/references/rich-text.md +309 -0
  28. package/skills/comet-block/references/select.md +176 -0
  29. package/skills/comet-block/references/site-patterns.md +202 -0
  30. package/skills/comet-mail-react/SKILL.md +539 -0
  31. package/skills/comet-mail-react/references/components-and-theme.md +395 -0
  32. package/skills/comet-mail-react/references/layout-patterns.md +315 -0
  33. package/skills/comet-mail-react/references/styling-and-customization.md +306 -0
  34. package/skills/comet-minor-update/SKILL.md +191 -0
  35. package/skills/dev-pm/SKILL.md +100 -0
@@ -0,0 +1,50 @@
1
+ ---
2
+ description: TypeScript style — options objects, named exports, async/await, enums
3
+ applyTo: "**/*.ts,**/*.tsx"
4
+ paths:
5
+ - "**/*.{ts,tsx}"
6
+ globs:
7
+ - "**/*.{ts,tsx}"
8
+ alwaysApply: false
9
+ ---
10
+
11
+ # TypeScript Rules
12
+
13
+ ## Function signatures
14
+
15
+ - If a function takes **more than 2** parameters, use a single options object. This makes call sites self-documenting and avoids argument-order mistakes.
16
+
17
+ ```ts
18
+ // Bad
19
+ getSortedJobs("createdAt", true, 20);
20
+
21
+ // Good
22
+ getSortedJobs({ orderBy: "createdAt", includeSoftDeleted: true, limit: 20 });
23
+ ```
24
+
25
+ - Use default argument values (`function f(name = "X")`) instead of `name || "X"` or conditional fallbacks inside the body.
26
+ - Prefer `param?: T` over `param: T | undefined` — the optional form does not require callers to explicitly pass `undefined`.
27
+
28
+ ## Imports / exports
29
+
30
+ - Use **named exports only**. Default exports are allowed only when technically required (e.g. a framework demands it).
31
+ - Use relative imports only for **sibling or child** files within the same module. Everything else uses the `@src/…` alias.
32
+
33
+ ## Async & iteration
34
+
35
+ - Prefer `async/await` over raw callbacks or `.then()` chains.
36
+ - Prefer `for…of` over `Array.prototype.forEach` — it supports `await`, `break`, `continue`, and all iterables.
37
+
38
+ ## Enums
39
+
40
+ - Enum **keys** and **values** use `camelCase`, and keys must equal their values.
41
+
42
+ ```ts
43
+ enum Direction {
44
+ north = "north",
45
+ northEast = "northEast",
46
+ // …
47
+ }
48
+ ```
49
+
50
+ - GraphQL-exposed enums follow a different rule — see [api-nestjs.instructions.md](api-nestjs.instructions.md#graphql-enums).
File without changes
@@ -0,0 +1,246 @@
1
+ ---
2
+ name: comet-block
3
+ description: Creates and edits Comet blocks (API, Admin, Site) from natural-language prompts, including block fixture services. Use when the user asks to create a new block, edit an existing block, add/remove/change fields or child blocks, scaffold block files, or create block fixtures in a Comet project.
4
+ ---
5
+
6
+ # Comet Block Skill
7
+
8
+ ## Table of Contents
9
+
10
+ 1. [When to use](#when-to-use)
11
+ 2. [Workflow routing](#workflow-routing)
12
+ 3. [Step 1 — Parse the prompt](#step-1--parse-the-prompt)
13
+ 4. [Step 2 — Discover the project](#step-2--discover-the-project)
14
+ 5. [Editing workflow](#editing-workflow) ← follow this for edits
15
+ 6. [Step 3 — Create the API block](#step-3--create-the-api-block) ← creation only
16
+ 7. [Step 4 — Create the Admin block](#step-4--create-the-admin-block) ← creation only
17
+ 8. [Step 5 — Create the Site block](#step-5--create-the-site-block) ← creation only
18
+ 9. [Step 6 — Register the block](#step-6--register-the-block)
19
+ 10. [Step 7 — Create block fixtures](#step-7--create-block-fixtures)
20
+ 11. [Naming conventions](#naming-conventions)
21
+ 12. [Code generation](#code-generation)
22
+ 13. [Cross-references](#cross-references)
23
+
24
+ ---
25
+
26
+ ## When to use
27
+
28
+ - Creating a **new** Comet block from a natural-language description.
29
+ - Scaffolding block files across API, Admin, and Site layers.
30
+ - Adding, removing, or changing fields or child blocks in an existing block.
31
+ - Changing enum values, field types, or property names in an existing block.
32
+ - Creating block fixture services for seeding development databases.
33
+
34
+ ---
35
+
36
+ ## Workflow routing
37
+
38
+ ```
39
+ User request
40
+
41
+ ├─ Creating a new block? → Steps 1 → 2 → 3 → 4 → 5 → 6 → 7
42
+
43
+ └─ Editing an existing block? → Steps 1 → 2 → Editing workflow → 7
44
+ ```
45
+
46
+ ---
47
+
48
+ ## Step 1 — Parse the prompt
49
+
50
+ Extract from the user's request:
51
+
52
+ 1. **Block name** — derive PascalCase for Admin/Site (`TeaserItemBlock`) and kebab-case for API (`teaser-item.block.ts`). See [Naming conventions](#naming-conventions).
53
+ 2. **Properties** — for each property determine its type. Common types:
54
+ - **String** — plain text (`title`, `label`).
55
+ - **Number** — numeric with min/max (`overlay`).
56
+ - **Boolean** — toggle/switch (`showOverlay`).
57
+ - **Numeric select** — fixed numeric options. Use `@IsInt()` + `@BlockField()` in the API (not `type: "enum"`); use `createCompositeBlockSelectField` with number options in Admin. See [select.md](references/select.md).
58
+ - **Enum/select** — fixed string values (`variant`, `alignment`). See [select.md](references/select.md).
59
+ - **RichText** — formatted text. Choose shared `RichTextBlock` or a scoped inline one. See [rich-text.md](references/rich-text.md).
60
+ - **Image** — choose `DamImageBlock`, `PixelImageBlock`, `SvgImageBlock`, or a project-specific `MediaBlock`. See [image.md](references/image.md).
61
+ - **Child block** — any other existing block used as a property.
62
+ - **List** — multiple instances of a child block; requires a list block + item block pair. See [block-types.md](references/block-types.md).
63
+ 3. **Registration target** — where the block is added. Default: `PageContentBlock` (and `ContentGroupBlock` if it exists). Ask if unclear.
64
+ 4. **Ambiguities** — if "image" is mentioned without specifics, or a referenced block may not exist, ask before proceeding.
65
+
66
+ For block type decision guidance, see [block-types.md](references/block-types.md).
67
+
68
+ ---
69
+
70
+ ## Step 2 — Discover the project
71
+
72
+ Before generating any code:
73
+
74
+ 1. **Locate `api`, `admin`, `site` directories.** The `site` directory is optional.
75
+ 2. **Find existing blocks directories** — typically `src/documents/pages/blocks/`. Some shared blocks live in `common/blocks/` or similar. Check both.
76
+ 3. **Verify referenced blocks exist** — search for any blocks named in the prompt (e.g., `HeadingBlock`, `LinkBlock`) in all layers and note their import paths.
77
+ 4. **Find registration targets** — search for `createBlocksBlock` usages to locate `PageContentBlock`, `ContentGroupBlock`, or other targets. Note file paths in all layers.
78
+ 5. **Confirm site directory** — if absent, skip all site steps.
79
+
80
+ ---
81
+
82
+ ## Editing workflow
83
+
84
+ Use this instead of Steps 3–5 when modifying an existing block.
85
+
86
+ ### Classify each change
87
+
88
+ | Change type | Description |
89
+ | ---------------------------- | ---------------------------------------------------- |
90
+ | **Add field/child block** | A new property on the block. |
91
+ | **Remove field/child block** | An existing property is deleted. |
92
+ | **Change field type** | One type replaces another (e.g., string → RichText). |
93
+ | **Change enum values** | Options added to or removed from a select field. |
94
+ | **Rename field** | Property keeps its type but gets a new name. |
95
+
96
+ A single request may involve multiple change types — classify each independently.
97
+
98
+ ### Determine if a migration is needed
99
+
100
+ Ask the user before creating a migration if they haven't mentioned it. Explain the old vs new data shape and confirm.
101
+
102
+ **Migration IS needed:**
103
+
104
+ - Adding a **required** field (existing data lacks it).
105
+ - **Changing** a field's type (old data has the old shape).
106
+ - **Removing** a field (recommended to keep data clean).
107
+ - **Renaming** a field (old data uses the old name).
108
+
109
+ **Migration is NOT needed:**
110
+
111
+ - Adding an **optional/nullable** field (`@IsUndefinable()` + `@BlockField({ nullable: true })`).
112
+ - Adding a new supported block to a `BlocksBlock` or `OneOfBlock`.
113
+ - Adding new enum values (existing data unaffected).
114
+
115
+ When in doubt, create a migration — it is always the safer choice. See [migration.md](references/migration.md) for the full decision matrix, class template, and annotated examples.
116
+
117
+ ### Apply changes in order
118
+
119
+ 1. **API block** — update `BlockData` and `BlockInput` classes, decorators, validators.
120
+ 2. **Migration** — create and register if needed. Update `createBlock` third argument to the options object. See [migration.md](references/migration.md).
121
+ 3. **Admin block** — update the `blocks` object: add/remove entries, labels, options.
122
+ 4. **Site block** (if exists) — update destructured fields and rendered output.
123
+ 5. **Block fixture** (if exists) — update `generateBlockInput()` to match changes. See [fixtures.md](references/fixtures.md).
124
+ 6. **Verify consistency** — confirm every property key name matches across all three layers.
125
+
126
+ ---
127
+
128
+ ## Step 3 — Create the API block
129
+
130
+ File: `{block-name}.block.ts` (kebab-case). Place in the blocks directory found in Step 2.
131
+
132
+ **Key patterns:**
133
+
134
+ - `BlockData` uses `@BlockField()` for fields, `@ChildBlock(X)` for child blocks.
135
+ - `BlockInput` uses validators + `@ChildBlockInput(X)` for child blocks; implement `transformToBlockData()` with `inputToData`.
136
+ - Export with `createBlock(BlockData, BlockInput, "BlockName")`.
137
+ - Enums require `@BlockField({ type: "enum", enum: MyEnum })` — never use `type: "enum"` for numeric options.
138
+ - For list blocks: create the item block first, then `createListBlock({ block: ItemBlock }, "MyList")`.
139
+
140
+ For field decorators, validator reference, savability rules, and complete examples, see [api-patterns.md](references/api-patterns.md).
141
+
142
+ ---
143
+
144
+ ## Step 4 — Create the Admin block
145
+
146
+ File: `{BlockName}Block.tsx` (PascalCase). Place in the blocks directory found in Step 2.
147
+
148
+ **Key patterns:**
149
+
150
+ - Use `createCompositeBlock` with a `blocks` object mapping property names to block configs.
151
+ - Helper functions: `createCompositeBlockTextField`, `createCompositeBlockSelectField`, `createCompositeBlockSwitchField`.
152
+ - `fullWidth` defaults to `true` in text and select helpers — omit it explicitly.
153
+ - All user-facing strings use `FormattedMessage` from `react-intl`. Follow ID convention: `{blockName}Block.{field}`.
154
+ - Set `BlockCategory` when the block is used inside a blocks block.
155
+ - Set `previewContent` in the override callback for meaningful block list previews.
156
+ - **`hiddenInSubroute` rule:** When the composite contains sub-route blocks (list, blocks-block, one-of), set `hiddenInSubroute: true` on every sibling entry that is _not_ a sub-route block. Never set it on sub-route block entries themselves.
157
+ - **`label` vs `title`:** when the helper has a `label`, omit `title` on the block entry. When it has no `label`, provide `title` on the entry. They are mutually exclusive.
158
+ - For list blocks: use `createListBlock` from `@comet/cms-admin` with `name`, `displayName`, `block`, `itemName`, `itemsName`.
159
+
160
+ For the full helper API, `BlockCategory` enum, and complete examples, see [admin-patterns.md](references/admin-patterns.md).
161
+
162
+ ---
163
+
164
+ ## Step 5 — Create the Site block (if site exists)
165
+
166
+ File: `{BlockName}Block.tsx` (PascalCase). Place in the blocks directory found in Step 2.
167
+
168
+ **Key patterns:**
169
+
170
+ - Wrap with `withPreview(Component, { label: "BlockName" })`.
171
+ - Type props as `PropsWithData<{BlockName}BlockData>` — import from `@src/blocks.generated`.
172
+ - Use `hasRichTextBlockContent` guard before rendering RichText. Never wrap `RichTextBlock` in a `Typography` component.
173
+ - `DamImageBlock` is **not** exported from `@comet/site-nextjs` — use the project-specific wrapper. See [image.md](references/image.md).
174
+ - Always validate `aspectRatio` against `allowedImageAspectRatios` in the API config. See [image.md](references/image.md).
175
+ - The `supportedBlocks` object (for `BlocksBlock`/`PageContent` site wrapper) must be defined at module level, not inside the component.
176
+
177
+ For `withPreview`, `OneOfBlock`/`OptionalBlock`, and complete site examples, see [site-patterns.md](references/site-patterns.md).
178
+
179
+ ---
180
+
181
+ ## Step 6 — Register the block
182
+
183
+ Add the new block to the registration target in **all three layers** using **identical camelCase key names**.
184
+
185
+ **Default target:** `PageContentBlock`. If `ContentGroupBlock` exists and mirrors `PageContentBlock` supported blocks, also register there.
186
+
187
+ ```ts
188
+ // API & Admin: supportedBlocks object inside createBlocksBlock
189
+ myBlock: MyBlock,
190
+
191
+ // Site: supportedBlocks object
192
+ myBlock: (props) => <MyBlock data={props} />,
193
+ ```
194
+
195
+ For the target hierarchy, multiple targets, and edge cases, see [registration.md](references/registration.md).
196
+
197
+ ---
198
+
199
+ ## Step 7 — Create block fixtures
200
+
201
+ Create a fixture service that generates realistic seed data for the new block.
202
+
203
+ 1. **Create the fixture service** matching the block type (composite, list, blocks-block, one-of).
204
+ 2. **Wire into the parent block's fixture.** If the parent has no fixture, ask the user before creating one.
205
+ 3. **Register in `fixtures.module.ts`** as a provider.
206
+ 4. **For nested list blocks:** create the list item fixture service first, then the parent composite fixture which injects and calls it.
207
+
208
+ For faker guidelines, patterns by block type, and full examples, see [fixtures.md](references/fixtures.md).
209
+
210
+ ---
211
+
212
+ ## Naming conventions
213
+
214
+ | Concept | Convention | Example |
215
+ | -------------------------------- | ------------------------- | --------------------------- |
216
+ | API file name | kebab-case `.block.ts` | `product-card.block.ts` |
217
+ | Admin / Site file name | PascalCase `Block.tsx` | `ProductCardBlock.tsx` |
218
+ | Export variable | PascalCase + "Block" | `ProductCardBlock` |
219
+ | Name string (`createBlock` etc.) | PascalCase, no "Block" | `"ProductCard"` |
220
+ | API `BlockData` class | PascalCase + "BlockData" | `ProductCardBlockData` |
221
+ | API `BlockInput` class | PascalCase + "BlockInput" | `ProductCardBlockInput` |
222
+ | Registration key | camelCase, no "Block" | `productCard` |
223
+ | Child block property | camelCase | `image`, `callToActionList` |
224
+ | `FormattedMessage` ID | camelCase block + field | `productCardBlock.headline` |
225
+
226
+ The `name` parameter in `createBlock`, `createCompositeBlock`, `createListBlock`, `createBlocksBlock`, and `createOneOfBlock` is PascalCase **without** a "Block" suffix and must be **identical** across all layers.
227
+
228
+ ---
229
+
230
+ ## Cross-references
231
+
232
+ | Topic | File |
233
+ | --------------------------------------------------- | --------------------------------------------------------- |
234
+ | Block type overview and decision guide | [block-types.md](references/block-types.md) |
235
+ | API decorator patterns, validators, savability | [api-patterns.md](references/api-patterns.md) |
236
+ | Admin helpers, `BlockCategory`, `hiddenInSubroute` | [admin-patterns.md](references/admin-patterns.md) |
237
+ | Site `withPreview`, rendering patterns | [site-patterns.md](references/site-patterns.md) |
238
+ | Registration targets, keys, edge cases | [registration.md](references/registration.md) |
239
+ | RichText configuration, scoped vs shared | [rich-text.md](references/rich-text.md) |
240
+ | Enum/select patterns, numeric options, multi-select | [select.md](references/select.md) |
241
+ | Image block selection and site wrappers | [image.md](references/image.md) |
242
+ | Migration decision matrix, class template, examples | [migration.md](references/migration.md) |
243
+ | Block fixture patterns and faker guidelines | [fixtures.md](references/fixtures.md) |
244
+ | Block loaders for server-side data fetching | [block-loader.md](references/block-loader.md) |
245
+ | Custom block fields for entity selection | [custom-block-field.md](references/custom-block-field.md) |
246
+ | Response template for creation/editing output | [response-summary.md](references/response-summary.md) |
@@ -0,0 +1,192 @@
1
+ # Admin Block Patterns
2
+
3
+ Admin blocks live in `admin/src/`, typically under `documents/pages/blocks/` or `common/blocks/`. File names use PascalCase: `{BlockName}Block.tsx`.
4
+
5
+ ---
6
+
7
+ ## Imports
8
+
9
+ ```tsx
10
+ import {
11
+ BlockCategory,
12
+ createCompositeBlock,
13
+ createCompositeBlockSelectField,
14
+ createCompositeBlockSwitchField,
15
+ createCompositeBlockTextField,
16
+ } from "@comet/cms-admin";
17
+ import { type MyBlockData } from "@src/blocks.generated";
18
+ import { FormattedMessage } from "react-intl";
19
+ ```
20
+
21
+ Image blocks (`DamImageBlock`, `PixelImageBlock`, `SvgImageBlock`) are imported from `@comet/cms-admin`.
22
+
23
+ ---
24
+
25
+ ## createCompositeBlock
26
+
27
+ ```tsx
28
+ export const MyBlock = createCompositeBlock(
29
+ {
30
+ name: "My", // PascalCase, no "Block" suffix; must match API
31
+ displayName: <FormattedMessage id="myBlock.displayName" defaultMessage="My" />,
32
+ category: BlockCategory.TextAndContent, // only when used inside a blocks block
33
+ blocks: {
34
+ /* ... */
35
+ },
36
+ },
37
+ (block) => {
38
+ block.previewContent = (state) => [{ type: "text", content: state.title }];
39
+ return block;
40
+ },
41
+ );
42
+ ```
43
+
44
+ The second argument (override function) is optional. Always return the block from it.
45
+
46
+ ---
47
+
48
+ ## Block Configuration Object Keys
49
+
50
+ Each key in `blocks` maps to a property from the API. The value is a configuration object with:
51
+
52
+ - `block` — the block instance or field helper (required)
53
+ - `title` — section heading above the block; use `FormattedMessage`
54
+ - `hiddenInSubroute` — hide when user navigates into a sub-route (see below)
55
+ - `nested` — render as a nested page with a navigation button instead of inline
56
+ - `paper` — wrap in a card container
57
+ - `divider` — show a divider below (only inside a paper container)
58
+ - `hiddenForState` — `(state) => boolean`; conditionally hide based on block state
59
+
60
+ ### `label` vs `title` — mutually exclusive
61
+
62
+ - Field helpers (`createCompositeBlockTextField`, `createCompositeBlockSelectField`, `createCompositeBlockSwitchField`) accept a `label` prop that renders inline with the field.
63
+ - The block configuration entry has a `title` prop that renders as a section heading above the block.
64
+ - **Never set both.** When the helper has a `label`, omit `title` on the entry. When the helper has no `label`, provide `title` on the entry.
65
+
66
+ ```tsx
67
+ // Correct: label on the helper, no title on the entry
68
+ title: {
69
+ block: createCompositeBlockTextField({
70
+ label: <FormattedMessage id="myBlock.title" defaultMessage="Title" />,
71
+ }),
72
+ },
73
+
74
+ // Correct: no label on the helper, title on the entry
75
+ image: {
76
+ block: DamImageBlock,
77
+ title: <FormattedMessage id="myBlock.image" defaultMessage="Image" />,
78
+ },
79
+ ```
80
+
81
+ ### `fullWidth` default
82
+
83
+ `createCompositeBlockTextField`, `createCompositeBlockSelectField`, and `createCompositeBlockSwitchField` all default `fullWidth` to `true`. Do not pass it explicitly unless overriding to `false`.
84
+
85
+ ---
86
+
87
+ ## hiddenInSubroute Rules
88
+
89
+ When a composite block contains sub-route children (list blocks, blocks blocks, one-of blocks), the admin renders them in a nested route. Sibling entries that are not sub-route blocks must be hidden while the user is inside the sub-route, otherwise the layout breaks.
90
+
91
+ **Rule:** Set `hiddenInSubroute: true` on every entry that is **not** itself a sub-route block. **Never set it on list/blocks/one-of block entries** — they must stay visible to show their sub-route content.
92
+
93
+ ```tsx
94
+ blocks: {
95
+ // Non-sub-route entries: hide in sub-route
96
+ title: {
97
+ block: createCompositeBlockTextField({
98
+ label: <FormattedMessage id="myBlock.title" defaultMessage="Title" />,
99
+ }),
100
+ hiddenInSubroute: true,
101
+ },
102
+ variant: {
103
+ block: createCompositeBlockSelectField<MyBlockData["variant"]>({
104
+ defaultValue: "primary",
105
+ options: variantOptions,
106
+ label: <FormattedMessage id="myBlock.variant" defaultMessage="Variant" />,
107
+ }),
108
+ hiddenInSubroute: true,
109
+ },
110
+ // Sub-route entry: do NOT set hiddenInSubroute
111
+ items: {
112
+ block: MyItemsListBlock,
113
+ },
114
+ },
115
+ ```
116
+
117
+ If the composite has **no** sub-route children, omit `hiddenInSubroute` entirely.
118
+
119
+ ---
120
+
121
+ ## BlockCategory Enum
122
+
123
+ Import from `@comet/cms-admin`. Only set when the block is used inside a blocks block.
124
+
125
+ | Value | Typical use |
126
+ | ------------------- | ------------------------------------- |
127
+ | `TextAndContent` | Text, headings, rich text, accordions |
128
+ | `Media` | Images, videos, galleries |
129
+ | `Navigation` | Links, CTAs, anchors |
130
+ | `Teaser` | Teasers, stage blocks |
131
+ | `StructuredContent` | Data-driven / structured content |
132
+ | `Layout` | Columns, spacers, layout wrappers |
133
+ | `Form` | Form blocks |
134
+ | `Other` | Default when no category fits |
135
+
136
+ Set via the `category` option or in the override function:
137
+
138
+ ```tsx
139
+ (block) => {
140
+ block.category = BlockCategory.Media;
141
+ return block;
142
+ };
143
+ ```
144
+
145
+ ---
146
+
147
+ ## previewContent Callback
148
+
149
+ Controls the collapsed preview row of a block inside list/blocks blocks. Set in the override function.
150
+
151
+ ```tsx
152
+ // Show a string field
153
+ block.previewContent = (state) => [{ type: "text", content: state.title }];
154
+
155
+ // Handle optional fields
156
+ block.previewContent = (state) => (state.title !== undefined ? [{ type: "text", content: state.title }] : []);
157
+
158
+ // Nothing to show
159
+ block.previewContent = () => [];
160
+ ```
161
+
162
+ Always set `previewContent` for blocks shown inside list or blocks blocks. Always guard optional fields to avoid returning undefined as content.
163
+
164
+ ---
165
+
166
+ ## FormattedMessage ID Conventions
167
+
168
+ All user-facing strings use `FormattedMessage` from `react-intl`. Always provide `defaultMessage`.
169
+
170
+ | Context | ID pattern | Example |
171
+ | --------------------- | -------------------------------------- | --------------------------------- |
172
+ | Block display name | `{blockName}Block.displayName` | `featureItemBlock.displayName` |
173
+ | Field label / title | `{blockName}Block.{fieldName}` | `featureItemBlock.title` |
174
+ | Select option label | `{blockName}Block.{fieldName}.{value}` | `featureItemBlock.alignment.left` |
175
+ | List block item name | `{blockName}Block.itemName` | `featureListBlock.itemName` |
176
+ | List block items name | `{blockName}Block.itemsName` | `featureListBlock.itemsName` |
177
+
178
+ IDs use camelCase dot-separated segments. The block name segment uses the full block name with "Block" suffix (`featureItemBlock`, not `featureItem`).
179
+
180
+ ---
181
+
182
+ ## Naming Conventions
183
+
184
+ | Element | Convention | Example |
185
+ | ---------------------------- | -------------------------------------- | ------------------------------------ |
186
+ | File name | PascalCase ending in `Block.tsx` | `ProductCardBlock.tsx` |
187
+ | Exported constant | `{BlockName}Block` | `ProductCardBlock` |
188
+ | `name` option | PascalCase, **no** "Block" suffix | `"ProductCard"` |
189
+ | Keys in `blocks` | camelCase, matching API property names | `callToActionList`, `titleHtmlTag` |
190
+ | Keys in `supportedBlocks` | camelCase, matching API and Site keys | `richText`, `heading`, `featureList` |
191
+ | FormattedMessage IDs | camelCase dot-separated | `productCardBlock.displayName` |
192
+ | Locally-scoped helper blocks | Descriptive PascalCase | `DescriptionRichTextBlock` |
@@ -0,0 +1,183 @@
1
+ # API Block Patterns
2
+
3
+ Comet-specific patterns for API-layer block definitions. All API blocks live in `{block-name}.block.ts` (kebab-case) inside `api/src/`, typically under `documents/pages/blocks/` or `common/blocks/`.
4
+
5
+ ---
6
+
7
+ ## Imports
8
+
9
+ ```ts
10
+ import {
11
+ BlockData,
12
+ BlockDataInterface,
13
+ BlockField,
14
+ BlockInput,
15
+ blockInputToData,
16
+ ChildBlock,
17
+ ChildBlockInput,
18
+ createBlock,
19
+ ExtractBlockInput,
20
+ IsUndefinable, // from @comet/cms-api, not class-validator
21
+ } from "@comet/cms-api";
22
+ ```
23
+
24
+ Factory functions (`createListBlock`, `createBlocksBlock`, `createOneOfBlock`, `createOptionalBlock`) are also imported from `@comet/cms-api`. Standard validators (`@IsString`, `@IsInt`, `@IsBoolean`, `@IsEnum`, `@Min`, `@Max`) come from `class-validator`.
25
+
26
+ ---
27
+
28
+ ## BlockData and BlockInput
29
+
30
+ `BlockData` declares the output shape; `BlockInput` declares what the admin sends. Neither class is exported — only the final block constant is.
31
+
32
+ ```ts
33
+ class MyBlockData extends BlockData {
34
+ @BlockField({ nullable: true })
35
+ title?: string;
36
+
37
+ @ChildBlock(DamImageBlock)
38
+ image: BlockDataInterface;
39
+ }
40
+
41
+ class MyBlockInput extends BlockInput {
42
+ @IsUndefinable()
43
+ @IsString()
44
+ @BlockField({ nullable: true })
45
+ title?: string;
46
+
47
+ @ChildBlockInput(DamImageBlock)
48
+ image: ExtractBlockInput<typeof DamImageBlock>;
49
+
50
+ transformToBlockData(): MyBlockData {
51
+ return blockInputToData(MyBlockData, this);
52
+ }
53
+ }
54
+
55
+ export const MyBlock = createBlock(MyBlockData, MyBlockInput, "My");
56
+ ```
57
+
58
+ Key rules:
59
+
60
+ - `transformToBlockData()` must call `blockInputToData(DataClass, this)` — never manually map properties.
61
+ - `@ChildBlock` properties in `BlockData` are always typed as `BlockDataInterface`.
62
+ - `@ChildBlockInput` properties in `BlockInput` are always typed as `ExtractBlockInput<typeof SomeBlock>`.
63
+ - The third argument to `createBlock` is PascalCase **without** a "Block" suffix and must be unique across the project.
64
+
65
+ ---
66
+
67
+ ## @IsUndefinable vs @IsOptional
68
+
69
+ **Always use `@IsUndefinable()` from `@comet/cms-api` for nullable fields — never `@IsOptional()`.**
70
+
71
+ - `@IsUndefinable()` permits only `undefined` and enforces all other validators on non-undefined values.
72
+ - `@IsOptional()` also allows `null` and silently skips all other validators — this can hide validation bugs.
73
+
74
+ ```ts
75
+ // Correct
76
+ @IsUndefinable()
77
+ @IsString()
78
+ @BlockField({ nullable: true })
79
+ title?: string;
80
+
81
+ // Wrong — @IsOptional() skips @IsString() when the value is absent
82
+ @IsOptional()
83
+ @IsString()
84
+ @BlockField({ nullable: true })
85
+ title?: string;
86
+ ```
87
+
88
+ ---
89
+
90
+ ## @BlockField options
91
+
92
+ | Option | Notes |
93
+ | ---------- | ---------------------------------------------------------------------------------- |
94
+ | `nullable` | Allow `undefined`. Always pair with `@IsUndefinable()` on the input property. |
95
+ | `type` | Required for `"enum"` and `"json"`. Auto-detected for string, number, and boolean. |
96
+ | `enum` | Required when `type: "enum"`. Pass the enum object or string array. |
97
+ | `array` | Mark the field as an array of the given type. |
98
+
99
+ ### Enum fields — always specify `type: "enum"`
100
+
101
+ The `type: "enum"` option is **required** — omitting it causes incorrect type inference.
102
+
103
+ ```ts
104
+ export enum Alignment {
105
+ left = "left",
106
+ center = "center",
107
+ }
108
+
109
+ // BlockData
110
+ @BlockField({ type: "enum", enum: Alignment })
111
+ alignment: Alignment;
112
+
113
+ // BlockInput
114
+ @IsEnum(Alignment)
115
+ @BlockField({ type: "enum", enum: Alignment })
116
+ alignment: Alignment;
117
+ ```
118
+
119
+ Enum fields are never nullable — always provide a `defaultValue` in the Admin.
120
+
121
+ ### Enum array (multi-select)
122
+
123
+ ```ts
124
+ // BlockData
125
+ @BlockField({ type: "enum", enum: ProductType, array: true })
126
+ types: ProductType[];
127
+
128
+ // BlockInput
129
+ @IsEnum(ProductType, { each: true })
130
+ @BlockField({ type: "enum", enum: ProductType, array: true })
131
+ types: ProductType[];
132
+ ```
133
+
134
+ `@IsEnum(X, { each: true })` accepts empty arrays, so multi-select fields are automatically savable.
135
+
136
+ ---
137
+
138
+ ## Savability
139
+
140
+ All blocks must be savable in their initial (empty) state — admin users must be able to add a block and save without filling in any content.
141
+
142
+ | Field type | API pattern | Admin default | Pitfall |
143
+ | ----------- | ---------------------------------------------------------------------------- | ----------------------- | ------------------------------------------------------------ |
144
+ | String | `@IsUndefinable()` + `@IsString()` + `@BlockField({ nullable: true })` + `?` | `""` (automatic) | `@IsString()` rejects `undefined` without `@IsUndefinable()` |
145
+ | Enum | `@IsEnum(X)` + `@BlockField({ type: "enum", enum: X })` | Must set `defaultValue` | `@IsEnum()` rejects `undefined` |
146
+ | Enum array | `@IsEnum(X, { each: true })` + `{ array: true }` | `[]` (automatic) | Accepts empty arrays by default |
147
+ | Number | `@IsInt()` + `@Min()` + `@Max()` + `@BlockField()` | Must set `defaultValue` | `@IsInt()` rejects `undefined` |
148
+ | Boolean | `@IsBoolean()` + `@BlockField()` | `false` (automatic) | `@IsBoolean()` rejects `undefined`; switch sends `false` |
149
+ | Child block | `@ChildBlockInput(X)` typed as `ExtractBlockInput<typeof X>` | N/A | Each child block handles its own empty state |
150
+
151
+ **Strings should almost always be optional** to allow saving with no content.
152
+
153
+ ---
154
+
155
+ ## Factory blocks (API)
156
+
157
+ Full block-type examples live in [block-types.md](block-types.md). The critical API-layer rules:
158
+
159
+ ```ts
160
+ // List block
161
+ export const FeatureListBlock = createListBlock({ block: FeatureItemBlock }, "FeatureList");
162
+
163
+ // Blocks block
164
+ export const PageContentBlock = createBlocksBlock({ supportedBlocks: { richText: RichTextBlock, heading: HeadingBlock } }, "PageContent");
165
+
166
+ // One-of block
167
+ export const MediaBlock = createOneOfBlock({ supportedBlocks: { image: DamImageBlock, damVideo: DamVideoBlock } }, "Media");
168
+ ```
169
+
170
+ Keys in `supportedBlocks` must be **camelCase** and **identical** across all layers (API, Admin, Site).
171
+
172
+ ---
173
+
174
+ ## Naming conventions (API layer)
175
+
176
+ | Element | Convention | Example |
177
+ | ------------------------------------- | ----------------------------------- | ----------------------- |
178
+ | File name | kebab-case ending in `.block.ts` | `product-card.block.ts` |
179
+ | `BlockData` class | `{BlockName}BlockData` | `ProductCardBlockData` |
180
+ | `BlockInput` class | `{BlockName}BlockInput` | `ProductCardBlockInput` |
181
+ | Exported constant | `{BlockName}Block` | `ProductCardBlock` |
182
+ | Block name (3rd arg of `createBlock`) | PascalCase, **no** "Block" suffix | `"ProductCard"` |
183
+ | Enum values | camelCase matching the string value | `left = "left"` |