@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.
@@ -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,3 @@
1
+ export * from './button.directive';
2
+ // Re-export types from core for convenience
3
+ export type { ButtonVariant, ButtonSize, ButtonVariants } from '@lumaui/core';
@@ -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
+ }
@@ -0,0 +1,5 @@
1
+ <div [class]="wrapperClasses()">
2
+ <div [class]="contentClasses()">
3
+ <ng-content></ng-content>
4
+ </div>
5
+ </div>