@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.
- 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-admin-ui/SKILL.md +492 -0
- package/skills/comet-block/SKILL.md +252 -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-core-admin-component-authoring/SKILL.md +92 -0
- package/skills/comet-mail-react/SKILL.md +621 -0
- package/skills/comet-mail-react/references/components-and-theme.md +431 -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-major-migration/SKILL.md +161 -0
- package/skills/comet-major-migration/references/migration-smoke-test.md +196 -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 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.
|