@comet/agent-features 9.0.0-beta.5

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 +541 -0
  31. package/skills/comet-mail-react/references/components-and-theme.md +441 -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,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` |