Migrate components

This commit is contained in:
Beau Findlay
2026-01-31 15:51:29 +00:00
parent ee136857d1
commit 2105d3b85d
32 changed files with 2003 additions and 692 deletions

View File

@@ -7,4 +7,8 @@
<BlazorDisableThrowNavigationException>true</BlazorDisableThrowNavigationException>
</PropertyGroup>
<ItemGroup>
<_ContentIncludedByDefault Remove="Components\Shared\Icon.razor"/>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,25 @@
@* External link component *@
<a href="@Href"
target="@Target"
class="@CombinedClasses">@ChildContent</a>
@code {
[Parameter]
public RenderFragment? ChildContent { get; set; }
[Parameter]
public string? Href { get; set; }
[Parameter]
public string Target { get; set; } = "_blank";
[Parameter]
public string? CssClass { get; set; }
private string CombinedClasses => string.IsNullOrEmpty(CssClass)
? "underline underline-offset-2"
: $"underline underline-offset-2 {CssClass}";
}

View File

@@ -2,19 +2,66 @@
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<base href="/" />
<ResourcePreloader />
<link rel="stylesheet" href="css/app.css" />
<ImportMap />
<link rel="icon" type="image/png" href="favicon.png" />
<HeadOutlet />
<meta charset="utf-8"/>
<meta name="viewport"
content="width=device-width, initial-scale=1.0"/>
<base href="/"/>
<ResourcePreloader/>
<!-- Styles -->
<link rel="stylesheet"
href="css/app.css"/>
<ImportMap/>
<!-- Standard Favicons -->
<link rel="icon"
type="image/x-icon"
href="images/favicon.ico"/>
<link rel="icon"
type="image/png"
sizes="16x16"
href="images/favicon-16x16.png"/>
<link rel="icon"
type="image/png"
sizes="32x32"
href="images/favicon-32x32.png"/>
<!-- Apple Touch Icons -->
<link rel="apple-touch-icon"
sizes="180x180"
href="images/apple-touch-icon.png"/>
<!-- Android/Chrome -->
<link rel="manifest"
href="site.webmanifest"/>
<!-- Microsoft Tiles -->
<meta name="msapplication-TileColor"
content="#1a1a1a"/>
<meta name="msapplication-config"
content="browserconfig.xml"/>
<!-- Theme Color -->
<meta name="theme-color"
content="#1a1a1a"/>
<!-- Open Graph / Social Sharing -->
<meta property="og:image"
content="images/og-image.png"/>
<meta property="og:image:width"
content="1200"/>
<meta property="og:image:height"
content="630"/>
<HeadOutlet/>
</head>
<body>
<Routes />
<script src="@Assets["_framework/blazor.web.js"]"></script>
<Routes/>
<script src="@Assets["_framework/blazor.web.js"]"></script>
</body>
</html>

View File

@@ -0,0 +1,10 @@
@* Contact call-to-action *@
<div class="contact-section">
<Text>If you think I can help with your project...</Text>
<a href="mailto:me@beaufindlay.com"
class="contact-button">
Get in touch
<Icon Type="IconType.SendArrow"/>
</a>
</div>

View File

@@ -0,0 +1,125 @@
@switch (Type)
{
case IconType.Github:
<svg viewBox="0 0 24 24"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg">
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
</svg>
break;
case IconType.LinkedIn:
<svg viewBox="0 0 24 24"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg">
<path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/>
</svg>
break;
case IconType.Email:
<svg xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor">
<path stroke-linecap="round"
stroke-linejoin="round"
d="M21.75 6.75v10.5a2.25 2.25 0 01-2.25 2.25h-15a2.25 2.25 0 01-2.25-2.25V6.75m19.5 0A2.25 2.25 0 0019.5 4.5h-15a2.25 2.25 0 00-2.25 2.25m19.5 0v.243a2.25 2.25 0 01-1.07 1.916l-7.5 4.615a2.25 2.25 0 01-2.36 0L3.32 8.91a2.25 2.25 0 01-1.07-1.916V6.75"/>
</svg>
break;
case IconType.Code:
<svg viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="1.5"
xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round"
stroke-linejoin="round"
d="M17.25 6.75L22.5 12l-5.25 5.25m-10.5 0L1.5 12l5.25-5.25m7.5-3l-4.5 16.5"/>
</svg>
break;
case IconType.Hosting:
<svg viewBox="0 0 24 24"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg">
<path d="M3 3h18v18H3V3zm16 16V5H5v14h14zm-8-2h2v-2h2v-2h-2v-2h2V9h-2V7h-2v2H9v2h2v2H9v2h2v2z"/>
</svg>
break;
case IconType.Blazor:
<svg viewBox="0 0 24 24"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg">
<path d="M15.5 2.5c-2.5 0-4.5 2-4.5 4.5v2.5l-2.5 2.5c-1.5 1.5-1.5 4 0 5.5s4 1.5 5.5 0L16.5 15V12.5c0-2.5 2-4.5 4.5-4.5V6c-2.5 0-4.5-2-4.5-4.5h-1zm-3 7.5v2l-1.5 1.5c-.8.8-2.2.8-3 0s-.8-2.2 0-3L9.5 9h3zm3-5c.8 0 1.5.7 1.5 1.5s-.7 1.5-1.5 1.5-1.5-.7-1.5-1.5.7-1.5 1.5-1.5z"/>
</svg>
break;
case IconType.React:
<svg viewBox="0 0 24 24"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg">
<circle cx="12"
cy="12"
r="2"/>
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8z"/>
<ellipse cx="12"
cy="12"
rx="8"
ry="3"
fill="none"
stroke="currentColor"
stroke-width="1"/>
<ellipse cx="12"
cy="12"
rx="3"
ry="8"
fill="none"
stroke="currentColor"
stroke-width="1"/>
</svg>
break;
case IconType.Database:
<svg viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="1.5"
xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round"
stroke-linejoin="round"
d="M20.25 6.375c0 2.278-3.694 4.125-8.25 4.125S3.75 8.653 3.75 6.375m16.5 0c0-2.278-3.694-4.125-8.25-4.125S3.75 4.097 3.75 6.375m16.5 0v11.25c0 2.278-3.694 4.125-8.25 4.125s-8.25-1.847-8.25-4.125V6.375m16.5 0v3.75m-16.5-3.75v3.75m16.5 0v3.75C20.25 16.153 16.556 18 12 18s-8.25-1.847-8.25-4.125v-3.75"/>
</svg>
break;
case IconType.Docker:
<svg viewBox="0 0 24 24"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg">
<path d="M13 2v7h7c0-3.87-3.13-7-7-7zm-2 0C7.13 2 4 5.13 4 9h7V2zM4 11c0 3.87 3.13 7 7 7v-7H4zm9 7c3.87 0 7-3.13 7-7h-7v7z"/>
</svg>
break;
case IconType.SendArrow:
<svg xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor">
<path stroke-linecap="round"
stroke-linejoin="round"
d="M6 12L3.269 3.126A59.768 59.768 0 0121.485 12 59.77 59.77 0 013.27 20.876L5.999 12zm0 0h7.5"/>
</svg>
break;
case IconType.Menu:
<svg xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
aria-hidden="true">
<path stroke-linecap="round"
stroke-linejoin="round"
d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5"/>
</svg>
break;
}
@code {
[Parameter]
public IconType Type { get; set; }
}

View File

@@ -0,0 +1,16 @@
namespace BlazorApp.Components.Icons;
public enum IconType
{
Github,
LinkedIn,
Email,
Code,
Hosting,
Blazor,
React,
Database,
Docker,
SendArrow,
Menu
}

View File

@@ -0,0 +1,19 @@
@* Social media links *@
<div class="social-icons-container">
<a href="https://github.com/bdfin"
class="social-icon-link">
<span class="sr-only">GitHub</span>
<Icon Type="IconType.Github"/>
</a>
<a href="https://www.linkedin.com/in/beau-findlay/"
class="social-icon-link">
<span class="sr-only">LinkedIn</span>
<Icon Type="IconType.LinkedIn"/>
</a>
<a href="mailto:me@beaufindlay.com"
class="social-icon-link">
<span class="sr-only">Email</span>
<Icon Type="IconType.Email"/>
</a>
</div>

View File

@@ -0,0 +1,14 @@
@* Footer component *@
<div class="page-footer">
<footer>
<div class="footer-container">
<div class="footer-content">
<SocialIcons/>
<p class="footer-text">
&copy; @DateTime.Now.Year Beau Findlay. All rights reserved.
</p>
</div>
</div>
</footer>
</div>

View File

@@ -1,17 +1,11 @@
@inherits LayoutComponentBase
<div class="page">
<div class="sidebar">
<NavMenu />
</div>
<main>
<div class="top-row px-4">
<a href="https://learn.microsoft.com/aspnet/core/" target="_blank">About</a>
</div>
<article class="content px-4">
<div class="px-6 lg:px-10">
<div class="flex flex-col min-h-screen mx-auto max-w-7xl fade-in">
<NavBar/>
<div class="flex-1 py-8">
@Body
</article>
</main>
</div>
<Footer/>
</div>
</div>

View File

@@ -0,0 +1,70 @@
@* Navigation bar with mobile menu *@
<div class="navbar">
<header>
<nav aria-label="Global">
<div class="logo-container">
<a href="/"
class="logo-link">
<span class="sr-only">Beau Findlay</span>
<img src="images/logo.webp"
alt="Logo"/>
</a>
</div>
<div class="mobile-menu-button-container">
<label for="mobile-menu-toggle"
class="menu-button">
<span class="sr-only">Open main menu</span>
<Icon Type="IconType.Menu"/>
</label>
</div>
<div class="desktop-nav">
<NavLink href="/experience">Experience</NavLink>
<NavLink href="/about">This app</NavLink>
</div>
</nav>
@* Mobile menu using CSS checkbox hack *@
<input type="checkbox"
id="mobile-menu-toggle"
class="menu-toggle"/>
<div class="mobile-menu-overlay"></div>
<div class="mobile-menu-content">
<div class="mobile-menu-inner">
<div class="mobile-menu-header">
<a href="/"
class="logo-link">
<span class="sr-only">Beau Findlay</span>
<img src="images/logo.webp"
alt="Logo"/>
</a>
<label for="mobile-menu-toggle"
class="close-button">
<span class="sr-only">Close menu</span>
<svg xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
aria-hidden="true">
<path stroke-linecap="round"
stroke-linejoin="round"
d="M6 18L18 6M6 6l12 12"/>
</svg>
</label>
</div>
<div class="mobile-menu-body">
<div class="mobile-nav-links">
<NavLink href="/experience">Experience</NavLink>
<NavLink href="/about">This App</NavLink>
</div>
<div class="mobile-social-divider">
<div class="mobile-social-container">
<SocialIcons/>
</div>
</div>
</div>
</div>
</div>
</header>
</div>

View File

@@ -1,22 +1,31 @@
<div class="top-row ps-3 navbar navbar-dark">
<div class="container-fluid">
<a class="navbar-brand" href="">BlazorApp</a>
<a class="navbar-brand"
href="">BlazorApp</a>
</div>
</div>
<input type="checkbox" title="Navigation menu" class="navbar-toggler" />
<input type="checkbox"
title="Navigation menu"
class="navbar-toggler"/>
<div class="nav-scrollable" onclick="document.querySelector('.navbar-toggler').click()">
<div class="nav-scrollable"
onclick="document.querySelector('.navbar-toggler').click()">
<nav class="nav flex-column">
<div class="nav-item px-3">
<NavLink class="nav-link" href="" Match="NavLinkMatch.All">
<span class="bi bi-house-door-fill-nav-menu" aria-hidden="true"></span> Home
<NavLink class="nav-link"
href=""
Match="NavLinkMatch.All">
<span class="bi bi-house-door-fill-nav-menu"
aria-hidden="true"></span> Home
</NavLink>
</div>
<div class="nav-item px-3">
<NavLink class="nav-link" href="weather">
<span class="bi bi-list-nested-nav-menu" aria-hidden="true"></span> Weather
<NavLink class="nav-link"
href="weather">
<span class="bi bi-list-nested-nav-menu"
aria-hidden="true"></span> Weather
</NavLink>
</div>
</nav>

View File

@@ -0,0 +1,41 @@
@page "/about"
<PageTitle>Beau Findlay - About</PageTitle>
<Title CssClass="text-center pb-4">This App</Title>
<Text>
Below is an overview of how this simple app is made and what
technologies are used. If you'd like to dive straight in, the full
project is available on my
<AnchorLink Href="https://github.com/bdfin/my-portfolio">
GitHub
</AnchorLink>.
</Text>
<section>
<Subtitle>App</Subtitle>
<Text>
This app was originally made using
<AnchorLink Href="https://dotnet.microsoft.com/en-us/apps/aspnet/web-apps/blazor">.NET Blazor WASM</AnchorLink>
and then re-written in
<AnchorLink Href="https://react.dev/">React</AnchorLink>
with
<AnchorLink Href="https://www.typescriptlang.org/">TypeScript</AnchorLink>
as a learning exercise. I've now migrated it back to .NET Blazor to take advantage of static server-side
rendering for maximum performance and to remove unnecessary dependencies on large JS and CSS libraries.
</Text>
<Text>
This version uses pure vanilla CSS with CSS variables for theming, eliminating all JavaScript and external
dependencies; the mobile menu uses a CSS checkbox hack for zero-JavaScript interactivity.
</Text>
</section>
<section class="mt-8">
<Subtitle>Hosting & Deployment</Subtitle>
<Text>
TODO: This section
</Text>
</section>

View File

@@ -15,22 +15,28 @@
<h3>Development Mode</h3>
<p>
Swapping to <strong>Development</strong> environment will display more detailed information about the error that occurred.
Swapping to <strong>Development</strong> environment will display more detailed information about the error that
occurred.
</p>
<p>
<strong>The Development environment shouldn't be enabled for deployed applications.</strong>
It can result in displaying sensitive information from exceptions to end users.
For local debugging, enable the <strong>Development</strong> environment by setting the <strong>ASPNETCORE_ENVIRONMENT</strong> environment variable to <strong>Development</strong>
For local debugging, enable the <strong>Development</strong> environment by setting the <strong>ASPNETCORE_ENVIRONMENT</strong>
environment variable to <strong>Development</strong>
and restarting the app.
</p>
@code{
[CascadingParameter]
private HttpContext? HttpContext { get; set; }
private string? RequestId { get; set; }
private bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
protected override void OnInitialized() =>
protected override void OnInitialized()
{
RequestId = Activity.Current?.Id ?? HttpContext?.TraceIdentifier;
}
}

View File

@@ -0,0 +1,107 @@
@page "/experience"
<PageTitle>Beau Findlay - Experience</PageTitle>
<Title CssClass="text-center">Experience</Title>
<p class="text-center text-xl font-semibold mb-10 ">
Software Engineer since 2018
</p>
<ol class="timeline">
@foreach (var item in experienceTimelineItems)
{
<li class="timeline-item">
<time class="timeline-date">
@item.StartDate - @(item.EndDate ?? "Present")
</time>
<h3 class="timeline-title">
@item.Title @("@") <AnchorLink Href="@item.CompanyUrl">@item.CompanyName</AnchorLink>
</h3>
@foreach (var content in item.Content)
{
<Text>@content</Text>
}
</li>
}
</ol>
<Contact/>
@code {
private readonly List<WorkTimelineItem> experienceTimelineItems =
[
new()
{
StartDate = "September 2021",
Title = "CTO",
CompanyName = "un:hurd music",
CompanyUrl = "https://unhurdmusic.com",
Content =
[
"As one of the founding developers at un:hurd music and now Chief Technology Officer, I built and scaled un:hurd's back-end and cloud infrastructure that serves automated marketing soloutions for tens-of-thousands of artists and musicians.",
"I lead a small but incredibly talented multi-disciplinary team building on the Azure cloud using a .NET backend, React web front-end and a Swift native iOS app."
]
},
new()
{
StartDate = "August 2020",
EndDate = "September 2021",
Title = "Software Development Lead",
CompanyName = "Vouch",
CompanyUrl = "https://vouch.co.uk/",
Content =
[
"At Vouch I lead the backend build of a new version of their tenant referencing software - an AI enhanced chat-bot based system utlising Azure Cognitive Services and various supporting serverless APIs written in .NET Core and hosted on Microsoft Azure."
]
},
new()
{
StartDate = "May 2020",
EndDate = "July 2020",
Title = "Software Developer",
CompanyName = "Paragon ID",
CompanyUrl = "https://www.paragon-id.com/en",
Content =
[
"I joined Paragon ID on a short-term contract where I wrote and deployed two key projects: A complex dashboard for a large construction equipment manufacturer to track assets across various manufacturing stages and a medical assets tracking dashboard deployed and used in multiple hospitals across the UK."
]
},
new()
{
StartDate = "July 2019",
EndDate = "May 2020",
Title = "Software Developer",
CompanyName = "Osborne Technologies",
CompanyUrl = "https://www.osbornetechnologies.co.uk/",
Content =
[
"I joined Osborne Technologies as the only cloud cloud-specialist and lead a project creating the first web-based version of their flag ship visitor management software utilising ASP.NET Core MVC and Microsoft SQL Server on the Microsoft Azure cloud."
]
},
new()
{
StartDate = "September 2018",
EndDate = "September 2019",
Title = " MSc Computing Student",
CompanyName = "Sheffield Hallam University",
CompanyUrl = "https://www.shu.ac.uk/courses/computing/msc-computing/full-time",
Content =
[
"I joined Sheffield Hallam University to study for a Master of Science in Computing. During my time there I completed modules in computer programming and web development, databases and big data, computer hardware, project management and my software development thesis; a .NET web application that compiles astronomy and space exploration data from various APIs into an accessible calendar."
]
}
];
private class WorkTimelineItem
{
public string StartDate { get; init; } = string.Empty;
public string? EndDate { get; init; }
public string Title { get; init; } = string.Empty;
public string CompanyName { get; init; } = string.Empty;
public string CompanyUrl { get; init; } = string.Empty;
public string[] Content { get; init; } = [];
}
}

View File

@@ -1,7 +1,18 @@
@page "/"
<PageTitle>Home</PageTitle>
<PageTitle>Beau Findlay - Home</PageTitle>
<h1>Hello, world!</h1>
Welcome to your new app.
<Title CssClass="text-center mb-8">Hi, I'm Beau.</Title>
<Text>
I'm a UK-based software engineer and I love building cool stuff.
</Text>
<Text>
I specialise in C#/.NET development and I've built systems that scale for hundreds-of-thousands of global users.
</Text>
<Text>
I've worked with businesses at all sizes and stages and I'm currently heading up the tech as CTO at a cool startup called
<AnchorLink Href="https://unhurdmusic.com">un:hurd music</AnchorLink>.
</Text>
<Text>
I believe in a privacy-first, information-focussed and performant internet. You won't find any trackers, analytics or the need for a cookie consent policy here.
</Text>

View File

@@ -1,5 +1,13 @@
@page "/not-found"
@layout MainLayout
<h3>Not Found</h3>
<p>Sorry, the content you are looking for does not exist.</p>
<PageTitle>Beau Findlay - Not Found</PageTitle>
<main class="grid min-h-full place-items-center px-6 py-24 sm:py-32 lg:px-8">
<div class="text-center">
<p class="text-base font-semibold">404</p>
<h1 class="mt-4 text-4xl font-bold tracking-tight">
Page not found
</h1>
<p class="mt-6 text-base leading-7">Sorry, this page doesn't exist.</p>
</div>
</main>

View File

@@ -1,64 +0,0 @@
@page "/weather"
@attribute [StreamRendering]
<PageTitle>Weather</PageTitle>
<h1>Weather</h1>
<p>This component demonstrates showing data.</p>
@if (forecasts == null)
{
<p><em>Loading...</em></p>
}
else
{
<table class="table">
<thead>
<tr>
<th>Date</th>
<th aria-label="Temperature in Celsius">Temp. (C)</th>
<th aria-label="Temperature in Fahrenheit">Temp. (F)</th>
<th>Summary</th>
</tr>
</thead>
<tbody>
@foreach (var forecast in forecasts)
{
<tr>
<td>@forecast.Date.ToShortDateString()</td>
<td>@forecast.TemperatureC</td>
<td>@forecast.TemperatureF</td>
<td>@forecast.Summary</td>
</tr>
}
</tbody>
</table>
}
@code {
private WeatherForecast[]? forecasts;
protected override async Task OnInitializedAsync()
{
// Simulate asynchronous loading to demonstrate streaming rendering
await Task.Delay(500);
var startDate = DateOnly.FromDateTime(DateTime.Now);
var summaries = new[] { "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" };
forecasts = Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
Date = startDate.AddDays(index),
TemperatureC = Random.Shared.Next(-20, 55),
Summary = summaries[Random.Shared.Next(summaries.Length)]
}).ToArray();
}
private class WeatherForecast
{
public DateOnly Date { get; set; }
public int TemperatureC { get; set; }
public string? Summary { get; set; }
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}
}

View File

@@ -1,6 +1,8 @@
<Router AppAssembly="typeof(Program).Assembly" NotFoundPage="typeof(Pages.NotFound)">
@using BlazorApp.Components.Pages
<Router AppAssembly="typeof(Program).Assembly"
NotFoundPage="typeof(NotFound)">
<Found Context="routeData">
<RouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)" />
<FocusOnNavigate RouteData="routeData" Selector="h1" />
<RouteView RouteData="routeData"
DefaultLayout="typeof(MainLayout)"/>
</Found>
</Router>

View File

@@ -0,0 +1,17 @@
@* H2 heading component *@
<h2 class="@CombinedClasses">@ChildContent</h2>
@code {
[Parameter]
public RenderFragment? ChildContent { get; set; }
[Parameter]
public string? CssClass { get; set; }
private string CombinedClasses => string.IsNullOrEmpty(CssClass)
? "flex items-center text-2xl py-4 font-semibold"
: $"flex items-center text-2xl py-4 font-semibold {CssClass}";
}

View File

@@ -0,0 +1,17 @@
@* Paragraph component *@
<p class="@CombinedClasses">@ChildContent</p>
@code {
[Parameter]
public RenderFragment? ChildContent { get; set; }
[Parameter]
public string? CssClass { get; set; }
private string CombinedClasses => string.IsNullOrEmpty(CssClass)
? "text-paragraph"
: $"text-paragraph {CssClass}";
}

View File

@@ -0,0 +1,17 @@
@* H1 heading component *@
<h1 class="@CombinedClasses">@ChildContent</h1>
@code {
[Parameter]
public RenderFragment? ChildContent { get; set; }
[Parameter]
public string? CssClass { get; set; }
private string CombinedClasses => string.IsNullOrEmpty(CssClass)
? "text-4xl py-4"
: $"text-4xl py-4 {CssClass}";
}

View File

@@ -9,3 +9,9 @@
@using BlazorApp
@using BlazorApp.Components
@using BlazorApp.Components.Layout
@using BlazorApp.Components.Icons
@using BlazorApp.Components.AnchorLink
@using BlazorApp.Components.Contact
@using BlazorApp.Components.Typography.Title
@using BlazorApp.Components.Typography.Subtitle
@using BlazorApp.Components.Typography.Text

View File

@@ -10,10 +10,11 @@ var app = builder.Build();
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error", createScopeForErrors: true);
app.UseExceptionHandler("/Error", true);
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseStatusCodePagesWithReExecute("/not-found", createScopeForStatusCodePages: true);
app.UseHttpsRedirection();

View File

@@ -20,4 +20,4 @@
}
}
}
}
}

View File

@@ -3,6 +3,7 @@
@import url('variables.css');
@import url('reset.css');
@import url('base.css');
@import url('utilities.css');
@import url('layout.css');
@import url('components.css');
@import url('animations.css');

View File

@@ -6,6 +6,8 @@ body {
font-family: var(--font-mono);
font-size: var(--font-size-base);
line-height: var(--line-height-normal);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
/* Typography */
@@ -38,11 +40,13 @@ p {
/* Links */
a {
color: var(--color-slate-50);
transition: color var(--transition-base) var(--transition-timing);
display: inline-block;
transition: transform var(--transition-base) var(--transition-timing);
}
a:hover {
color: var(--color-slate-300);
transform: translateY(2px);
color: var(--color-slate-50);
}
a:focus-visible {

View File

@@ -99,52 +99,66 @@
border: 1px solid var(--color-slate-800);
}
/* Navigation */
.nav {
/* NavBar */
.navbar header {
padding-top: var(--space-6);
}
.navbar nav {
margin-left: auto;
margin-right: auto;
display: flex;
max-width: 80rem;
align-items: center;
justify-content: space-between;
padding: var(--space-6) 0;
}
.nav-link {
padding: var(--space-2) var(--space-4);
color: var(--color-slate-50);
font-size: var(--font-size-sm);
transition: color var(--transition-base) var(--transition-timing);
.navbar .logo-container {
display: flex;
}
.nav-link:hover {
color: var(--color-slate-300);
.navbar .logo-link {
margin: -0.375rem;
padding: 0.375rem;
}
.nav-link-active {
font-weight: var(--font-weight-semibold);
.navbar .logo-link img {
height: 4rem;
width: auto;
}
/* Footer */
.footer {
padding: var(--space-8) 0;
margin-top: auto;
border-top: 1px solid var(--color-slate-800);
text-align: center;
font-size: var(--font-size-sm);
color: var(--color-slate-400);
.navbar .mobile-menu-button-container {
display: flex;
}
/* Mobile Menu (CSS-only) */
.mobile-menu-toggle {
.navbar .menu-button {
margin: -0.625rem;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: var(--radius-md);
padding: 0.625rem;
cursor: pointer;
}
.navbar .menu-button svg {
height: 1.5rem;
width: 1.5rem;
}
.navbar .desktop-nav {
display: none;
}
.mobile-menu-button {
display: flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
cursor: pointer;
font-size: var(--font-size-2xl);
.navbar .desktop-nav a {
font-size: var(--font-size-sm);
font-weight: var(--font-weight-semibold);
line-height: 1.5;
}
/* Mobile Menu (CSS-only) */
.menu-toggle {
display: none;
}
.mobile-menu-overlay {
@@ -156,39 +170,316 @@
z-index: var(--z-overlay);
}
.mobile-menu {
.menu-toggle:checked ~ .mobile-menu-overlay {
display: block;
}
.mobile-menu-content {
display: none;
position: fixed;
top: 0;
right: 0;
bottom: 0;
width: 80%;
max-width: 300px;
background-color: var(--color-black);
border-left: 1px solid var(--color-slate-800);
padding: var(--space-6);
left: 0;
z-index: var(--z-modal);
background-color: var(--color-black);
width: 100%;
padding: var(--space-6);
color: white;
transform: translateX(100%);
transition: transform var(--transition-slow) var(--transition-timing);
}
.mobile-menu-toggle:checked ~ .mobile-menu-overlay {
display: block;
}
.mobile-menu-toggle:checked ~ .mobile-menu {
.menu-toggle:checked ~ .mobile-menu-content {
display: block;
transform: translateX(0);
}
.mobile-menu-close {
.mobile-menu-inner {
display: flex;
justify-content: flex-end;
margin-bottom: var(--space-6);
font-size: var(--font-size-2xl);
flex-direction: column;
height: 100%;
}
.mobile-menu-header {
display: flex;
align-items: center;
justify-content: space-between;
}
.close-button {
margin: -0.625rem;
border-radius: var(--radius-md);
padding: 0.625rem;
cursor: pointer;
}
.close-button svg {
height: 1.5rem;
width: 1.5rem;
}
.mobile-menu-body {
margin-top: var(--space-6);
flex: 1 1 0%;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.mobile-nav-links {
margin-top: var(--space-2);
}
.mobile-nav-links a {
margin-left: -0.75rem;
margin-right: -0.75rem;
display: block;
border-radius: var(--radius-lg);
padding: 0.75rem;
padding-top: var(--space-2);
padding-bottom: var(--space-2);
font-size: var(--font-size-base);
font-weight: var(--font-weight-semibold);
line-height: 1.75;
}
.mobile-social-divider {
border-top: 1px solid rgba(226, 232, 240, 0.1);
padding-top: var(--space-8);
}
.mobile-social-container {
display: flex;
justify-content: center;
align-items: center;
}
/* Prevent body scroll when mobile menu is open */
.menu-toggle:checked ~ * {
overflow: hidden;
}
body:has(.menu-toggle:checked) {
overflow: hidden;
}
@media (min-width: 640px) {
.mobile-menu-content {
max-width: 24rem;
box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.1);
border-left: 2px solid var(--color-slate-800);
}
}
@media (min-width: 1024px) {
.navbar .logo-container {
flex: 1 1 0%;
}
.navbar .mobile-menu-button-container {
display: none;
}
.navbar .desktop-nav {
display: flex;
column-gap: var(--space-12);
}
.mobile-menu-content {
display: none;
}
}
/* Footer */
.page-footer footer {
margin-top: auto;
}
.page-footer .footer-container {
margin-left: auto;
margin-right: auto;
padding-top: var(--space-8);
padding-bottom: var(--space-8);
}
.page-footer .footer-content {
display: flex;
flex-direction: column;
align-items: center;
gap: var(--space-4);
}
.page-footer .footer-text {
font-size: var(--font-size-xs);
line-height: 1.25;
color: var(--color-slate-50);
margin-bottom: 0;
}
@media (min-width: 768px) {
.page-footer .footer-content {
flex-direction: row;
align-items: center;
justify-content: space-between;
gap: 0;
}
.page-footer .footer-text {
order: 1;
}
}
/* Social Icons */
.social-icons-container {
display: flex;
gap: var(--space-6);
}
.social-icon-link {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 40px;
min-height: 40px;
color: var(--color-slate-50);
transition: transform var(--transition-base) var(--transition-timing);
}
.social-icon-link svg {
width: 24px;
height: 24px;
}
.social-icon-link:hover {
transform: translateY(2px);
color: var(--color-slate-50);
}
@media (min-width: 768px) {
.social-icons-container {
order: 2;
}
.social-icon-link {
min-width: auto;
min-height: auto;
}
.social-icon-link svg {
width: 20px;
height: 20px;
}
}
/* Tech Icons */
.tech-icons-wrapper {
margin-left: auto;
margin-right: auto;
max-width: 64rem;
}
.tech-icons-title {
font-size: var(--font-size-xl);
text-align: center;
margin-bottom: var(--space-10);
font-weight: var(--font-weight-semibold);
}
.tech-icons-grid {
display: flex;
flex-direction: column;
gap: var(--space-6);
text-align: center;
margin-left: auto;
margin-right: auto;
margin-top: var(--space-4);
}
.tech-icon-item {
display: flex;
flex-direction: column;
align-items: center;
}
.tech-icon-item svg {
width: 34px;
height: 34px;
}
.tech-icon-item p {
margin-top: var(--space-2);
font-size: var(--font-size-sm);
}
@media (min-width: 768px) {
.tech-icons-grid {
flex-direction: row;
justify-content: space-evenly;
gap: 0;
}
}
/* AboutTabs */
.tabs-subtitle {
font-weight: var(--font-weight-bold);
font-size: var(--font-size-lg);
margin-top: var(--space-4);
margin-bottom: var(--space-4);
}
/* Contact Section */
.contact-section {
margin-bottom: var(--space-10);
margin-top: var(--space-12);
text-align: center;
}
.contact-button {
display: inline-flex;
align-items: center;
justify-content: center;
gap: var(--space-2);
border: 1px solid var(--color-slate-700);
background-color: var(--color-black);
padding: 0.625rem 0.875rem;
margin-top: var(--space-2);
font-size: var(--font-size-sm);
font-weight: var(--font-weight-semibold);
color: var(--color-slate-50);
transition: transform var(--transition-base) var(--transition-timing);
}
.contact-button:hover {
transform: translateY(2px);
color: var(--color-slate-50);
}
.contact-button:focus-visible {
outline: 2px solid var(--color-slate-50);
outline-offset: 2px;
}
.contact-button svg {
width: 16px;
height: 16px;
flex-shrink: 0;
}
/* Text Paragraph */
.text-paragraph {
font-size: var(--font-size-lg);
padding-top: var(--space-4);
padding-bottom: var(--space-4);
}
@media (min-width: 768px) {
.text-paragraph {
padding-top: var(--space-2);
padding-bottom: var(--space-2);
}
}
/* Links */
.link {
color: var(--color-slate-50);
@@ -212,3 +503,97 @@
.icon-button:hover {
opacity: 0.7;
}
/* Timeline */
.timeline {
position: relative;
border-left: 1px solid var(--color-slate-600);
list-style: none;
padding-left: 0;
}
.timeline-item {
margin-bottom: var(--space-10);
margin-left: var(--space-4);
position: relative;
}
.timeline-marker {
position: absolute;
width: 0.75rem;
height: 0.75rem;
background-color: var(--color-slate-600);
border-radius: 50%;
left: -1.875rem;
top: 0.375rem;
border: 2px solid var(--color-black);
}
.timeline-date {
display: block;
margin-bottom: var(--space-1);
font-size: var(--font-size-sm);
font-weight: var(--font-weight-normal);
line-height: 1;
color: var(--color-slate-400);
}
.timeline-title {
font-size: var(--font-size-2xl);
font-weight: var(--font-weight-semibold);
color: var(--color-slate-50);
margin-bottom: var(--space-2);
}
/* Tabs (CSS Radio Buttons) */
.tabs-container {
margin-top: var(--space-4);
}
.tab-radio {
display: none;
}
.tab-labels {
display: flex;
gap: var(--space-8);
border-bottom: 1px solid var(--color-slate-700);
margin-bottom: var(--space-10);
overflow-x: auto;
}
.tab-label {
padding: var(--space-6) 0;
font-size: var(--font-size-sm);
font-weight: var(--font-weight-medium);
color: var(--color-slate-400);
border-bottom: 4px solid transparent;
margin-bottom: -1px;
cursor: pointer;
white-space: nowrap;
transition: all var(--transition-base) var(--transition-timing);
}
.tab-label:hover {
color: var(--color-slate-200);
border-bottom-color: var(--color-slate-700);
}
#tab0:checked ~ .tab-labels label[for="tab0"],
#tab1:checked ~ .tab-labels label[for="tab1"],
#tab2:checked ~ .tab-labels label[for="tab2"],
#tab3:checked ~ .tab-labels label[for="tab3"] {
color: var(--color-slate-200);
border-bottom-color: var(--color-slate-300);
}
.tab-panel {
display: none;
}
#tab0:checked ~ .tab-panels #panel0,
#tab1:checked ~ .tab-panels #panel1,
#tab2:checked ~ .tab-panels #panel2,
#tab3:checked ~ .tab-panels #panel3 {
display: block;
}

View File

@@ -16,225 +16,3 @@
padding-right: var(--space-10);
}
}
/* Flexbox */
.flex {
display: flex;
}
.flex-col {
flex-direction: column;
}
.flex-row {
flex-direction: row;
}
.flex-1 {
flex: 1 1 0%;
}
.items-start {
align-items: flex-start;
}
.items-center {
align-items: center;
}
.items-end {
align-items: flex-end;
}
.justify-start {
justify-content: flex-start;
}
.justify-center {
justify-content: center;
}
.justify-end {
justify-content: flex-end;
}
.justify-between {
justify-content: space-between;
}
.gap-1 {
gap: var(--space-1);
}
.gap-2 {
gap: var(--space-2);
}
.gap-3 {
gap: var(--space-3);
}
.gap-4 {
gap: var(--space-4);
}
.gap-6 {
gap: var(--space-6);
}
.gap-8 {
gap: var(--space-8);
}
/* Grid */
.grid {
display: grid;
}
.grid-cols-1 {
grid-template-columns: repeat(1, minmax(0, 1fr));
}
.grid-cols-2 {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.grid-cols-3 {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.grid-cols-4 {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
@media (min-width: 640px) {
.sm\:grid-cols-2 {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.sm\:grid-cols-3 {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
}
@media (min-width: 768px) {
.md\:grid-cols-3 {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.md\:grid-cols-4 {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
}
@media (min-width: 1024px) {
.lg\:grid-cols-4 {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
.lg\:grid-cols-5 {
grid-template-columns: repeat(5, minmax(0, 1fr));
}
}
/* Spacing */
.min-h-screen {
min-height: 100vh;
}
.p-2 { padding: var(--space-2); }
.p-4 { padding: var(--space-4); }
.p-6 { padding: var(--space-6); }
.p-8 { padding: var(--space-8); }
.px-2 { padding-left: var(--space-2); padding-right: var(--space-2); }
.px-4 { padding-left: var(--space-4); padding-right: var(--space-4); }
.px-6 { padding-left: var(--space-6); padding-right: var(--space-6); }
.px-8 { padding-left: var(--space-8); padding-right: var(--space-8); }
.py-2 { padding-top: var(--space-2); padding-bottom: var(--space-2); }
.py-4 { padding-top: var(--space-4); padding-bottom: var(--space-4); }
.py-6 { padding-top: var(--space-6); padding-bottom: var(--space-6); }
.py-8 { padding-top: var(--space-8); padding-bottom: var(--space-8); }
.m-2 { margin: var(--space-2); }
.m-4 { margin: var(--space-4); }
.m-6 { margin: var(--space-6); }
.m-8 { margin: var(--space-8); }
.mx-auto { margin-left: auto; margin-right: auto; }
.mt-2 { margin-top: var(--space-2); }
.mt-4 { margin-top: var(--space-4); }
.mt-6 { margin-top: var(--space-6); }
.mt-8 { margin-top: var(--space-8); }
.mb-2 { margin-bottom: var(--space-2); }
.mb-4 { margin-bottom: var(--space-4); }
.mb-6 { margin-bottom: var(--space-6); }
.mb-8 { margin-bottom: var(--space-8); }
/* Display */
.hidden {
display: none;
}
.block {
display: block;
}
.inline-block {
display: inline-block;
}
/* Responsive */
@media (min-width: 1024px) {
.lg\:flex {
display: flex;
}
.lg\:hidden {
display: none;
}
.lg\:block {
display: block;
}
}
/* Widths */
.w-full {
width: 100%;
}
.max-w-sm {
max-width: 24rem;
}
.max-w-md {
max-width: 28rem;
}
.max-w-lg {
max-width: 32rem;
}
.max-w-xl {
max-width: 36rem;
}
.max-w-2xl {
max-width: 42rem;
}
.max-w-7xl {
max-width: 80rem;
}
/* Text */
.text-center {
text-align: center;
}
.text-left {
text-align: left;
}
.text-right {
text-align: right;
}

View File

@@ -0,0 +1,641 @@
/* Utility Classes */
/* Display */
.hidden {
display: none;
}
.block {
display: block;
}
.inline-block {
display: inline-block;
}
.inline-flex {
display: inline-flex;
}
.flow-root {
display: flow-root;
}
/* Flexbox */
.flex {
display: flex;
}
.flex-col {
flex-direction: column;
}
.flex-row {
flex-direction: row;
}
.flex-1 {
flex: 1 1 0;
}
.items-start {
align-items: flex-start;
}
.items-center {
align-items: center;
}
.items-end {
align-items: flex-end;
}
.justify-start {
justify-content: flex-start;
}
.justify-center {
justify-content: center;
}
.justify-end {
justify-content: flex-end;
}
.justify-between {
justify-content: space-between;
}
/* Gap */
.gap-1 {
gap: var(--space-1);
}
.gap-2 {
gap: var(--space-2);
}
.gap-3 {
gap: var(--space-3);
}
.gap-4 {
gap: var(--space-4);
}
.gap-6 {
gap: var(--space-6);
}
.gap-8 {
gap: var(--space-8);
}
/* Grid */
.grid {
display: grid;
}
.grid-cols-1 {
grid-template-columns: repeat(1, minmax(0, 1fr));
}
.grid-cols-2 {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.grid-cols-3 {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.grid-cols-4 {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
.place-items-center {
place-items: center;
}
/* Spacing */
.min-h-screen {
min-height: 100vh;
}
.min-h-full {
min-height: 100%;
}
.h-full {
height: 100%;
}
.h-6 {
height: 1.5rem;
}
.h-16 {
height: 4rem;
}
.w-6 {
width: 1.5rem;
}
.w-auto {
width: auto;
}
.w-full {
width: 100%;
}
/* Max Widths */
.max-w-sm {
max-width: 24rem;
}
.max-w-md {
max-width: 28rem;
}
.max-w-lg {
max-width: 32rem;
}
.max-w-xl {
max-width: 36rem;
}
.max-w-2xl {
max-width: 42rem;
}
.max-w-4xl {
max-width: 64rem;
}
.max-w-7xl {
max-width: 80rem;
}
/* Padding */
.p-2 {
padding: var(--space-2);
}
.p-4 {
padding: var(--space-4);
}
.p-6 {
padding: var(--space-6);
}
.p-8 {
padding: var(--space-8);
}
.p-1\.5 {
padding: 0.375rem;
}
.p-2\.5 {
padding: 0.625rem;
}
.px-2 {
padding-left: var(--space-2);
padding-right: var(--space-2);
}
.px-3 {
padding-left: 0.75rem;
padding-right: 0.75rem;
}
.px-4 {
padding-left: var(--space-4);
padding-right: var(--space-4);
}
.px-6 {
padding-left: var(--space-6);
padding-right: var(--space-6);
}
.px-8 {
padding-left: var(--space-8);
padding-right: var(--space-8);
}
.py-2 {
padding-top: var(--space-2);
padding-bottom: var(--space-2);
}
.py-4 {
padding-top: var(--space-4);
padding-bottom: var(--space-4);
}
.py-6 {
padding-top: var(--space-6);
padding-bottom: var(--space-6);
}
.py-8 {
padding-top: var(--space-8);
padding-bottom: var(--space-8);
}
.py-24 {
padding-top: 6rem;
padding-bottom: 6rem;
}
.pt-6 {
padding-top: var(--space-6);
}
.pt-8 {
padding-top: var(--space-8);
}
.pb-4 {
padding-bottom: var(--space-4);
}
/* Margins */
.m-2 {
margin: var(--space-2);
}
.m-4 {
margin: var(--space-4);
}
.m-6 {
margin: var(--space-6);
}
.m-8 {
margin: var(--space-8);
}
.-m-1\.5 {
margin: -0.375rem;
}
.-m-2\.5 {
margin: -0.625rem;
}
.mx-auto {
margin-left: auto;
margin-right: auto;
}
.-mx-3 {
margin-left: -0.75rem;
margin-right: -0.75rem;
}
.-my-6 {
margin-top: -1.5rem;
margin-bottom: -1.5rem;
}
.mt-2 {
margin-top: var(--space-2);
}
.mt-4 {
margin-top: var(--space-4);
}
.mt-6 {
margin-top: var(--space-6);
}
.mt-8 {
margin-top: var(--space-8);
}
.mt-12 {
margin-top: var(--space-12);
}
.mt-auto {
margin-top: auto;
}
.mb-2 {
margin-bottom: var(--space-2);
}
.mb-4 {
margin-bottom: var(--space-4);
}
.mb-6 {
margin-bottom: var(--space-6);
}
.mb-8 {
margin-bottom: var(--space-8);
}
.mb-10 {
margin-bottom: var(--space-10);
}
.mb-12 {
margin-bottom: var(--space-12);
}
/* Space Between */
.space-x-6 > * + * {
margin-left: var(--space-6);
}
.space-y-2 > * + * {
margin-top: var(--space-2);
}
.space-y-10 > * + * {
margin-top: var(--space-10);
}
/* Typography */
.text-center {
text-align: center;
}
.text-left {
text-align: left;
}
.text-right {
text-align: right;
}
.text-xs {
font-size: var(--font-size-xs);
}
.text-sm {
font-size: var(--font-size-sm);
}
.text-base {
font-size: var(--font-size-base);
}
.text-lg {
font-size: var(--font-size-lg);
}
.text-xl {
font-size: var(--font-size-xl);
}
.text-2xl {
font-size: var(--font-size-2xl);
}
.text-3xl {
font-size: var(--font-size-3xl);
}
.text-4xl {
font-size: var(--font-size-4xl);
}
.font-normal {
font-weight: var(--font-weight-normal);
}
.font-medium {
font-weight: var(--font-weight-medium);
}
.font-semibold {
font-weight: var(--font-weight-semibold);
}
.font-bold {
font-weight: var(--font-weight-bold);
}
.leading-tight {
line-height: var(--line-height-tight);
}
.leading-normal {
line-height: var(--line-height-normal);
}
.leading-relaxed {
line-height: var(--line-height-relaxed);
}
.leading-6 {
line-height: 1.5;
}
.leading-7 {
line-height: 1.75;
}
.tracking-tight {
letter-spacing: -0.025em;
}
.underline {
text-decoration: underline;
}
.underline-offset-2 {
text-underline-offset: 2px;
}
.underline-offset-4 {
text-underline-offset: 4px;
}
/* Positioning */
.fixed {
position: fixed;
}
.inset-0 {
top: 0;
right: 0;
bottom: 0;
left: 0;
}
.inset-y-0 {
top: 0;
bottom: 0;
}
/* Border */
.border-t {
border-top: 1px solid rgba(226, 232, 240, 0.1);
}
.divide-y > * + * {
border-top: 1px solid rgba(226, 232, 240, 0.1);
}
.rounded-lg {
border-radius: var(--radius-lg);
}
.rounded-md {
border-radius: var(--radius-md);
}
/* Z-index */
.z-10 {
z-index: var(--z-dropdown);
}
/* Overflow */
.overflow-y-auto {
overflow-y: auto;
}
/* Cursor */
.cursor-pointer {
cursor: pointer;
}
/* Screen Reader Only */
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border-width: 0;
}
/* Responsive Utilities */
@media (min-width: 640px) {
.sm\:max-w-sm {
max-width: 24rem;
}
.sm\:ring-1 {
box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.1);
}
.sm\:border-l-2 {
border-left: 2px solid var(--color-slate-800);
}
.sm\:py-32 {
padding-top: 8rem;
padding-bottom: 8rem;
}
.sm\:grid-cols-2 {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.sm\:grid-cols-3 {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
}
@media (min-width: 768px) {
.md\:flex {
display: flex;
}
.md\:flex-row {
flex-direction: row;
}
.md\:justify-evenly {
justify-content: space-evenly;
}
.md\:items-center {
align-items: center;
}
.md\:justify-between {
justify-content: space-between;
}
.md\:order-1 {
order: 1;
}
.md\:order-2 {
order: 2;
}
.md\:mt-0 {
margin-top: 0;
}
.md\:py-3 {
padding-top: var(--space-3);
padding-bottom: var(--space-3);
}
.md\:grid-cols-3 {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.md\:grid-cols-4 {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
}
@media (min-width: 1024px) {
.lg\:flex {
display: flex;
}
.lg\:hidden {
display: none;
}
.lg\:block {
display: block;
}
.lg\:px-8 {
padding-left: var(--space-8);
padding-right: var(--space-8);
}
.lg\:px-10 {
padding-left: var(--space-10);
padding-right: var(--space-10);
}
.lg\:flex-1 {
flex: 1 1 0;
}
.lg\:gap-x-12 {
column-gap: var(--space-12);
}
.lg\:grid-cols-4 {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
.lg\:grid-cols-5 {
grid-template-columns: repeat(5, minmax(0, 1fr));
}
}