@smuikit/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.
@@ -0,0 +1,161 @@
1
+ import {
2
+ Component,
3
+ Input,
4
+ Output,
5
+ EventEmitter,
6
+ NgZone,
7
+ OnInit,
8
+ OnDestroy,
9
+ OnChanges,
10
+ SimpleChanges,
11
+ ElementRef,
12
+ ViewChild,
13
+ ChangeDetectionStrategy,
14
+ ChangeDetectorRef,
15
+ } from '@angular/core';
16
+ import { CommonModule } from '@angular/common';
17
+ import {
18
+ createButtonMachine,
19
+ connectButton,
20
+ type ButtonConnectReturn,
21
+ type ButtonState,
22
+ type ButtonContext,
23
+ type ButtonEvent,
24
+ } from '@smuikit/core';
25
+ import { MachineAdapter } from '../../use-machine.service';
26
+ import { normalizeProps, applyProps } from '../../normalize-props';
27
+
28
+ @Component({
29
+ selector: 'smui-button',
30
+ standalone: true,
31
+ imports: [CommonModule],
32
+ changeDetection: ChangeDetectionStrategy.OnPush,
33
+ template: `
34
+ <button
35
+ #buttonEl
36
+ [class]="hostClass"
37
+ [attr.data-state]="api.state"
38
+ [attr.data-variant]="variant"
39
+ [attr.data-size]="size"
40
+ >
41
+ <span
42
+ *ngIf="api.isLoading"
43
+ class="smui-button__loader"
44
+ aria-hidden="true"
45
+ >
46
+ <span class="smui-button__spinner"></span>
47
+ </span>
48
+ <span
49
+ class="smui-button__content"
50
+ [style.visibility]="api.isLoading ? 'hidden' : 'visible'"
51
+ >
52
+ <ng-content></ng-content>
53
+ </span>
54
+ </button>
55
+ `,
56
+ styles: [
57
+ `
58
+ :host {
59
+ display: inline-block;
60
+ }
61
+ .smui-button__loader {
62
+ position: absolute;
63
+ display: inline-flex;
64
+ align-items: center;
65
+ justify-content: center;
66
+ }
67
+ .smui-button__content {
68
+ display: inline-flex;
69
+ align-items: center;
70
+ gap: 8px;
71
+ }
72
+ `,
73
+ ],
74
+ })
75
+ export class ButtonComponent implements OnInit, OnDestroy, OnChanges {
76
+ @Input() variant: ButtonContext['variant'] = 'primary';
77
+ @Input() size: ButtonContext['size'] = 'md';
78
+ @Input() disabled = false;
79
+ @Input() loading = false;
80
+
81
+ @Output() buttonClick = new EventEmitter<Event>();
82
+
83
+ @ViewChild('buttonEl', { static: true }) buttonElRef!: ElementRef<HTMLButtonElement>;
84
+
85
+ api!: ButtonConnectReturn;
86
+
87
+ private adapter!: MachineAdapter<ButtonState, ButtonContext, ButtonEvent>;
88
+ private listenerCleanups: (() => void)[] = [];
89
+
90
+ get hostClass(): string {
91
+ return `smui-button smui-button--${this.variant} smui-button--${this.size}`;
92
+ }
93
+
94
+ constructor(
95
+ private ngZone: NgZone,
96
+ private cdr: ChangeDetectorRef,
97
+ ) {}
98
+
99
+ ngOnInit(): void {
100
+ this.adapter = MachineAdapter.create(
101
+ this.ngZone,
102
+ createButtonMachine({
103
+ variant: this.variant,
104
+ size: this.size,
105
+ disabled: this.disabled,
106
+ loading: this.loading,
107
+ }),
108
+ );
109
+ this.updateApi();
110
+ this.applyPropsToElement();
111
+ }
112
+
113
+ ngOnChanges(changes: SimpleChanges): void {
114
+ if (!this.adapter) return;
115
+
116
+ if (changes['disabled']) {
117
+ this.adapter.send({ type: 'SET_DISABLED', disabled: this.disabled });
118
+ }
119
+ if (changes['loading']) {
120
+ this.adapter.send({ type: 'SET_LOADING', loading: this.loading });
121
+ }
122
+
123
+ this.updateApi();
124
+ this.applyPropsToElement();
125
+ }
126
+
127
+ ngOnDestroy(): void {
128
+ this.cleanupListeners();
129
+ this.adapter?.destroy();
130
+ }
131
+
132
+ private updateApi(): void {
133
+ this.api = connectButton({
134
+ state: this.adapter.state,
135
+ context: this.adapter.context,
136
+ send: (event) => {
137
+ this.adapter.send(event as ButtonEvent);
138
+ this.updateApi();
139
+ this.applyPropsToElement();
140
+ this.cdr.markForCheck();
141
+ },
142
+ onClick: (event) => {
143
+ this.buttonClick.emit(event as Event);
144
+ },
145
+ });
146
+ }
147
+
148
+ private applyPropsToElement(): void {
149
+ if (!this.buttonElRef?.nativeElement) return;
150
+ this.cleanupListeners();
151
+ const normalized = normalizeProps(this.api.rootProps);
152
+ this.listenerCleanups = applyProps(this.buttonElRef.nativeElement, normalized);
153
+ }
154
+
155
+ private cleanupListeners(): void {
156
+ for (const cleanup of this.listenerCleanups) {
157
+ cleanup();
158
+ }
159
+ this.listenerCleanups = [];
160
+ }
161
+ }
@@ -0,0 +1,197 @@
1
+ import {
2
+ Component,
3
+ Input,
4
+ Output,
5
+ EventEmitter,
6
+ OnInit,
7
+ OnChanges,
8
+ SimpleChanges,
9
+ ElementRef,
10
+ ViewChild,
11
+ ChangeDetectionStrategy,
12
+ } from '@angular/core';
13
+ import { CommonModule } from '@angular/common';
14
+ import {
15
+ connectCard,
16
+ type CardConnectReturn,
17
+ type CardConnectOptions,
18
+ } from '@smuikit/core';
19
+ import { normalizeProps, applyProps } from '../../normalize-props';
20
+
21
+ // ---------------------------------------------------------------------------
22
+ // CardHeaderComponent
23
+ // ---------------------------------------------------------------------------
24
+ @Component({
25
+ selector: 'smui-card-header',
26
+ standalone: true,
27
+ template: `
28
+ <div #headerEl class="smui-card__header" data-part="header">
29
+ <ng-content></ng-content>
30
+ </div>
31
+ `,
32
+ styles: [
33
+ `
34
+ :host {
35
+ display: block;
36
+ }
37
+ .smui-card__header {
38
+ padding: var(--smui-card-header-padding, 16px 24px);
39
+ border-bottom: 1px solid var(--smui-color-border, #e5e7eb);
40
+ }
41
+ `,
42
+ ],
43
+ })
44
+ export class CardHeaderComponent {}
45
+
46
+ // ---------------------------------------------------------------------------
47
+ // CardBodyComponent
48
+ // ---------------------------------------------------------------------------
49
+ @Component({
50
+ selector: 'smui-card-body',
51
+ standalone: true,
52
+ template: `
53
+ <div #bodyEl class="smui-card__body" data-part="body">
54
+ <ng-content></ng-content>
55
+ </div>
56
+ `,
57
+ styles: [
58
+ `
59
+ :host {
60
+ display: block;
61
+ }
62
+ .smui-card__body {
63
+ padding: var(--smui-card-body-padding, 24px);
64
+ }
65
+ `,
66
+ ],
67
+ })
68
+ export class CardBodyComponent {}
69
+
70
+ // ---------------------------------------------------------------------------
71
+ // CardFooterComponent
72
+ // ---------------------------------------------------------------------------
73
+ @Component({
74
+ selector: 'smui-card-footer',
75
+ standalone: true,
76
+ template: `
77
+ <div #footerEl class="smui-card__footer" data-part="footer">
78
+ <ng-content></ng-content>
79
+ </div>
80
+ `,
81
+ styles: [
82
+ `
83
+ :host {
84
+ display: block;
85
+ }
86
+ .smui-card__footer {
87
+ padding: var(--smui-card-footer-padding, 16px 24px);
88
+ border-top: 1px solid var(--smui-color-border, #e5e7eb);
89
+ }
90
+ `,
91
+ ],
92
+ })
93
+ export class CardFooterComponent {}
94
+
95
+ // ---------------------------------------------------------------------------
96
+ // CardComponent
97
+ // ---------------------------------------------------------------------------
98
+ @Component({
99
+ selector: 'smui-card',
100
+ standalone: true,
101
+ imports: [CommonModule],
102
+ changeDetection: ChangeDetectionStrategy.OnPush,
103
+ template: `
104
+ <div
105
+ #cardEl
106
+ class="smui-card"
107
+ [class.smui-card--interactive]="interactive"
108
+ [attr.data-variant]="variant"
109
+ [attr.data-padding]="padding"
110
+ [attr.role]="interactive ? 'button' : null"
111
+ [attr.tabindex]="interactive ? 0 : null"
112
+ (click)="onCardClick($event)"
113
+ (keydown)="onCardKeydown($event)"
114
+ >
115
+ <ng-content></ng-content>
116
+ </div>
117
+ `,
118
+ styles: [
119
+ `
120
+ :host {
121
+ display: block;
122
+ }
123
+ .smui-card {
124
+ border-radius: var(--smui-card-radius, 8px);
125
+ overflow: hidden;
126
+ }
127
+ .smui-card[data-variant='elevated'] {
128
+ box-shadow: var(
129
+ --smui-card-shadow,
130
+ 0 1px 3px rgba(0, 0, 0, 0.1),
131
+ 0 1px 2px rgba(0, 0, 0, 0.06)
132
+ );
133
+ }
134
+ .smui-card[data-variant='outlined'] {
135
+ border: 1px solid var(--smui-color-border, #e5e7eb);
136
+ }
137
+ .smui-card[data-variant='filled'] {
138
+ background: var(--smui-color-surface, #f9fafb);
139
+ }
140
+ .smui-card--interactive {
141
+ cursor: pointer;
142
+ transition: box-shadow 0.2s ease;
143
+ }
144
+ .smui-card--interactive:hover {
145
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
146
+ }
147
+ .smui-card--interactive:focus-visible {
148
+ outline: 2px solid var(--smui-color-primary, #6200ee);
149
+ outline-offset: 2px;
150
+ }
151
+ `,
152
+ ],
153
+ })
154
+ export class CardComponent implements OnInit, OnChanges {
155
+ @Input() variant: 'elevated' | 'outlined' | 'filled' = 'elevated';
156
+ @Input() padding: 'none' | 'sm' | 'md' | 'lg' = 'md';
157
+ @Input() interactive = false;
158
+
159
+ @Output() cardClick = new EventEmitter<Event>();
160
+
161
+ @ViewChild('cardEl', { static: true }) cardElRef!: ElementRef<HTMLElement>;
162
+
163
+ private api!: CardConnectReturn;
164
+ private listenerCleanups: (() => void)[] = [];
165
+
166
+ ngOnInit(): void {
167
+ this.updateApi();
168
+ }
169
+
170
+ ngOnChanges(changes: SimpleChanges): void {
171
+ this.updateApi();
172
+ }
173
+
174
+ onCardClick(event: Event): void {
175
+ if (this.interactive) {
176
+ this.cardClick.emit(event);
177
+ }
178
+ }
179
+
180
+ onCardKeydown(event: KeyboardEvent): void {
181
+ if (this.interactive && (event.key === 'Enter' || event.key === ' ')) {
182
+ event.preventDefault();
183
+ this.cardClick.emit(event);
184
+ }
185
+ }
186
+
187
+ private updateApi(): void {
188
+ this.api = connectCard({
189
+ variant: this.variant,
190
+ padding: this.padding,
191
+ interactive: this.interactive,
192
+ onClick: (event) => {
193
+ this.cardClick.emit(event as Event);
194
+ },
195
+ });
196
+ }
197
+ }
@@ -0,0 +1,197 @@
1
+ import {
2
+ Component,
3
+ Input,
4
+ Output,
5
+ EventEmitter,
6
+ NgZone,
7
+ OnInit,
8
+ OnDestroy,
9
+ OnChanges,
10
+ SimpleChanges,
11
+ ElementRef,
12
+ ViewChild,
13
+ ChangeDetectionStrategy,
14
+ ChangeDetectorRef,
15
+ } from '@angular/core';
16
+ import { CommonModule } from '@angular/common';
17
+ import {
18
+ createInputMachine,
19
+ connectInput,
20
+ type InputConnectReturn,
21
+ type InputState,
22
+ type InputContext,
23
+ type InputEvent,
24
+ } from '@smuikit/core';
25
+ import { MachineAdapter } from '../../use-machine.service';
26
+ import { normalizeProps, applyProps } from '../../normalize-props';
27
+
28
+ @Component({
29
+ selector: 'smui-input',
30
+ standalone: true,
31
+ imports: [CommonModule],
32
+ changeDetection: ChangeDetectionStrategy.OnPush,
33
+ template: `
34
+ <div
35
+ class="smui-input"
36
+ [attr.data-state]="api.state"
37
+ [attr.data-disabled]="disabled ? '' : null"
38
+ >
39
+ <label
40
+ *ngIf="label"
41
+ #labelEl
42
+ class="smui-input__label"
43
+ [attr.for]="inputId"
44
+ >
45
+ {{ label }}
46
+ <span *ngIf="required" class="smui-input__required" aria-hidden="true">*</span>
47
+ </label>
48
+
49
+ <input
50
+ #inputEl
51
+ class="smui-input__field"
52
+ [attr.id]="inputId"
53
+ [attr.placeholder]="placeholder"
54
+ [value]="api.value"
55
+ />
56
+
57
+ <div
58
+ *ngIf="api.hasError && error"
59
+ #errorEl
60
+ class="smui-input__error"
61
+ role="alert"
62
+ aria-live="polite"
63
+ >
64
+ {{ error }}
65
+ </div>
66
+ </div>
67
+ `,
68
+ styles: [
69
+ `
70
+ :host {
71
+ display: block;
72
+ }
73
+ .smui-input__label {
74
+ display: block;
75
+ margin-bottom: 4px;
76
+ }
77
+ .smui-input__required {
78
+ color: var(--smui-color-danger, #dc2626);
79
+ margin-left: 2px;
80
+ }
81
+ .smui-input__field {
82
+ display: block;
83
+ width: 100%;
84
+ box-sizing: border-box;
85
+ }
86
+ .smui-input__error {
87
+ color: var(--smui-color-danger, #dc2626);
88
+ font-size: 0.875rem;
89
+ margin-top: 4px;
90
+ }
91
+ `,
92
+ ],
93
+ })
94
+ export class InputComponent implements OnInit, OnDestroy, OnChanges {
95
+ @Input() label = '';
96
+ @Input() placeholder = '';
97
+ @Input() disabled = false;
98
+ @Input() error: string | null = null;
99
+ @Input() required = false;
100
+ @Input() value = '';
101
+ @Input() inputId = `smui-input-${Date.now()}`;
102
+
103
+ @Output() valueChange = new EventEmitter<string>();
104
+
105
+ @ViewChild('inputEl', { static: true }) inputElRef!: ElementRef<HTMLInputElement>;
106
+ @ViewChild('labelEl') labelElRef?: ElementRef<HTMLLabelElement>;
107
+ @ViewChild('errorEl') errorElRef?: ElementRef<HTMLDivElement>;
108
+
109
+ api!: InputConnectReturn;
110
+
111
+ private adapter!: MachineAdapter<InputState, InputContext, InputEvent>;
112
+ private listenerCleanups: (() => void)[] = [];
113
+
114
+ constructor(
115
+ private ngZone: NgZone,
116
+ private cdr: ChangeDetectorRef,
117
+ ) {}
118
+
119
+ ngOnInit(): void {
120
+ this.adapter = MachineAdapter.create(
121
+ this.ngZone,
122
+ createInputMachine({
123
+ value: this.value,
124
+ disabled: this.disabled,
125
+ required: this.required,
126
+ error: this.error,
127
+ placeholder: this.placeholder,
128
+ label: this.label,
129
+ id: this.inputId,
130
+ }),
131
+ );
132
+ this.updateApi();
133
+ this.applyPropsToInput();
134
+ }
135
+
136
+ ngOnChanges(changes: SimpleChanges): void {
137
+ if (!this.adapter) return;
138
+
139
+ if (changes['error']) {
140
+ this.adapter.send({ type: 'SET_ERROR', error: this.error });
141
+ }
142
+ if (changes['value'] && !changes['value'].firstChange) {
143
+ this.adapter.send({ type: 'CHANGE', value: this.value });
144
+ }
145
+
146
+ this.updateApi();
147
+ this.applyPropsToInput();
148
+ }
149
+
150
+ ngOnDestroy(): void {
151
+ this.cleanupListeners();
152
+ this.adapter?.destroy();
153
+ }
154
+
155
+ private updateApi(): void {
156
+ this.api = connectInput({
157
+ state: this.adapter.state,
158
+ context: this.adapter.context,
159
+ send: (event) => {
160
+ this.adapter.send(event as InputEvent);
161
+ this.updateApi();
162
+ this.applyPropsToInput();
163
+ this.cdr.markForCheck();
164
+ },
165
+ onChange: (val: string) => {
166
+ this.valueChange.emit(val);
167
+ },
168
+ });
169
+ }
170
+
171
+ private applyPropsToInput(): void {
172
+ if (!this.inputElRef?.nativeElement) return;
173
+ this.cleanupListeners();
174
+
175
+ const normalized = normalizeProps(this.api.inputProps);
176
+ this.listenerCleanups = applyProps(this.inputElRef.nativeElement, normalized);
177
+
178
+ // Apply label props
179
+ if (this.labelElRef?.nativeElement) {
180
+ const labelNorm = normalizeProps(this.api.labelProps);
181
+ this.listenerCleanups.push(...applyProps(this.labelElRef.nativeElement, labelNorm));
182
+ }
183
+
184
+ // Apply error props
185
+ if (this.errorElRef?.nativeElement) {
186
+ const errorNorm = normalizeProps(this.api.errorProps);
187
+ this.listenerCleanups.push(...applyProps(this.errorElRef.nativeElement, errorNorm));
188
+ }
189
+ }
190
+
191
+ private cleanupListeners(): void {
192
+ for (const cleanup of this.listenerCleanups) {
193
+ cleanup();
194
+ }
195
+ this.listenerCleanups = [];
196
+ }
197
+ }