@lumaui/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.
- package/README.md +165 -0
- package/package.json +46 -0
- package/src/index.ts +9 -0
- package/src/lib/button/button.directive.spec.ts +1350 -0
- package/src/lib/button/button.directive.ts +34 -0
- package/src/lib/button/button.docs.md +209 -0
- package/src/lib/button/index.ts +3 -0
- package/src/lib/card/card-content.directive.ts +13 -0
- package/src/lib/card/card-description.directive.ts +19 -0
- package/src/lib/card/card-directives.spec.ts +309 -0
- package/src/lib/card/card-header.directive.ts +12 -0
- package/src/lib/card/card-title.directive.ts +16 -0
- package/src/lib/card/card.component.html +5 -0
- package/src/lib/card/card.component.spec.ts +379 -0
- package/src/lib/card/card.component.ts +33 -0
- package/src/lib/card/card.docs.md +452 -0
- package/src/lib/card/index.ts +19 -0
- package/src/test-setup.ts +5 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { Directive, input, computed, HostBinding } from '@angular/core';
|
|
2
|
+
import {
|
|
3
|
+
buttonVariants,
|
|
4
|
+
type ButtonVariant,
|
|
5
|
+
type ButtonSize,
|
|
6
|
+
} from '@lumaui/core';
|
|
7
|
+
|
|
8
|
+
@Directive({
|
|
9
|
+
selector: 'button[lumaButton], a[lumaButton]',
|
|
10
|
+
host: {
|
|
11
|
+
'[attr.type]': 'lmType()',
|
|
12
|
+
'[attr.disabled]': 'lmDisabled() ? "" : null',
|
|
13
|
+
},
|
|
14
|
+
})
|
|
15
|
+
export class ButtonDirective {
|
|
16
|
+
// Signal-based inputs with lm prefix (Angular 20+)
|
|
17
|
+
lmVariant = input<ButtonVariant>('primary');
|
|
18
|
+
lmSize = input<ButtonSize>('md');
|
|
19
|
+
lmDisabled = input<boolean>(false);
|
|
20
|
+
lmType = input<'button' | 'submit' | 'reset'>('button');
|
|
21
|
+
|
|
22
|
+
// Computed class string
|
|
23
|
+
classes = computed(() =>
|
|
24
|
+
buttonVariants({
|
|
25
|
+
variant: this.lmVariant(),
|
|
26
|
+
size: this.lmSize(),
|
|
27
|
+
}),
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
@HostBinding('class')
|
|
31
|
+
get hostClasses(): string {
|
|
32
|
+
return this.classes();
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Button
|
|
3
|
+
type: directive
|
|
4
|
+
selector: button[lumaButton], a[lumaButton]
|
|
5
|
+
category: Form
|
|
6
|
+
description: Versatile, accessible button element that follows Neo-Minimal design principles with calm interactions and visual silence.
|
|
7
|
+
inputs:
|
|
8
|
+
- name: lmVariant
|
|
9
|
+
type: "'primary' | 'outline' | 'ghost' | 'danger'"
|
|
10
|
+
default: "'primary'"
|
|
11
|
+
description: Visual style variant of the button
|
|
12
|
+
- name: lmSize
|
|
13
|
+
type: "'sm' | 'md' | 'lg' | 'full'"
|
|
14
|
+
default: "'md'"
|
|
15
|
+
description: Size of the button
|
|
16
|
+
- name: lmDisabled
|
|
17
|
+
type: boolean
|
|
18
|
+
default: 'false'
|
|
19
|
+
description: Whether the button is disabled
|
|
20
|
+
- name: lmType
|
|
21
|
+
type: "'button' | 'submit' | 'reset'"
|
|
22
|
+
default: "'button'"
|
|
23
|
+
description: HTML button type attribute
|
|
24
|
+
tokens:
|
|
25
|
+
- name: --luma-button-primary-bg
|
|
26
|
+
value: oklch(0.54 0.1 230)
|
|
27
|
+
description: Primary button background
|
|
28
|
+
- name: --luma-button-primary-bg-hover
|
|
29
|
+
value: oklch(0.49 0.09 230)
|
|
30
|
+
description: Primary button hover background
|
|
31
|
+
- name: --luma-button-primary-bg-active
|
|
32
|
+
value: oklch(0.44 0.08 230)
|
|
33
|
+
description: Primary button active background
|
|
34
|
+
- name: --luma-button-primary-text
|
|
35
|
+
value: oklch(1 0 0)
|
|
36
|
+
description: Primary button text color
|
|
37
|
+
- name: --luma-button-outline-border
|
|
38
|
+
value: oklch(0.5 0.01 0)
|
|
39
|
+
description: Outline button border color
|
|
40
|
+
- name: --luma-button-outline-border-hover
|
|
41
|
+
value: oklch(0.2 0.005 0)
|
|
42
|
+
description: Outline button hover border
|
|
43
|
+
- name: --luma-button-outline-bg-hover
|
|
44
|
+
value: oklch(0.96 0.01 230)
|
|
45
|
+
description: Outline button hover background
|
|
46
|
+
- name: --luma-button-outline-text
|
|
47
|
+
value: oklch(0.2 0.005 0)
|
|
48
|
+
description: Outline button text color
|
|
49
|
+
- name: --luma-button-ghost-bg
|
|
50
|
+
value: transparent
|
|
51
|
+
description: Ghost button background
|
|
52
|
+
- name: --luma-button-ghost-bg-hover
|
|
53
|
+
value: oklch(0.96 0.01 230)
|
|
54
|
+
description: Ghost button hover background
|
|
55
|
+
- name: --luma-button-ghost-text
|
|
56
|
+
value: oklch(0.2 0.005 0)
|
|
57
|
+
description: Ghost button text color
|
|
58
|
+
- name: --luma-button-danger-bg
|
|
59
|
+
value: oklch(0.55 0.22 25)
|
|
60
|
+
description: Danger button background
|
|
61
|
+
- name: --luma-button-danger-bg-hover
|
|
62
|
+
value: oklch(0.50 0.20 25)
|
|
63
|
+
description: Danger button hover background
|
|
64
|
+
- name: --luma-button-danger-bg-active
|
|
65
|
+
value: oklch(0.45 0.18 25)
|
|
66
|
+
description: Danger button active background
|
|
67
|
+
- name: --luma-button-danger-text
|
|
68
|
+
value: oklch(1 0 0)
|
|
69
|
+
description: Danger button text color
|
|
70
|
+
- name: --luma-button-radius
|
|
71
|
+
value: 10px
|
|
72
|
+
description: Button border radius
|
|
73
|
+
- name: --luma-button-transition-duration
|
|
74
|
+
value: 200ms
|
|
75
|
+
description: Transition duration
|
|
76
|
+
- name: --luma-button-focus-ring-width
|
|
77
|
+
value: 2px
|
|
78
|
+
description: Focus ring width
|
|
79
|
+
- name: --luma-button-focus-ring-color
|
|
80
|
+
value: oklch(0.54 0.1 230 / 0.25)
|
|
81
|
+
description: Focus ring color
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
# Button
|
|
85
|
+
|
|
86
|
+
## Purpose
|
|
87
|
+
|
|
88
|
+
The Button component provides a versatile, accessible button element that follows Neo-Minimal design principles with calm interactions and visual silence.
|
|
89
|
+
|
|
90
|
+
## States
|
|
91
|
+
|
|
92
|
+
- **Default**: Base appearance with calm visual presence
|
|
93
|
+
- **Hover**: Gentle background color transition, no scale or shadow
|
|
94
|
+
- **Focus**: Clear ring outline for keyboard navigation
|
|
95
|
+
- **Active**: Slightly darker background on click
|
|
96
|
+
- **Disabled**: Reduced opacity (50%) with disabled cursor
|
|
97
|
+
|
|
98
|
+
## Usage Examples
|
|
99
|
+
|
|
100
|
+
### Basic Button
|
|
101
|
+
|
|
102
|
+
```html
|
|
103
|
+
<button lumaButton>Click me</button>
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Variants
|
|
107
|
+
|
|
108
|
+
```html
|
|
109
|
+
<button lumaButton lmVariant="primary">Primary</button>
|
|
110
|
+
<button lumaButton lmVariant="outline">Outline</button>
|
|
111
|
+
<button lumaButton lmVariant="ghost">Ghost</button>
|
|
112
|
+
<button lumaButton lmVariant="danger">Delete</button>
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### Sizes
|
|
116
|
+
|
|
117
|
+
```html
|
|
118
|
+
<button lumaButton lmSize="sm">Small</button>
|
|
119
|
+
<button lumaButton lmSize="md">Medium</button>
|
|
120
|
+
<button lumaButton lmSize="lg">Large</button>
|
|
121
|
+
<button lumaButton lmSize="full">Full Width</button>
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Disabled State
|
|
125
|
+
|
|
126
|
+
```html
|
|
127
|
+
<button lumaButton [lmDisabled]="true">Disabled</button>
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### Link as Button
|
|
131
|
+
|
|
132
|
+
```html
|
|
133
|
+
<a lumaButton href="/path" lmVariant="primary">Link Button</a>
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## Customizing
|
|
137
|
+
|
|
138
|
+
The button appearance can be customized using CSS variables. The most common customization is the border-radius, controlled by `--luma-button-radius`.
|
|
139
|
+
|
|
140
|
+
### Override Globally
|
|
141
|
+
|
|
142
|
+
Override the button border-radius in your global styles or component:
|
|
143
|
+
|
|
144
|
+
```css
|
|
145
|
+
/* In your global styles.css */
|
|
146
|
+
:root {
|
|
147
|
+
--luma-button-radius: 4px; /* More sharp */
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### Override Per Theme
|
|
152
|
+
|
|
153
|
+
Apply different radius values for light and dark themes:
|
|
154
|
+
|
|
155
|
+
```css
|
|
156
|
+
/* Light mode - subtle rounding */
|
|
157
|
+
:root {
|
|
158
|
+
--luma-button-radius: 8px;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/* Dark mode - more pronounced rounding */
|
|
162
|
+
.dark {
|
|
163
|
+
--luma-button-radius: 16px;
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### Override Per Component
|
|
168
|
+
|
|
169
|
+
Scope the radius change to specific contexts:
|
|
170
|
+
|
|
171
|
+
```css
|
|
172
|
+
/* Only buttons in the header */
|
|
173
|
+
.header {
|
|
174
|
+
--luma-button-radius: 4px;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/* Only buttons in cards */
|
|
178
|
+
.card {
|
|
179
|
+
--luma-button-radius: 12px;
|
|
180
|
+
}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
**Default value:** `var(--luma-radius-md)` → `10px`
|
|
184
|
+
|
|
185
|
+
## Neo-Minimal Principles
|
|
186
|
+
|
|
187
|
+
The button design embodies core Neo-Minimal values:
|
|
188
|
+
|
|
189
|
+
- **Visual Silence**: Colors are slightly desaturated, close to gray, comfortable for long viewing
|
|
190
|
+
- **Calm Interactions**: Transitions are gentle (150ms) with no scale or elastic effects
|
|
191
|
+
- **Organic Geometry**: Border radius is generous enough to feel "drawn" not "calculated"
|
|
192
|
+
- **Functional Whitespace**: Padding uses design tokens for consistent rhythm
|
|
193
|
+
- **Silent Accessibility**: Focus states are clear but discrete, touch areas are comfortable (44px+)
|
|
194
|
+
|
|
195
|
+
## Accessibility
|
|
196
|
+
|
|
197
|
+
- ✅ **WCAG AA compliant**: All variants meet 4.5:1 contrast ratio
|
|
198
|
+
- ✅ **Keyboard accessible**: Full keyboard navigation with visible focus states
|
|
199
|
+
- ✅ **Screen reader friendly**: Proper ARIA attributes and semantic HTML
|
|
200
|
+
- ✅ **Touch-friendly**: Minimum touch target size of 44x44px
|
|
201
|
+
- ✅ **Disabled state**: Properly communicated via `disabled` attribute
|
|
202
|
+
|
|
203
|
+
## Implementation Notes
|
|
204
|
+
|
|
205
|
+
- Uses Angular 20+ signal-based inputs (`input()`)
|
|
206
|
+
- Styled with Tailwind CSS v4 arbitrary values
|
|
207
|
+
- Type-safe variants via class-variance-authority (CVA)
|
|
208
|
+
- OnPush change detection for optimal performance
|
|
209
|
+
- Works as both `<button>` and `<a>` elements
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Directive } from '@angular/core';
|
|
2
|
+
|
|
3
|
+
@Directive({
|
|
4
|
+
selector: '[lumaCardContent]',
|
|
5
|
+
host: {
|
|
6
|
+
class: '',
|
|
7
|
+
},
|
|
8
|
+
})
|
|
9
|
+
export class CardContentDirective {
|
|
10
|
+
// Semantic marker for card content region
|
|
11
|
+
// No styles applied to preserve maximum flexibility
|
|
12
|
+
// Maintained for backward compatibility and semantic HTML structure
|
|
13
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Directive, input, computed } from '@angular/core';
|
|
2
|
+
import {
|
|
3
|
+
cardDescriptionVariants,
|
|
4
|
+
type CardDescriptionSize,
|
|
5
|
+
} from '@lumaui/core';
|
|
6
|
+
|
|
7
|
+
@Directive({
|
|
8
|
+
selector: '[lumaCardDescription]',
|
|
9
|
+
host: {
|
|
10
|
+
'[class]': 'classes()',
|
|
11
|
+
},
|
|
12
|
+
})
|
|
13
|
+
export class CardDescriptionDirective {
|
|
14
|
+
// Signal-based inputs with lm prefix (Angular 20+)
|
|
15
|
+
lmSize = input<CardDescriptionSize>('normal');
|
|
16
|
+
|
|
17
|
+
// Computed class string
|
|
18
|
+
classes = computed(() => cardDescriptionVariants({ size: this.lmSize() }));
|
|
19
|
+
}
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CardContentDirective,
|
|
3
|
+
CardDescriptionDirective,
|
|
4
|
+
CardHeaderDirective,
|
|
5
|
+
CardTitleDirective,
|
|
6
|
+
} from './';
|
|
7
|
+
import { Component, DebugElement } from '@angular/core';
|
|
8
|
+
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
|
9
|
+
|
|
10
|
+
import { By } from '@angular/platform-browser';
|
|
11
|
+
|
|
12
|
+
// Test host components for each directive
|
|
13
|
+
@Component({
|
|
14
|
+
template: `<h3 lumaCardTitle [lmSize]="lmSize">Test Title</h3>`,
|
|
15
|
+
imports: [CardTitleDirective],
|
|
16
|
+
})
|
|
17
|
+
class CardTitleTestComponent {
|
|
18
|
+
lmSize: 'small' | 'normal' | 'large' = 'normal';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
@Component({
|
|
22
|
+
template: `<p lumaCardDescription [lmSize]="lmSize">Test Description</p>`,
|
|
23
|
+
imports: [CardDescriptionDirective],
|
|
24
|
+
})
|
|
25
|
+
class CardDescriptionTestComponent {
|
|
26
|
+
lmSize: 'small' | 'normal' | 'large' = 'normal';
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
@Component({
|
|
30
|
+
template: `<div lumaCardHeader>Test Header</div>`,
|
|
31
|
+
imports: [CardHeaderDirective],
|
|
32
|
+
})
|
|
33
|
+
class CardHeaderTestComponent {}
|
|
34
|
+
|
|
35
|
+
@Component({
|
|
36
|
+
template: `<div lumaCardContent>Test Content</div>`,
|
|
37
|
+
imports: [CardContentDirective],
|
|
38
|
+
})
|
|
39
|
+
class CardContentTestComponent {}
|
|
40
|
+
|
|
41
|
+
describe('CardTitleDirective', () => {
|
|
42
|
+
let fixture: ComponentFixture<CardTitleTestComponent>;
|
|
43
|
+
let _component: CardTitleTestComponent;
|
|
44
|
+
let titleElement: DebugElement;
|
|
45
|
+
let directive: CardTitleDirective;
|
|
46
|
+
|
|
47
|
+
beforeEach(async () => {
|
|
48
|
+
await TestBed.configureTestingModule({
|
|
49
|
+
imports: [CardTitleDirective, CardTitleTestComponent],
|
|
50
|
+
}).compileComponents();
|
|
51
|
+
|
|
52
|
+
fixture = TestBed.createComponent(CardTitleTestComponent);
|
|
53
|
+
_component = fixture.componentInstance;
|
|
54
|
+
titleElement = fixture.debugElement.query(By.directive(CardTitleDirective));
|
|
55
|
+
directive = titleElement.injector.get(CardTitleDirective);
|
|
56
|
+
await fixture.whenStable();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('should create', () => {
|
|
60
|
+
expect(directive).toBeTruthy();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('should apply base classes', () => {
|
|
64
|
+
fixture.detectChanges();
|
|
65
|
+
|
|
66
|
+
const classes = directive.classes();
|
|
67
|
+
|
|
68
|
+
expect(classes).toContain('font-medium');
|
|
69
|
+
expect(classes).toContain('tracking-tight');
|
|
70
|
+
expect(classes).toContain('mb-1');
|
|
71
|
+
expect(classes).toContain('lm-text-primary');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('should apply normal size by default', () => {
|
|
75
|
+
fixture.detectChanges();
|
|
76
|
+
|
|
77
|
+
const classes = directive.classes();
|
|
78
|
+
expect(classes).toContain('text-lg');
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('should use computed signal for classes', () => {
|
|
82
|
+
expect(typeof directive.classes).toBe('function');
|
|
83
|
+
|
|
84
|
+
const classes = directive.classes();
|
|
85
|
+
expect(typeof classes).toBe('string');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('should use signal-based input for size', () => {
|
|
89
|
+
expect(typeof directive.lmSize).toBe('function');
|
|
90
|
+
|
|
91
|
+
const size = directive.lmSize();
|
|
92
|
+
expect(size).toBe('normal');
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
describe('CardDescriptionDirective', () => {
|
|
97
|
+
let fixture: ComponentFixture<CardDescriptionTestComponent>;
|
|
98
|
+
let _component: CardDescriptionTestComponent;
|
|
99
|
+
let descriptionElement: DebugElement;
|
|
100
|
+
let directive: CardDescriptionDirective;
|
|
101
|
+
|
|
102
|
+
beforeEach(async () => {
|
|
103
|
+
await TestBed.configureTestingModule({
|
|
104
|
+
imports: [CardDescriptionDirective, CardDescriptionTestComponent],
|
|
105
|
+
}).compileComponents();
|
|
106
|
+
|
|
107
|
+
fixture = TestBed.createComponent(CardDescriptionTestComponent);
|
|
108
|
+
_component = fixture.componentInstance;
|
|
109
|
+
descriptionElement = fixture.debugElement.query(
|
|
110
|
+
By.directive(CardDescriptionDirective),
|
|
111
|
+
);
|
|
112
|
+
directive = descriptionElement.injector.get(CardDescriptionDirective);
|
|
113
|
+
await fixture.whenStable();
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('should create', () => {
|
|
117
|
+
expect(directive).toBeTruthy();
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('should apply base classes', () => {
|
|
121
|
+
fixture.detectChanges();
|
|
122
|
+
|
|
123
|
+
const classes = directive.classes();
|
|
124
|
+
|
|
125
|
+
expect(classes).toContain('lm-text-secondary');
|
|
126
|
+
expect(classes).toContain('mb-5');
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('should apply normal size by default', () => {
|
|
130
|
+
fixture.detectChanges();
|
|
131
|
+
|
|
132
|
+
const classes = directive.classes();
|
|
133
|
+
expect(classes).toContain('text-sm');
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('should use computed signal for classes', () => {
|
|
137
|
+
expect(typeof directive.classes).toBe('function');
|
|
138
|
+
|
|
139
|
+
const classes = directive.classes();
|
|
140
|
+
expect(typeof classes).toBe('string');
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('should use signal-based input for size', () => {
|
|
144
|
+
expect(typeof directive.lmSize).toBe('function');
|
|
145
|
+
|
|
146
|
+
const size = directive.lmSize();
|
|
147
|
+
expect(size).toBe('normal');
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
describe('CardHeaderDirective', () => {
|
|
152
|
+
let fixture: ComponentFixture<CardHeaderTestComponent>;
|
|
153
|
+
let headerElement: DebugElement;
|
|
154
|
+
let directive: CardHeaderDirective;
|
|
155
|
+
|
|
156
|
+
beforeEach(async () => {
|
|
157
|
+
await TestBed.configureTestingModule({
|
|
158
|
+
imports: [CardHeaderDirective, CardHeaderTestComponent],
|
|
159
|
+
}).compileComponents();
|
|
160
|
+
|
|
161
|
+
fixture = TestBed.createComponent(CardHeaderTestComponent);
|
|
162
|
+
headerElement = fixture.debugElement.query(
|
|
163
|
+
By.directive(CardHeaderDirective),
|
|
164
|
+
);
|
|
165
|
+
directive = headerElement.injector.get(CardHeaderDirective);
|
|
166
|
+
await fixture.whenStable();
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('should create', () => {
|
|
170
|
+
expect(directive).toBeTruthy();
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('should apply mb-4 class for spacing', () => {
|
|
174
|
+
fixture.detectChanges();
|
|
175
|
+
|
|
176
|
+
const element = headerElement.nativeElement as HTMLElement;
|
|
177
|
+
expect(element.className).toContain('mb-4');
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('should be a structural directive with no inputs', () => {
|
|
181
|
+
expect(directive).toBeDefined();
|
|
182
|
+
|
|
183
|
+
expect(directive).toBeTruthy();
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
describe('CardContentDirective', () => {
|
|
188
|
+
let fixture: ComponentFixture<CardContentTestComponent>;
|
|
189
|
+
let contentElement: DebugElement;
|
|
190
|
+
let directive: CardContentDirective;
|
|
191
|
+
|
|
192
|
+
beforeEach(async () => {
|
|
193
|
+
await TestBed.configureTestingModule({
|
|
194
|
+
imports: [CardContentDirective, CardContentTestComponent],
|
|
195
|
+
}).compileComponents();
|
|
196
|
+
|
|
197
|
+
fixture = TestBed.createComponent(CardContentTestComponent);
|
|
198
|
+
contentElement = fixture.debugElement.query(
|
|
199
|
+
By.directive(CardContentDirective),
|
|
200
|
+
);
|
|
201
|
+
directive = contentElement.injector.get(CardContentDirective);
|
|
202
|
+
await fixture.whenStable();
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it('should create', () => {
|
|
206
|
+
expect(directive).toBeTruthy();
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it('should exist as semantic marker without applying styles', () => {
|
|
210
|
+
fixture.detectChanges();
|
|
211
|
+
|
|
212
|
+
// Should have empty class or just the directive itself
|
|
213
|
+
// No specific styling should be applied
|
|
214
|
+
expect(directive).toBeDefined();
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it('should be a structural directive with no inputs', () => {
|
|
218
|
+
// Verify it's a simple structural directive
|
|
219
|
+
expect(directive).toBeDefined();
|
|
220
|
+
|
|
221
|
+
// Directive exists for semantic HTML structure
|
|
222
|
+
// No API surface to test - existence is sufficient
|
|
223
|
+
expect(directive).toBeTruthy();
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it('should serve as backward compatibility marker', () => {
|
|
227
|
+
// Directive exists primarily for semantic HTML and backward compatibility
|
|
228
|
+
// No functional behavior to test beyond existence
|
|
229
|
+
expect(directive).toBeTruthy();
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
// Integration test for all directives together
|
|
234
|
+
@Component({
|
|
235
|
+
template: `
|
|
236
|
+
<div lumaCardHeader>
|
|
237
|
+
<h3 lumaCardTitle lmSize="large">Integration Test</h3>
|
|
238
|
+
<p lumaCardDescription lmSize="small">Testing all directives together</p>
|
|
239
|
+
</div>
|
|
240
|
+
<div lumaCardContent>
|
|
241
|
+
<p>Content area</p>
|
|
242
|
+
</div>
|
|
243
|
+
`,
|
|
244
|
+
imports: [
|
|
245
|
+
CardHeaderDirective,
|
|
246
|
+
CardTitleDirective,
|
|
247
|
+
CardDescriptionDirective,
|
|
248
|
+
CardContentDirective,
|
|
249
|
+
],
|
|
250
|
+
})
|
|
251
|
+
class IntegrationTestComponent {}
|
|
252
|
+
|
|
253
|
+
describe('Card Directives Integration', () => {
|
|
254
|
+
let fixture: ComponentFixture<IntegrationTestComponent>;
|
|
255
|
+
|
|
256
|
+
beforeEach(async () => {
|
|
257
|
+
await TestBed.configureTestingModule({
|
|
258
|
+
imports: [
|
|
259
|
+
CardHeaderDirective,
|
|
260
|
+
CardTitleDirective,
|
|
261
|
+
CardDescriptionDirective,
|
|
262
|
+
CardContentDirective,
|
|
263
|
+
IntegrationTestComponent,
|
|
264
|
+
],
|
|
265
|
+
}).compileComponents();
|
|
266
|
+
|
|
267
|
+
fixture = TestBed.createComponent(IntegrationTestComponent);
|
|
268
|
+
await fixture.whenStable();
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it('should work together in a composed structure', () => {
|
|
272
|
+
fixture.detectChanges();
|
|
273
|
+
|
|
274
|
+
const headerElement = fixture.debugElement.query(
|
|
275
|
+
By.directive(CardHeaderDirective),
|
|
276
|
+
);
|
|
277
|
+
const titleElement = fixture.debugElement.query(
|
|
278
|
+
By.directive(CardTitleDirective),
|
|
279
|
+
);
|
|
280
|
+
const descriptionElement = fixture.debugElement.query(
|
|
281
|
+
By.directive(CardDescriptionDirective),
|
|
282
|
+
);
|
|
283
|
+
const contentElement = fixture.debugElement.query(
|
|
284
|
+
By.directive(CardContentDirective),
|
|
285
|
+
);
|
|
286
|
+
|
|
287
|
+
expect(headerElement).toBeTruthy();
|
|
288
|
+
expect(titleElement).toBeTruthy();
|
|
289
|
+
expect(descriptionElement).toBeTruthy();
|
|
290
|
+
expect(contentElement).toBeTruthy();
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it('should apply all directive classes correctly', () => {
|
|
294
|
+
fixture.detectChanges();
|
|
295
|
+
|
|
296
|
+
const titleDirective = fixture.debugElement
|
|
297
|
+
.query(By.directive(CardTitleDirective))
|
|
298
|
+
.injector.get(CardTitleDirective);
|
|
299
|
+
const descriptionDirective = fixture.debugElement
|
|
300
|
+
.query(By.directive(CardDescriptionDirective))
|
|
301
|
+
.injector.get(CardDescriptionDirective);
|
|
302
|
+
|
|
303
|
+
const titleClasses = titleDirective.classes();
|
|
304
|
+
const descriptionClasses = descriptionDirective.classes();
|
|
305
|
+
|
|
306
|
+
expect(titleClasses).toContain('text-2xl'); // large size
|
|
307
|
+
expect(descriptionClasses).toContain('text-xs'); // small size
|
|
308
|
+
});
|
|
309
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Directive } from '@angular/core';
|
|
2
|
+
|
|
3
|
+
@Directive({
|
|
4
|
+
selector: '[lumaCardHeader]',
|
|
5
|
+
host: {
|
|
6
|
+
class: 'mb-4',
|
|
7
|
+
},
|
|
8
|
+
})
|
|
9
|
+
export class CardHeaderDirective {
|
|
10
|
+
// Structural directive for semantic card header region
|
|
11
|
+
// Provides bottom margin for spacing between header and content
|
|
12
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Directive, input, computed } from '@angular/core';
|
|
2
|
+
import { cardTitleVariants, type CardTitleSize } from '@lumaui/core';
|
|
3
|
+
|
|
4
|
+
@Directive({
|
|
5
|
+
selector: '[lumaCardTitle]',
|
|
6
|
+
host: {
|
|
7
|
+
'[class]': 'classes()',
|
|
8
|
+
},
|
|
9
|
+
})
|
|
10
|
+
export class CardTitleDirective {
|
|
11
|
+
// Signal-based inputs with lm prefix (Angular 20+)
|
|
12
|
+
lmSize = input<CardTitleSize>('normal');
|
|
13
|
+
|
|
14
|
+
// Computed class string
|
|
15
|
+
classes = computed(() => cardTitleVariants({ size: this.lmSize() }));
|
|
16
|
+
}
|