diff --git a/scripts/src/generate-font-subsets.mts b/scripts/src/generate-font-subsets.mts index 8a687ccc..7c290b5e 100644 --- a/scripts/src/generate-font-subsets.mts +++ b/scripts/src/generate-font-subsets.mts @@ -1,23 +1,16 @@ import * as fs from 'fs'; import subsetFont from 'subset-font'; import { isMain, Log } from './utils.mjs'; +import MaterialSymbols from '../../src/app/material-symbols.js'; async function generateFonts() { Log.info("Generating font subset for Material Symbols Outlined") const materialSymbolsFont = fs.readFileSync('assets/material-symbols-outlined.woff2'); const fontBuffer = Buffer.from(materialSymbolsFont); - const glyphs = [ - 'dark_mode', - 'light_mode', - 'code', - 'history', - 'apps', - 'api', - 'build', - ]; - Log.info("Glyphs to include in font") - glyphs.forEach((glyph) => Log.item(glyph)); + // If using ligatures, file size increases by mystery + const glyphs = Object.values(MaterialSymbols); + Log.info("%d glyphs to include in font", glyphs.length) const glyphText = glyphs.join(''); diff --git a/src/app/_app-theme.scss b/src/app/_app-theme.scss index 15bb79c4..f2fb28e4 100644 --- a/src/app/_app-theme.scss +++ b/src/app/_app-theme.scss @@ -5,7 +5,7 @@ $text-palette: map-get($theme, text); body { - background: map-get($background-palette, main); + background: map-get($background-palette, z0); color: map-get($text-palette, primary); } } diff --git a/src/app/about/_about-theme.scss b/src/app/about/_about-theme.scss new file mode 100644 index 00000000..09c99725 --- /dev/null +++ b/src/app/about/_about-theme.scss @@ -0,0 +1,22 @@ +@use "animations"; +@use "profile-picture/profile-picture-theme"; +@use "contact-social-icons/contact-social-icons-theme"; +@use "contact-traditional-icons/contact-traditional-icons-theme"; + +@mixin color($theme) { + app-about { + @include profile-picture-theme.color($theme); + @include contact-traditional-icons-theme.color($theme); + @include contact-social-icons-theme.color($theme); + } +} + +@mixin motion() { + app-about { + @include profile-picture-theme.motion() + } +} + +@mixin theme($theme) { + @include color($theme); +} diff --git a/src/app/about/about.component.html b/src/app/about/about.component.html new file mode 100644 index 00000000..eace09b0 --- /dev/null +++ b/src/app/about/about.component.html @@ -0,0 +1,14 @@ +
+
+ +

+ {{ realName }} + @{{ nickname }} +

+
+ + +
+
+ +
diff --git a/src/app/about/about.component.scss b/src/app/about/about.component.scss new file mode 100644 index 00000000..1eb6b338 --- /dev/null +++ b/src/app/about/about.component.scss @@ -0,0 +1,62 @@ +@use "borders"; +@use "margins"; +@use "paddings"; +@use "breakpoints"; + +:host { + section { + position: absolute; + width: 100%; + min-height: 100%; + display: flex; + flex-direction: row; + justify-content: center; + + @include breakpoints.xs { + flex-direction: column; + align-items: center; + } + + > * { + padding: paddings.$m; + } + + .profile { + display: flex; + flex-direction: column; + flex-wrap: wrap; + justify-content: center; + gap: margins.$m; + + @include breakpoints.xs { + flex-direction: row; + align-items: center; + } + + h1 { + font-size: 1.5em; + display: flex; + flex-direction: column; + } + + .contacts { + display: flex; + gap: margins.$m; + flex-wrap: wrap; + + @include breakpoints.xs { + flex-basis: 100%; + justify-content: center; + } + + > * { + flex-basis: 50%; + + @include breakpoints.xs { + flex-basis: unset; + } + } + } + } + } +} diff --git a/src/app/about/about.component.spec.ts b/src/app/about/about.component.spec.ts new file mode 100644 index 00000000..546af5e5 --- /dev/null +++ b/src/app/about/about.component.spec.ts @@ -0,0 +1,62 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +import { MockComponents, MockProvider } from 'ng-mocks'; +import { ensureHasComponents } from '../../test/helpers/component-testers'; +import { METADATA } from '../common/injection-tokens'; +import { Metadata } from '../metadata'; + +import { AboutComponent } from './about.component'; +import { ContactSocialIconsComponent } from './contact-social-icons/contact-social-icons.component'; +import { ContactTraditionalIconsComponent } from './contact-traditional-icons/contact-traditional-icons.component'; +import { DescriptionComponent } from './description/description.component'; +import { ProfilePictureComponent } from './profile-picture/profile-picture.component'; + +describe('AboutComponent', () => { + let component: AboutComponent; + let fixture: ComponentFixture; + const fakeMetadata: Metadata = ({ + nickname: 'bar', + realName: 'Foo', + } as Pick) as Metadata; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [ + AboutComponent, + MockComponents( + ProfilePictureComponent, + ContactTraditionalIconsComponent, + ContactSocialIconsComponent, + DescriptionComponent, + ) + ], + providers: [ + MockProvider(METADATA, fakeMetadata), + ], + }); + fixture = TestBed.createComponent(AboutComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should display real name in header', () => { + const h1 = fixture.debugElement.query(By.css('h1')); + expect(h1.nativeElement.textContent).toContain(fakeMetadata.realName); + }) + + it('should display nickname preceded by \'@\' in header', () => { + const h1 = fixture.debugElement.query(By.css('h1')); + expect(h1.nativeElement.textContent).toContain(`@${fakeMetadata.nickname}`); + }) + + ensureHasComponents(() => fixture, + ProfilePictureComponent, + ContactTraditionalIconsComponent, + ContactSocialIconsComponent, + DescriptionComponent, + ) +}); diff --git a/src/app/about/about.component.ts b/src/app/about/about.component.ts new file mode 100644 index 00000000..df9a9f06 --- /dev/null +++ b/src/app/about/about.component.ts @@ -0,0 +1,19 @@ +import { Component, Inject } from '@angular/core'; +import { METADATA } from '../common/injection-tokens'; +import { Metadata } from '../metadata'; + +@Component({ + selector: 'app-about', + templateUrl: './about.component.html', + styleUrls: ['./about.component.scss'], +}) +export class AboutComponent { + public realName = this.metadata.realName; + public nickname = this.metadata.nickname; + + constructor( + @Inject(METADATA) private metadata: Metadata, + ) { + } + +} diff --git a/src/app/about/contact-social-icons/_contact-social-icons-theme.scss b/src/app/about/contact-social-icons/_contact-social-icons-theme.scss new file mode 100644 index 00000000..f8c650e2 --- /dev/null +++ b/src/app/about/contact-social-icons/_contact-social-icons-theme.scss @@ -0,0 +1,11 @@ +@mixin color($theme) { + $text-palette: map-get($theme, text); + + app-contact-social-icons { + color: map-get($theme, icon); + + li:hover { + color: map-get($text-palette, secondary); + } + } +} diff --git a/src/app/about/contact-social-icons/contact-social-icons.component.html b/src/app/about/contact-social-icons/contact-social-icons.component.html new file mode 100644 index 00000000..225eeba8 --- /dev/null +++ b/src/app/about/contact-social-icons/contact-social-icons.component.html @@ -0,0 +1,7 @@ + diff --git a/src/app/about/contact-social-icons/contact-social-icons.component.scss b/src/app/about/contact-social-icons/contact-social-icons.component.scss new file mode 100644 index 00000000..927c408e --- /dev/null +++ b/src/app/about/contact-social-icons/contact-social-icons.component.scss @@ -0,0 +1,28 @@ +@use "breakpoints"; +@use "margins"; + +:host { + ul { + @include breakpoints.xs { + margin-left: 0; + } + + // Position in same place as Material Symbols from app-contact-traditional-icons + margin-left: 2.75px; + display: flex; + flex-direction: row; + gap: margins.$m; + + @include breakpoints.xs() { + justify-content: center; + } + + li { + height: 32px; + } + + fa-icon { + font-size: 26.5px; // to take 32px height; + } + } +} diff --git a/src/app/about/contact-social-icons/contact-social-icons.component.spec.ts b/src/app/about/contact-social-icons/contact-social-icons.component.spec.ts new file mode 100644 index 00000000..5f198650 --- /dev/null +++ b/src/app/about/contact-social-icons/contact-social-icons.component.spec.ts @@ -0,0 +1,69 @@ +import { DebugElement } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +import { FaIconComponent } from '@fortawesome/angular-fontawesome'; +import { MockProvider } from 'ng-mocks'; +import { getComponentSelector } from '../../../test/helpers/component-testers'; +import { METADATA } from '../../common/injection-tokens'; +import { Metadata } from '../../metadata'; + +import { ContactSocialIconsComponent } from './contact-social-icons.component'; + +describe('ContactSocialIconsComponent', () => { + let component: ContactSocialIconsComponent; + let fixture: ComponentFixture; + const nickname = 'foo'; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [ + ContactSocialIconsComponent, + FaIconComponent, + ], + providers: [ + MockProvider(METADATA, {nickname: nickname} as Pick as Metadata) + ] + }); + fixture = TestBed.createComponent(ContactSocialIconsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + describe('#items', () => { + it('should have the nickname in every url', () => { + component.items.forEach((item, index) => { + expect(item.url.pathname).withContext(`item ${index}`).toContain(nickname); + }) + }) + it('should have an icon related to the name', () => { + component.items.forEach((item, index) => { + const iconNameWithoutDashes = item.icon.iconName.replace('-', ''); + expect(iconNameWithoutDashes).withContext(`item ${index}`).toContain(item.name.toLowerCase()); + }) + }) + }) + + it('should list all contact methods with their icons, links and accessibility labels', () => { + const itemElements = fixture.debugElement.queryAll(By.css('li')) + expect(itemElements.length).withContext('same number of items').toBe(component.items.length) + component.items.forEach((item, index) => { + const itemElement = itemElements[index]; + const iconElement = itemElement.query(By.css(getComponentSelector(FaIconComponent))); + expect(getIconNameFromFontAwesomeElement(iconElement)).withContext(`item ${index} icon`).toEqual(item.icon.iconName); + + // noinspection DuplicatedCode + const anchorElement = itemElement.query(By.css('a')); + expect(anchorElement).withContext(`item ${index} link exists`).toBeTruthy(); + expect(anchorElement.attributes['href']).withContext(`item ${index} link URL`).toEqual(item.url.toString()); + expect(anchorElement.attributes['aria-label']).withContext(`item ${index} accessibility label`).toEqual(item.name); + }) + }) + + function getIconNameFromFontAwesomeElement(faElement: DebugElement): string { + return faElement.children[0].nativeElement.getAttribute('data-icon') + } +}); diff --git a/src/app/about/contact-social-icons/contact-social-icons.component.ts b/src/app/about/contact-social-icons/contact-social-icons.component.ts new file mode 100644 index 00000000..ed69ea42 --- /dev/null +++ b/src/app/about/contact-social-icons/contact-social-icons.component.ts @@ -0,0 +1,39 @@ +import { Component, Inject } from '@angular/core'; +import { faGithub, faLinkedinIn, faStackOverflow, faTwitter, IconDefinition } from '@fortawesome/free-brands-svg-icons'; +import { METADATA } from '../../common/injection-tokens'; +import { Metadata } from '../../metadata'; + +@Component({ + selector: 'app-contact-social-icons', + templateUrl: './contact-social-icons.component.html', + styleUrls: ['./contact-social-icons.component.scss'], +}) +export class ContactSocialIconsComponent { + public items: ReadonlyArray<{ name: string, icon: IconDefinition, url: URL }> = [ + { + name: "GitHub", + icon: faGithub, + url: new URL(`https://github.com/${this.metadata.nickname}`), + }, + { + name: "LinkedIn", + icon: faLinkedinIn, + url: new URL(`https://www.linkedin.com/in/${this.metadata.nickname}`), + }, + { + name: "StackOverflow", + icon: faStackOverflow, + url: new URL(`https://stackoverflow.com/users/3263250/${this.metadata.nickname}`), + }, + { + name: "Twitter", + icon: faTwitter, + url: new URL(`https://twitter.com/${this.metadata.nickname}`), + }, + ] + + constructor( + @Inject(METADATA) private metadata: Metadata, + ) { + } +} diff --git a/src/app/about/contact-traditional-icons/_contact-traditional-icons-theme.scss b/src/app/about/contact-traditional-icons/_contact-traditional-icons-theme.scss new file mode 100644 index 00000000..daa67d2d --- /dev/null +++ b/src/app/about/contact-traditional-icons/_contact-traditional-icons-theme.scss @@ -0,0 +1,11 @@ +@mixin color($theme) { + $text-palette: map-get($theme, text); + + app-contact-traditional-icons { + color: map-get($theme, icon); + + li:hover { + color: map-get($text-palette, secondary); + } + } +} diff --git a/src/app/about/contact-traditional-icons/contact-traditional-icons.component.html b/src/app/about/contact-traditional-icons/contact-traditional-icons.component.html new file mode 100644 index 00000000..75ad1a64 --- /dev/null +++ b/src/app/about/contact-traditional-icons/contact-traditional-icons.component.html @@ -0,0 +1,5 @@ + diff --git a/src/app/about/contact-traditional-icons/contact-traditional-icons.component.scss b/src/app/about/contact-traditional-icons/contact-traditional-icons.component.scss new file mode 100644 index 00000000..28e5cf16 --- /dev/null +++ b/src/app/about/contact-traditional-icons/contact-traditional-icons.component.scss @@ -0,0 +1,23 @@ +@use "breakpoints"; +@use "margins"; + +:host { + ul { + display: flex; + flex-direction: row; + gap: margins.$s; + + @include breakpoints.xs() { + justify-content: center; + } + } + + li { + height: 32px; + + .material-symbols-outlined { + font-variation-settings: "FILL" 1, "wght" 700, "GRAD" 0, "opsz" 32; + font-size: 32px; + } + } +} diff --git a/src/app/about/contact-traditional-icons/contact-traditional-icons.component.spec.ts b/src/app/about/contact-traditional-icons/contact-traditional-icons.component.spec.ts new file mode 100644 index 00000000..d9623418 --- /dev/null +++ b/src/app/about/contact-traditional-icons/contact-traditional-icons.component.spec.ts @@ -0,0 +1,38 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; + +import { ContactTraditionalIconsComponent } from './contact-traditional-icons.component'; + +describe('ContactTraditionalIconsComponent', () => { + let component: ContactTraditionalIconsComponent; + let fixture: ComponentFixture; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [ContactTraditionalIconsComponent] + }); + fixture = TestBed.createComponent(ContactTraditionalIconsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should list all contact methods with their icons, links and accessibility labels', () => { + const itemElements = fixture.debugElement.queryAll(By.css('li')) + expect(itemElements.length).withContext('same number of items').toBe(component.items.length) + component.items.forEach((item, index) => { + const itemElement = itemElements[index]; + const iconText = itemElement.nativeElement.textContent; + expect(iconText).withContext(`item ${index} icon`).toEqual(item.icon); + + // noinspection DuplicatedCode + const anchorElement = itemElement.query(By.css('a')); + expect(anchorElement).withContext(`item ${index} link exists`).toBeTruthy(); + expect(anchorElement.attributes['href']).withContext(`item ${index} link URL`).toEqual(item.url.toString()); + expect(anchorElement.attributes['aria-label']).withContext(`item ${index} accessibility label`).toEqual(item.name); + }) + }) +}); diff --git a/src/app/about/contact-traditional-icons/contact-traditional-icons.component.ts b/src/app/about/contact-traditional-icons/contact-traditional-icons.component.ts new file mode 100644 index 00000000..7365432a --- /dev/null +++ b/src/app/about/contact-traditional-icons/contact-traditional-icons.component.ts @@ -0,0 +1,16 @@ +import { Component } from '@angular/core'; +import { Call, Email, MyLocation } from '../../material-symbols'; + +@Component({ + selector: 'app-contact-traditional-icons', + templateUrl: './contact-traditional-icons.component.html', + styleUrls: ['./contact-traditional-icons.component.scss'] +}) +export class ContactTraditionalIconsComponent { + public items: ReadonlyArray<{name: string, icon: string, url: URL}> = [ + {name: 'Email', icon: Email, url: new URL('mailto:mail@davidlj95.com')}, + {name: 'Phone', icon: Call, url: new URL('tel:+34 644 449 360')}, + {name: 'Location', icon: MyLocation, url: new URL('https://meet.barcelona')}, + ] + +} diff --git a/src/app/about/description/description.component.html b/src/app/about/description/description.component.html new file mode 100644 index 00000000..2ba78ad0 --- /dev/null +++ b/src/app/about/description/description.component.html @@ -0,0 +1,4 @@ +

+ {{ descriptionLine.symbol }} + +

diff --git a/src/app/about/description/description.component.scss b/src/app/about/description/description.component.scss new file mode 100644 index 00000000..c0e3155e --- /dev/null +++ b/src/app/about/description/description.component.scss @@ -0,0 +1,21 @@ +@use "margins"; + +:host { + display: flex; + flex-direction: column; + justify-content: center; + gap: margins.$s; + + h2 { + font-size: 1.1em; + text-align: left; + width: fit-content; + display: flex; + align-items: center; + gap: margins.$s; + + > span { + font-size: 1em; + } + } +} diff --git a/src/app/about/description/description.component.spec.ts b/src/app/about/description/description.component.spec.ts new file mode 100644 index 00000000..0bd0965b --- /dev/null +++ b/src/app/about/description/description.component.spec.ts @@ -0,0 +1,53 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +import { MockProvider } from 'ng-mocks'; +import { MATERIAL_SYMBOLS_CLASS_SELECTOR } from '../../../test/constants'; +import { METADATA } from '../../common/injection-tokens'; +import { Metadata } from '../../metadata'; + +import { DescriptionComponent } from './description.component'; + +describe('DescriptionComponent', () => { + let component: DescriptionComponent; + let fixture: ComponentFixture; + const fakeMetadata: Metadata = ({ + descriptionLines: [ + {symbol: 'code', text: 'Foo bar'}, + {symbol: 'build', text: 'Bar foo'}, + ], + } as Pick) as Metadata; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [DescriptionComponent], + providers: [ + MockProvider(METADATA, fakeMetadata) + ] + }); + fixture = TestBed.createComponent(DescriptionComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should contain description lines with symbol as a Material Symbol', () => { + const h2s = fixture.debugElement.queryAll(By.css('h2')); + h2s.forEach((h2, index) => { + const descriptionLine = fakeMetadata.descriptionLines[index]; + const materialSymbolSpan = h2.query(MATERIAL_SYMBOLS_CLASS_SELECTOR) + expect(materialSymbolSpan.nativeElement.textContent).toEqual(descriptionLine.symbol) + }); + }) + + it('should contain description lines with text', () => { + const h2s = fixture.debugElement.queryAll(By.css('h2')); + h2s.forEach((h2, index) => { + const descriptionLine = fakeMetadata.descriptionLines[index]; + const textSpan = h2.query(By.css('span:nth-child(2)')) + expect(textSpan.nativeElement.textContent).toEqual(descriptionLine.text) + }); + }) +}); diff --git a/src/app/profile/profile.component.ts b/src/app/about/description/description.component.ts similarity index 50% rename from src/app/profile/profile.component.ts rename to src/app/about/description/description.component.ts index 57ab3556..21ee9039 100644 --- a/src/app/profile/profile.component.ts +++ b/src/app/about/description/description.component.ts @@ -1,23 +1,20 @@ import { Component, Inject } from '@angular/core'; -import { DomSanitizer } from "@angular/platform-browser"; -import { METADATA } from '../common/injection-tokens'; -import { Metadata } from '../metadata'; +import { DomSanitizer } from '@angular/platform-browser'; +import { METADATA } from '../../common/injection-tokens'; +import { Metadata } from '../../metadata'; @Component({ - selector: 'app-profile', - templateUrl: './profile.component.html', - styleUrls: ['./profile.component.scss'], + selector: 'app-description', + templateUrl: './description.component.html', + styleUrls: ['./description.component.scss'] }) -export class ProfileComponent { - public realName = this.metadata.realName; - public nickname = this.metadata.nickname; +export class DescriptionComponent { public descriptionLines = this.metadata.descriptionLines .map((descriptionLine) => ({ ...descriptionLine, text: this.sanitizer.bypassSecurityTrustHtml(descriptionLine.text), }), ); - constructor( @Inject(METADATA) private metadata: Metadata, private sanitizer: DomSanitizer, diff --git a/src/app/about/profile-picture/_profile-picture-theme.scss b/src/app/about/profile-picture/_profile-picture-theme.scss new file mode 100644 index 00000000..6554c0b7 --- /dev/null +++ b/src/app/about/profile-picture/_profile-picture-theme.scss @@ -0,0 +1,45 @@ +@use "animations"; + +@mixin color($theme) { + $background-palette: map-get($theme, background); + $image-opts: map-get($theme, image); + + app-profile-picture { + img { + background-color: map-get($background-palette, z1); + border-color: map-get($theme, hairline); + filter: map-get($image-opts, filter); + } + + .comment { + background-color: map-get($background-palette, z2); + + &:before { + border-right-color: map-get($background-palette, z2) !important; + } + } + + // Bubble speech is at top on super small screens + @media screen and (max-width: 279.98px) { + .comment:before { + border-right-color: transparent !important; + border-bottom-color: map-get($background-palette, z2) !important; + } + } + } +} + +@mixin motion() { + app-profile-picture { + img { + transition: opacity animations.$standard-duration animations.$standard-easing; + transition-property: filter, background-color; + @include animations.transition(animations.$emphasized-style) + } + + .comment { + transition-property: opacity, visibility; + @include animations.transition(animations.$standard-style) + } + } +} diff --git a/src/app/about/profile-picture/profile-picture.component.html b/src/app/about/profile-picture/profile-picture.component.html new file mode 100644 index 00000000..6930d407 --- /dev/null +++ b/src/app/about/profile-picture/profile-picture.component.html @@ -0,0 +1,10 @@ + +Another portrait of {{ realName }} wearing the same and in the same pose. But appears surprised +A portrait of {{ realName }}. Slightly smiling. Wears 80'ish glasses and a green and black plaid shirt +
+ Hey! 👋 Did you taphover me? +
diff --git a/src/app/about/profile-picture/profile-picture.component.scss b/src/app/about/profile-picture/profile-picture.component.scss new file mode 100644 index 00000000..abd1947c --- /dev/null +++ b/src/app/about/profile-picture/profile-picture.component.scss @@ -0,0 +1,114 @@ +@use "touch-or-pointer"; +@use "margins"; +@use "paddings"; + +$picture-size: 128px; + +:host { + position: relative; + width: $picture-size; + border-radius: $picture-size; + + img { + border-radius: $picture-size; + border-style: solid; + border-width: 1px; + } + + .huh { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + opacity: 0; + } + + .comment { + visibility: hidden; + opacity: 0; + + position: absolute; + + top: 49px; + left: 82px; + + width: max-content; + + padding: paddings.$s; + border-radius: paddings.$s; + + // Speech bubble by + // https://freefrontend.com/css-speech-bubbles/ + &:before { + position: absolute; + content: ""; + border: 8px solid transparent; + + border-left: 0; + left: -8px; + top: 50%; + margin-top: -8px; + } + + .touch { + display: none + } + + @include touch-or-pointer.primaryInputCannotHover { + .touch { + display: inline-block; + } + .hover { + display: none; + } + } + } + + // Reduce comment on small screens, doesn't fit width + @media screen and (max-width: 350px) { + .comment { + width: 128px; + } + } + + // Place it below picture in super small screens + @media screen and (max-width: 279.98px) { + .comment { + margin-top: margins.$s; + position: static; + display: none; + + &:before { + position: absolute; + border-left: 8px solid transparent; + border-top: 0; + top: calc($picture-size + 12px); + left: 50%; + margin-left: -8px; + } + } + } + + &:hover { + .comment { + visibility: visible; + opacity: 1; + } + + // On small screens, element is fully hidden to avoid layout issues, given it is positioned below + @media screen and (max-width: 279.98px) { + .comment { + display: block; + } + } + + .main { + opacity: 0; + } + + .huh { + opacity: 1; + } + } +} diff --git a/src/app/about/profile-picture/profile-picture.component.spec.ts b/src/app/about/profile-picture/profile-picture.component.spec.ts new file mode 100644 index 00000000..db3fc668 --- /dev/null +++ b/src/app/about/profile-picture/profile-picture.component.spec.ts @@ -0,0 +1,39 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; + +import { ProfilePictureComponent } from './profile-picture.component'; + +describe('ProfilePictureComponent', () => { + let component: ProfilePictureComponent; + let fixture: ComponentFixture; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [ProfilePictureComponent] + }); + fixture = TestBed.createComponent(ProfilePictureComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should display profile picture', () => { + const profilePic = fixture.debugElement.query(By.css('img.main')); + expect(profilePic).toBeTruthy(); + expect(profilePic.attributes['ngSrc']).toBeDefined(); + expect(profilePic.attributes['ngSrc']).toContain('profile.png'); + }) + + it('should display "huh" profile picture hidden', () => { + const huhProfilePic = fixture.debugElement.query(By.css('img.huh')); + expect(huhProfilePic).toBeTruthy(); + expect(huhProfilePic.attributes['ngSrc']).toBeDefined(); + expect(huhProfilePic.attributes['ngSrc']).toContain('profile_huh.png'); + const styles = getComputedStyle(huhProfilePic.nativeElement); + expect(styles).toBeTruthy(); + expect(styles.opacity).toEqual('0'); + }) +}); diff --git a/src/app/about/profile-picture/profile-picture.component.ts b/src/app/about/profile-picture/profile-picture.component.ts new file mode 100644 index 00000000..0afc3c83 --- /dev/null +++ b/src/app/about/profile-picture/profile-picture.component.ts @@ -0,0 +1,17 @@ +import { Component, Inject } from '@angular/core'; +import { METADATA } from '../../common/injection-tokens'; +import { Metadata } from '../../metadata'; + +@Component({ + selector: 'app-profile-picture', + templateUrl: './profile-picture.component.html', + styleUrls: ['./profile-picture.component.scss'] +}) +export class ProfilePictureComponent { + protected realName: string = this.metadata.realName; + + constructor( + @Inject(METADATA) private metadata: Metadata, + ) { + } +} diff --git a/src/app/app.component.html b/src/app/app.component.html index 4f9df12d..2cf1181d 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -1,5 +1,11 @@ +
- + +
+ +
diff --git a/src/app/app.component.scss b/src/app/app.component.scss index 90b65db6..ae8ed0bb 100644 --- a/src/app/app.component.scss +++ b/src/app/app.component.scss @@ -1,3 +1,5 @@ +@use "header"; + :host { // You may wonder: why aren't you just using "min-height: 100vh"? Well, good question. // Seems that choosing "vh" as units isn't a wise choice. Given the viewport height can change due to @@ -5,19 +7,18 @@ // // Concretely, had this issue with my Android phone's Chrome address bar and had to switch to this way after that // https://blog.logrocket.com/improving-mobile-design-latest-css-viewport-units/ - position: absolute; - top: 0; - bottom: 0; - left: 0; - right: 0; - - // Weird things happen if the root acts as flex container - // Given the root is fixed to take full screen, but isn't prepared to overflow main { - display: flex; - justify-content: center; - align-items: center; + position: absolute; + top: header.$height; + min-height: calc(100% - #{header.$height}); + width: 100%; - min-height: 100%; + // Second wrapper to allow for the noscript notice in case it displays + // min-height and top changes in case noscript displays. Check index.html styles. + .contents { + position: absolute; + width: 100%; + min-height: 100%; + } } } diff --git a/src/app/app.component.spec.ts b/src/app/app.component.spec.ts index c60c357e..29cd0aa0 100644 --- a/src/app/app.component.spec.ts +++ b/src/app/app.component.spec.ts @@ -2,10 +2,12 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { RouterTestingModule } from '@angular/router/testing'; import { MockComponents } from 'ng-mocks'; import { ensureHasComponents } from '../test/helpers/component-testers'; +import { AboutComponent } from './about/about.component'; import { AppComponent } from './app.component'; +import { HeaderComponent } from './header/header.component'; import { JsonldMetadataComponent } from './jsonld-metadata/jsonld-metadata.component'; +import { NoScriptComponent } from './no-script/no-script.component'; import { ReleaseInfoComponent } from './release-info/release-info.component'; -import { WindowComponent } from './window/window.component'; describe('AppComponent', () => { let fixture: ComponentFixture; @@ -18,8 +20,10 @@ describe('AppComponent', () => { AppComponent, MockComponents( JsonldMetadataComponent, - WindowComponent, ReleaseInfoComponent, + NoScriptComponent, + HeaderComponent, + AboutComponent, ), ], }); @@ -33,5 +37,8 @@ describe('AppComponent', () => { expect(component).toBeTruthy(); }); - ensureHasComponents(() => fixture, JsonldMetadataComponent, WindowComponent, ReleaseInfoComponent) + ensureHasComponents(() => fixture, + JsonldMetadataComponent, ReleaseInfoComponent, + NoScriptComponent, HeaderComponent, AboutComponent, + ) }); diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 5f80c768..d366f98d 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -12,24 +12,30 @@ import { JsonldMetadataComponent } from './jsonld-metadata/jsonld-metadata.compo import { METADATA } from './metadata'; import { NavigationTabsComponent } from './navigation-tabs/navigation-tabs.component'; import { NoScriptComponent } from './no-script/no-script.component'; -import { ProfileComponent } from './profile/profile.component'; +import { AboutComponent } from './about/about.component'; import { ReleaseInfoComponent } from './release-info/release-info.component'; import { SocialComponent } from './social/social.component'; -import { ToolbarComponent } from './toolbar/toolbar.component'; -import { WindowComponent } from './window/window.component'; +import { HeaderComponent } from './header/header.component'; +import { ProfilePictureComponent } from './about/profile-picture/profile-picture.component'; +import { ContactTraditionalIconsComponent } from './about/contact-traditional-icons/contact-traditional-icons.component'; +import { ContactSocialIconsComponent } from './about/contact-social-icons/contact-social-icons.component'; +import { DescriptionComponent } from './about/description/description.component'; @NgModule({ declarations: [ AppComponent, - ToolbarComponent, - ProfileComponent, + HeaderComponent, + AboutComponent, NoScriptComponent, ContactsComponent, SocialComponent, NavigationTabsComponent, - WindowComponent, JsonldMetadataComponent, ReleaseInfoComponent, + ProfilePictureComponent, + ContactTraditionalIconsComponent, + ContactSocialIconsComponent, + DescriptionComponent, ], imports: [ BrowserModule, diff --git a/src/app/toolbar/_toolbar-theme.scss b/src/app/header/_header-theme.scss similarity index 65% rename from src/app/toolbar/_toolbar-theme.scss rename to src/app/header/_header-theme.scss index 806941d3..014da3c9 100644 --- a/src/app/toolbar/_toolbar-theme.scss +++ b/src/app/header/_header-theme.scss @@ -1,8 +1,14 @@ @use "touch-or-pointer"; +@use "borders"; + @mixin color($theme) { $text-palette: map-get($theme, text); + $background-palette: map-get($theme, background); + + app-header { + background-color: map-get($background-palette, z1); + border-bottom-color: borders.panel-color($theme); - app-toolbar { .dark-light-scheme-toggle { color: map-get($theme, icon); diff --git a/src/app/toolbar/color-scheme.service.spec.ts b/src/app/header/color-scheme.service.spec.ts similarity index 100% rename from src/app/toolbar/color-scheme.service.spec.ts rename to src/app/header/color-scheme.service.spec.ts diff --git a/src/app/toolbar/color-scheme.service.ts b/src/app/header/color-scheme.service.ts similarity index 100% rename from src/app/toolbar/color-scheme.service.ts rename to src/app/header/color-scheme.service.ts diff --git a/src/app/header/header.component.html b/src/app/header/header.component.html new file mode 100644 index 00000000..288794e5 --- /dev/null +++ b/src/app/header/header.component.html @@ -0,0 +1,10 @@ +
+
+ +
+
+ diff --git a/src/app/header/header.component.scss b/src/app/header/header.component.scss new file mode 100644 index 00000000..86a0c4ca --- /dev/null +++ b/src/app/header/header.component.scss @@ -0,0 +1,30 @@ +@use "borders"; +@use "paddings"; +@use "header"; +@use "z-index"; + + +:host { + position: fixed; + width: 100%; + @include borders.panel-style-width(bottom); + z-index: z-index.$header; + + .bar { + display: flex; + padding: header.$vertical-padding; + flex-direction: row; + justify-content: flex-end; + min-height: header.$icons-height + header.$vertical-padding-height; + + .buttons { + height: header.$icons-height; + + button { + border-style: none; + background: transparent; + font-size: header.$icons-height; + } + } + } +} diff --git a/src/app/toolbar/toolbar.component.spec.ts b/src/app/header/header.component.spec.ts similarity index 78% rename from src/app/toolbar/toolbar.component.spec.ts rename to src/app/header/header.component.spec.ts index 1181ad0c..4ab41844 100644 --- a/src/app/toolbar/toolbar.component.spec.ts +++ b/src/app/header/header.component.spec.ts @@ -1,21 +1,21 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { MockProviders, ngMocks } from 'ng-mocks'; import { ColorSchemeService } from './color-scheme.service'; -import { ToolbarComponent } from './toolbar.component'; +import { HeaderComponent } from './header.component'; describe('ToolbarComponent', () => { - let component: ToolbarComponent; - let fixture: ComponentFixture; + let component: HeaderComponent; + let fixture: ComponentFixture; beforeEach(() => { ngMocks.autoSpy('jasmine'); TestBed.configureTestingModule({ - declarations: [ToolbarComponent], + declarations: [HeaderComponent], providers: [ MockProviders(ColorSchemeService), ] }); - fixture = TestBed.createComponent(ToolbarComponent); + fixture = TestBed.createComponent(HeaderComponent); component = fixture.componentInstance; fixture.detectChanges(); }); diff --git a/src/app/header/header.component.ts b/src/app/header/header.component.ts new file mode 100644 index 00000000..3bf1fb7c --- /dev/null +++ b/src/app/header/header.component.ts @@ -0,0 +1,21 @@ +import { Component, HostBinding } from '@angular/core'; +import { DarkTheme, LightTheme } from '../material-symbols'; +import { ColorSchemeService } from './color-scheme.service'; + +@Component({ + selector: 'app-header', + templateUrl: './header.component.html', + styleUrls: ['./header.component.scss'] +}) +export class HeaderComponent { + @HostBinding('attr.role') ariaRole = 'toolbar'; + public Icons = { + DarkTheme: DarkTheme, + LightTheme: LightTheme, + }; + + constructor( + protected colorSchemeService: ColorSchemeService, + ) { + } +} diff --git a/src/app/material-symbols.ts b/src/app/material-symbols.ts new file mode 100644 index 00000000..6de42fcc --- /dev/null +++ b/src/app/material-symbols.ts @@ -0,0 +1,11 @@ +export const DarkTheme = '\ue51c' +export const LightTheme = '\ue518' +export const Warning = '\ue002' +export const Email = '\ue158' +export const Call = '\ue0b0' +export const MyLocation = '\ue55c' +export const Code = '\ue86f' +export const History = '\ue889' +export const Apps = '\ue5c3' +export const Api = '\uf1b7' +export const Build = '\ue869' diff --git a/src/app/metadata.ts b/src/app/metadata.ts index bb26d30b..0c4e9530 100644 --- a/src/app/metadata.ts +++ b/src/app/metadata.ts @@ -1,3 +1,5 @@ +import { Api, Apps, Build, Code, History } from './material-symbols'; + /** * Metadata used around the app. Either in the Angular app or accessory files. * @@ -14,23 +16,23 @@ const YEARS_OF_EXPERIENCE = Math.abs( ) const DESCRIPTION_LINES: ReadonlyArray = [ { - symbol: 'code', + symbol: Code, text: 'Full stack software engineer', }, { - symbol: 'history', + symbol: History, text: `${YEARS_OF_EXPERIENCE}+ years of experience`, }, { - symbol: 'apps', + symbol: Apps, text: 'Web apps & hybrid mobile apps', }, { - symbol: 'api', + symbol: Api, text: 'REST APIs backends', }, { - symbol: 'build', + symbol: Build, text: 'CI/CD, DevOps, Cloud', }, ] diff --git a/src/app/no-script/_no-script-theme.scss b/src/app/no-script/_no-script-theme.scss new file mode 100644 index 00000000..49428a20 --- /dev/null +++ b/src/app/no-script/_no-script-theme.scss @@ -0,0 +1,10 @@ +@use "borders"; + +@mixin color($theme) { + $background-palette: map-get($theme, background); + + app-no-script { + background: map-get($background-palette, z1); + border-bottom-color: borders.panel-color($theme); + } +} diff --git a/src/app/no-script/no-script.component.html b/src/app/no-script/no-script.component.html index e38d101a..b3156732 100644 --- a/src/app/no-script/no-script.component.html +++ b/src/app/no-script/no-script.component.html @@ -1,3 +1,8 @@ -

🤔 Seems you don't have Javascript enabled 👀

-You are viewing a simplified version of the website
+

+ {{ Icons.Warning }} + Seems you don't have JavaScript enabled +

+

+You are viewing a simplified version of the website
Enable it for a better user experience +

diff --git a/src/app/no-script/no-script.component.scss b/src/app/no-script/no-script.component.scss index 2fea2487..689b42b4 100644 --- a/src/app/no-script/no-script.component.scss +++ b/src/app/no-script/no-script.component.scss @@ -1,11 +1,29 @@ +@use "borders"; +@use "margins"; @use "paddings"; -:host { - display: block; +app-no-script { + display: flex; + flex-direction: column; + justify-content: center; padding: paddings.$m; + gap: margins.$xs; text-align: center; + height: 100px; + + @include borders.panel-style-width(bottom); p:first-child { - margin-top: 0; + display: flex; + justify-content: center; + align-items: center; + gap: margins.$s; + } + p:not(:first-child) { + font-size: 14px; + } + .material-symbols-outlined { + font-size: 1em; + font-variation-settings: "FILL" 0, "wght" 700, "GRAD" 0, "opsz" 16; } } diff --git a/src/app/no-script/no-script.component.ts b/src/app/no-script/no-script.component.ts index 6a3343f9..9b7886c3 100644 --- a/src/app/no-script/no-script.component.ts +++ b/src/app/no-script/no-script.component.ts @@ -1,10 +1,14 @@ -import { Component } from '@angular/core'; +import { Component, ViewEncapsulation } from '@angular/core'; +import { Warning } from '../material-symbols'; @Component({ selector: 'app-no-script', templateUrl: './no-script.component.html', - styleUrls: ['./no-script.component.scss'] + styleUrls: ['./no-script.component.scss'], + encapsulation: ViewEncapsulation.None, }) export class NoScriptComponent { - + protected Icons = { + Warning: Warning, + } } diff --git a/src/app/profile/_profile-theme.scss b/src/app/profile/_profile-theme.scss deleted file mode 100644 index 18594e6d..00000000 --- a/src/app/profile/_profile-theme.scss +++ /dev/null @@ -1,53 +0,0 @@ -@use "animations"; - -@mixin color($theme) { - $background-palette: map-get($theme, background); - $image-opts: map-get($theme, image); - - app-profile { - .picture { - img { - background-color: map-get($background-palette, z1); - border-color: map-get($theme, hairline); - filter: map-get($image-opts, filter); - } - - .comment { - background-color: map-get($background-palette, z2); - - &:before { - border-right-color: map-get($background-palette, z2) !important; - } - } - - // Bubble speech is at top on super small screens - @media screen and (max-width: 279.98px) { - .comment:before { - border-right-color: transparent !important; - border-bottom-color: map-get($background-palette, z2) !important; - } - } - } - } -} - -@mixin motion() { - app-profile { - .picture { - img { - transition: opacity animations.$standard-duration animations.$standard-easing; - transition-property: filter, background-color; - @include animations.transition(animations.$emphasized-style) - } - - .comment { - transition-property: opacity, visibility; - @include animations.transition(animations.$standard-style) - } - } - } -} - -@mixin theme($theme) { - @include color($theme); -} diff --git a/src/app/profile/profile.component.html b/src/app/profile/profile.component.html deleted file mode 100644 index 72e2998f..00000000 --- a/src/app/profile/profile.component.html +++ /dev/null @@ -1,22 +0,0 @@ - -
- Another portrait of {{ realName }} wearing the same and in the same pose. But appears surprised - A portrait of {{ realName }}. Slightly smiling. Wears 80'ish glasses and a green and black plaid shirt -
- Hey! 👋 Did you taphover me? -
-
-

- {{ realName }} - @{{ nickname }} -

-
-

- {{ descriptionLine.symbol }} - -

-
- diff --git a/src/app/profile/profile.component.scss b/src/app/profile/profile.component.scss deleted file mode 100644 index cd221625..00000000 --- a/src/app/profile/profile.component.scss +++ /dev/null @@ -1,147 +0,0 @@ -@use "margins"; -@use "paddings"; -@use "touch-or-pointer"; - -:host { - display: flex; - flex-direction: column; - padding: paddings.$m; - gap: margins.$m; - - $picture-size: 128px; - - .picture { - position: relative; - width: $picture-size; - border-radius: $picture-size; - - img { - border-radius: $picture-size; - border-style: solid; - border-width: 1px; - } - - .huh { - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - opacity: 0; - } - - .comment { - visibility: hidden; - opacity: 0; - - position: absolute; - - top: 49px; - left: 82px; - - width: max-content; - - padding: paddings.$s; - border-radius: paddings.$s; - - // Speech bubble by - // https://freefrontend.com/css-speech-bubbles/ - &:before { - position: absolute; - content: ""; - border: 8px solid transparent; - - border-left: 0; - left: -8px; - top: 50%; - margin-top: -8px; - } - - .touch { - display: none - } - - @include touch-or-pointer.primaryInputCannotHover { - .touch { - display: inline-block; - } - .hover { - display: none; - } - } - } - - // Reduce comment on small screens, doesn't fit width - @media screen and (max-width: 350px) { - .comment { - width: 128px; - } - } - - // Place it below picture in super small screens - @media screen and (max-width: 279.98px) { - .comment { - margin-top: margins.$s; - position: static; - display: none; - - &:before { - position: absolute; - border-left: 8px solid transparent; - border-top: 0; - top: calc($picture-size + 12px); - left: 50%; - margin-left: -8px; - } - } - } - - &:hover { - .comment { - visibility: visible; - opacity: 1; - } - - // On small screens, element is fully hidden to avoid layout issues, given it is positioned below - @media screen and (max-width: 279.98px) { - .comment { - display: block; - } - } - - .main { - opacity: 0; - } - - .huh { - opacity: 1; - } - } - } - - h1 { - font-size: 1.5em; - display: flex; - flex-direction: column; - line-height: 1em; - } - - div.descriptions { - display: flex; - flex-direction: column; - gap: margins.$s; - } - - h2 { - font-size: 1.1em; - text-align: left; - width: fit-content; - display: flex; - align-items: center; - gap: margins.$s; - - > span { - font-size: 1em; - } - } -} diff --git a/src/app/profile/profile.component.spec.ts b/src/app/profile/profile.component.spec.ts deleted file mode 100644 index 3daac5cf..00000000 --- a/src/app/profile/profile.component.spec.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { By } from '@angular/platform-browser'; -import { MockProvider } from 'ng-mocks'; -import { MATERIAL_SYMBOLS_CLASS_SELECTOR } from '../../test/constants'; -import { METADATA } from '../common/injection-tokens'; -import { Metadata } from '../metadata'; - -import { ProfileComponent } from './profile.component'; - -describe('ProfileComponent', () => { - let component: ProfileComponent; - let fixture: ComponentFixture; - const fakeMetadata: Metadata = ({ - nickname: 'bar', - realName: 'Foo', - descriptionLines: [ - {symbol: 'code', text: 'Foo bar'}, - {symbol: 'code', text: 'Bar foo'}, - ], - } as Pick) as Metadata; - - beforeEach(() => { - TestBed.configureTestingModule({ - declarations: [ProfileComponent], - providers: [ - MockProvider(METADATA, fakeMetadata), - ], - }); - fixture = TestBed.createComponent(ProfileComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); - - it('should display profile picture', () => { - const profilePic = fixture.debugElement.query(By.css('img.main')); - expect(profilePic).toBeTruthy(); - expect(profilePic.attributes['ngSrc']).toBeDefined(); - expect(profilePic.attributes['ngSrc']).toContain('profile.png'); - }) - - it('should display "huh" profile picture hidden', () => { - const huhProfilePic = fixture.debugElement.query(By.css('img.huh')); - expect(huhProfilePic).toBeTruthy(); - expect(huhProfilePic.attributes['ngSrc']).toBeDefined(); - expect(huhProfilePic.attributes['ngSrc']).toContain('profile_huh.png'); - const styles = getComputedStyle(huhProfilePic.nativeElement); - expect(styles).toBeTruthy(); - expect(styles.opacity).toEqual('0'); - }) - - it('should display real name in header', () => { - const h1 = fixture.debugElement.query(By.css('h1')); - expect(h1.nativeElement.textContent).toContain(fakeMetadata.realName); - }) - - it('should display nickname preceded by \'@\' in header', () => { - const h1 = fixture.debugElement.query(By.css('h1')); - expect(h1.nativeElement.textContent).toContain(`@${fakeMetadata.nickname}`); - }) - - it('should contain description lines with symbol as a Material Symbol', () => { - const h2s = fixture.debugElement.queryAll(By.css('h2')); - h2s.forEach((h2, index) => { - const descriptionLine = fakeMetadata.descriptionLines[index]; - const materialSymbolSpan = h2.query(MATERIAL_SYMBOLS_CLASS_SELECTOR) - expect(materialSymbolSpan.nativeElement.textContent).toEqual(descriptionLine.symbol) - }); - }) - - it('should contain description lines with text', () => { - const h2s = fixture.debugElement.queryAll(By.css('h2')); - h2s.forEach((h2, index) => { - const descriptionLine = fakeMetadata.descriptionLines[index]; - const textSpan = h2.query(By.css('span:nth-child(2)')) - expect(textSpan.nativeElement.textContent).toEqual(descriptionLine.text) - }); - }) -}); diff --git a/src/app/toolbar/toolbar.component.html b/src/app/toolbar/toolbar.component.html deleted file mode 100644 index ef88bc20..00000000 --- a/src/app/toolbar/toolbar.component.html +++ /dev/null @@ -1,5 +0,0 @@ - diff --git a/src/app/toolbar/toolbar.component.scss b/src/app/toolbar/toolbar.component.scss deleted file mode 100644 index 83fae271..00000000 --- a/src/app/toolbar/toolbar.component.scss +++ /dev/null @@ -1,17 +0,0 @@ -@use "paddings"; - -:host { - padding: paddings.$xs; - display: flex; - flex-direction: row; - justify-content: flex-end; - - button { - border-style: none; - background: transparent; - - span { - font-size: 24px; - } - } -} diff --git a/src/app/toolbar/toolbar.component.ts b/src/app/toolbar/toolbar.component.ts deleted file mode 100644 index abe659d7..00000000 --- a/src/app/toolbar/toolbar.component.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Component, HostBinding } from '@angular/core'; -import { ColorSchemeService } from './color-scheme.service'; - -@Component({ - selector: 'app-toolbar', - templateUrl: './toolbar.component.html', - styleUrls: ['./toolbar.component.scss'] -}) -export class ToolbarComponent { - @HostBinding('attr.role') ariaRole = 'toolbar'; - - constructor( - public colorSchemeService: ColorSchemeService, - ) { - } -} diff --git a/src/app/window/_window-theme.scss b/src/app/window/_window-theme.scss deleted file mode 100644 index f1b4e099..00000000 --- a/src/app/window/_window-theme.scss +++ /dev/null @@ -1,24 +0,0 @@ -@use "animations"; - -@mixin color($theme) { - $background-palette: map-get($theme, background); - - app-window { - background: map-get($background-palette, z0); - - > * { - border-color: map-get($theme, hairline); - } - } -} - -@mixin motion() { - app-window { - transition-property: background-color; - @include animations.transition(animations.$emphasized-style) - } -} - -@mixin theme($theme) { - @include color($theme); -} diff --git a/src/app/window/window.component.html b/src/app/window/window.component.html deleted file mode 100644 index 7e21aa88..00000000 --- a/src/app/window/window.component.html +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - diff --git a/src/app/window/window.component.scss b/src/app/window/window.component.scss deleted file mode 100644 index 05ff291f..00000000 --- a/src/app/window/window.component.scss +++ /dev/null @@ -1,30 +0,0 @@ -@use "breakpoints"; -@use "margins"; - -:host { - display: block; - max-width: calc(600px - #{margins.$m * 2}); - margin: margins.$m; - - > * { - border-style: solid; - border-width: 1px; - border-bottom-style: none; - } - - > *:last-child, > .active { - border-bottom-style: solid; - } - - noscript { - display: block; - } - - .tab-item { - display: none; - - &.active { - display: block !important; - } - } -} diff --git a/src/app/window/window.component.spec.ts b/src/app/window/window.component.spec.ts deleted file mode 100644 index 51669124..00000000 --- a/src/app/window/window.component.spec.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { EventEmitter } from '@angular/core'; -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { By } from '@angular/platform-browser'; -import { ActivatedRoute } from '@angular/router'; -import { MockComponents } from 'ng-mocks'; -import { ensureHasComponents, getComponentSelector } from '../../test/helpers/component-testers'; -import { getSampleFromArray } from '../../test/helpers/random'; -import { ContactsComponent } from '../contacts/contacts.component'; -import { NavigationTabsComponent, TabId } from '../navigation-tabs/navigation-tabs.component'; -import { NoScriptComponent } from '../no-script/no-script.component'; -import { ProfileComponent } from '../profile/profile.component'; -import { SocialComponent } from '../social/social.component'; -import { ToolbarComponent } from '../toolbar/toolbar.component'; - -import { WindowComponent } from './window.component'; - -describe('WindowComponent', () => { - let component: WindowComponent; - let fixture: ComponentFixture; - let fragment$: EventEmitter; - - beforeEach(() => { - fragment$ = new EventEmitter(); - TestBed.configureTestingModule({ - declarations: [ - WindowComponent, - MockComponents( - ToolbarComponent, - ProfileComponent, - NoScriptComponent, - NavigationTabsComponent, - ContactsComponent, - SocialComponent, - ), - ], - providers: [ - {provide: ActivatedRoute, useValue: {fragment: fragment$}}, - ], - }); - fixture = TestBed.createComponent(WindowComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); - - ensureHasComponents(() => fixture, - ToolbarComponent, ProfileComponent, NavigationTabsComponent, ContactsComponent, SocialComponent, - ); - - describe('when route fragment changes', () => { - describe('when fragment does not represent a tab', () => { - it('should do nothing and stay at same tab', () => { - const defaultTab = component.selectedTab; - - fragment$.emit('definitely-not-a-tab-id') - - expect(component.selectedTab).toEqual(defaultTab); - }) - }) - describe('when fragment represents a tab', () => { - it('should update the active tab', () => { - const defaultTab = component.selectedTab; - const otherTabs = Object.values(TabId).filter((tabId) => tabId != defaultTab); - const anotherTab = getSampleFromArray(otherTabs); - - fragment$.emit(anotherTab); - - expect(component.selectedTab).toEqual(anotherTab); - - fixture.detectChanges() - const navigationTabsElement = fixture.debugElement.query( - By.css(getComponentSelector(NavigationTabsComponent)), - ) - expect(navigationTabsElement.attributes['ng-reflect-tab']).toEqual(anotherTab); - }) - }) - }) -}); diff --git a/src/app/window/window.component.ts b/src/app/window/window.component.ts deleted file mode 100644 index 04c74689..00000000 --- a/src/app/window/window.component.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { Component, OnDestroy } from '@angular/core'; -import { ActivatedRoute } from '@angular/router'; -import { isTabId, TabId } from "../navigation-tabs/navigation-tabs.component"; - -@Component({ - selector: 'app-window', - templateUrl: './window.component.html', - styleUrls: ['./window.component.scss'] -}) -export class WindowComponent implements OnDestroy { - static readonly DEFAULT_TAB_ID: TabId = TabId.Contact; - - public selectedTab: TabId = WindowComponent.DEFAULT_TAB_ID; - private fragmentSubscription = this.activatedRoute.fragment.subscribe( - (fragment) => { - if (fragment && isTabId(fragment)) { - this.selectedTab = fragment; - } - } - ); - - constructor( - private activatedRoute: ActivatedRoute, - ) { - } - - ngOnDestroy(): void { - this.fragmentSubscription.unsubscribe(); - } -} diff --git a/src/index.html b/src/index.html index 49e77be2..7ae5c8f5 100644 --- a/src/index.html +++ b/src/index.html @@ -28,19 +28,29 @@ diff --git a/src/sass/_borders.scss b/src/sass/_borders.scss new file mode 100644 index 00000000..1836c1e4 --- /dev/null +++ b/src/sass/_borders.scss @@ -0,0 +1,20 @@ +@use "sass:list"; +@use "sass:map"; + +$panel-width: 1px; + +@function panel-color($theme) { + @return map.get($theme, hairline); +} + +@mixin panel-style-width($side) { + $panel-style: solid; + + @if ($side == all) { + border-width: $panel-width; + border-style: $panel-style; + } @else { + border-#{$side}-style: $panel-style; + border-#{$side}-width: $panel-width; + } +} diff --git a/src/sass/_header.scss b/src/sass/_header.scss new file mode 100644 index 00000000..ca1b4c60 --- /dev/null +++ b/src/sass/_header.scss @@ -0,0 +1,9 @@ +@use "paddings"; +@use "borders"; + +$vertical-padding: paddings.$xs; +$icons-height: 24px; +$vertical-padding-height: calc(2*$vertical-padding); +$border-height: borders.$panel-width; + +$height: calc($icons-height + $vertical-padding-height + $border-height); diff --git a/src/sass/_z-index.scss b/src/sass/_z-index.scss new file mode 100644 index 00000000..ff4c00ad --- /dev/null +++ b/src/sass/_z-index.scss @@ -0,0 +1 @@ +$header: 255; diff --git a/src/styles.scss b/src/styles.scss index 32ac31c7..778319f1 100644 --- a/src/styles.scss +++ b/src/styles.scss @@ -1,7 +1,7 @@ @use 'app/app-theme' as root-theme; -@use 'app/window/window-theme'; -@use 'app/profile/profile-theme'; -@use 'app/toolbar/toolbar-theme'; +@use 'app/about/about-theme'; +@use 'app/header/header-theme'; +@use 'app/no-script/no-script-theme'; @use 'app/navigation-tabs/navigation-tabs-theme'; @use 'app/contacts/contacts-theme'; @use 'app/social/social-theme'; @@ -13,6 +13,8 @@ body { @include typographies.body-font(); + // Do not allow scroll past boundaries + overscroll-behavior-y: none; } code, pre { @@ -23,9 +25,9 @@ code, pre { @mixin app-color($theme) { @include root-theme.color($theme); - @include window-theme.color($theme); - @include profile-theme.color($theme); - @include toolbar-theme.color($theme); + @include header-theme.color($theme); + @include no-script-theme.color($theme); + @include about-theme.color($theme); @include navigation-tabs-theme.color($theme); @include contacts-theme.color($theme); @include social-theme.color($theme); @@ -61,7 +63,6 @@ code, pre { // Add animations html:not([data-no-motion]) { @include root-theme.motion; - @include window-theme.motion; - @include profile-theme.motion; + @include about-theme.motion; @include navigation-tabs-theme.motion; } diff --git a/src/test/color-scheme.spec.ts b/src/test/color-scheme.spec.ts index 65414e02..17ca92cb 100644 --- a/src/test/color-scheme.spec.ts +++ b/src/test/color-scheme.spec.ts @@ -1,7 +1,7 @@ import { DOCUMENT } from '@angular/common'; import { TestBed } from '@angular/core/testing'; import { AppModule } from '../app/app.module'; -import { ColorSchemeService, Scheme } from '../app/toolbar/color-scheme.service'; +import { ColorSchemeService, Scheme } from '../app/header/color-scheme.service'; describe('App color scheme', () => { let bodyElement: HTMLElement;