@mcp-elements/angular 0.1.0

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 (42) hide show
  1. package/LICENSE +21 -0
  2. package/package.json +22 -0
  3. package/src/accordion.component.ts +74 -0
  4. package/src/ai-badge.component.ts +26 -0
  5. package/src/alert.component.ts +25 -0
  6. package/src/avatar.component.ts +24 -0
  7. package/src/badge.component.ts +17 -0
  8. package/src/button.component.ts +27 -0
  9. package/src/card.component.ts +46 -0
  10. package/src/chat-bubble.component.ts +53 -0
  11. package/src/chips.component.ts +33 -0
  12. package/src/counter.component.ts +48 -0
  13. package/src/dialog.component.ts +42 -0
  14. package/src/drawer.component.ts +48 -0
  15. package/src/dropdown-menu.component.ts +62 -0
  16. package/src/feedback.component.ts +71 -0
  17. package/src/index.ts +86 -0
  18. package/src/input.component.ts +46 -0
  19. package/src/loader.component.ts +12 -0
  20. package/src/mcp/index.ts +9 -0
  21. package/src/mcp/mcp-app-frame.component.ts +60 -0
  22. package/src/mcp/mcp-consent-dialog.component.ts +63 -0
  23. package/src/mcp/mcp-resource-browser.component.ts +86 -0
  24. package/src/mcp/mcp-scope-inspector.component.ts +81 -0
  25. package/src/mcp/mcp-server-status.component.ts +44 -0
  26. package/src/mcp/mcp-tool-call.component.ts +105 -0
  27. package/src/mcp/mcp-tool-form.component.ts +127 -0
  28. package/src/password-input.component.ts +35 -0
  29. package/src/popover.component.ts +40 -0
  30. package/src/progress.component.ts +20 -0
  31. package/src/prompt-input.component.ts +70 -0
  32. package/src/select.component.ts +106 -0
  33. package/src/separator.component.ts +15 -0
  34. package/src/skeleton.component.ts +11 -0
  35. package/src/source-card.component.ts +34 -0
  36. package/src/streaming-text.component.ts +43 -0
  37. package/src/suggestion-chips.component.ts +23 -0
  38. package/src/switch.component.ts +32 -0
  39. package/src/tabs.component.ts +95 -0
  40. package/src/textarea.component.ts +22 -0
  41. package/src/toast.component.ts +62 -0
  42. package/src/tooltip.directive.ts +63 -0
@@ -0,0 +1,127 @@
1
+ import { Component, input, output, signal, computed, OnInit } from '@angular/core'
2
+ import { CommonModule } from '@angular/common'
3
+ import { cn, schemaToFields } from '@mcp-elements/core'
4
+ import type { JsonSchema, FieldDescriptor } from '@mcp-elements/core'
5
+
6
+ @Component({
7
+ selector: 'mcpe-mcp-tool-form',
8
+ standalone: true,
9
+ imports: [CommonModule],
10
+ template: `
11
+ <form [class]="classes()" (ngSubmit)="handleSubmit()">
12
+ @if (fields().length === 0) {
13
+ <p class="text-sm text-muted-foreground">This tool takes no inputs.</p>
14
+ }
15
+ @for (field of fields(); track field.key) {
16
+ <div class="mcpe-mcp-tool-form-field">
17
+ <label
18
+ [for]="field.key"
19
+ [class]="labelClass(field)"
20
+ >{{ field.label }}</label>
21
+ @switch (field.kind) {
22
+ @case ('switch') {
23
+ <input type="checkbox" [id]="field.key" class="mcpe-switch"
24
+ [checked]="getBool(field.key)"
25
+ (change)="onCheckChange(field.key, $event)" />
26
+ }
27
+ @case ('select') {
28
+ <select [id]="field.key" class="mcpe-select"
29
+ [value]="getStr(field.key)"
30
+ (change)="onInputChange(field.key, $event)">
31
+ <option value="">Select…</option>
32
+ @for (opt of field.options ?? []; track opt.value) {
33
+ <option [value]="opt.value">{{ opt.label }}</option>
34
+ }
35
+ </select>
36
+ }
37
+ @case ('textarea') {
38
+ <textarea [id]="field.key" class="mcpe-textarea" rows="4"
39
+ [value]="getStr(field.key)"
40
+ (input)="onInputChange(field.key, $event)"></textarea>
41
+ }
42
+ @case ('number') {
43
+ <input type="number" [id]="field.key" class="mcpe-input"
44
+ [value]="getStr(field.key)"
45
+ (input)="onNumberChange(field.key, $event)" />
46
+ }
47
+ @default {
48
+ <input [type]="inputType(field)" [id]="field.key" class="mcpe-input"
49
+ [value]="getStr(field.key)"
50
+ (input)="onInputChange(field.key, $event)" />
51
+ }
52
+ }
53
+ @if (field.help) {
54
+ <p class="mcpe-mcp-tool-form-help">{{ field.help }}</p>
55
+ }
56
+ </div>
57
+ }
58
+ <div class="mcpe-mcp-tool-form-submit">
59
+ <button type="submit" class="mcpe-btn mcpe-btn-primary mcpe-btn-sm" [disabled]="loading()">
60
+ {{ loading() ? 'Running…' : submitLabel() }}
61
+ </button>
62
+ </div>
63
+ </form>
64
+ `,
65
+ })
66
+ export class McpeMcpToolFormComponent implements OnInit {
67
+ schema = input.required<JsonSchema>()
68
+ loading = input(false)
69
+ submitLabel = input('Run')
70
+ class = input('')
71
+ onSubmit = output<Record<string, unknown>>()
72
+
73
+ fields = computed(() => schemaToFields(this.schema()))
74
+ values = signal<Record<string, unknown>>({})
75
+
76
+ ngOnInit() {
77
+ const defaults: Record<string, unknown> = {}
78
+ for (const f of this.fields()) {
79
+ if (f.defaultValue !== undefined) defaults[f.key] = f.defaultValue
80
+ }
81
+ this.values.set(defaults)
82
+ }
83
+
84
+ classes = computed(() => cn('mcpe-mcp-tool-form', this.class()))
85
+
86
+ setValue(key: string, value: unknown) {
87
+ this.values.update((v: Record<string, unknown>) => ({ ...v, [key]: value }))
88
+ }
89
+
90
+ onInputChange(key: string, event: Event) {
91
+ this.setValue(key, (event.target as HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement).value)
92
+ }
93
+
94
+ onNumberChange(key: string, event: Event) {
95
+ this.setValue(key, (event.target as HTMLInputElement).valueAsNumber)
96
+ }
97
+
98
+ onCheckChange(key: string, event: Event) {
99
+ this.setValue(key, (event.target as HTMLInputElement).checked)
100
+ }
101
+
102
+ getStr(key: string): string {
103
+ const v = this.values()[key]
104
+ return v == null ? '' : String(v)
105
+ }
106
+
107
+ getBool(key: string): boolean {
108
+ return Boolean(this.values()[key])
109
+ }
110
+
111
+ labelClass(field: FieldDescriptor): string {
112
+ return cn('mcpe-mcp-tool-form-label', field.required ? 'mcpe-mcp-tool-form-label-required' : '')
113
+ }
114
+
115
+ inputType(field: FieldDescriptor): string {
116
+ switch (field.kind) {
117
+ case 'email': return 'email'
118
+ case 'url': return 'url'
119
+ case 'date': return 'date'
120
+ default: return 'text'
121
+ }
122
+ }
123
+
124
+ handleSubmit() {
125
+ this.onSubmit.emit(this.values())
126
+ }
127
+ }
@@ -0,0 +1,35 @@
1
+ import { Component, input, signal, computed } from '@angular/core'
2
+
3
+ @Component({
4
+ selector: 'mcpe-password-input',
5
+ standalone: true,
6
+ template: `
7
+ <div class="mcpe-password-input-wrapper">
8
+ <input
9
+ [type]="showPassword() ? 'text' : 'password'"
10
+ class="mcpe-password-input"
11
+ [placeholder]="placeholder()"
12
+ [disabled]="disabled()"
13
+ />
14
+ <button type="button" class="mcpe-password-toggle" (click)="showPassword.update(v => !v)"
15
+ [attr.aria-label]="showPassword() ? 'Hide password' : 'Show password'" tabindex="-1">
16
+ @if (showPassword()) {
17
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
18
+ <path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24" />
19
+ <line x1="1" y1="1" x2="23" y2="23" />
20
+ </svg>
21
+ } @else {
22
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
23
+ <path d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z" />
24
+ <circle cx="12" cy="12" r="3" />
25
+ </svg>
26
+ }
27
+ </button>
28
+ </div>
29
+ `,
30
+ })
31
+ export class SnxPasswordInputComponent {
32
+ placeholder = input('')
33
+ disabled = input(false)
34
+ showPassword = signal(false)
35
+ }
@@ -0,0 +1,40 @@
1
+ import { Component, input, signal, ElementRef, viewChild } from '@angular/core'
2
+
3
+ @Component({
4
+ selector: 'mcpe-popover',
5
+ standalone: true,
6
+ template: `
7
+ <div class="relative inline-block" #container>
8
+ <div (click)="toggle()">
9
+ <ng-content select="[trigger]" />
10
+ </div>
11
+ @if (isOpen()) {
12
+ <div
13
+ class="mcpe-popover-content absolute top-full mt-2"
14
+ role="dialog"
15
+ (keydown.escape)="close()"
16
+ >
17
+ <ng-content />
18
+ </div>
19
+ }
20
+ </div>
21
+ `,
22
+ host: {
23
+ '(document:click)': 'onDocumentClick($event)',
24
+ },
25
+ })
26
+ export class SnxPopoverComponent {
27
+ isOpen = signal(false)
28
+ container = viewChild<ElementRef>('container')
29
+
30
+ toggle() { this.isOpen.update(v => !v) }
31
+ close() { this.isOpen.set(false) }
32
+ show() { this.isOpen.set(true) }
33
+
34
+ onDocumentClick(event: MouseEvent) {
35
+ const el = this.container()?.nativeElement
36
+ if (el && !el.contains(event.target as Node)) {
37
+ this.isOpen.set(false)
38
+ }
39
+ }
40
+ }
@@ -0,0 +1,20 @@
1
+ import { Component, input, computed } from '@angular/core'
2
+
3
+ @Component({
4
+ selector: 'mcpe-progress',
5
+ standalone: true,
6
+ template: `
7
+ <div class="mcpe-progress" role="progressbar"
8
+ [attr.aria-valuenow]="value()" [attr.aria-valuemin]="0" [attr.aria-valuemax]="max()">
9
+ <div class="mcpe-progress-indicator" [style.transform]="transform()"></div>
10
+ </div>
11
+ `,
12
+ })
13
+ export class SnxProgressComponent {
14
+ value = input(0)
15
+ max = input(100)
16
+ transform = computed(() => {
17
+ const pct = Math.min(Math.max((this.value() / this.max()) * 100, 0), 100)
18
+ return `translateX(-${100 - pct}%)`
19
+ })
20
+ }
@@ -0,0 +1,70 @@
1
+ import { Component, input, output, computed } from '@angular/core'
2
+
3
+ @Component({
4
+ selector: 'mcpe-prompt-input',
5
+ standalone: true,
6
+ template: `<div [class]="classes()"><ng-content /></div>`,
7
+ })
8
+ export class SnxPromptInputComponent {
9
+ class = input('')
10
+ classes = computed(() => ['mcpe-prompt-input', this.class()].filter(Boolean).join(' '))
11
+ }
12
+
13
+ @Component({
14
+ selector: 'mcpe-prompt-input-textarea',
15
+ standalone: true,
16
+ template: `<textarea class="mcpe-prompt-input-textarea" [placeholder]="placeholder()" [rows]="rows()"></textarea>`,
17
+ })
18
+ export class SnxPromptInputTextareaComponent {
19
+ placeholder = input('')
20
+ rows = input(3)
21
+ }
22
+
23
+ @Component({
24
+ selector: 'mcpe-prompt-input-footer',
25
+ standalone: true,
26
+ template: `<div class="mcpe-prompt-input-footer"><ng-content /></div>`,
27
+ })
28
+ export class SnxPromptInputFooterComponent {}
29
+
30
+ @Component({
31
+ selector: 'mcpe-prompt-input-actions',
32
+ standalone: true,
33
+ template: `<div class="mcpe-prompt-input-actions"><ng-content /></div>`,
34
+ })
35
+ export class SnxPromptInputActionsComponent {}
36
+
37
+ @Component({
38
+ selector: 'mcpe-prompt-input-char-count',
39
+ standalone: true,
40
+ template: `<span class="mcpe-prompt-input-char-count">{{ count() }}@if (max()) { / {{ max() }} }</span>`,
41
+ })
42
+ export class SnxPromptInputCharCountComponent {
43
+ count = input(0)
44
+ max = input<number | undefined>(undefined)
45
+ }
46
+
47
+ @Component({
48
+ selector: 'mcpe-prompt-input-attachments',
49
+ standalone: true,
50
+ template: `<div class="mcpe-prompt-input-attachments"><ng-content /></div>`,
51
+ })
52
+ export class SnxPromptInputAttachmentsComponent {}
53
+
54
+ @Component({
55
+ selector: 'mcpe-prompt-input-attachment',
56
+ standalone: true,
57
+ template: `
58
+ <span class="mcpe-prompt-input-attachment">
59
+ <ng-content />
60
+ <button type="button" class="mcpe-prompt-input-attachment-remove" (click)="remove.emit()" aria-label="Remove">
61
+ <svg xmlns="http://www.w3.org/2000/svg" width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
62
+ <path d="M18 6 6 18" /><path d="m6 6 12 12" />
63
+ </svg>
64
+ </button>
65
+ </span>
66
+ `,
67
+ })
68
+ export class SnxPromptInputAttachmentComponent {
69
+ remove = output()
70
+ }
@@ -0,0 +1,106 @@
1
+ import { Component, input, output, signal, computed, ElementRef, viewChild, HostListener } from '@angular/core'
2
+ import { createSelect, type SelectOption } from '@mcp-elements/core'
3
+
4
+ @Component({
5
+ selector: 'mcpe-select',
6
+ standalone: true,
7
+ template: `
8
+ <div class="relative" #container>
9
+ <button
10
+ class="mcpe-select-trigger"
11
+ [attr.aria-expanded]="isOpen()"
12
+ aria-haspopup="listbox"
13
+ (click)="toggle()"
14
+ (keydown)="onKeyDown($event)"
15
+ >
16
+ <span>{{ selectedLabel() || placeholder() }}</span>
17
+ <svg class="h-4 w-4 opacity-50" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
18
+ <path d="m6 9 6 6 6-6" />
19
+ </svg>
20
+ </button>
21
+ @if (isOpen()) {
22
+ <div class="mcpe-select-content absolute top-full mt-1 w-full">
23
+ <div class="mcpe-select-viewport" role="listbox">
24
+ @for (option of options(); track option.value; let i = $index) {
25
+ <div
26
+ role="option"
27
+ [attr.aria-selected]="option.value === selectedValue()"
28
+ [attr.aria-disabled]="option.disabled"
29
+ [class]="getItemClasses(option, i)"
30
+ (click)="selectOption(option)"
31
+ (mouseenter)="highlightedIndex.set(i)"
32
+ >
33
+ {{ option.label }}
34
+ </div>
35
+ }
36
+ </div>
37
+ </div>
38
+ }
39
+ </div>
40
+ `,
41
+ host: {
42
+ '(document:click)': 'onDocumentClick($event)',
43
+ },
44
+ })
45
+ export class SnxSelectComponent {
46
+ options = input<SelectOption[]>([])
47
+ placeholder = input('Select...')
48
+ valueChange = output<string>()
49
+
50
+ isOpen = signal(false)
51
+ selectedValue = signal<string | null>(null)
52
+ highlightedIndex = signal(0)
53
+
54
+ container = viewChild<ElementRef>('container')
55
+
56
+ selectedLabel = computed(() => {
57
+ const val = this.selectedValue()
58
+ return this.options().find(o => o.value === val)?.label ?? ''
59
+ })
60
+
61
+ toggle() { this.isOpen.update(v => !v) }
62
+
63
+ selectOption(option: SelectOption) {
64
+ if (option.disabled) return
65
+ this.selectedValue.set(option.value)
66
+ this.valueChange.emit(option.value)
67
+ this.isOpen.set(false)
68
+ }
69
+
70
+ getItemClasses(option: SelectOption, index: number): string {
71
+ return [
72
+ 'mcpe-select-item',
73
+ index === this.highlightedIndex() ? 'mcpe-select-item-active' : '',
74
+ option.value === this.selectedValue() ? 'mcpe-select-item-selected' : '',
75
+ ].filter(Boolean).join(' ')
76
+ }
77
+
78
+ onKeyDown(event: KeyboardEvent) {
79
+ const opts = this.options().filter(o => !o.disabled)
80
+ if (event.key === 'ArrowDown') {
81
+ event.preventDefault()
82
+ if (!this.isOpen()) { this.isOpen.set(true); return }
83
+ this.highlightedIndex.update(i => (i + 1) % opts.length)
84
+ } else if (event.key === 'ArrowUp') {
85
+ event.preventDefault()
86
+ this.highlightedIndex.update(i => (i - 1 + opts.length) % opts.length)
87
+ } else if (event.key === 'Enter' || event.key === ' ') {
88
+ event.preventDefault()
89
+ if (this.isOpen()) {
90
+ const opt = opts[this.highlightedIndex()]
91
+ if (opt) this.selectOption(opt)
92
+ } else {
93
+ this.isOpen.set(true)
94
+ }
95
+ } else if (event.key === 'Escape') {
96
+ this.isOpen.set(false)
97
+ }
98
+ }
99
+
100
+ onDocumentClick(event: MouseEvent) {
101
+ const el = this.container()?.nativeElement
102
+ if (el && !el.contains(event.target as Node)) {
103
+ this.isOpen.set(false)
104
+ }
105
+ }
106
+ }
@@ -0,0 +1,15 @@
1
+ import { Component, input, computed } from '@angular/core'
2
+
3
+ @Component({
4
+ selector: 'mcpe-separator',
5
+ standalone: true,
6
+ template: `<div [class]="classes()" role="separator" [attr.aria-orientation]="orientation()"></div>`,
7
+ })
8
+ export class SnxSeparatorComponent {
9
+ orientation = input<'horizontal' | 'vertical'>('horizontal')
10
+ class = input('')
11
+
12
+ classes = computed(() =>
13
+ ['mcpe-separator', `mcpe-separator-${this.orientation()}`, this.class()].filter(Boolean).join(' ')
14
+ )
15
+ }
@@ -0,0 +1,11 @@
1
+ import { Component, input, computed } from '@angular/core'
2
+
3
+ @Component({
4
+ selector: 'mcpe-skeleton',
5
+ standalone: true,
6
+ template: `<div [class]="classes()"></div>`,
7
+ })
8
+ export class SnxSkeletonComponent {
9
+ class = input('')
10
+ classes = computed(() => ['mcpe-skeleton', this.class()].filter(Boolean).join(' '))
11
+ }
@@ -0,0 +1,34 @@
1
+ import { Component, input } from '@angular/core'
2
+
3
+ @Component({
4
+ selector: 'mcpe-source-cards',
5
+ standalone: true,
6
+ template: `<div class="mcpe-source-cards"><ng-content /></div>`,
7
+ })
8
+ export class SnxSourceCardsComponent {}
9
+
10
+ @Component({
11
+ selector: 'mcpe-source-card',
12
+ standalone: true,
13
+ template: `
14
+ <a [href]="href()" class="mcpe-source-card" target="_blank" rel="noopener noreferrer">
15
+ @if (favicon()) {
16
+ <img class="mcpe-source-card-favicon" [src]="favicon()" [alt]="domain()" />
17
+ }
18
+ <div class="mcpe-source-card-body">
19
+ <p class="mcpe-source-card-title">{{ title() }}</p>
20
+ <p class="mcpe-source-card-domain">{{ domain() }}</p>
21
+ </div>
22
+ @if (index()) {
23
+ <span class="mcpe-source-card-index">{{ index() }}</span>
24
+ }
25
+ </a>
26
+ `,
27
+ })
28
+ export class SnxSourceCardComponent {
29
+ href = input.required<string>()
30
+ favicon = input('')
31
+ title = input('')
32
+ domain = input('')
33
+ index = input<number | undefined>(undefined)
34
+ }
@@ -0,0 +1,43 @@
1
+ import { Component } from '@angular/core'
2
+
3
+ @Component({
4
+ selector: 'mcpe-streaming-text',
5
+ standalone: true,
6
+ template: `<div class="mcpe-streaming-text-cursor"><ng-content /></div>`,
7
+ })
8
+ export class SnxStreamingTextComponent {}
9
+
10
+ @Component({
11
+ selector: 'mcpe-streaming-text-fade-in',
12
+ standalone: true,
13
+ template: `<span class="mcpe-streaming-text-fade-in"><ng-content /></span>`,
14
+ })
15
+ export class SnxStreamingTextFadeInComponent {}
16
+
17
+ @Component({
18
+ selector: 'mcpe-streaming-text-word',
19
+ standalone: true,
20
+ template: `<span class="mcpe-streaming-text-word"><ng-content /></span>`,
21
+ })
22
+ export class SnxStreamingTextWordComponent {}
23
+
24
+ @Component({
25
+ selector: 'mcpe-streaming-text-line',
26
+ standalone: true,
27
+ template: `<div class="mcpe-streaming-text-line"><ng-content /></div>`,
28
+ })
29
+ export class SnxStreamingTextLineComponent {}
30
+
31
+ @Component({
32
+ selector: 'mcpe-streaming-text-skeleton',
33
+ standalone: true,
34
+ template: `<div class="mcpe-streaming-text-skeleton"><ng-content /></div>`,
35
+ })
36
+ export class SnxStreamingTextSkeletonComponent {}
37
+
38
+ @Component({
39
+ selector: 'mcpe-streaming-text-skeleton-line',
40
+ standalone: true,
41
+ template: `<div class="mcpe-streaming-text-skeleton-line"></div>`,
42
+ })
43
+ export class SnxStreamingTextSkeletonLineComponent {}
@@ -0,0 +1,23 @@
1
+ import { Component, input, computed } from '@angular/core'
2
+
3
+ type SuggestionChipVariant = 'default' | 'primary' | 'outline'
4
+
5
+ @Component({
6
+ selector: 'mcpe-suggestion-chips',
7
+ standalone: true,
8
+ template: `<div class="mcpe-suggestion-chips"><ng-content /></div>`,
9
+ })
10
+ export class SnxSuggestionChipsComponent {}
11
+
12
+ @Component({
13
+ selector: 'mcpe-suggestion-chip',
14
+ standalone: true,
15
+ template: `<button [class]="classes()" type="button"><ng-content /></button>`,
16
+ })
17
+ export class SnxSuggestionChipComponent {
18
+ variant = input<SuggestionChipVariant>('default')
19
+ class = input('')
20
+ classes = computed(() =>
21
+ ['mcpe-suggestion-chip', `mcpe-suggestion-chip-${this.variant()}`, this.class()].filter(Boolean).join(' ')
22
+ )
23
+ }
@@ -0,0 +1,32 @@
1
+ import { Component, input, output, signal, computed } from '@angular/core'
2
+
3
+ @Component({
4
+ selector: 'mcpe-switch',
5
+ standalone: true,
6
+ template: `
7
+ <button
8
+ type="button"
9
+ role="switch"
10
+ [attr.aria-checked]="checked()"
11
+ [attr.aria-disabled]="disabled()"
12
+ [disabled]="disabled()"
13
+ [class]="'mcpe-switch'"
14
+ (click)="toggle()"
15
+ (keydown.space)="$event.preventDefault(); toggle()"
16
+ (keydown.enter)="$event.preventDefault(); toggle()"
17
+ >
18
+ <span class="mcpe-switch-thumb"></span>
19
+ </button>
20
+ `,
21
+ })
22
+ export class SnxSwitchComponent {
23
+ checked = signal(false)
24
+ disabled = input(false)
25
+ checkedChange = output<boolean>()
26
+
27
+ toggle() {
28
+ if (this.disabled()) return
29
+ this.checked.update(v => !v)
30
+ this.checkedChange.emit(this.checked())
31
+ }
32
+ }
@@ -0,0 +1,95 @@
1
+ import { Component, input, output, signal, computed, effect, contentChildren, ElementRef } from '@angular/core'
2
+ import { createTabs, type TabItem } from '@mcp-elements/core'
3
+
4
+ @Component({
5
+ selector: 'mcpe-tabs',
6
+ standalone: true,
7
+ template: `<div><ng-content /></div>`,
8
+ })
9
+ export class SnxTabsComponent {
10
+ items = input<TabItem[]>([])
11
+ defaultValue = input<string>('')
12
+ activeValue = signal('')
13
+
14
+ private api = computed(() =>
15
+ createTabs(this.items(), {
16
+ defaultValue: this.defaultValue(),
17
+ onValueChange: (v) => this.activeValue.set(v),
18
+ })
19
+ )
20
+
21
+ constructor() {
22
+ effect(() => {
23
+ const def = this.defaultValue()
24
+ const items = this.items()
25
+ if (def) {
26
+ this.activeValue.set(def)
27
+ } else if (items.length > 0) {
28
+ this.activeValue.set(items[0].value)
29
+ }
30
+ })
31
+ }
32
+
33
+ getTabProps(value: string) {
34
+ return this.api().getTriggerProps(value, this.activeValue())
35
+ }
36
+
37
+ getPanelProps(value: string) {
38
+ return this.api().getPanelProps(value, this.activeValue())
39
+ }
40
+
41
+ isActive(value: string): boolean {
42
+ return this.activeValue() === value
43
+ }
44
+
45
+ select(value: string) {
46
+ this.activeValue.set(value)
47
+ }
48
+ }
49
+
50
+ @Component({
51
+ selector: 'mcpe-tabs-list',
52
+ standalone: true,
53
+ template: `<div role="tablist" class="mcpe-tabs-list"><ng-content /></div>`,
54
+ })
55
+ export class SnxTabsListComponent {}
56
+
57
+ @Component({
58
+ selector: 'mcpe-tabs-trigger',
59
+ standalone: true,
60
+ template: `
61
+ <button
62
+ role="tab"
63
+ [class]="classes()"
64
+ [attr.aria-selected]="isActive()"
65
+ [attr.tabindex]="isActive() ? 0 : -1"
66
+ (click)="onClick.emit()"
67
+ >
68
+ <ng-content />
69
+ </button>
70
+ `,
71
+ })
72
+ export class SnxTabsTriggerComponent {
73
+ isActive = input(false)
74
+ onClick = output<void>()
75
+ class = input('')
76
+
77
+ classes = computed(() =>
78
+ ['mcpe-tabs-trigger', this.isActive() ? 'mcpe-tabs-trigger-active' : '', this.class()]
79
+ .filter(Boolean)
80
+ .join(' ')
81
+ )
82
+ }
83
+
84
+ @Component({
85
+ selector: 'mcpe-tabs-content',
86
+ standalone: true,
87
+ template: `
88
+ @if (isActive()) {
89
+ <div role="tabpanel" class="mcpe-tabs-content"><ng-content /></div>
90
+ }
91
+ `,
92
+ })
93
+ export class SnxTabsContentComponent {
94
+ isActive = input(false)
95
+ }
@@ -0,0 +1,22 @@
1
+ import { Component, input, computed } from '@angular/core'
2
+
3
+ @Component({
4
+ selector: 'mcpe-textarea',
5
+ standalone: true,
6
+ template: `
7
+ <textarea
8
+ [class]="classes()"
9
+ [placeholder]="placeholder()"
10
+ [disabled]="disabled()"
11
+ [rows]="rows()"
12
+ ><ng-content /></textarea>
13
+ `,
14
+ })
15
+ export class SnxTextareaComponent {
16
+ placeholder = input('')
17
+ disabled = input(false)
18
+ rows = input(3)
19
+ class = input('')
20
+
21
+ classes = computed(() => ['mcpe-textarea', this.class()].filter(Boolean).join(' '))
22
+ }