@dextinity/agent-features 2.0.0-canary-20260729062014

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 (39) 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-admin-ui/SKILL.md +492 -0
  17. package/skills/comet-block/SKILL.md +252 -0
  18. package/skills/comet-block/references/admin-patterns.md +192 -0
  19. package/skills/comet-block/references/api-patterns.md +183 -0
  20. package/skills/comet-block/references/block-loader.md +368 -0
  21. package/skills/comet-block/references/block-types.md +210 -0
  22. package/skills/comet-block/references/custom-block-field.md +266 -0
  23. package/skills/comet-block/references/fixtures.md +436 -0
  24. package/skills/comet-block/references/image.md +341 -0
  25. package/skills/comet-block/references/migration.md +597 -0
  26. package/skills/comet-block/references/registration.md +167 -0
  27. package/skills/comet-block/references/response-summary.md +102 -0
  28. package/skills/comet-block/references/rich-text.md +309 -0
  29. package/skills/comet-block/references/select.md +176 -0
  30. package/skills/comet-block/references/site-patterns.md +202 -0
  31. package/skills/comet-core-admin-component-authoring/SKILL.md +92 -0
  32. package/skills/comet-mail-react/SKILL.md +621 -0
  33. package/skills/comet-mail-react/references/components-and-theme.md +431 -0
  34. package/skills/comet-mail-react/references/layout-patterns.md +315 -0
  35. package/skills/comet-mail-react/references/styling-and-customization.md +306 -0
  36. package/skills/comet-major-migration/SKILL.md +161 -0
  37. package/skills/comet-major-migration/references/migration-smoke-test.md +196 -0
  38. package/skills/comet-minor-update/SKILL.md +191 -0
  39. 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 package (see [Step 2](../SKILL.md#step-2--discover-the-project) for how to detect one).
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 "@dextinity/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 `@dextinity` 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 "@dextinity/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 `@dextinity/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` |
@@ -0,0 +1,92 @@
1
+ ---
2
+ name: comet-core-admin-component-authoring
3
+ description: Authoring or changing a component in @dextinity/admin or a sibling package that uses its slot and theme machinery (admin-color-picker, admin-date-time, admin-rte) so it supports MUI-style customization: per-instance sx/className overrides, theme styleOverrides, and defaultProps. Use whenever adding a new component or editing an existing one's slots, props, class keys, overridable icons, or theme-type registration — even for small changes.
4
+ metadata:
5
+ internal: true
6
+ ---
7
+
8
+ # Authoring customizable @dextinity/admin components
9
+
10
+ Every component shipped from `@dextinity/admin` and its sibling packages must be
11
+ customizable the same way MUI components are: consumers can override a single
12
+ instance (`sx`/`className`), restyle every instance through the theme
13
+ (`styleOverrides`), and set default props through the theme (`defaultProps`). This
14
+ only works if the component is built with the slot/theme machinery. A component
15
+ that hardcodes its markup and styling looks fine in isolation but silently breaks all
16
+ three customization paths, and the gap is only found later, once someone tries to
17
+ customize it. Add the customization from the start.
18
+
19
+ This applies to any component built with the slot and theme machinery —
20
+ `createComponentSlot` and `ThemedComponentBaseProps`, both from `@dextinity/admin`. Today
21
+ that means components in `@dextinity/admin` and the sibling packages that use it:
22
+ `admin-color-picker`, `admin-date-time`, `admin-rte`. Other packages under
23
+ `packages/admin/`, such as `cms-admin`, don't use this pattern and are out of scope.
24
+
25
+ ## How to use this skill
26
+
27
+ Read the reference doc for how to build the component, then use the checklist below as a
28
+ verification pass over what you wrote — it is the list of things that are easy to miss
29
+ and that otherwise get caught only in review. The reference doc is the single source for the
30
+ conventions and their worked code examples:
31
+
32
+ `docs/docs/8-comet-core-development/1-creating-customizable-admin-components.md`
33
+
34
+ Every item below points at a section of that doc. When an item here and the doc disagree,
35
+ follow the doc — and tell the user so this checklist can be corrected.
36
+
37
+ ## When not to use this skill
38
+
39
+ If you are building admin UI in a **project that consumes** `@dextinity/admin` (pages,
40
+ dashboards, dialogs, composing existing components), use the **`comet-admin-ui`** skill
41
+ instead — that is a different job with different rules. This skill is only for authoring
42
+ the library's own components.
43
+
44
+ ## Verification checklist
45
+
46
+ Each item names a rule and why it matters; the doc has the exact syntax for each.
47
+
48
+ 1. **`index.ts` exports the component, its props type (`<Name>Props`), and its class-key
49
+ type (`<Name>ClassKey`).** Consumers need the props and class-key types to type their
50
+ theme overrides.
51
+
52
+ 2. **The `ClassKey` type lists every visual slot and every modifier/variant state, with
53
+ `root` as the outermost slot.** These become the stable, prefixed class names consumers
54
+ target, so they belong in the public type.
55
+
56
+ 3. **Each slot is built with `createComponentSlot`**, passing the base element/component,
57
+ the `componentName`, and the `slotName`. This is what connects the slot to
58
+ `styleOverrides` and produces the deterministic class name.
59
+
60
+ 4. **The props type extends `ThemedComponentBaseProps`**, with a generic mapping each slot
61
+ name to its base element/component. This is what gives consumers `slotProps` and the
62
+ root `sx`/`className`. Each key's value type must be the same element or component
63
+ passed to `createComponentSlot` for that slot — the two are declared separately, so a
64
+ mismatch types that slot's `slotProps` against the wrong component.
65
+
66
+ 5. **Props are read through `useThemeProps`, never the raw `inProps`, with the name
67
+ prefixed `CometAdmin`.** This merges the theme's `defaultProps` with the passed props —
68
+ skip it and `defaultProps` customization is dead.
69
+
70
+ 6. **`slotProps?.<slot>` is spread into each slot, and `restProps` (carrying `sx` and
71
+ `className`) into the `root` slot — after the slot's hardcoded props.** Spreading after
72
+ is what lets consumers override; the exception is props that must never be overridden,
73
+ which go last. This ordering is easy to get wrong.
74
+
75
+ 7. **Style-affecting values are passed through `ownerState`** to the slots that need them,
76
+ rather than writing conditional styling into the markup.
77
+
78
+ 8. **Conditional modifier class names come from `classesResolver(ownerState)`**, returning
79
+ the relevant class keys. This lets consumers target a state from outside the component.
80
+
81
+ 9. **Icons are overridable via an `iconMapping` prop** — an object whose keys say what each
82
+ icon is for. Destructure with an `Icon` suffix and a default, so the component works
83
+ with no mapping but every icon can be replaced.
84
+
85
+ 10. **The component is registered in MUI's theme types** — the `CometAdmin<Name>` key added
86
+ to `ComponentsPropsList`, `ComponentNameToClassKey`, and `Components` (with
87
+ `defaultProps?: Partial<...>` and `styleOverrides`). Without this, consumers get no type
88
+ safety on their overrides.
89
+
90
+ Two final checks that catch the most frequent review findings: the `CometAdmin<Name>` name
91
+ is identical in `useThemeProps` and in all three theme-type interfaces, and the package
92
+ type-checks (`tsc`) — the theme-type augmentation is the usual failure point.