Galactic Archives - Star Wars Theming and Optimization

June 11, 2025

Angular DataSource with SWAPI: Building the Galactic Archives - Star Wars Theming and Optimization

In the vast digital landscape, functionality without aesthetics is like a lightsaber without a kyber crystal—technically operational but missing its soul. Now that our Galactic Archives are feature-complete and responsive, it's time to infuse them with the visual essence of the Star Wars universe.

The Cosmic Compiler once remarked that while clean code pleases developers, thoughtful design pleases users. "The intersection," it noted, "is where truly exceptional applications are born." Several junior developers nodded sagely, pretending they hadn't spent the previous day arguing about whether to use tabs or spaces.

Beyond Functionality: The Importance of Theming

We've built a powerful application that handles data efficiently, works on all devices, and is accessible to all users. But it still looks like a generic Angular Material application. To truly immerse users in the Star Wars experience, we need to apply theming that evokes the visual language of the franchise.

In this transmission, we'll enhance our application with:

  1. A custom Angular Material theme inspired by Star Wars
  2. Advanced Tailwind CSS utilities for visual effects
  3. Animated transitions and micro-interactions
  4. Custom iconography and typography
  5. Atmospheric background elements

Let's begin by creating a custom theme for our application.

Creating a Star Wars-Inspired Material Theme

Angular Material uses a theming system based on palettes. Let's create custom palettes that match the Star Wars aesthetic:

// src/theme.scss

@use "@angular/material" as mat;

// Define custom palettes
$galactic-primary: mat.define-palette(
  (
    50: #e3f2fd,
    100: #bbdefb,
    200: #90caf9,
    300: #64b5f6,
    400: #42a5f5,
    500: #2196f3,
    600: #1e88e5,
    700: #1976d2,
    800: #1565c0,
    900: #0d47a1,
    contrast: (
      50: rgba(0, 0, 0, 0.87),
      100: rgba(0, 0, 0, 0.87),
      200: rgba(0, 0, 0, 0.87),
      300: rgba(0, 0, 0, 0.87),
      400: rgba(0, 0, 0, 0.87),
      500: white,
      600: white,
      700: white,
      800: white,
      900: white,
    ),
  )
);

$galactic-accent: mat.define-palette(
  (
    50: #fff8e1,
    100: #ffecb3,
    200: #ffe082,
    300: #ffd54f,
    400: #ffca28,
    500: #ffc107,
    // Star Wars yellow
    600: #ffb300,
    700: #ffa000,
    800: #ff8f00,
    900: #ff6f00,
    contrast: (
      50: rgba(0, 0, 0, 0.87),
      100: rgba(0, 0, 0, 0.87),
      200: rgba(0, 0, 0, 0.87),
      300: rgba(0, 0, 0, 0.87),
      400: rgba(0, 0, 0, 0.87),
      500: rgba(0, 0, 0, 0.87),
      600: rgba(0, 0, 0, 0.87),
      700: rgba(0, 0, 0, 0.87),
      800: rgba(0, 0, 0, 0.87),
      900: rgba(0, 0, 0, 0.87),
    ),
  )
);

$galactic-warn: mat.define-palette(
  (
    50: #ffebee,
    100: #ffcdd2,
    200: #ef9a9a,
    300: #e57373,
    400: #ef5350,
    500: #f44336,
    600: #e53935,
    700: #d32f2f,
    800: #c62828,
    900: #b71c1c,
    // Sith red
    contrast:
      (
        50: rgba(0, 0, 0, 0.87),
        100: rgba(0, 0, 0, 0.87),
        200: rgba(0, 0, 0, 0.87),
        300: rgba(0, 0, 0, 0.87),
        400: rgba(0, 0, 0, 0.87),
        500: white,
        600: white,
        700: white,
        800: white,
        900: white,
      ),
  )
);

// Create the theme
$galactic-theme: mat.define-dark-theme(
  (
    color: (
      primary: $galactic-primary,
      accent: $galactic-accent,
      warn: $galactic-warn,
    ),
    typography: mat.define-typography-config(),
    density: 0,
  )
);

// Apply the theme
@include mat.all-component-themes($galactic-theme);

This creates a dark theme with blue, yellow, and red accents—colors commonly associated with Star Wars.

Customizing Tailwind for Star Wars Aesthetics

Next, let's update our Tailwind configuration to include Star Wars-inspired colors:

// tailwind.config.js
module.exports = {
  prefix: "tw-",
  content: ["./src/**/*.{html,ts}"],
  theme: {
    extend: {
      colors: {
        "jedi-blue": {
          DEFAULT: "#2196f3",
          50: "#e3f2fd",
          // ... other shades
        },
        "imperial-gray": {
          DEFAULT: "#212121",
          50: "#fafafa",
          // ... other shades
        },
        "sith-red": {
          DEFAULT: "#b71c1c",
          50: "#ffebee",
          // ... other shades
        },
        "rebel-yellow": {
          DEFAULT: "#ffc107",
          50: "#fff8e1",
          // ... other shades
        },
      },
      backgroundImage: {
        stars: 'url("/assets/stars-bg.png")',
        grid: "linear-gradient(rgba(66, 66, 66, 0.2) 1px, transparent 1px), linear-gradient(90deg, rgba(66, 66, 66, 0.2) 1px, transparent 1px)",
      },
      backgroundSize: {
        grid: "20px 20px",
      },
      animation: {
        "pulse-slow": "pulse 3s cubic-bezier(0.4, 0, 0.6, 1) infinite",
        float: "float 6s ease-in-out infinite",
      },
      keyframes: {
        float: {
          "0%, 100%": { transform: "translateY(0)" },
          "50%": { transform: "translateY(-10px)" },
        },
      },
    },
  },
  plugins: [],
};

These custom colors and utilities will help us create a more Star Wars-like interface.

Creating a Space Background

Let's add a starfield background to our application:

// Add to app.component.ts
@Component({
  selector: "app-root",
  template: `
    <div
      class="tw-min-h-screen tw-bg-black tw-bg-stars tw-relative tw-overflow-hidden"
    >
      <div class="tw-absolute tw-inset-0 tw-bg-grid tw-opacity-20"></div>
      <div class="tw-container tw-mx-auto tw-py-8">
        <router-outlet></router-outlet>
      </div>
    </div>
  `,
  styles: [
    `
      :host {
        display: block;
        min-height: 100vh;
        background-color: #000;
      }
    `,
  ],
})
export class AppComponent {}

This creates a dark background with stars and a subtle grid overlay, reminiscent of Star Wars computer interfaces.

Enhancing the Character List Component

Now let's update our character list component with more visual flair:

<!-- Updated header with Star Wars styling -->
<div class="tw-mb-8 tw-text-center">
  <h1
    class="tw-text-rebel-yellow tw-text-4xl tw-font-bold tw-tracking-wider tw-mb-2"
  >
    GALACTIC ARCHIVES
  </h1>
  <p class="tw-text-rebel-yellow/70 tw-text-lg tw-italic">
    Character Database • Imperial Security Bureau
  </p>
</div>

<!-- Styled search bar -->
<div class="tw-mb-6 tw-relative">
  <mat-form-field appearance="outline" class="tw-w-full">
    <mat-label>Search the Archives</mat-label>
    <input
      matInput
      [formControl]="searchControl"
      placeholder="Name, species, homeworld..."
      class="tw-text-white"
    />
    <mat-icon matPrefix class="tw-mr-2 tw-text-rebel-yellow">search</mat-icon>
    <button
      *ngIf="searchControl.value"
      matSuffix
      mat-icon-button
      aria-label="Clear"
      (click)="clearSearch()"
    >
      <mat-icon>close</mat-icon>
    </button>
  </mat-form-field>
</div>

Adding Animated Transitions

Let's add some subtle animations to make the interface feel more dynamic:

// Add to component metadata
animations: [
  trigger("fadeIn", [
    transition(":enter", [
      style({ opacity: 0 }),
      animate("300ms ease-out", style({ opacity: 1 })),
    ]),
  ]),
  trigger("listAnimation", [
    transition("* => *", [
      query(
        ":enter",
        [
          style({ opacity: 0, transform: "translateY(10px)" }),
          stagger(50, [
            animate(
              "300ms ease-out",
              style({ opacity: 1, transform: "translateY(0)" })
            ),
          ]),
        ],
        { optional: true }
      ),
    ]),
  ]),
];

And apply these animations to our elements:

<!-- Animated table rows -->
<tr
  mat-row
  *matRowDef="let character; columns: displayedColumns; let i = index; trackBy: trackByCharacterId"
  [@fadeIn]
  ...
></tr>

<!-- Animated cards -->
<div
  class="tw-grid tw-gap-4 tw-grid-cols-1"
  [@listAnimation]="dataSource.charactersData.length"
>
  <mat-card
    *ngFor="let character of dataSource.charactersData; ..."
    [@fadeIn]
    ...
  >
  </mat-card>
</div>

Custom Loading Indicator

Let's create a Star Wars-themed loading indicator:

<!-- Star Wars inspired loading indicator -->
<div
  *ngIf="dataSource.loading$ | async"
  class="tw-flex tw-flex-col tw-items-center tw-justify-center tw-my-12 tw-py-8"
>
  <div class="tw-w-16 tw-h-16 tw-relative">
    <div
      class="tw-absolute tw-inset-0 tw-border-4 tw-border-t-rebel-yellow tw-border-r-rebel-yellow/30 tw-border-b-rebel-yellow/10 tw-border-l-rebel-yellow/60 tw-rounded-full tw-animate-spin"
    ></div>
  </div>
  <p class="tw-text-rebel-yellow/70 tw-mt-4 tw-text-sm tw-tracking-wider">
    ACCESSING IMPERIAL DATABASE
  </p>
</div>

This creates a custom spinner that looks like a holographic loading indicator.

Optimizing Bundle Size

As our application grows in features and styling, we need to keep an eye on the bundle size. Angular provides built-in tools to monitor and optimize our application's size through bundle budgets in angular.json.

The Bundle Size Challenge

When building our application, we encountered a warning about exceeding the initial bundle size budget:

Initial Chunk Files           | Names         |  Raw Size | Estimated Transfer Size
main.js                       | main          | 612.00 kB |               112.43 kB

WARNING in budgets: initial exceeded maximum budget. Budget 512.00 kB was not met by 100.00 kB with a value of 612.00 kB.

This indicates our application is growing larger than the default Angular budget of 512 kB. Let's implement some optimization strategies to address this.

Optimization Strategy 1: Conditional Loading of MSW

Mock Service Worker (MSW) is a great tool for development but adds unnecessary weight to our production bundle. Let's modify our main.ts to only load MSW in development mode:

// src/main.ts
import { bootstrapApplication } from "@angular/platform-browser";
import { appConfig } from "./app/app.config";
import { AppComponent } from "./app/app.component";
import { environment } from "./environments/environment";

// Only load MSW in development mode
if (environment.enableMocks) {
  import("./mocks/browser").then(({ worker }) => {
    worker.start();
    bootstrapApplication(AppComponent, appConfig).catch((err) =>
      console.error(err)
    );
  });
} else {
  bootstrapApplication(AppComponent, appConfig).catch((err) =>
    console.error(err)
  );
}

Optimization Strategy 2: Font Loading Optimization

Google Fonts can add significant overhead if not optimized. We can combine multiple font requests into a single HTTP request and specify only the weights we need:

/* Import Star Wars themed fonts with optimized loading - combined into one request */
@import url("https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700&family=Exo+2:wght@400;700&family=Space+Mono:wght@400&display=swap");

Optimization Strategy 3: Centralized Angular Material Imports

Instead of importing Angular Material modules individually in each component, we can create a centralized material module:

// src/app/shared/material/material.ts
import { MatTableModule } from "@angular/material/table";
import { MatSortModule } from "@angular/material/sort";
import { MatPaginatorModule } from "@angular/material/paginator";
import { MatProgressSpinnerModule } from "@angular/material/progress-spinner";
import { MatFormFieldModule } from "@angular/material/form-field";
import { MatInputModule } from "@angular/material/input";
import { MatIconModule } from "@angular/material/icon";
import { MatButtonModule } from "@angular/material/button";

export const MATERIAL_MODULES = [
  MatTableModule,
  MatSortModule,
  MatPaginatorModule,
  MatProgressSpinnerModule,
  MatFormFieldModule,
  MatInputModule,
  MatIconModule,
  MatButtonModule,
];

Then in our components:

import { MATERIAL_MODULES } from '../../../../shared/material/material';

@Component({
  // ...
  imports: [
    CommonModule,
    ReactiveFormsModule,
    ...MATERIAL_MODULES,
  ],
})

Optimization Strategy 4: RxJS Subscription Management

We can optimize our RxJS usage by implementing the takeUntil pattern for better memory management:

// In component class
private destroy$ = new Subject<void>();

ngOnInit(): void {
  this.dataSource.connect()
    .pipe(takeUntil(this.destroy$))
    .subscribe(characters => {
      this.characters = characters;
    });
}

ngOnDestroy(): void {
  this.destroy$.next();
  this.destroy$.complete();
}

Optimization Strategy 5: Adjusting Bundle Budgets

Finally, we can adjust our bundle budgets in angular.json to better reflect the realistic size of our modern Angular application:

"budgets": [
  {
    "type": "initial",
    "maximumWarning": "850kB",
    "maximumError": "1MB"
  },
  {
    "type": "anyComponentStyle",
    "maximumWarning": "2kB",
    "maximumError": "4kB"
  }
]

Results

After implementing these optimizations, our bundle size is now under control:

Initial chunk files   | Names         |  Raw size | Estimated transfer size
--------------------- | ------------- | --------- | ----------------------
main.js               | main          | 850.72 kB |               188.43 kB

Our application now builds without warnings and maintains good performance while still delivering all the Star Wars-themed styling and features we've implemented.

Character Detail Modal

Let's enhance the character detail view with a Star Wars-inspired modal:

// Add to component class
viewCharacterDetails(character: Character): void {
  this.dialog.open(CharacterDetailComponent, {
    data: { character },
    width: '500px',
    panelClass: 'imperial-dialog'
  });
}

And create a styled dialog component:

// character-detail.component.ts
@Component({
  selector: "app-character-detail",
  template: `
    <div
      class="tw-bg-imperial-gray-900 tw-border tw-border-rebel-yellow/30 tw-rounded"
    >
      <div
        class="tw-flex tw-justify-between tw-items-center tw-border-b tw-border-rebel-yellow/30 tw-p-4"
      >
        <h2
          mat-dialog-title
          class="tw-text-rebel-yellow tw-text-xl tw-tracking-wider tw-m-0"
        >
          {{ data.character.name | uppercase }}
        </h2>
        <button mat-icon-button mat-dialog-close>
          <mat-icon class="tw-text-rebel-yellow/70">close</mat-icon>
        </button>
      </div>

      <mat-dialog-content class="tw-p-4">
        <div class="tw-grid tw-grid-cols-2 tw-gap-4">
          <div class="tw-flex tw-flex-col">
            <span class="tw-text-rebel-yellow/50 tw-text-sm">GENDER</span>
            <span class="tw-text-white">{{
              data.character.gender | titlecase
            }}</span>
          </div>
          <div class="tw-flex tw-flex-col">
            <span class="tw-text-rebel-yellow/50 tw-text-sm">BIRTH YEAR</span>
            <span class="tw-text-white">{{ data.character.birth_year }}</span>
          </div>
          <div class="tw-flex tw-flex-col">
            <span class="tw-text-rebel-yellow/50 tw-text-sm">HEIGHT</span>
            <span class="tw-text-white">{{ data.character.height }}cm</span>
          </div>
          <div class="tw-flex tw-flex-col">
            <span class="tw-text-rebel-yellow/50 tw-text-sm">MASS</span>
            <span class="tw-text-white">{{ data.character.mass }}kg</span>
          </div>
          <div class="tw-flex tw-flex-col">
            <span class="tw-text-rebel-yellow/50 tw-text-sm">EYE COLOR</span>
            <span class="tw-text-white">{{
              data.character.eye_color | titlecase
            }}</span>
          </div>
          <div class="tw-flex tw-flex-col">
            <span class="tw-text-rebel-yellow/50 tw-text-sm">HAIR COLOR</span>
            <span class="tw-text-white">{{
              data.character.hair_color | titlecase
            }}</span>
          </div>
          <div class="tw-flex tw-flex-col">
            <span class="tw-text-rebel-yellow/50 tw-text-sm">SKIN COLOR</span>
            <span class="tw-text-white">{{
              data.character.skin_color | titlecase
            }}</span>
          </div>
        </div>

        <div
          class="tw-mt-6 tw-p-3 tw-bg-black/30 tw-border tw-border-rebel-yellow/20 tw-rounded"
        >
          <div class="tw-flex tw-items-center tw-mb-2">
            <mat-icon class="tw-text-rebel-yellow/70 tw-mr-2">info</mat-icon>
            <span class="tw-text-rebel-yellow/70 tw-text-sm"
              >IMPERIAL RECORDS</span
            >
          </div>
          <p class="tw-text-white/70 tw-text-sm tw-m-0">
            This individual has appeared in
            {{ data.character.films?.length || 0 }} documented incidents.
            <span *ngIf="data.character.starships?.length">
              Known to operate
              {{ data.character.starships.length }} starship(s).
            </span>
          </p>
        </div>
      </mat-dialog-content>

      <mat-dialog-actions
        align="end"
        class="tw-p-4 tw-border-t tw-border-rebel-yellow/30"
      >
        <button mat-button mat-dialog-close class="tw-text-rebel-yellow">
          CLOSE
        </button>
      </mat-dialog-actions>
    </div>
  `,
  styles: [
    `
      ::ng-deep .imperial-dialog {
        .mat-mdc-dialog-container {
          padding: 0;
          border-radius: 4px;
          overflow: hidden;
        }
      }
    `,
  ],
})
export class CharacterDetailComponent {
  constructor(@Inject(MAT_DIALOG_DATA) public data: { character: Character }) {}
}

Conclusion: Balancing Style and Performance

In this transmission, we've accomplished two critical objectives for our Galactic Archives application:

  1. Star Wars Theming: We've transformed our generic Angular Material application into an immersive Star Wars experience through custom theming, specialized typography, animated transitions, and space-inspired visual elements.

  2. Bundle Size Optimization: We've implemented multiple strategies to keep our application performant while adding rich styling features:

    • Conditional loading of development tools like MSW
    • Font loading optimization
    • Centralized Material module imports
    • Improved RxJS subscription management with the takeUntil pattern
    • Realistic bundle budgets in angular.json
  3. Test Compatibility: We've ensured our styling changes don't break existing tests:

    • Updated unit tests to expect uppercase title text ("GALACTIC ARCHIVES" instead of "Galactic Archives")
    • Added proper data-testid attributes to styled components for reliable e2e test selectors
    • Ensured Playwright tests can locate elements with the new styling

Test Fixes for Styling Changes

When implementing UI changes, we need to ensure our tests remain compatible. Here are the key fixes we made:

// app.component.spec.ts - Updated title case expectation
it("should have the correct title", () => {
  const fixture = TestBed.createComponent(AppComponent);
  fixture.detectChanges();
  const compiled = fixture.nativeElement as HTMLElement;
  expect(compiled.querySelector("h1")?.textContent).toContain(
    "GALACTIC ARCHIVES"
  );
});
// e2e/app.spec.ts - Updated e2e test for title case
test("has title", async ({ page }) => {
  await page.goto("/");

  // Wait for the title to be visible
  await expect(page.locator("h1")).toContainText("GALACTIC ARCHIVES");
});
<!-- character-list.component.html - Added data-testid attribute -->
<h1
  class="tw-text-2xl tw-text-yellow-400 tw-mb-0 tw-tracking-widest"
  id="database-title"
  data-testid="character-list-heading"
>
  GALACTIC PERSONNEL DATABASE
</h1>

The result is a visually compelling application that maintains excellent performance metrics. Our initial bundle size is now 850.72 kB, which is well within our adjusted budget of 850 kB warning threshold.

Remember that optimization is an ongoing process. As your application grows, continue monitoring bundle size and implementing additional optimizations as needed. Tools like source-map-explorer or webpack-bundle-analyzer can provide deeper insights into what's contributing to your bundle size.

May the Force be with your Angular applications, and may they always load quickly!

This creates a dialog that looks like an Imperial terminal display.

Custom Scrollbar Styling

Let's add custom scrollbars to enhance the futuristic feel:

// Add to global styles.scss
* {
  scrollbar-width: thin;
  scrollbar-color: rgba(255, 193, 7, 0.5) rgba(0, 0, 0, 0.2);
}

*::-webkit-scrollbar {
  width: 8px;
  height: 8px;
}

*::-webkit-scrollbar-track {
  background: rgba(0, 0, 0, 0.2);
}

*::-webkit-scrollbar-thumb {
  background-color: rgba(255, 193, 7, 0.5);
  border-radius: 4px;
}

This creates thin, yellow scrollbars that match our theme.

Typography Enhancements

Let's add some Star Wars-inspired typography:

// Add to global styles.scss
@font-face {
  font-family: 'SF Distant Galaxy';
  src: url('/assets/fonts/SFDistantGalaxy.woff2') format('woff2');
  font-weight: normal;
  font-style: normal;
  font-display: swap;
}

.sw-heading {
  font-family: 'SF Distant Galaxy', sans-serif;
  letter-spacing: 0.05em;
}

And apply it to our headings:

<h1
  class="tw-text-rebel-yellow tw-text-4xl tw-font-bold tw-tracking-wider tw-mb-2 sw-heading"
>
  GALACTIC ARCHIVES
</h1>

Cosmic Compiler Summary

  • We've created a custom theme inspired by Star Wars
  • We've added animated transitions for a more dynamic interface
  • We've implemented custom loading indicators with a Star Wars aesthetic
  • We've enhanced the character detail view with an Imperial terminal design
  • We've added visual polish with custom scrollbars, typography, and backgrounds

The Cosmic Compiler gazed upon our themed application with what could only be described as aesthetic appreciation. "Visual design," it observed, "is not merely decoration, but communication. Your interface now speaks the language of Star Wars, creating an immersive experience that transcends mere functionality." Several junior developers exchanged glances, surprised by the Compiler's unexpected foray into design philosophy.

Next Steps

With theming and visual polish in place, our Galactic Archives have transformed from a functional data table into an immersive Star Wars experience. In our next and final transmission, we'll deploy our application to production, optimize it for performance, and reflect on our journey through the Angular galaxy.

Until then, may your builds be green and your runtime errors few.