Initial commit
This commit is contained in:
14
.claude-plugin/plugin.json
Normal file
14
.claude-plugin/plugin.json
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"name": "angular",
|
||||||
|
"description": "A plugin to support generation of good looking and well designed api components",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"author": {
|
||||||
|
"name": "Reto Ryter"
|
||||||
|
},
|
||||||
|
"agents": [
|
||||||
|
"./agents"
|
||||||
|
],
|
||||||
|
"commands": [
|
||||||
|
"./commands"
|
||||||
|
]
|
||||||
|
}
|
||||||
3
README.md
Normal file
3
README.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# angular
|
||||||
|
|
||||||
|
A plugin to support generation of good looking and well designed api components
|
||||||
0
agents/architecture.md
Normal file
0
agents/architecture.md
Normal file
376
commands/create-component.md
Normal file
376
commands/create-component.md
Normal file
@@ -0,0 +1,376 @@
|
|||||||
|
# UI Component Command
|
||||||
|
|
||||||
|
Generate modern Angular standalone components with signals, using Spartan UI components from `libs/ui`.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```
|
||||||
|
/ui-component <component-name> <reference-description>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Arguments:**
|
||||||
|
|
||||||
|
- `component-name` (required): Name for the component in kebab-case (e.g., "user-card", "task-list", "metric-display")
|
||||||
|
- `reference-description` (required): Description of what the component should do and how it should look
|
||||||
|
|
||||||
|
**Examples:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
/ui-component user-card "A card component that displays user info with avatar, name, email, and a follow button"
|
||||||
|
/ui-component metric-display "Display a metric with a large number, label, trend indicator (up/down arrow), and percentage change"
|
||||||
|
/ui-component task-list "A list of tasks with checkboxes, due dates, priority badges, and delete buttons"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Output Structure
|
||||||
|
|
||||||
|
Generated files are saved to:
|
||||||
|
|
||||||
|
```
|
||||||
|
sentinel-frontend/libs/shared/ui-custom/src/lib/components/<component-name>/
|
||||||
|
├── <component-name>.component.ts
|
||||||
|
├── <component-name>.component.html
|
||||||
|
└── <component-name>.component.css
|
||||||
|
```
|
||||||
|
|
||||||
|
## Instructions
|
||||||
|
|
||||||
|
When this command is invoked, follow these steps:
|
||||||
|
|
||||||
|
### 1. Gather Context from Angular Docs (Optional)
|
||||||
|
|
||||||
|
If Context7 is available, use it to fetch relevant Angular documentation:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx context7 search angular
|
||||||
|
# Then use the correct project path:
|
||||||
|
npx context7 <angular-project-path> "angular signals input output model"
|
||||||
|
npx context7 <angular-project-path> "standalone components"
|
||||||
|
```
|
||||||
|
|
||||||
|
If Context7 is not available, proceed with the following Angular patterns:
|
||||||
|
|
||||||
|
- Modern signal APIs: `signal()`, `computed()`, `effect()`
|
||||||
|
- Input/output patterns: `input()`, `input.required()`, `output()`, `model()`
|
||||||
|
- Standalone component syntax with `standalone: true`
|
||||||
|
- Modern control flow: `@if`, `@for`, `@switch`, `@defer`
|
||||||
|
- Use `inject()` for dependency injection instead of constructor injection
|
||||||
|
|
||||||
|
### 2. Identify Spartan UI Components to Use
|
||||||
|
|
||||||
|
Available Spartan UI components in `libs/ui`:
|
||||||
|
|
||||||
|
- **Layout**: `ui-card-helm`, `ui-separator-helm`, `ui-accordion-helm`, `ui-sheet-helm`, `ui-dialog-helm`, `ui-sidebar-helm`
|
||||||
|
- **Forms**: `ui-input-helm`, `ui-textarea-helm`, `ui-checkbox-helm`, `ui-radio-group-helm`, `ui-select-helm`, `ui-switch-helm`, `ui-slider-helm`, `ui-form-field-helm`
|
||||||
|
- **Buttons**: `ui-button-helm`, `ui-button-group-helm`, `ui-toggle-helm`, `ui-toggle-group-helm`
|
||||||
|
- **Data Display**: `ui-table-helm`, `ui-badge-helm`, `ui-avatar-helm`, `ui-progress-helm`, `ui-skeleton-helm`, `ui-empty-helm`
|
||||||
|
- **Feedback**: `ui-alert-helm`, `ui-alert-dialog-helm`, `ui-tooltip-helm`, `ui-popover-helm`, `ui-hover-card-helm`, `ui-sonner-helm`, `ui-spinner-helm`
|
||||||
|
- **Navigation**: `ui-tabs-helm`, `ui-menu-helm`, `ui-breadcrumb-helm`, `ui-pagination-helm`
|
||||||
|
- **Utilities**: `ui-icon-helm`, `ui-label-helm`, `ui-typography-helm`, `ui-scroll-area-helm`, `ui-resizable-helm`, `ui-kbd-helm`
|
||||||
|
|
||||||
|
Choose appropriate components based on the reference description.
|
||||||
|
|
||||||
|
### 3. Generate Component TypeScript File
|
||||||
|
|
||||||
|
Create a **standalone** component following these patterns:
|
||||||
|
|
||||||
|
**Component Structure:**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { ChangeDetectionStrategy, Component, input, output, model, signal, computed } from '@angular/core';
|
||||||
|
import { HlmButtonDirective } from '@spartan-ng/helm/button';
|
||||||
|
import { HlmCardDirective, HlmCardHeaderDirective, HlmCardContentDirective } from '@spartan-ng/helm/card';
|
||||||
|
// Import other Spartan UI components as needed
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-<component-name>',
|
||||||
|
standalone: true,
|
||||||
|
imports: [
|
||||||
|
// List all Spartan UI components used
|
||||||
|
HlmButtonDirective,
|
||||||
|
HlmCardDirective,
|
||||||
|
HlmCardHeaderDirective,
|
||||||
|
HlmCardContentDirective,
|
||||||
|
// Add others as needed
|
||||||
|
],
|
||||||
|
templateUrl: './<component-name>.component.html',
|
||||||
|
styleUrls: ['./<component-name>.component.css'],
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
})
|
||||||
|
export class <ComponentName>Component {
|
||||||
|
// Use input() for read-only inputs
|
||||||
|
readonly title = input.required<string>();
|
||||||
|
readonly subtitle = input<string>('');
|
||||||
|
|
||||||
|
// Use output() for events
|
||||||
|
readonly clicked = output<void>();
|
||||||
|
|
||||||
|
// Use model() for two-way binding
|
||||||
|
readonly value = model<number>(0);
|
||||||
|
|
||||||
|
// Use signal() for internal state
|
||||||
|
protected readonly isExpanded = signal(false);
|
||||||
|
|
||||||
|
// Use computed() for derived state
|
||||||
|
protected readonly displayValue = computed(() => {
|
||||||
|
return `${this.title()}: ${this.value()}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Methods
|
||||||
|
protected handleClick() {
|
||||||
|
this.clicked.emit();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected toggle() {
|
||||||
|
this.isExpanded.update(v => !v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Key Patterns:**
|
||||||
|
|
||||||
|
- Use `ChangeDetectionStrategy.OnPush` for better performance
|
||||||
|
- Use `input()` for props, `input.required()` for required props
|
||||||
|
- Use `output()` for events
|
||||||
|
- Use `model()` for two-way bound properties
|
||||||
|
- Use `signal()` for internal reactive state
|
||||||
|
- Use `computed()` for derived values
|
||||||
|
- Use `readonly` for public API properties
|
||||||
|
- Use `protected` for template-only methods and properties
|
||||||
|
- Import Spartan UI directives/components, not modules
|
||||||
|
|
||||||
|
### 4. Generate Component HTML Template
|
||||||
|
|
||||||
|
Use modern Angular template syntax:
|
||||||
|
|
||||||
|
**Control Flow:**
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!-- Use @if instead of *ngIf -->
|
||||||
|
@if (isExpanded()) {
|
||||||
|
<div>Expanded content</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<!-- Use @for instead of *ngFor -->
|
||||||
|
@for (item of items(); track item.id) {
|
||||||
|
<div>{{ item.name }}</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<!-- Use @switch instead of *ngSwitchCase -->
|
||||||
|
@switch (status()) { @case ('loading') {
|
||||||
|
<app-spinner />
|
||||||
|
} @case ('success') {
|
||||||
|
<div>Success!</div>
|
||||||
|
} @default {
|
||||||
|
<div>Unknown status</div>
|
||||||
|
} }
|
||||||
|
|
||||||
|
<!-- Use @defer for lazy loading -->
|
||||||
|
@defer (on viewport) {
|
||||||
|
<heavy-component />
|
||||||
|
} @placeholder {
|
||||||
|
<app-skeleton />
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Spartan UI Components:**
|
||||||
|
Use Spartan UI components with their directive syntax:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<button hlmBtn (click)="handleClick()">Click me</button>
|
||||||
|
|
||||||
|
<div hlmCard>
|
||||||
|
<div hlmCardHeader>
|
||||||
|
<h3 hlmCardTitle>{{ title() }}</h3>
|
||||||
|
<p hlmCardDescription>{{ subtitle() }}</p>
|
||||||
|
</div>
|
||||||
|
<div hlmCardContent>
|
||||||
|
<!-- Content here -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input hlmInput [value]="inputValue()" (input)="updateValue($event)" />
|
||||||
|
|
||||||
|
<span hlmBadge>Badge</span>
|
||||||
|
|
||||||
|
<div hlmAvatar>
|
||||||
|
<img [src]="avatarUrl()" alt="Avatar" />
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Generate Component SCSS File
|
||||||
|
|
||||||
|
Create minimal SCSS for custom styling:
|
||||||
|
|
||||||
|
- Only add SCSS that can't be achieved with Spartan UI or Tailwind
|
||||||
|
- Use SCSS custom properties for theming when possible
|
||||||
|
- Keep styles scoped to the component
|
||||||
|
- Add comments explaining custom styles
|
||||||
|
|
||||||
|
```scss
|
||||||
|
/* Custom styles that extend Spartan UI */
|
||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. Create the Component Files
|
||||||
|
|
||||||
|
Use the Write tool to create all three files:
|
||||||
|
|
||||||
|
1. `<component-name>.component.ts`
|
||||||
|
2. `<component-name>.component.html`
|
||||||
|
3. `<component-name>.component.css`
|
||||||
|
|
||||||
|
<!-- TODO: fix path instruction -->
|
||||||
|
|
||||||
|
Save them to: `sentinel-frontend/libs/shared/ui-custom/src/lib/components/<component-name>/`
|
||||||
|
|
||||||
|
### 7. Generate Usage Example
|
||||||
|
|
||||||
|
Provide a clear usage example showing:
|
||||||
|
|
||||||
|
- How to import the component
|
||||||
|
- Example template usage with all inputs/outputs
|
||||||
|
- Common use cases
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// In another component
|
||||||
|
import { <ComponentName>Component } from '@shared/ui-custom';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
imports: [<ComponentName>Component],
|
||||||
|
template: `
|
||||||
|
<app-<component-name>
|
||||||
|
[title]="myTitle"
|
||||||
|
[subtitle]="mySubtitle"
|
||||||
|
[(value)]="currentValue"
|
||||||
|
(clicked)="handleClick()"
|
||||||
|
/>
|
||||||
|
`
|
||||||
|
})
|
||||||
|
export class MyComponent {
|
||||||
|
protected myTitle = 'Example';
|
||||||
|
protected mySubtitle = 'Subtitle';
|
||||||
|
protected currentValue = signal(42);
|
||||||
|
|
||||||
|
protected handleClick() {
|
||||||
|
console.log('Component clicked!');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Important Notes
|
||||||
|
|
||||||
|
- **Always use modern Angular syntax**: signals, standalone components, new control flow
|
||||||
|
- **No NgModules**: Components should be standalone
|
||||||
|
- **Use inject() instead of constructor DI**: More modern and flexible
|
||||||
|
- **Spartan UI over custom HTML**: Use Spartan UI components as building blocks
|
||||||
|
- **OnPush change detection**: Always use for better performance
|
||||||
|
- **Type safety**: Always provide proper TypeScript types
|
||||||
|
- **Accessibility**: Ensure proper ARIA labels, semantic HTML, keyboard navigation
|
||||||
|
- **Naming convention**: Components should be kebab-case, class names should be PascalCase
|
||||||
|
- **Template syntax**: Use `@if/@for/@switch` not `*ngIf/*ngFor/*ngSwitch`
|
||||||
|
- **Signal calls**: Remember to call signals with `()`: `mySignal()` not `mySignal`
|
||||||
|
|
||||||
|
## Common Patterns
|
||||||
|
|
||||||
|
### Presentational Component (Dumb Component)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
@Component({
|
||||||
|
selector: "app-user-card",
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
})
|
||||||
|
export class UserCardComponent {
|
||||||
|
// Inputs only
|
||||||
|
readonly user = input.required<User>();
|
||||||
|
readonly compact = input(false);
|
||||||
|
|
||||||
|
// Outputs only
|
||||||
|
readonly userClicked = output<User>();
|
||||||
|
|
||||||
|
// No internal state or business logic
|
||||||
|
protected handleClick() {
|
||||||
|
this.userClicked.emit(this.user());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Form Control Component
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
@Component({
|
||||||
|
selector: "app-rating-input",
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
})
|
||||||
|
export class RatingInputComponent {
|
||||||
|
// Two-way binding with model()
|
||||||
|
readonly rating = model.required<number>();
|
||||||
|
readonly max = input(5);
|
||||||
|
readonly disabled = input(false);
|
||||||
|
|
||||||
|
protected setRating(value: number) {
|
||||||
|
if (!this.disabled()) {
|
||||||
|
this.rating.set(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Stateful Component
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
@Component({
|
||||||
|
selector: "app-expandable-section",
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
})
|
||||||
|
export class ExpandableSectionComponent {
|
||||||
|
// External inputs
|
||||||
|
readonly title = input.required<string>();
|
||||||
|
readonly defaultExpanded = input(false);
|
||||||
|
|
||||||
|
// Internal state
|
||||||
|
protected readonly isExpanded = signal(false);
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
// Initialize from input
|
||||||
|
effect(() => {
|
||||||
|
this.isExpanded.set(this.defaultExpanded());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected toggle() {
|
||||||
|
this.isExpanded.update((v) => !v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
|
||||||
|
After generating the component:
|
||||||
|
|
||||||
|
1. Check that all imports are correct
|
||||||
|
2. Verify signal syntax (remember the `()` calls)
|
||||||
|
3. Ensure proper TypeScript types
|
||||||
|
4. Confirm Spartan UI components are used correctly
|
||||||
|
5. Validate template syntax uses modern control flow
|
||||||
|
6. Check that component is standalone
|
||||||
|
7. Verify change detection strategy is OnPush
|
||||||
|
|
||||||
|
## Summary Output
|
||||||
|
|
||||||
|
Provide to the user:
|
||||||
|
|
||||||
|
- Paths to all generated files
|
||||||
|
- Brief description of the component
|
||||||
|
- List of Spartan UI components used
|
||||||
|
- Usage example
|
||||||
|
- Any notes about customization or extension
|
||||||
49
plugin.lock.json
Normal file
49
plugin.lock.json
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
{
|
||||||
|
"$schema": "internal://schemas/plugin.lock.v1.json",
|
||||||
|
"pluginId": "gh:rryter/claude-plugins:angular",
|
||||||
|
"normalized": {
|
||||||
|
"repo": null,
|
||||||
|
"ref": "refs/tags/v20251128.0",
|
||||||
|
"commit": "3ebd96907c49162ad6b7985eff7cd00ff59e40fe",
|
||||||
|
"treeHash": "17eeb4ed8deae4655eecc58db9692d26e96f5ffd4f9618d89eec41f62a413d9c",
|
||||||
|
"generatedAt": "2025-11-28T10:28:03.447032Z",
|
||||||
|
"toolVersion": "publish_plugins.py@0.2.0"
|
||||||
|
},
|
||||||
|
"origin": {
|
||||||
|
"remote": "git@github.com:zhongweili/42plugin-data.git",
|
||||||
|
"branch": "master",
|
||||||
|
"commit": "aa1497ed0949fd50e99e70d6324a29c5b34f9390",
|
||||||
|
"repoRoot": "/Users/zhongweili/projects/openmind/42plugin-data"
|
||||||
|
},
|
||||||
|
"manifest": {
|
||||||
|
"name": "angular",
|
||||||
|
"description": "A plugin to support generation of good looking and well designed api components",
|
||||||
|
"version": "0.0.1"
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"files": [
|
||||||
|
{
|
||||||
|
"path": "README.md",
|
||||||
|
"sha256": "945d86b3fc0f9f4f105c0f722cd832ee7b8d56dcf429a05a5105c28cc059885d"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "agents/architecture.md",
|
||||||
|
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": ".claude-plugin/plugin.json",
|
||||||
|
"sha256": "fe5338ec9cbe7513d3fa8a896429bd2a9a19fdc584e04542483ba7071755e12d"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "commands/create-component.md",
|
||||||
|
"sha256": "a5ae0741cba19d0b2cace7512541909fed14a01ed5ed90cad4d94ced9d58841b"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"dirSha256": "17eeb4ed8deae4655eecc58db9692d26e96f5ffd4f9618d89eec41f62a413d9c"
|
||||||
|
},
|
||||||
|
"security": {
|
||||||
|
"scannedAt": null,
|
||||||
|
"scannerVersion": null,
|
||||||
|
"flags": []
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user