一键导入
editor-component
Create a new Lit web component for the editor module. Use when building a new UI component, panel, or interactive element for the template editor.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Create a new Lit web component for the editor module. Use when building a new UI component, panel, or interactive element for the template editor.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
| name | editor-component |
| description | Create a new Lit web component for the editor module. Use when building a new UI component, panel, or interactive element for the template editor. |
Create a new Lit web component in the editor module.
Input: The component name and its purpose/behavior.
Ask the user (if not already specified):
EditorEngine?There are two distinct patterns based on how the component interacts with the engine:
Reference: EpistolaPreview.ts — modules/editor/src/main/typescript/ui/EpistolaPreview.ts
The component subscribes to engine events and updates internal @state() when events fire.
@customElement('epistola-my-component')
export class EpistolaMyComponent extends LitElement {
override createRenderRoot() { return this }
@property({ attribute: false }) engine?: EditorEngine
@state() private _someState: SomeType = initial
private _unsub?: () => void
override connectedCallback(): void {
super.connectedCallback()
this._setup()
}
override updated(changed: Map<string, unknown>): void {
if (changed.has('engine')) {
this._teardown()
this._setup()
}
}
override disconnectedCallback(): void {
this._teardown()
super.disconnectedCallback()
}
private _setup(): void {
if (!this.engine) return
this._unsub = this.engine.events.on('doc:change', ({ doc }) => {
this._someState = /* derive from doc */
})
}
private _teardown(): void {
this._unsub?.()
this._unsub = undefined
}
override render() {
if (!this.engine) return nothing
return html`<div class="my-component">...</div>`
}
}
When: The component needs to react to document changes, selection changes, or example changes over time (previews, status indicators, tree views).
Reference: EpistolaInspector.ts — modules/editor/src/main/typescript/ui/EpistolaInspector.ts
The component receives data through @property() and renders based on current prop values. It dispatches commands back to the engine.
@customElement("epistola-my-inspector")
export class EpistolaMyInspector extends LitElement {
override createRenderRoot() {
return this;
}
@property({ attribute: false }) engine?: EditorEngine;
@property({ attribute: false }) doc?: TemplateDocument;
@property({ attribute: false }) selectedNodeId?: string | null;
override render() {
if (!this.engine || !this.doc) return nothing;
// Render based on current props
return html`...`;
}
private _handleChange(value: string) {
this.engine?.execute({
type: "UpdateNodeProps",
nodeId: this.selectedNodeId!,
props: { someField: value },
});
}
}
When: The component displays/edits current state passed from a parent — no need for event subscriptions because the parent re-passes updated props after each change.
The @customElement('epistola-xxx') decorator self-registers the element. The browser knows about it once the module is imported.
Exports in lib.ts are for TypeScript types only — so that other modules can import the class for type annotations or instanceof checks. They do NOT affect registration:
// lib.ts — exports are for types, not registration
export { EpistolaMyComponent } from "./ui/EpistolaMyComponent.js";
Reference: modules/editor/src/main/typescript/engine/commands.ts
The Command union type defines all operations the engine supports:
type Command =
| InsertNode
| RemoveNode
| MoveNode
| UpdateNodeProps
| UpdateNodeStyles
| SetStylePreset
| UpdateDocumentStyles
| UpdatePageSettings
| AddColumnSlot
| RemoveColumnSlot;
Each command includes:
applyCommand() function that transforms the documentCommandResult = { ok: true, doc, inverse, structureChanged } | { ok: false, error }To add a new command: add the type to the union, implement applyCommand(), and handle it in EditorEngine.execute().
Reference: modules/editor/src/main/typescript/engine/events.ts
type EngineEvents = {
"doc:change": { doc: TemplateDocument; indexes: DocumentIndexes };
"selection:change": { nodeId: NodeId | null };
"example:change": { index: number; example: object | undefined };
};
Subscribe: engine.events.on('doc:change', handler) — returns an unsubscribe function.
Reference: modules/editor/src/main/typescript/theme-editor/
The theme editor is a separate entry point with its own component tree:
mountThemeEditor(options) in theme-editor-lib.tsEpistolaThemeEditor.ts with init(themeData, onSave) methodsections/ subdirectory with BasicInfoSection, DocumentStylesSection, PageSettingsSection, PresetsSectionThemeEditorState.ts manages form state, dirty tracking, autosaveThe init() method pattern is unique to the theme editor — it initializes from server-provided data after the element is in the DOM.
epistola-<kebab-name> (e.g., epistola-color-picker)Epistola<PascalName> (e.g., EpistolaColorPicker)override createRenderRoot() { return this }@property({ attribute: false }) for complex objects (engine, doc, etc.)@state() for internal reactive stateif (!this.engine) return nothingPreviewService, SaveService)| What | Where |
|---|---|
| UI components | modules/editor/src/main/typescript/ui/ |
| Engine logic | modules/editor/src/main/typescript/engine/ |
| Theme editor | modules/editor/src/main/typescript/theme-editor/ |
| DnD | modules/editor/src/main/typescript/dnd/ |
| ProseMirror | modules/editor/src/main/typescript/prosemirror/ |
| Types | modules/editor/src/main/typescript/types/ |
| Tests | Co-located as *.test.ts next to source |
| CSS | modules/editor/src/main/resources/static/css/ |
modules/editor/src/main/typescript/ui/lib.ts (for type access)pnpm --filter @epistola/editor testpnpm --filter @epistola/editor buildlit/decorators.js (with .js extension).js extension (TypeScript module resolution)nothing from lit is used to render nothing (not null or empty string)execute() method returns CommandResult — check result.ok before assuming successdeepFreeze is applied to the document model — never mutate doc objects directlyCreate a new HTMX form page with Thymeleaf template, UI handler, routes, and search. Use when adding a new create/edit form, list page, or CRUD flow for a domain entity.
Release a Helm chart (epistola or epistola-grafana) to a new version. Use when the user wants to release, publish, or cut a new Helm chart version.
Create a new release. Use when the user wants to release a new version, cut a release, or publish a version.
Review a branch/PR against epistola's architecture conventions and recorded preferences. Use when reviewing code changes; complements /code-review (which finds correctness bugs).
Create a Playwright UI test for browser-based testing. Use when testing HTMX interactions, form submissions, page navigation, or JavaScript behavior in the browser.
Create a new CQRS command or query with handler. Use when adding new business operations (state changes) or data retrieval logic.