@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.
- package/LICENSE +24 -0
- package/package.json +19 -0
- package/rules/coding-guidelines/api-nestjs.instructions.md +107 -0
- package/rules/coding-guidelines/cdn.instructions.md +24 -0
- package/rules/coding-guidelines/general.instructions.md +30 -0
- package/rules/coding-guidelines/git.instructions.md +37 -0
- package/rules/coding-guidelines/kubernetes.instructions.md +59 -0
- package/rules/coding-guidelines/libraries.instructions.md +34 -0
- package/rules/coding-guidelines/naming.instructions.md +39 -0
- package/rules/coding-guidelines/postgresql.instructions.md +40 -0
- package/rules/coding-guidelines/react.instructions.md +102 -0
- package/rules/coding-guidelines/security.instructions.md +44 -0
- package/rules/coding-guidelines/styling.instructions.md +50 -0
- package/rules/coding-guidelines/typescript.instructions.md +50 -0
- package/skills/.gitkeep +0 -0
- package/skills/comet-block/SKILL.md +246 -0
- package/skills/comet-block/references/admin-patterns.md +192 -0
- package/skills/comet-block/references/api-patterns.md +183 -0
- package/skills/comet-block/references/block-loader.md +368 -0
- package/skills/comet-block/references/block-types.md +210 -0
- package/skills/comet-block/references/custom-block-field.md +266 -0
- package/skills/comet-block/references/fixtures.md +436 -0
- package/skills/comet-block/references/image.md +341 -0
- package/skills/comet-block/references/migration.md +597 -0
- package/skills/comet-block/references/registration.md +167 -0
- package/skills/comet-block/references/response-summary.md +102 -0
- package/skills/comet-block/references/rich-text.md +309 -0
- package/skills/comet-block/references/select.md +176 -0
- package/skills/comet-block/references/site-patterns.md +202 -0
- package/skills/comet-mail-react/SKILL.md +539 -0
- package/skills/comet-mail-react/references/components-and-theme.md +395 -0
- package/skills/comet-mail-react/references/layout-patterns.md +315 -0
- package/skills/comet-mail-react/references/styling-and-customization.md +306 -0
- package/skills/comet-minor-update/SKILL.md +191 -0
- package/skills/dev-pm/SKILL.md +100 -0
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# Select Field Patterns
|
|
2
|
+
|
|
3
|
+
Comet-specific patterns for select (dropdown) fields. Load this file when a block contains a select/enum field.
|
|
4
|
+
|
|
5
|
+
The basic enum definition (`export enum Variant { ... }`, `@IsEnum`, `@BlockField({ type: "enum" })`) is covered in api-patterns.md. The full `createCompositeBlockSelectField` options table is in admin-patterns.md. This file covers the **unique patterns** only.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Numeric Select (Non-Enum)
|
|
10
|
+
|
|
11
|
+
Select fields work for `number` values too — no enum needed in the API. Use `@IsInt` (or `@IsNumber`) instead of `@IsEnum`, and plain `number` as the TypeScript type.
|
|
12
|
+
|
|
13
|
+
**API:**
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
class ExampleBlockData extends BlockData {
|
|
17
|
+
@BlockField()
|
|
18
|
+
overlay: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
class ExampleBlockInput extends BlockInput {
|
|
22
|
+
@IsInt()
|
|
23
|
+
@Min(0)
|
|
24
|
+
@Max(90)
|
|
25
|
+
@BlockField()
|
|
26
|
+
overlay: number;
|
|
27
|
+
|
|
28
|
+
transformToBlockData(): ExampleBlockData {
|
|
29
|
+
return blockInputToData(ExampleBlockData, this);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
**Admin** — use `FormattedNumber` for locale-aware labels:
|
|
35
|
+
|
|
36
|
+
```tsx
|
|
37
|
+
const overlayOptions: Array<{ value: ExampleBlockData["overlay"]; label: ReactNode }> = [
|
|
38
|
+
{ value: 90, label: <FormattedNumber value={0.9} style="percent" /> },
|
|
39
|
+
{ value: 70, label: <FormattedNumber value={0.7} style="percent" /> },
|
|
40
|
+
{ value: 50, label: <FormattedNumber value={0.5} style="percent" /> },
|
|
41
|
+
{ value: 30, label: <FormattedNumber value={0.3} style="percent" /> },
|
|
42
|
+
{ value: 0, label: <FormattedNumber value={0} style="percent" /> },
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
overlay: {
|
|
46
|
+
block: createCompositeBlockSelectField<ExampleBlockData["overlay"]>({
|
|
47
|
+
defaultValue: 50,
|
|
48
|
+
options: overlayOptions,
|
|
49
|
+
}),
|
|
50
|
+
title: <FormattedMessage id="exampleBlock.overlay" defaultMessage="Overlay" />,
|
|
51
|
+
},
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
> Pass percentages as fractions (`0.9` for 90%) to `FormattedNumber`.
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## Generated Options (`.map`)
|
|
59
|
+
|
|
60
|
+
When options follow a regular pattern, generate them programmatically instead of listing each manually:
|
|
61
|
+
|
|
62
|
+
```tsx
|
|
63
|
+
titleHtmlTag: {
|
|
64
|
+
block: createCompositeBlockSelectField<ExampleItemBlockData["titleHtmlTag"]>({
|
|
65
|
+
label: <FormattedMessage id="exampleItemBlock.titleHtmlTag" defaultMessage="Title HTML tag" />,
|
|
66
|
+
defaultValue: "h3",
|
|
67
|
+
options: ([1, 2, 3, 4, 5, 6] as const).map((level) => ({
|
|
68
|
+
value: `h${level}` as ExampleItemBlockData["titleHtmlTag"],
|
|
69
|
+
label: <FormattedMessage id="exampleItemBlock.headline" defaultMessage="Headline {level}" values={{ level }} />,
|
|
70
|
+
})),
|
|
71
|
+
required: true,
|
|
72
|
+
}),
|
|
73
|
+
},
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## `label` vs `title` — Mutual Exclusivity
|
|
79
|
+
|
|
80
|
+
| Scenario | Use |
|
|
81
|
+
| ---------------------------------------------------------------------- | ----------------------------------------------------------------- |
|
|
82
|
+
| Field has its own inline label (visible inside the dropdown component) | `label` on the field options; **omit** `title` on the block entry |
|
|
83
|
+
| No inline label | **Omit** `label`; use `title` on the block entry instead |
|
|
84
|
+
|
|
85
|
+
Using both creates redundant, stacked headings.
|
|
86
|
+
|
|
87
|
+
```tsx
|
|
88
|
+
// label only — no title
|
|
89
|
+
variant: {
|
|
90
|
+
block: createCompositeBlockSelectField<ExampleBlockData["variant"]>({
|
|
91
|
+
label: <FormattedMessage id="exampleBlock.variant" defaultMessage="Variant" />,
|
|
92
|
+
defaultValue: "contained",
|
|
93
|
+
options: variantOptions,
|
|
94
|
+
}),
|
|
95
|
+
},
|
|
96
|
+
|
|
97
|
+
// title only — no label
|
|
98
|
+
alignment: {
|
|
99
|
+
block: createCompositeBlockSelectField<ExampleBlockData["alignment"]>({
|
|
100
|
+
defaultValue: "left",
|
|
101
|
+
options: alignmentOptions,
|
|
102
|
+
}),
|
|
103
|
+
title: <FormattedMessage id="exampleBlock.alignment" defaultMessage="Alignment" />,
|
|
104
|
+
},
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## Multi-Select
|
|
110
|
+
|
|
111
|
+
**API** — add `array: true` to `@BlockField` and `{ each: true }` to `@IsEnum`:
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
class MyBlockData extends BlockData {
|
|
115
|
+
@BlockField({ type: "enum", enum: Category, array: true })
|
|
116
|
+
categories: Category[];
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
class MyBlockInput extends BlockInput {
|
|
120
|
+
@IsEnum(Category, { each: true })
|
|
121
|
+
@BlockField({ type: "enum", enum: Category, array: true })
|
|
122
|
+
categories: Category[];
|
|
123
|
+
...
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
An empty array `[]` is valid — `{ each: true }` passes on zero elements.
|
|
128
|
+
|
|
129
|
+
**Admin** — set `multiple: true` and `defaultValue: []`:
|
|
130
|
+
|
|
131
|
+
```tsx
|
|
132
|
+
categories: {
|
|
133
|
+
block: createCompositeBlockSelectField<MyBlockData["categories"]>({
|
|
134
|
+
label: <FormattedMessage id="myBlock.categories" defaultMessage="Categories" />,
|
|
135
|
+
defaultValue: [],
|
|
136
|
+
options: [
|
|
137
|
+
{ value: "news", label: <FormattedMessage id="myBlock.categories.news" defaultMessage="News" /> },
|
|
138
|
+
{ value: "blog", label: <FormattedMessage id="myBlock.categories.blog" defaultMessage="Blog" /> },
|
|
139
|
+
{ value: "event", label: <FormattedMessage id="myBlock.categories.event" defaultMessage="Event" /> },
|
|
140
|
+
],
|
|
141
|
+
multiple: true,
|
|
142
|
+
}),
|
|
143
|
+
},
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## `required` Option
|
|
149
|
+
|
|
150
|
+
`required: true` removes the empty/placeholder option from the dropdown — the user must always have a value selected.
|
|
151
|
+
|
|
152
|
+
Use it for fields with no meaningful "none" state (e.g., HTML tag choice). For most enum fields, prefer `defaultValue` over `required` — this keeps the block savable immediately while still letting users change the value.
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
## Default Value Rules
|
|
157
|
+
|
|
158
|
+
| Scenario | `defaultValue` |
|
|
159
|
+
| ------------------------------ | ---------------------------------------------------------------- |
|
|
160
|
+
| Single select, logical default | Most common/neutral value (`"left"`, `"contained"`, `"default"`) |
|
|
161
|
+
| HTML tag select | Most semantically common tag (`"h3"`) |
|
|
162
|
+
| Numeric select | Middle or most common value (`50` for overlay percentage) |
|
|
163
|
+
| Multi-select | `[]` |
|
|
164
|
+
|
|
165
|
+
Enum fields are always required in the API (`@IsEnum` rejects `undefined`), so the Admin **must** provide a `defaultValue`.
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
## Common Pitfalls
|
|
170
|
+
|
|
171
|
+
1. **`label` and `title` both set** — creates duplicate headings; use one or the other.
|
|
172
|
+
2. **Multi-select: missing `{ each: true }`** — `@IsEnum(MyEnum)` without it rejects arrays.
|
|
173
|
+
3. **Multi-select: missing `array: true` in `@BlockField`** — field serialises as a single value instead of an array.
|
|
174
|
+
4. **Missing `defaultValue`** — block fails to save on first add because `@IsEnum` rejects `undefined`.
|
|
175
|
+
5. **Enum not exported** — must be exported from the API file to appear in the generated GraphQL schema and `@src/blocks.generated` types.
|
|
176
|
+
6. **`@IsOptional()` on an enum field** — enum fields must always have a value; making them optional creates an API/Admin inconsistency.
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
# Site Block Patterns
|
|
2
|
+
|
|
3
|
+
Comet-specific conventions for the site layer. All site blocks live in `{BlockName}Block.tsx` (PascalCase) under `site/src/`, typically `documents/pages/blocks/` or `common/blocks/`.
|
|
4
|
+
|
|
5
|
+
Skip site block creation when the project has no `site` directory.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Imports
|
|
10
|
+
|
|
11
|
+
```tsx
|
|
12
|
+
import {
|
|
13
|
+
BlocksBlock,
|
|
14
|
+
hasRichTextBlockContent,
|
|
15
|
+
ListBlock,
|
|
16
|
+
OneOfBlock,
|
|
17
|
+
OptionalBlock,
|
|
18
|
+
PreviewSkeleton,
|
|
19
|
+
type PropsWithData,
|
|
20
|
+
SvgImageBlock,
|
|
21
|
+
type SupportedBlocks,
|
|
22
|
+
withPreview,
|
|
23
|
+
} from "@comet/site-nextjs";
|
|
24
|
+
import { type MyBlockData } from "@src/blocks.generated";
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
`@src/blocks.generated` types are auto-generated from the API layer — always import with `type`, never maintain manually.
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## withPreview
|
|
32
|
+
|
|
33
|
+
Wrap **every** site block with `withPreview`. This enables the admin preview overlay. The `label` appears in the overlay.
|
|
34
|
+
|
|
35
|
+
```tsx
|
|
36
|
+
export const MyBlock = withPreview(
|
|
37
|
+
({ data: { title, image } }: PropsWithData<MyBlockData>) => (
|
|
38
|
+
<div>
|
|
39
|
+
<h2>{title}</h2>
|
|
40
|
+
<DamImageBlock data={image} aspectRatio="16x9" />
|
|
41
|
+
</div>
|
|
42
|
+
),
|
|
43
|
+
{ label: "My Block" },
|
|
44
|
+
);
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
**Exception:** Top-level `PageContentBlock` and `PageContent`-wrapper variants are **not** wrapped with `withPreview` — they delegate to the inner block which is already wrapped.
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## DamImageBlock Site Wrapper
|
|
52
|
+
|
|
53
|
+
The site layer does **not** use `DamImageBlock` from a `@comet` package. It uses the site-specific wrapper, typically at `@src/common/blocks/DamImageBlock`:
|
|
54
|
+
|
|
55
|
+
```tsx
|
|
56
|
+
// Wrong: import from cms-api
|
|
57
|
+
import { DamImageBlock } from "@comet/cms-api";
|
|
58
|
+
|
|
59
|
+
// Correct: import site wrapper
|
|
60
|
+
import { DamImageBlock } from "@src/common/blocks/DamImageBlock";
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Always pass `aspectRatio`. Optionally pass `sizes` for responsive images:
|
|
64
|
+
|
|
65
|
+
```tsx
|
|
66
|
+
<DamImageBlock data={image} aspectRatio="16x9" sizes="(max-width: 768px) 100vw, 50vw" />
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## Guarding Optional Content
|
|
72
|
+
|
|
73
|
+
**Rich text** — always use `hasRichTextBlockContent`. Never render a `RichTextBlock` without this guard:
|
|
74
|
+
|
|
75
|
+
```tsx
|
|
76
|
+
{
|
|
77
|
+
hasRichTextBlockContent(description) && <RichTextBlock data={description} />;
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
**SVG image blocks** — check `damFile`:
|
|
82
|
+
|
|
83
|
+
```tsx
|
|
84
|
+
{
|
|
85
|
+
icon.damFile && <SvgImageBlock data={icon} width={48} height={48} />;
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
**Nullable strings** — standard JS truthiness:
|
|
90
|
+
|
|
91
|
+
```tsx
|
|
92
|
+
{
|
|
93
|
+
title && <h2>{title}</h2>;
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
`BlocksBlock`, `ListBlock`, `OneOfBlock`, and `OptionalBlock` handle their own empty states — no guard needed.
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## BlocksBlock (PageContentBlock)
|
|
102
|
+
|
|
103
|
+
Define `supportedBlocks` as a **module-level constant**, never inside the component body (prevents recreation on every render).
|
|
104
|
+
|
|
105
|
+
Keys must exactly match the API and Admin layer keys.
|
|
106
|
+
|
|
107
|
+
```tsx
|
|
108
|
+
const supportedBlocks: SupportedBlocks = {
|
|
109
|
+
richText: (props) => <StandaloneRichTextBlock data={props} />,
|
|
110
|
+
heading: (props) => <StandaloneHeadingBlock data={props} />,
|
|
111
|
+
media: (props) => <StandaloneMediaBlock data={props} />,
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
export const PageContentBlock = ({ data }: PropsWithData<PageContentBlockData>) => {
|
|
115
|
+
return <BlocksBlock data={data} supportedBlocks={supportedBlocks} />;
|
|
116
|
+
};
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## ListBlock
|
|
122
|
+
|
|
123
|
+
Pass the entire `data` object (not `data.blocks`). To access item count, use `data.blocks.length` alongside the `ListBlock`:
|
|
124
|
+
|
|
125
|
+
```tsx
|
|
126
|
+
export const FeatureListBlock = withPreview(
|
|
127
|
+
({ data }: PropsWithData<FeatureListBlockData>) => (
|
|
128
|
+
<div style={{ "--list-item-count": data.blocks.length }}>
|
|
129
|
+
<ListBlock data={data} block={(block) => <FeatureItemBlock data={block} />} />
|
|
130
|
+
</div>
|
|
131
|
+
),
|
|
132
|
+
{ label: "Feature List" },
|
|
133
|
+
);
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## OneOfBlock and OptionalBlock
|
|
139
|
+
|
|
140
|
+
See [block-types.md](block-types.md) for full usage. Quick reference:
|
|
141
|
+
|
|
142
|
+
- `OneOfBlock` — renders the selected block from a mutually exclusive set. Returns `null` when nothing is selected.
|
|
143
|
+
- `OptionalBlock` — renders a block with a visibility toggle. Returns `null` when `visible` is `false`.
|
|
144
|
+
|
|
145
|
+
Both accept a `supportedBlocks` map defined at module level.
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
## Standalone and PageContent Wrapper Pattern
|
|
150
|
+
|
|
151
|
+
When a block appears in both `PageContentBlock` (needs layout wrapper) and other contexts (layout provided by parent), export two components:
|
|
152
|
+
|
|
153
|
+
```tsx
|
|
154
|
+
// Inner block — used standalone or nested; always withPreview
|
|
155
|
+
export const StandaloneHeadingBlock = withPreview(
|
|
156
|
+
({ data: { heading } }: PropsWithData<StandaloneHeadingBlockData>) => (
|
|
157
|
+
<div>
|
|
158
|
+
<HeadingBlock data={heading} />
|
|
159
|
+
</div>
|
|
160
|
+
),
|
|
161
|
+
{ label: "Heading" },
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
// PageContent wrapper — adds layout; NOT withPreview (delegates to inner)
|
|
165
|
+
export const PageContentStandaloneHeadingBlock = (props: PropsWithData<StandaloneHeadingBlockData>) => (
|
|
166
|
+
<PageLayout grid>
|
|
167
|
+
<div className={styles.pageLayoutContent}>
|
|
168
|
+
<StandaloneHeadingBlock {...props} />
|
|
169
|
+
</div>
|
|
170
|
+
</PageLayout>
|
|
171
|
+
);
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Register the `PageContent`-prefixed variant in the `PageContentBlock` `supportedBlocks` map and the plain variant wherever the block appears nested in other blocks.
|
|
175
|
+
|
|
176
|
+
---
|
|
177
|
+
|
|
178
|
+
## "use client" Directive
|
|
179
|
+
|
|
180
|
+
Add `"use client"` at the top of the file when the block uses:
|
|
181
|
+
|
|
182
|
+
- React hooks (`useState`, `useEffect`, `useRef`, `useId`, `useContext`, etc.)
|
|
183
|
+
- Event handlers (`onClick`, `onChange`, etc.)
|
|
184
|
+
- Client-only library imports
|
|
185
|
+
- `usePreview()` from `@comet/site-nextjs`
|
|
186
|
+
|
|
187
|
+
Most simple composite and list blocks do **not** need `"use client"`.
|
|
188
|
+
|
|
189
|
+
---
|
|
190
|
+
|
|
191
|
+
## Naming Conventions
|
|
192
|
+
|
|
193
|
+
| Element | Convention | Example |
|
|
194
|
+
| ---------------------- | -------------------------------------------------------- | ----------------------------------- |
|
|
195
|
+
| File name | PascalCase ending in `Block.tsx` | `ProductCardBlock.tsx` |
|
|
196
|
+
| Exported constant | `{BlockName}Block` | `ProductCardBlock` |
|
|
197
|
+
| `withPreview` label | Short human-readable name | `"Stage"`, `"Feature Item"` |
|
|
198
|
+
| `supportedBlocks` keys | camelCase, matching API and Admin keys exactly | `richText`, `heading` |
|
|
199
|
+
| CSS module file | `{BlockName}Block.module.scss` | `ProductCardBlock.module.scss` |
|
|
200
|
+
| Data type import | `type {BlockName}BlockData` from `@src/blocks.generated` | `type ProductCardBlockData` |
|
|
201
|
+
| PageContent wrapper | `PageContent{BlockName}Block` | `PageContentStandaloneHeadingBlock` |
|
|
202
|
+
| Local/nested blocks | Descriptive PascalCase, not exported | `AccordionContentBlock` |
|