@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,379 @@
|
|
|
1
|
+
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
|
2
|
+
|
|
3
|
+
import { CardComponent } from './card.component';
|
|
4
|
+
import { Component } from '@angular/core';
|
|
5
|
+
|
|
6
|
+
// ============================================================
|
|
7
|
+
// TEST HOST COMPONENTS
|
|
8
|
+
// ============================================================
|
|
9
|
+
|
|
10
|
+
@Component({
|
|
11
|
+
template: `
|
|
12
|
+
<luma-card>
|
|
13
|
+
<div class="test-content">Projected content</div>
|
|
14
|
+
</luma-card>
|
|
15
|
+
`,
|
|
16
|
+
imports: [CardComponent],
|
|
17
|
+
})
|
|
18
|
+
class TestHostComponent {}
|
|
19
|
+
|
|
20
|
+
// ============================================================
|
|
21
|
+
// TOKEN DEFINITIONS
|
|
22
|
+
// ============================================================
|
|
23
|
+
|
|
24
|
+
const CARD_TOKENS = {
|
|
25
|
+
background: 'oklch(0.99 0 0)',
|
|
26
|
+
gradientFrom: 'oklch(0.92 0.005 0 / 0.6)',
|
|
27
|
+
gradientTo: 'oklch(0.96 0.003 0 / 0.6)',
|
|
28
|
+
padding: '1.5rem',
|
|
29
|
+
} as const;
|
|
30
|
+
|
|
31
|
+
const DARK_CARD_TOKENS = {
|
|
32
|
+
background: 'oklch(0.17 0 0)',
|
|
33
|
+
gradientFrom: 'oklch(0.25 0.01 0 / 0.6)',
|
|
34
|
+
gradientTo: 'oklch(0.2 0.008 0 / 0.6)',
|
|
35
|
+
} as const;
|
|
36
|
+
|
|
37
|
+
// ============================================================
|
|
38
|
+
// TOKEN SETUP/CLEANUP UTILITIES
|
|
39
|
+
// ============================================================
|
|
40
|
+
|
|
41
|
+
function setupCardTokens(): void {
|
|
42
|
+
const root = document.documentElement;
|
|
43
|
+
root.style.setProperty('--luma-card-background', CARD_TOKENS.background);
|
|
44
|
+
root.style.setProperty('--luma-card-gradient-from', CARD_TOKENS.gradientFrom);
|
|
45
|
+
root.style.setProperty('--luma-card-gradient-to', CARD_TOKENS.gradientTo);
|
|
46
|
+
root.style.setProperty('--luma-card-padding', CARD_TOKENS.padding);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function cleanupCardTokens(): void {
|
|
50
|
+
const root = document.documentElement;
|
|
51
|
+
root.style.removeProperty('--luma-card-background');
|
|
52
|
+
root.style.removeProperty('--luma-card-gradient-from');
|
|
53
|
+
root.style.removeProperty('--luma-card-gradient-to');
|
|
54
|
+
root.style.removeProperty('--luma-card-padding');
|
|
55
|
+
root.classList.remove('dark');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function applyDarkTheme(): void {
|
|
59
|
+
const root = document.documentElement;
|
|
60
|
+
root.classList.add('dark');
|
|
61
|
+
root.style.setProperty('--luma-card-background', DARK_CARD_TOKENS.background);
|
|
62
|
+
root.style.setProperty(
|
|
63
|
+
'--luma-card-gradient-from',
|
|
64
|
+
DARK_CARD_TOKENS.gradientFrom,
|
|
65
|
+
);
|
|
66
|
+
root.style.setProperty(
|
|
67
|
+
'--luma-card-gradient-to',
|
|
68
|
+
DARK_CARD_TOKENS.gradientTo,
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
describe('CardComponent', () => {
|
|
73
|
+
let component: CardComponent;
|
|
74
|
+
let fixture: ComponentFixture<CardComponent>;
|
|
75
|
+
|
|
76
|
+
beforeEach(async () => {
|
|
77
|
+
await TestBed.configureTestingModule({
|
|
78
|
+
imports: [CardComponent],
|
|
79
|
+
}).compileComponents();
|
|
80
|
+
|
|
81
|
+
fixture = TestBed.createComponent(CardComponent);
|
|
82
|
+
component = fixture.componentInstance;
|
|
83
|
+
await fixture.whenStable();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('should create', () => {
|
|
87
|
+
expect(component).toBeTruthy();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('should apply wrapper classes with gradient border technique', () => {
|
|
91
|
+
fixture.detectChanges();
|
|
92
|
+
|
|
93
|
+
const wrapperClasses = component.wrapperClasses();
|
|
94
|
+
|
|
95
|
+
// Gradient border technique classes
|
|
96
|
+
expect(wrapperClasses).toContain('relative');
|
|
97
|
+
expect(wrapperClasses).toContain('lm-rounded-lg');
|
|
98
|
+
expect(wrapperClasses).toContain('p-[1px]');
|
|
99
|
+
expect(wrapperClasses).toContain('bg-gradient-to-b');
|
|
100
|
+
expect(wrapperClasses).toContain('lm-from-card-gradient-from');
|
|
101
|
+
expect(wrapperClasses).toContain('lm-to-card-gradient-to');
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('should apply content classes with background and padding', () => {
|
|
105
|
+
fixture.detectChanges();
|
|
106
|
+
|
|
107
|
+
const contentClasses = component.contentClasses();
|
|
108
|
+
|
|
109
|
+
// Inner content classes
|
|
110
|
+
expect(contentClasses).toContain('rounded-[17px]');
|
|
111
|
+
expect(contentClasses).toContain('lm-bg-card-background');
|
|
112
|
+
expect(contentClasses).toContain('lm-p-card');
|
|
113
|
+
expect(contentClasses).toContain('lm-text-primary');
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('should render wrapper and content divs in template', () => {
|
|
117
|
+
fixture.detectChanges();
|
|
118
|
+
|
|
119
|
+
const compiled = fixture.nativeElement as HTMLElement;
|
|
120
|
+
const wrapperDiv = compiled.querySelector('div:first-child');
|
|
121
|
+
const contentDiv = compiled.querySelector('div:first-child > div');
|
|
122
|
+
|
|
123
|
+
expect(wrapperDiv).toBeTruthy();
|
|
124
|
+
expect(contentDiv).toBeTruthy();
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('should project content correctly via ng-content', async () => {
|
|
128
|
+
const hostFixture = TestBed.createComponent(TestHostComponent);
|
|
129
|
+
await hostFixture.whenStable();
|
|
130
|
+
hostFixture.detectChanges();
|
|
131
|
+
|
|
132
|
+
const compiled = hostFixture.nativeElement as HTMLElement;
|
|
133
|
+
const projectedContent = compiled.querySelector('.test-content');
|
|
134
|
+
|
|
135
|
+
expect(projectedContent).toBeTruthy();
|
|
136
|
+
expect(projectedContent?.textContent).toBe('Projected content');
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('should use OnPush change detection', () => {
|
|
140
|
+
// Verify component is configured with OnPush
|
|
141
|
+
// OnPush means the component only checks for changes when inputs change
|
|
142
|
+
// or events are triggered, improving performance
|
|
143
|
+
expect(component).toBeTruthy();
|
|
144
|
+
|
|
145
|
+
// The component is stateless with only computed signals,
|
|
146
|
+
// which work perfectly with OnPush change detection
|
|
147
|
+
const wrapperClasses = component.wrapperClasses();
|
|
148
|
+
const contentClasses = component.contentClasses();
|
|
149
|
+
|
|
150
|
+
expect(wrapperClasses).toBeTruthy();
|
|
151
|
+
expect(contentClasses).toBeTruthy();
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('should have computed signal for wrapperClasses', () => {
|
|
155
|
+
// Verify wrapperClasses is a computed signal
|
|
156
|
+
expect(typeof component.wrapperClasses).toBe('function');
|
|
157
|
+
|
|
158
|
+
// Should return string when called
|
|
159
|
+
const classes = component.wrapperClasses();
|
|
160
|
+
expect(typeof classes).toBe('string');
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('should have computed signal for contentClasses', () => {
|
|
164
|
+
// Verify contentClasses is a computed signal
|
|
165
|
+
expect(typeof component.contentClasses).toBe('function');
|
|
166
|
+
|
|
167
|
+
// Should return string when called
|
|
168
|
+
const classes = component.contentClasses();
|
|
169
|
+
expect(typeof classes).toBe('string');
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('should maintain consistent class generation', () => {
|
|
173
|
+
fixture.detectChanges();
|
|
174
|
+
|
|
175
|
+
// Multiple calls should return the same classes
|
|
176
|
+
const wrapperClasses1 = component.wrapperClasses();
|
|
177
|
+
const wrapperClasses2 = component.wrapperClasses();
|
|
178
|
+
const contentClasses1 = component.contentClasses();
|
|
179
|
+
const contentClasses2 = component.contentClasses();
|
|
180
|
+
|
|
181
|
+
expect(wrapperClasses1).toBe(wrapperClasses2);
|
|
182
|
+
expect(contentClasses1).toBe(contentClasses2);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// ============================================================
|
|
186
|
+
// DESIGN TOKEN INTEGRATION TESTS
|
|
187
|
+
// ============================================================
|
|
188
|
+
|
|
189
|
+
describe('Design Token Integration', () => {
|
|
190
|
+
beforeEach(() => {
|
|
191
|
+
setupCardTokens();
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
afterEach(() => {
|
|
195
|
+
cleanupCardTokens();
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// ----------------------------------------------------------
|
|
199
|
+
// CSS Variable Definition Tests
|
|
200
|
+
// ----------------------------------------------------------
|
|
201
|
+
|
|
202
|
+
describe('CSS Variable Definition', () => {
|
|
203
|
+
it('should define --luma-card-background css variable', () => {
|
|
204
|
+
const value = getComputedStyle(document.documentElement)
|
|
205
|
+
.getPropertyValue('--luma-card-background')
|
|
206
|
+
.trim();
|
|
207
|
+
expect(value).toBe(CARD_TOKENS.background);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('should define --luma-card-gradient-from css variable', () => {
|
|
211
|
+
const value = getComputedStyle(document.documentElement)
|
|
212
|
+
.getPropertyValue('--luma-card-gradient-from')
|
|
213
|
+
.trim();
|
|
214
|
+
expect(value).toBe(CARD_TOKENS.gradientFrom);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it('should define --luma-card-gradient-to css variable', () => {
|
|
218
|
+
const value = getComputedStyle(document.documentElement)
|
|
219
|
+
.getPropertyValue('--luma-card-gradient-to')
|
|
220
|
+
.trim();
|
|
221
|
+
expect(value).toBe(CARD_TOKENS.gradientTo);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it('should define --luma-card-padding css variable', () => {
|
|
225
|
+
const value = getComputedStyle(document.documentElement)
|
|
226
|
+
.getPropertyValue('--luma-card-padding')
|
|
227
|
+
.trim();
|
|
228
|
+
expect(value).toBe(CARD_TOKENS.padding);
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
// ----------------------------------------------------------
|
|
233
|
+
// Token Consumption Tests
|
|
234
|
+
// ----------------------------------------------------------
|
|
235
|
+
// CSS variables are inherited from document.documentElement.
|
|
236
|
+
// We verify that tokens are accessible and component uses the correct classes.
|
|
237
|
+
|
|
238
|
+
describe('Token Consumption', () => {
|
|
239
|
+
it('should have access to --luma-card-background token', () => {
|
|
240
|
+
fixture.detectChanges();
|
|
241
|
+
|
|
242
|
+
const value = getComputedStyle(document.documentElement)
|
|
243
|
+
.getPropertyValue('--luma-card-background')
|
|
244
|
+
.trim();
|
|
245
|
+
expect(value).toBe(CARD_TOKENS.background);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it('should have access to --luma-card-gradient-from token', () => {
|
|
249
|
+
fixture.detectChanges();
|
|
250
|
+
|
|
251
|
+
const value = getComputedStyle(document.documentElement)
|
|
252
|
+
.getPropertyValue('--luma-card-gradient-from')
|
|
253
|
+
.trim();
|
|
254
|
+
expect(value).toBe(CARD_TOKENS.gradientFrom);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it('should have access to --luma-card-gradient-to token', () => {
|
|
258
|
+
fixture.detectChanges();
|
|
259
|
+
|
|
260
|
+
const value = getComputedStyle(document.documentElement)
|
|
261
|
+
.getPropertyValue('--luma-card-gradient-to')
|
|
262
|
+
.trim();
|
|
263
|
+
expect(value).toBe(CARD_TOKENS.gradientTo);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it('should have access to --luma-card-padding token', () => {
|
|
267
|
+
fixture.detectChanges();
|
|
268
|
+
|
|
269
|
+
const value = getComputedStyle(document.documentElement)
|
|
270
|
+
.getPropertyValue('--luma-card-padding')
|
|
271
|
+
.trim();
|
|
272
|
+
expect(value).toBe(CARD_TOKENS.padding);
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
// ----------------------------------------------------------
|
|
277
|
+
// Token Override Tests
|
|
278
|
+
// ----------------------------------------------------------
|
|
279
|
+
|
|
280
|
+
describe('Token Override', () => {
|
|
281
|
+
it('should respect custom background token override', () => {
|
|
282
|
+
const customBackground = '#ffffff';
|
|
283
|
+
document.documentElement.style.setProperty(
|
|
284
|
+
'--luma-card-background',
|
|
285
|
+
customBackground,
|
|
286
|
+
);
|
|
287
|
+
fixture.detectChanges();
|
|
288
|
+
|
|
289
|
+
const value = getComputedStyle(document.documentElement)
|
|
290
|
+
.getPropertyValue('--luma-card-background')
|
|
291
|
+
.trim();
|
|
292
|
+
expect(value).toBe(customBackground);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it('should respect custom padding token override', () => {
|
|
296
|
+
const customPadding = '2rem';
|
|
297
|
+
document.documentElement.style.setProperty(
|
|
298
|
+
'--luma-card-padding',
|
|
299
|
+
customPadding,
|
|
300
|
+
);
|
|
301
|
+
fixture.detectChanges();
|
|
302
|
+
|
|
303
|
+
const value = getComputedStyle(document.documentElement)
|
|
304
|
+
.getPropertyValue('--luma-card-padding')
|
|
305
|
+
.trim();
|
|
306
|
+
expect(value).toBe(customPadding);
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
it('should respect custom gradient-from token override', () => {
|
|
310
|
+
const customGradient = 'oklch(0.85 0.01 0 / 0.8)';
|
|
311
|
+
document.documentElement.style.setProperty(
|
|
312
|
+
'--luma-card-gradient-from',
|
|
313
|
+
customGradient,
|
|
314
|
+
);
|
|
315
|
+
fixture.detectChanges();
|
|
316
|
+
|
|
317
|
+
const value = getComputedStyle(document.documentElement)
|
|
318
|
+
.getPropertyValue('--luma-card-gradient-from')
|
|
319
|
+
.trim();
|
|
320
|
+
expect(value).toBe(customGradient);
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
it('should respect custom gradient-to token override', () => {
|
|
324
|
+
const customGradient = 'oklch(0.90 0.005 0 / 0.8)';
|
|
325
|
+
document.documentElement.style.setProperty(
|
|
326
|
+
'--luma-card-gradient-to',
|
|
327
|
+
customGradient,
|
|
328
|
+
);
|
|
329
|
+
fixture.detectChanges();
|
|
330
|
+
|
|
331
|
+
const value = getComputedStyle(document.documentElement)
|
|
332
|
+
.getPropertyValue('--luma-card-gradient-to')
|
|
333
|
+
.trim();
|
|
334
|
+
expect(value).toBe(customGradient);
|
|
335
|
+
});
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
// ----------------------------------------------------------
|
|
339
|
+
// Dark Theme Tests
|
|
340
|
+
// ----------------------------------------------------------
|
|
341
|
+
|
|
342
|
+
describe('Dark Theme', () => {
|
|
343
|
+
beforeEach(() => {
|
|
344
|
+
applyDarkTheme();
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
it('should have access to dark theme background token', () => {
|
|
348
|
+
fixture.detectChanges();
|
|
349
|
+
|
|
350
|
+
const value = getComputedStyle(document.documentElement)
|
|
351
|
+
.getPropertyValue('--luma-card-background')
|
|
352
|
+
.trim();
|
|
353
|
+
expect(value).toBe(DARK_CARD_TOKENS.background);
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
it('should have access to dark theme gradient-from token', () => {
|
|
357
|
+
fixture.detectChanges();
|
|
358
|
+
|
|
359
|
+
const value = getComputedStyle(document.documentElement)
|
|
360
|
+
.getPropertyValue('--luma-card-gradient-from')
|
|
361
|
+
.trim();
|
|
362
|
+
expect(value).toBe(DARK_CARD_TOKENS.gradientFrom);
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
it('should have access to dark theme gradient-to token', () => {
|
|
366
|
+
fixture.detectChanges();
|
|
367
|
+
|
|
368
|
+
const value = getComputedStyle(document.documentElement)
|
|
369
|
+
.getPropertyValue('--luma-card-gradient-to')
|
|
370
|
+
.trim();
|
|
371
|
+
expect(value).toBe(DARK_CARD_TOKENS.gradientTo);
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
it('should have dark class on document element', () => {
|
|
375
|
+
expect(document.documentElement.classList.contains('dark')).toBe(true);
|
|
376
|
+
});
|
|
377
|
+
});
|
|
378
|
+
});
|
|
379
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ChangeDetectionStrategy,
|
|
3
|
+
Component,
|
|
4
|
+
computed,
|
|
5
|
+
input,
|
|
6
|
+
} from '@angular/core';
|
|
7
|
+
import {
|
|
8
|
+
cardVariants,
|
|
9
|
+
cardContentVariants,
|
|
10
|
+
type CardVariant,
|
|
11
|
+
} from '@lumaui/core';
|
|
12
|
+
|
|
13
|
+
@Component({
|
|
14
|
+
selector: 'luma-card',
|
|
15
|
+
templateUrl: './card.component.html',
|
|
16
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
17
|
+
})
|
|
18
|
+
export class CardComponent {
|
|
19
|
+
/**
|
|
20
|
+
* Card visual style variant
|
|
21
|
+
* - default: Gradient border wrapper style (default)
|
|
22
|
+
* - shadow: Elevated card with shadow for primary content
|
|
23
|
+
* - nested: Subtle background for sections within cards
|
|
24
|
+
* - preview: For documentation examples
|
|
25
|
+
*/
|
|
26
|
+
lmVariant = input<CardVariant>('default');
|
|
27
|
+
|
|
28
|
+
// Computed class strings based on variant
|
|
29
|
+
wrapperClasses = computed(() => cardVariants({ variant: this.lmVariant() }));
|
|
30
|
+
contentClasses = computed(() =>
|
|
31
|
+
cardContentVariants({ variant: this.lmVariant() }),
|
|
32
|
+
);
|
|
33
|
+
}
|