feat(clock): add 10 new clock face designs

Add 5 new analog clock faces:
- Vintage: Antique clock with aged patina and spade hands
- Nautical: Marine-style brass ship clock with compass rose
- Industrial: Factory-style with rivets and metal accents
- Bauhaus: Geometric design with primary color accents
- Railway: Swiss station clock (SBB-style) with red lollipop second hand

Add 5 new digital clock faces:
- Retro: Pixel-style CRT display with scanlines
- Gradient: Modern display with dynamic color shifts
- Terminal: Command-line interface style
- Typewriter: Vintage mechanical keyboard style
- Radar: Military radar screen with sweep animation

Includes translations for all 5 languages (DE, EN, ES, FR, IT)
This commit is contained in:
Till-JS 2025-12-04 15:37:34 +01:00
parent 39b8a5682f
commit 9eb0b51c4c
29 changed files with 5937 additions and 10 deletions

View file

@ -0,0 +1,61 @@
<script lang="ts">
import type { ClockFaceType } from '$lib/stores/clock-face.svelte';
import ClockFaceClassic from './ClockFaceClassic.svelte';
import ClockFaceMinimalist from './ClockFaceMinimalist.svelte';
import ClockFaceModern from './ClockFaceModern.svelte';
import ClockFaceElegant from './ClockFaceElegant.svelte';
import ClockFaceSporty from './ClockFaceSporty.svelte';
import ClockFaceVintage from './ClockFaceVintage.svelte';
import ClockFaceNautical from './ClockFaceNautical.svelte';
import ClockFaceIndustrial from './ClockFaceIndustrial.svelte';
import ClockFaceBauhaus from './ClockFaceBauhaus.svelte';
import ClockFaceRailway from './ClockFaceRailway.svelte';
import ClockFaceLCD from './ClockFaceLCD.svelte';
import ClockFaceFlip from './ClockFaceFlip.svelte';
import ClockFaceMatrix from './ClockFaceMatrix.svelte';
import ClockFaceNeon from './ClockFaceNeon.svelte';
import ClockFaceBinary from './ClockFaceBinary.svelte';
import ClockFaceRetro from './ClockFaceRetro.svelte';
import ClockFaceGradient from './ClockFaceGradient.svelte';
import ClockFaceTerminal from './ClockFaceTerminal.svelte';
import ClockFaceTypewriter from './ClockFaceTypewriter.svelte';
import ClockFaceRadar from './ClockFaceRadar.svelte';
interface Props {
type: ClockFaceType;
hours: number;
minutes: number;
seconds: number;
size?: number;
}
let { type, hours, minutes, seconds, size = 280 }: Props = $props();
// Map of clock face types to components
const clockFaceComponents = {
classic: ClockFaceClassic,
minimalist: ClockFaceMinimalist,
modern: ClockFaceModern,
elegant: ClockFaceElegant,
sporty: ClockFaceSporty,
vintage: ClockFaceVintage,
nautical: ClockFaceNautical,
industrial: ClockFaceIndustrial,
bauhaus: ClockFaceBauhaus,
railway: ClockFaceRailway,
lcd: ClockFaceLCD,
flip: ClockFaceFlip,
matrix: ClockFaceMatrix,
neon: ClockFaceNeon,
binary: ClockFaceBinary,
retro: ClockFaceRetro,
gradient: ClockFaceGradient,
terminal: ClockFaceTerminal,
typewriter: ClockFaceTypewriter,
radar: ClockFaceRadar,
} as const;
let ClockComponent = $derived(clockFaceComponents[type] || ClockFaceModern);
</script>
<ClockComponent {hours} {minutes} {seconds} {size} />

View file

@ -0,0 +1,203 @@
<script lang="ts">
interface Props {
hours: number;
minutes: number;
seconds: number;
size?: number;
}
let { hours, minutes, seconds, size = 280 }: Props = $props();
// Clock hand rotations
let secondRotation = $derived((seconds / 60) * 360);
let minuteRotation = $derived(((minutes + seconds / 60) / 60) * 360);
let hourRotation = $derived((((hours % 12) + minutes / 60) / 12) * 360);
</script>
<div class="clock-face-bauhaus" style="--size: {size}px;">
<!-- Simple border -->
<div class="frame"></div>
<!-- Clean white background -->
<div class="clock-bg"></div>
<!-- Geometric hour markers - simple lines -->
{#each Array(12) as _, i}
<div
class="marker"
class:marker-quarter={i % 3 === 0}
style="transform: rotate({i * 30}deg)"
></div>
{/each}
<!-- Primary color accents at quarters -->
<div class="accent accent-12"></div>
<div class="accent accent-3"></div>
<div class="accent accent-6"></div>
<div class="accent accent-9"></div>
<!-- Clock hands - geometric shapes -->
<div class="hands-container">
<!-- Hour hand - rectangle -->
<div class="hand hour-hand" style="transform: rotate({hourRotation}deg)"></div>
<!-- Minute hand - longer rectangle -->
<div class="hand minute-hand" style="transform: rotate({minuteRotation}deg)"></div>
<!-- Second hand - thin with circle -->
<div class="hand second-hand" style="transform: rotate({secondRotation}deg)">
<div class="second-line"></div>
<div class="second-dot"></div>
</div>
</div>
<!-- Center - simple circle -->
<div class="center"></div>
</div>
<style>
.clock-face-bauhaus {
position: relative;
width: var(--size);
height: var(--size);
border-radius: 50%;
}
.frame {
position: absolute;
inset: 0;
border-radius: 50%;
background: #1a1a1a;
}
.clock-bg {
position: absolute;
inset: calc(var(--size) * 0.02);
border-radius: 50%;
background: #fafafa;
}
.marker {
position: absolute;
top: 50%;
left: 50%;
width: 2px;
height: calc(var(--size) * 0.06);
margin-left: -1px;
margin-top: calc(var(--size) * -0.45);
background: #1a1a1a;
transform-origin: center calc(var(--size) * 0.45);
}
.marker-quarter {
width: 4px;
margin-left: -2px;
height: calc(var(--size) * 0.1);
margin-top: calc(var(--size) * -0.45);
}
/* Primary color accents - Bauhaus colors */
.accent {
position: absolute;
width: calc(var(--size) * 0.04);
height: calc(var(--size) * 0.04);
}
.accent-12 {
top: calc(var(--size) * 0.08);
left: 50%;
transform: translateX(-50%);
background: #e63946; /* Red */
}
.accent-3 {
top: 50%;
right: calc(var(--size) * 0.08);
transform: translateY(-50%);
background: #f4a261; /* Yellow/Orange */
border-radius: 50%;
}
.accent-6 {
bottom: calc(var(--size) * 0.08);
left: 50%;
transform: translateX(-50%);
background: #2a9d8f; /* Teal */
clip-path: polygon(50% 0%, 100% 100%, 0% 100%);
}
.accent-9 {
top: 50%;
left: calc(var(--size) * 0.08);
transform: translateY(-50%);
background: #264653; /* Dark blue */
}
.hands-container {
position: absolute;
inset: 0;
}
.hand {
position: absolute;
top: 50%;
left: 50%;
transform-origin: center bottom;
}
.hour-hand {
width: calc(var(--size) * 0.04);
height: calc(var(--size) * 0.22);
margin-left: calc(var(--size) * -0.02);
margin-top: calc(var(--size) * -0.22);
background: #1a1a1a;
}
.minute-hand {
width: calc(var(--size) * 0.025);
height: calc(var(--size) * 0.32);
margin-left: calc(var(--size) * -0.0125);
margin-top: calc(var(--size) * -0.32);
background: #1a1a1a;
}
.second-hand {
width: calc(var(--size) * 0.008);
height: calc(var(--size) * 0.44);
margin-left: calc(var(--size) * -0.004);
margin-top: calc(var(--size) * -0.36);
transform-origin: center 81.82%;
}
.second-line {
position: absolute;
top: 18%;
left: 50%;
transform: translateX(-50%);
width: 100%;
height: 82%;
background: #e63946;
}
.second-dot {
position: absolute;
top: 8%;
left: 50%;
transform: translateX(-50%);
width: calc(var(--size) * 0.03);
height: calc(var(--size) * 0.03);
background: #e63946;
border-radius: 50%;
}
.center {
position: absolute;
top: 50%;
left: 50%;
width: calc(var(--size) * 0.045);
height: calc(var(--size) * 0.045);
margin: calc(var(--size) * -0.0225);
border-radius: 50%;
background: #1a1a1a;
}
</style>

View file

@ -0,0 +1,209 @@
<script lang="ts">
interface Props {
hours: number;
minutes: number;
seconds: number;
size?: number;
}
let { hours, minutes, seconds, size = 280 }: Props = $props();
// Convert to BCD (Binary Coded Decimal)
function toBCD(value: number): { tens: number[]; ones: number[] } {
const tens = Math.floor(value / 10);
const ones = value % 10;
return {
tens: [(tens >> 2) & 1, (tens >> 1) & 1, tens & 1],
ones: [(ones >> 3) & 1, (ones >> 2) & 1, (ones >> 1) & 1, ones & 1],
};
}
let hoursBCD = $derived(toBCD(hours));
let minutesBCD = $derived(toBCD(minutes));
let secondsBCD = $derived(toBCD(seconds));
let timeString = $derived(
`${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`
);
</script>
<div class="clock-face-binary" style="--size: {size}px;">
<div class="binary-display">
<!-- Column headers -->
<div class="row header-row">
<div class="cell header"></div>
<div class="cell header label">H</div>
<div class="cell header label">H</div>
<div class="cell header spacer"></div>
<div class="cell header label">M</div>
<div class="cell header label">M</div>
<div class="cell header spacer"></div>
<div class="cell header label">S</div>
<div class="cell header label">S</div>
</div>
<!-- Row 8 (only for ones columns) -->
<div class="row">
<div class="cell weight">8</div>
<div class="cell empty"></div>
<div class="led" class:on={hoursBCD.ones[0] === 1}></div>
<div class="cell spacer"></div>
<div class="cell empty"></div>
<div class="led" class:on={minutesBCD.ones[0] === 1}></div>
<div class="cell spacer"></div>
<div class="cell empty"></div>
<div class="led led-small" class:on={secondsBCD.ones[0] === 1}></div>
</div>
<!-- Row 4 -->
<div class="row">
<div class="cell weight">4</div>
<div class="led" class:on={hoursBCD.tens[0] === 1}></div>
<div class="led" class:on={hoursBCD.ones[1] === 1}></div>
<div class="cell spacer"></div>
<div class="led" class:on={minutesBCD.tens[0] === 1}></div>
<div class="led" class:on={minutesBCD.ones[1] === 1}></div>
<div class="cell spacer"></div>
<div class="led led-small" class:on={secondsBCD.tens[0] === 1}></div>
<div class="led led-small" class:on={secondsBCD.ones[1] === 1}></div>
</div>
<!-- Row 2 -->
<div class="row">
<div class="cell weight">2</div>
<div class="led" class:on={hoursBCD.tens[1] === 1}></div>
<div class="led" class:on={hoursBCD.ones[2] === 1}></div>
<div class="cell spacer"></div>
<div class="led" class:on={minutesBCD.tens[1] === 1}></div>
<div class="led" class:on={minutesBCD.ones[2] === 1}></div>
<div class="cell spacer"></div>
<div class="led led-small" class:on={secondsBCD.tens[1] === 1}></div>
<div class="led led-small" class:on={secondsBCD.ones[2] === 1}></div>
</div>
<!-- Row 1 -->
<div class="row">
<div class="cell weight">1</div>
<div class="led" class:on={hoursBCD.tens[2] === 1}></div>
<div class="led" class:on={hoursBCD.ones[3] === 1}></div>
<div class="cell spacer"></div>
<div class="led" class:on={minutesBCD.tens[2] === 1}></div>
<div class="led" class:on={minutesBCD.ones[3] === 1}></div>
<div class="cell spacer"></div>
<div class="led led-small" class:on={secondsBCD.tens[2] === 1}></div>
<div class="led led-small" class:on={secondsBCD.ones[3] === 1}></div>
</div>
<!-- Decimal display -->
<div class="decimal-row">
<span class="decimal-time">{timeString}</span>
</div>
</div>
</div>
<style>
.clock-face-binary {
display: flex;
align-items: center;
justify-content: center;
}
.binary-display {
background: linear-gradient(180deg, #1a1a1a 0%, #0a0a0a 100%);
border-radius: 12px;
padding: 16px 20px;
box-shadow:
0 8px 24px rgba(0, 0, 0, 0.4),
inset 0 1px 1px rgba(255, 255, 255, 0.05);
}
.row {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 6px;
}
.header-row {
margin-bottom: 10px;
padding-bottom: 8px;
border-bottom: 1px solid #333;
}
.cell {
display: flex;
align-items: center;
justify-content: center;
}
.cell.header {
height: 16px;
}
.cell.label {
width: 22px;
font-family: 'JetBrains Mono', monospace;
font-size: 0.65rem;
font-weight: 600;
color: #555;
}
.cell.weight {
width: 16px;
font-family: 'JetBrains Mono', monospace;
font-size: 0.6rem;
font-weight: 600;
color: #444;
text-align: right;
padding-right: 4px;
}
.cell.empty {
width: 22px;
height: 22px;
}
.cell.spacer {
width: 10px;
}
.led {
width: 22px;
height: 22px;
border-radius: 50%;
background: #1a1a1a;
border: 2px solid #2a2a2a;
transition: all 120ms;
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.4);
}
.led.on {
background: radial-gradient(circle at 30% 30%, #00ff44 0%, #00cc33 50%, #00aa22 100%);
border-color: #00ff44;
box-shadow:
0 0 8px #00ff44,
0 0 16px rgba(0, 255, 68, 0.5),
inset 0 -2px 4px rgba(0, 0, 0, 0.2),
inset 0 2px 4px rgba(255, 255, 255, 0.3);
}
.led-small {
width: 16px;
height: 16px;
}
.decimal-row {
margin-top: 12px;
padding-top: 10px;
border-top: 1px solid #333;
text-align: center;
}
.decimal-time {
font-family: 'JetBrains Mono', monospace;
font-size: 0.9rem;
font-weight: 500;
color: #666;
letter-spacing: 0.15em;
}
</style>

View file

@ -0,0 +1,302 @@
<script lang="ts">
interface Props {
hours: number;
minutes: number;
seconds: number;
size?: number;
}
let { hours, minutes, seconds, size = 280 }: Props = $props();
// Clock hand rotations
let secondRotation = $derived((seconds / 60) * 360);
let minuteRotation = $derived(((minutes + seconds / 60) / 60) * 360);
let hourRotation = $derived((((hours % 12) + minutes / 60) / 12) * 360);
// Roman numerals
const romanNumerals = ['XII', 'I', 'II', 'III', 'IV', 'V', 'VI', 'VII', 'VIII', 'IX', 'X', 'XI'];
</script>
<div class="clock-face-classic" style="--size: {size}px;">
<!-- Outer bezel -->
<div class="bezel"></div>
<!-- Background -->
<div class="clock-bg">
<!-- Decorative pattern -->
<div class="pattern"></div>
</div>
<!-- Minute tick marks -->
{#each Array(60) as _, i}
{#if i % 5 !== 0}
<div class="tick tick-minute" style="transform: rotate({i * 6}deg)"></div>
{/if}
{/each}
<!-- Hour tick marks -->
{#each Array(12) as _, i}
<div class="tick tick-hour" style="transform: rotate({i * 30}deg)"></div>
{/each}
<!-- Roman numerals -->
{#each romanNumerals as numeral, i}
<span class="numeral" style="--angle: {i * 30}deg;">
{numeral}
</span>
{/each}
<!-- Inner decorative ring -->
<div class="inner-ring"></div>
<!-- Clock hands -->
<div class="hands-container">
<div class="hand hour-hand" style="transform: rotate({hourRotation}deg)">
<div class="hand-body"></div>
<div class="hand-tail"></div>
</div>
<div class="hand minute-hand" style="transform: rotate({minuteRotation}deg)">
<div class="hand-body"></div>
<div class="hand-tail"></div>
</div>
<div class="hand second-hand" style="transform: rotate({secondRotation}deg)">
<div class="hand-body"></div>
<div class="hand-counterweight"></div>
</div>
</div>
<!-- Center cap -->
<div class="center-cap">
<div class="center-inner"></div>
</div>
</div>
<style>
.clock-face-classic {
position: relative;
width: var(--size);
height: var(--size);
border-radius: 50%;
}
.bezel {
position: absolute;
inset: 0;
border-radius: 50%;
background: linear-gradient(
135deg,
hsl(var(--color-foreground) / 0.15) 0%,
hsl(var(--color-foreground) / 0.05) 50%,
hsl(var(--color-foreground) / 0.2) 100%
);
box-shadow:
0 8px 24px rgba(0, 0, 0, 0.15),
0 2px 8px rgba(0, 0, 0, 0.1),
inset 0 1px 2px rgba(255, 255, 255, 0.2);
}
.clock-bg {
position: absolute;
inset: 6px;
border-radius: 50%;
background: linear-gradient(
180deg,
hsl(var(--color-surface)) 0%,
hsl(var(--color-background)) 100%
);
box-shadow:
inset 0 2px 8px rgba(0, 0, 0, 0.08),
inset 0 -1px 2px rgba(255, 255, 255, 0.5);
overflow: hidden;
}
.pattern {
position: absolute;
inset: 0;
background: radial-gradient(circle at 30% 30%, rgba(255, 255, 255, 0.1) 0%, transparent 50%);
}
.tick {
position: absolute;
top: 50%;
left: 50%;
transform-origin: center center;
}
.tick-minute {
width: 1px;
height: calc(var(--size) * 0.03);
margin-left: -0.5px;
margin-top: calc(var(--size) * -0.46);
background: hsl(var(--color-muted-foreground) / 0.4);
}
.tick-hour {
width: 2px;
height: calc(var(--size) * 0.05);
margin-left: -1px;
margin-top: calc(var(--size) * -0.46);
background: hsl(var(--color-foreground) / 0.6);
border-radius: 1px;
}
.numeral {
position: absolute;
top: 50%;
left: 50%;
font-family: 'Palatino Linotype', 'Book Antiqua', Palatino, Georgia, serif;
font-size: calc(var(--size) * 0.065);
font-weight: 500;
color: hsl(var(--color-foreground));
text-shadow: 0 1px 1px rgba(0, 0, 0, 0.1);
transform: translate(-50%, -50%) rotate(var(--angle)) translateY(calc(var(--size) * -0.36))
rotate(calc(var(--angle) * -1));
}
.inner-ring {
position: absolute;
top: 50%;
left: 50%;
width: 60%;
height: 60%;
transform: translate(-50%, -50%);
border-radius: 50%;
border: 1px solid hsl(var(--color-border) / 0.3);
}
.hands-container {
position: absolute;
inset: 0;
}
.hand {
position: absolute;
top: 50%;
left: 50%;
transform-origin: center bottom;
}
.hour-hand {
width: calc(var(--size) * 0.03);
height: calc(var(--size) * 0.28);
margin-left: calc(var(--size) * -0.015);
margin-top: calc(var(--size) * -0.28);
}
.hour-hand .hand-body {
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 100%;
height: 85%;
background: linear-gradient(
90deg,
hsl(var(--color-foreground) / 0.7) 0%,
hsl(var(--color-foreground)) 50%,
hsl(var(--color-foreground) / 0.7) 100%
);
clip-path: polygon(30% 0%, 70% 0%, 100% 100%, 0% 100%);
}
.hour-hand .hand-tail {
position: absolute;
top: 85%;
left: 50%;
transform: translateX(-50%);
width: 60%;
height: 20%;
background: hsl(var(--color-foreground));
border-radius: 0 0 2px 2px;
}
.minute-hand {
width: calc(var(--size) * 0.022);
height: calc(var(--size) * 0.38);
margin-left: calc(var(--size) * -0.011);
margin-top: calc(var(--size) * -0.38);
}
.minute-hand .hand-body {
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 100%;
height: 88%;
background: linear-gradient(
90deg,
hsl(var(--color-foreground) / 0.7) 0%,
hsl(var(--color-foreground)) 50%,
hsl(var(--color-foreground) / 0.7) 100%
);
clip-path: polygon(25% 0%, 75% 0%, 100% 100%, 0% 100%);
}
.minute-hand .hand-tail {
position: absolute;
top: 88%;
left: 50%;
transform: translateX(-50%);
width: 70%;
height: 18%;
background: hsl(var(--color-foreground));
border-radius: 0 0 1px 1px;
}
.second-hand {
width: calc(var(--size) * 0.008);
height: calc(var(--size) * 0.48);
margin-left: calc(var(--size) * -0.004);
margin-top: calc(var(--size) * -0.38);
transform-origin: center 79.17%;
}
.second-hand .hand-body {
position: absolute;
bottom: 20%;
left: 50%;
transform: translateX(-50%);
width: 100%;
height: 80%;
background: hsl(var(--color-primary));
border-radius: 1px;
}
.second-hand .hand-counterweight {
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 200%;
height: 20%;
background: hsl(var(--color-primary));
border-radius: 2px;
}
.center-cap {
position: absolute;
top: 50%;
left: 50%;
width: calc(var(--size) * 0.06);
height: calc(var(--size) * 0.06);
margin: calc(var(--size) * -0.03);
border-radius: 50%;
background: linear-gradient(
135deg,
hsl(var(--color-foreground)) 0%,
hsl(var(--color-muted-foreground)) 100%
);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
display: flex;
align-items: center;
justify-content: center;
}
.center-inner {
width: 40%;
height: 40%;
border-radius: 50%;
background: hsl(var(--color-primary));
}
</style>

View file

@ -0,0 +1,270 @@
<script lang="ts">
interface Props {
hours: number;
minutes: number;
seconds: number;
size?: number;
}
let { hours, minutes, seconds, size = 280 }: Props = $props();
// Clock hand rotations
let secondRotation = $derived((seconds / 60) * 360);
let minuteRotation = $derived(((minutes + seconds / 60) / 60) * 360);
let hourRotation = $derived((((hours % 12) + minutes / 60) / 12) * 360);
</script>
<div class="clock-face-elegant" style="--size: {size}px;">
<!-- Outer golden bezel -->
<div class="bezel-outer"></div>
<div class="bezel-inner"></div>
<!-- Background with texture -->
<div class="clock-bg">
<div class="texture"></div>
<div class="guilloche"></div>
</div>
<!-- Minute markers -->
{#each Array(60) as _, i}
<div class="marker" class:marker-5={i % 5 === 0} style="transform: rotate({i * 6}deg)"></div>
{/each}
<!-- Hour indices (diamond shaped) -->
{#each Array(12) as _, i}
<div class="hour-index" style="--angle: {i * 30}deg;">
<div class="diamond"></div>
</div>
{/each}
<!-- Decorative rings -->
<div class="ring ring-outer"></div>
<div class="ring ring-inner"></div>
<!-- Brand text -->
<div class="brand">ELEGANT</div>
<!-- Clock hands -->
<div class="hands-container">
<div class="hand hour-hand" style="transform: rotate({hourRotation}deg)">
<div class="hand-blade"></div>
</div>
<div class="hand minute-hand" style="transform: rotate({minuteRotation}deg)">
<div class="hand-blade"></div>
</div>
<div class="hand second-hand" style="transform: rotate({secondRotation}deg)"></div>
</div>
<!-- Center jewel -->
<div class="center-jewel">
<div class="jewel-inner"></div>
</div>
</div>
<style>
.clock-face-elegant {
position: relative;
width: var(--size);
height: var(--size);
border-radius: 50%;
}
.bezel-outer {
position: absolute;
inset: 0;
border-radius: 50%;
background: linear-gradient(
135deg,
#f5e7a3 0%,
#d4af37 25%,
#f5e7a3 50%,
#d4af37 75%,
#f5e7a3 100%
);
box-shadow:
0 8px 24px rgba(0, 0, 0, 0.2),
inset 0 1px 2px rgba(255, 255, 255, 0.5);
}
.bezel-inner {
position: absolute;
inset: 4px;
border-radius: 50%;
background: linear-gradient(180deg, #b8960c 0%, #d4af37 50%, #8b7229 100%);
}
.clock-bg {
position: absolute;
inset: 8px;
border-radius: 50%;
background: linear-gradient(
180deg,
hsl(var(--color-surface)) 0%,
hsl(var(--color-background) / 0.95) 100%
);
overflow: hidden;
}
.texture {
position: absolute;
inset: 0;
background: radial-gradient(circle at 50% 30%, rgba(255, 255, 255, 0.15) 0%, transparent 60%);
}
.guilloche {
position: absolute;
inset: 25%;
border-radius: 50%;
background: repeating-conic-gradient(
from 0deg,
hsl(var(--color-border) / 0.05) 0deg 3deg,
transparent 3deg 6deg
);
}
.marker {
position: absolute;
top: 50%;
left: 50%;
width: 1px;
height: calc(var(--size) * 0.02);
margin-left: -0.5px;
margin-top: calc(var(--size) * -0.44);
background: #d4af37;
opacity: 0.4;
transform-origin: center calc(var(--size) * 0.44);
}
.marker-5 {
width: 2px;
margin-left: -1px;
height: calc(var(--size) * 0.035);
margin-top: calc(var(--size) * -0.44);
opacity: 0.7;
}
.hour-index {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%) rotate(var(--angle)) translateY(calc(var(--size) * -0.36));
}
.diamond {
width: calc(var(--size) * 0.025);
height: calc(var(--size) * 0.04);
background: linear-gradient(135deg, #f5e7a3 0%, #d4af37 50%, #8b7229 100%);
clip-path: polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%);
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
}
.ring {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
border-radius: 50%;
border: 1px solid #d4af37;
}
.ring-outer {
width: 75%;
height: 75%;
opacity: 0.2;
}
.ring-inner {
width: 55%;
height: 55%;
opacity: 0.15;
}
.brand {
position: absolute;
top: 32%;
left: 50%;
transform: translateX(-50%);
font-family: 'Palatino Linotype', 'Book Antiqua', Palatino, Georgia, serif;
font-size: calc(var(--size) * 0.035);
font-weight: 400;
letter-spacing: 0.25em;
color: #d4af37;
opacity: 0.6;
}
.hands-container {
position: absolute;
inset: 0;
}
.hand {
position: absolute;
top: 50%;
left: 50%;
transform-origin: center bottom;
}
.hour-hand {
width: calc(var(--size) * 0.025);
height: calc(var(--size) * 0.24);
margin-left: calc(var(--size) * -0.0125);
margin-top: calc(var(--size) * -0.24);
}
.hour-hand .hand-blade {
width: 100%;
height: 100%;
background: linear-gradient(90deg, #8b7229 0%, #d4af37 50%, #8b7229 100%);
clip-path: polygon(40% 0%, 60% 0%, 100% 100%, 0% 100%);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
}
.minute-hand {
width: calc(var(--size) * 0.018);
height: calc(var(--size) * 0.34);
margin-left: calc(var(--size) * -0.009);
margin-top: calc(var(--size) * -0.34);
}
.minute-hand .hand-blade {
width: 100%;
height: 100%;
background: linear-gradient(90deg, #8b7229 0%, #d4af37 50%, #8b7229 100%);
clip-path: polygon(35% 0%, 65% 0%, 100% 100%, 0% 100%);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
}
.second-hand {
width: calc(var(--size) * 0.006);
height: calc(var(--size) * 0.4);
margin-left: calc(var(--size) * -0.003);
margin-top: calc(var(--size) * -0.32);
transform-origin: center 80%;
background: #d4af37;
border-radius: 1px;
}
.center-jewel {
position: absolute;
top: 50%;
left: 50%;
width: calc(var(--size) * 0.06);
height: calc(var(--size) * 0.06);
margin: calc(var(--size) * -0.03);
border-radius: 50%;
background: linear-gradient(135deg, #f5e7a3 0%, #d4af37 50%, #8b7229 100%);
box-shadow:
0 2px 6px rgba(0, 0, 0, 0.3),
inset 0 1px 2px rgba(255, 255, 255, 0.5);
display: flex;
align-items: center;
justify-content: center;
}
.jewel-inner {
width: 40%;
height: 40%;
border-radius: 50%;
background: radial-gradient(circle, #fff 0%, #f5e7a3 100%);
}
</style>

View file

@ -0,0 +1,238 @@
<script lang="ts">
interface Props {
hours: number;
minutes: number;
seconds: number;
size?: number;
}
let { hours, minutes, seconds, size = 280 }: Props = $props();
let h1 = $derived(Math.floor(hours / 10));
let h2 = $derived(hours % 10);
let m1 = $derived(Math.floor(minutes / 10));
let m2 = $derived(minutes % 10);
let s1 = $derived(Math.floor(seconds / 10));
let s2 = $derived(seconds % 10);
</script>
<div class="clock-face-flip" style="--size: {size}px;">
<div class="flip-container">
<!-- Hours -->
<div class="flip-group">
<div class="flip-card">
<div class="flip-top">
<span>{h1}</span>
</div>
<div class="flip-bottom">
<span>{h1}</span>
</div>
<div class="flip-hinge"></div>
</div>
<div class="flip-card">
<div class="flip-top">
<span>{h2}</span>
</div>
<div class="flip-bottom">
<span>{h2}</span>
</div>
<div class="flip-hinge"></div>
</div>
</div>
<div class="flip-separator">
<div class="sep-dot"></div>
<div class="sep-dot"></div>
</div>
<!-- Minutes -->
<div class="flip-group">
<div class="flip-card">
<div class="flip-top">
<span>{m1}</span>
</div>
<div class="flip-bottom">
<span>{m1}</span>
</div>
<div class="flip-hinge"></div>
</div>
<div class="flip-card">
<div class="flip-top">
<span>{m2}</span>
</div>
<div class="flip-bottom">
<span>{m2}</span>
</div>
<div class="flip-hinge"></div>
</div>
</div>
<div class="flip-separator flip-separator-small">
<div class="sep-dot sep-dot-small"></div>
<div class="sep-dot sep-dot-small"></div>
</div>
<!-- Seconds -->
<div class="flip-group flip-group-small">
<div class="flip-card flip-card-small">
<div class="flip-top">
<span>{s1}</span>
</div>
<div class="flip-bottom">
<span>{s1}</span>
</div>
<div class="flip-hinge"></div>
</div>
<div class="flip-card flip-card-small">
<div class="flip-top">
<span>{s2}</span>
</div>
<div class="flip-bottom">
<span>{s2}</span>
</div>
<div class="flip-hinge"></div>
</div>
</div>
</div>
</div>
<style>
.clock-face-flip {
display: flex;
align-items: center;
justify-content: center;
}
.flip-container {
display: flex;
align-items: center;
gap: 6px;
padding: 12px 16px;
background: linear-gradient(180deg, #1a1a1a 0%, #0a0a0a 100%);
border-radius: 8px;
box-shadow:
0 8px 24px rgba(0, 0, 0, 0.4),
inset 0 1px 1px rgba(255, 255, 255, 0.05);
}
.flip-group {
display: flex;
gap: 3px;
}
.flip-card {
position: relative;
width: 44px;
height: 64px;
perspective: 300px;
}
.flip-card-small {
width: 30px;
height: 44px;
}
.flip-top,
.flip-bottom {
position: absolute;
width: 100%;
height: 50%;
overflow: hidden;
display: flex;
justify-content: center;
}
.flip-top {
top: 0;
background: linear-gradient(180deg, #3a3a3a 0%, #2a2a2a 100%);
border-radius: 6px 6px 0 0;
align-items: flex-end;
border-bottom: 1px solid #1a1a1a;
}
.flip-top span {
transform: translateY(50%);
}
.flip-bottom {
bottom: 0;
background: linear-gradient(180deg, #2a2a2a 0%, #1a1a1a 100%);
border-radius: 0 0 6px 6px;
align-items: flex-start;
}
.flip-bottom span {
transform: translateY(-50%);
}
.flip-top span,
.flip-bottom span {
font-family: 'Oswald', 'Bebas Neue', 'Impact', sans-serif;
font-size: 48px;
font-weight: 500;
color: #f0f0f0;
line-height: 1;
}
.flip-card-small .flip-top span,
.flip-card-small .flip-bottom span {
font-size: 32px;
}
.flip-hinge {
position: absolute;
top: 50%;
left: 0;
right: 0;
height: 2px;
margin-top: -1px;
background: linear-gradient(
90deg,
transparent 0%,
rgba(0, 0, 0, 0.8) 10%,
rgba(0, 0, 0, 0.8) 90%,
transparent 100%
);
z-index: 10;
}
.flip-hinge::before {
content: '';
position: absolute;
top: -1px;
left: 5%;
right: 5%;
height: 1px;
background: rgba(255, 255, 255, 0.1);
}
.flip-separator {
display: flex;
flex-direction: column;
gap: 12px;
padding: 0 4px;
}
.flip-separator-small {
gap: 8px;
padding: 0 2px;
}
.sep-dot {
width: 6px;
height: 6px;
background: #666;
border-radius: 50%;
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.5);
}
.sep-dot-small {
width: 4px;
height: 4px;
}
.flip-group-small {
align-self: flex-end;
margin-bottom: 2px;
}
</style>

View file

@ -0,0 +1,170 @@
<script lang="ts">
interface Props {
hours: number;
minutes: number;
seconds: number;
size?: number;
}
let { hours, minutes, seconds, size = 280 }: Props = $props();
let timeString = $derived(
`${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}`
);
let secondsString = $derived(seconds.toString().padStart(2, '0'));
// Calculate progress through the day for gradient animation
let dayProgress = $derived((hours * 3600 + minutes * 60 + seconds) / 86400);
let hue = $derived(Math.round(dayProgress * 360));
</script>
<div class="clock-face-gradient" style="--size: {size}px; --hue: {hue};">
<div class="gradient-container">
<!-- Animated background -->
<div class="bg-gradient"></div>
<div class="bg-overlay"></div>
<!-- Glass effect -->
<div class="glass-effect"></div>
<!-- Time display -->
<div class="time-wrapper">
<span class="time-shadow">{timeString}</span>
<span class="time-text">{timeString}</span>
</div>
<!-- Seconds with different style -->
<div class="seconds-wrapper">
<span class="seconds-text">{secondsString}</span>
</div>
<!-- Progress bar for seconds -->
<div class="progress-bar">
<div class="progress-fill" style="width: {(seconds / 60) * 100}%"></div>
</div>
</div>
</div>
<style>
.clock-face-gradient {
display: flex;
align-items: center;
justify-content: center;
}
.gradient-container {
position: relative;
padding: 24px 36px;
border-radius: 20px;
overflow: hidden;
box-shadow:
0 8px 32px rgba(0, 0, 0, 0.3),
0 0 0 1px rgba(255, 255, 255, 0.1);
}
.bg-gradient {
position: absolute;
inset: 0;
background: linear-gradient(
135deg,
hsl(var(--hue), 70%, 45%) 0%,
hsl(calc(var(--hue) + 60), 70%, 35%) 50%,
hsl(calc(var(--hue) + 120), 70%, 25%) 100%
);
transition: background 1s ease;
}
.bg-overlay {
position: absolute;
inset: 0;
background:
radial-gradient(ellipse at 30% 20%, rgba(255, 255, 255, 0.15) 0%, transparent 50%),
radial-gradient(ellipse at 70% 80%, rgba(0, 0, 0, 0.2) 0%, transparent 50%);
}
.glass-effect {
position: absolute;
top: 0;
left: 0;
right: 0;
height: 50%;
background: linear-gradient(
180deg,
rgba(255, 255, 255, 0.15) 0%,
rgba(255, 255, 255, 0.05) 50%,
transparent 100%
);
border-radius: 20px 20px 0 0;
}
.time-wrapper {
position: relative;
display: flex;
align-items: center;
justify-content: center;
}
.time-shadow {
position: absolute;
font-family:
'SF Pro Display',
-apple-system,
BlinkMacSystemFont,
sans-serif;
font-size: 3.5rem;
font-weight: 200;
letter-spacing: 0.05em;
color: rgba(0, 0, 0, 0.3);
transform: translate(2px, 2px);
white-space: nowrap;
}
.time-text {
position: relative;
font-family:
'SF Pro Display',
-apple-system,
BlinkMacSystemFont,
sans-serif;
font-size: 3.5rem;
font-weight: 200;
letter-spacing: 0.05em;
color: #ffffff;
text-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
white-space: nowrap;
}
.seconds-wrapper {
position: relative;
text-align: center;
margin-top: 4px;
}
.seconds-text {
font-family:
'SF Pro Display',
-apple-system,
BlinkMacSystemFont,
sans-serif;
font-size: 1.25rem;
font-weight: 300;
color: rgba(255, 255, 255, 0.7);
letter-spacing: 0.2em;
}
.progress-bar {
position: relative;
margin-top: 16px;
height: 3px;
background: rgba(255, 255, 255, 0.2);
border-radius: 2px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: rgba(255, 255, 255, 0.8);
border-radius: 2px;
transition: width 0.3s linear;
}
</style>

View file

@ -0,0 +1,278 @@
<script lang="ts">
interface Props {
hours: number;
minutes: number;
seconds: number;
size?: number;
}
let { hours, minutes, seconds, size = 280 }: Props = $props();
// Clock hand rotations
let secondRotation = $derived((seconds / 60) * 360);
let minuteRotation = $derived(((minutes + seconds / 60) / 60) * 360);
let hourRotation = $derived((((hours % 12) + minutes / 60) / 12) * 360);
const numbers = [12, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11];
</script>
<div class="clock-face-industrial" style="--size: {size}px;">
<!-- Riveted metal frame -->
<div class="frame">
{#each Array(12) as _, i}
<div class="rivet" style="transform: rotate({i * 30}deg)"></div>
{/each}
</div>
<!-- Metal dial -->
<div class="clock-bg">
<div class="brushed-texture"></div>
<div class="inner-ring"></div>
</div>
<!-- All tick marks -->
{#each Array(60) as _, i}
<div class="marker" class:marker-5={i % 5 === 0} style="transform: rotate({i * 6}deg)"></div>
{/each}
<!-- Numbers with industrial font -->
{#each numbers as num, i}
<span class="number" style="--angle: {i * 30}deg;">
{num.toString().padStart(2, '0')}
</span>
{/each}
<!-- Warning stripes decoration -->
<div class="warning-ring"></div>
<!-- Clock hands -->
<div class="hands-container">
<div class="hand hour-hand" style="transform: rotate({hourRotation}deg)">
<div class="hand-body"></div>
<div class="hand-highlight"></div>
</div>
<div class="hand minute-hand" style="transform: rotate({minuteRotation}deg)">
<div class="hand-body"></div>
<div class="hand-highlight"></div>
</div>
<div class="hand second-hand" style="transform: rotate({secondRotation}deg)"></div>
</div>
<!-- Center bolt -->
<div class="center-bolt">
<div class="bolt-slot"></div>
</div>
</div>
<style>
.clock-face-industrial {
position: relative;
width: var(--size);
height: var(--size);
border-radius: 50%;
}
.frame {
position: absolute;
inset: 0;
border-radius: 50%;
background: linear-gradient(180deg, #5a5a5a 0%, #3d3d3d 50%, #2a2a2a 100%);
box-shadow:
0 8px 24px rgba(0, 0, 0, 0.4),
inset 0 2px 4px rgba(255, 255, 255, 0.1),
inset 0 -2px 4px rgba(0, 0, 0, 0.3);
}
.rivet {
position: absolute;
top: 50%;
left: 50%;
width: calc(var(--size) * 0.025);
height: calc(var(--size) * 0.025);
margin-left: calc(var(--size) * -0.0125);
margin-top: calc(var(--size) * -0.48);
background: radial-gradient(circle at 30% 30%, #888 0%, #444 100%);
border-radius: 50%;
box-shadow:
inset 0 1px 2px rgba(255, 255, 255, 0.3),
0 1px 2px rgba(0, 0, 0, 0.4);
transform-origin: center calc(var(--size) * 0.48);
}
.clock-bg {
position: absolute;
inset: calc(var(--size) * 0.045);
border-radius: 50%;
background: linear-gradient(180deg, #e8e8e8 0%, #d0d0d0 50%, #b8b8b8 100%);
box-shadow: inset 0 4px 12px rgba(0, 0, 0, 0.2);
overflow: hidden;
}
.brushed-texture {
position: absolute;
inset: 0;
background: repeating-linear-gradient(
90deg,
transparent,
transparent 1px,
rgba(255, 255, 255, 0.03) 1px,
rgba(255, 255, 255, 0.03) 2px
);
}
.inner-ring {
position: absolute;
inset: 15%;
border-radius: 50%;
border: 3px solid rgba(0, 0, 0, 0.08);
}
.marker {
position: absolute;
top: 50%;
left: 50%;
width: 2px;
height: calc(var(--size) * 0.03);
margin-left: -1px;
margin-top: calc(var(--size) * -0.43);
background: #333;
transform-origin: center calc(var(--size) * 0.43);
}
.marker-5 {
width: 4px;
margin-left: -2px;
height: calc(var(--size) * 0.05);
background: #1a1a1a;
border-radius: 1px;
}
.number {
position: absolute;
top: 50%;
left: 50%;
font-family: 'Share Tech Mono', 'Courier New', monospace;
font-size: calc(var(--size) * 0.058);
font-weight: 700;
color: #1a1a1a;
transform: translate(-50%, -50%) rotate(var(--angle)) translateY(calc(var(--size) * -0.33))
rotate(calc(var(--angle) * -1));
}
.warning-ring {
position: absolute;
top: 50%;
left: 50%;
width: 25%;
height: 25%;
transform: translate(-50%, -50%);
border-radius: 50%;
background: repeating-conic-gradient(from 0deg, #f59e0b 0deg 15deg, #1a1a1a 15deg 30deg);
opacity: 0.2;
}
.hands-container {
position: absolute;
inset: 0;
}
.hand {
position: absolute;
top: 50%;
left: 50%;
transform-origin: center bottom;
}
.hour-hand {
width: calc(var(--size) * 0.04);
height: calc(var(--size) * 0.22);
margin-left: calc(var(--size) * -0.02);
margin-top: calc(var(--size) * -0.22);
}
.hour-hand .hand-body {
width: 100%;
height: 100%;
background: linear-gradient(90deg, #2a2a2a 0%, #4a4a4a 50%, #2a2a2a 100%);
clip-path: polygon(20% 0%, 80% 0%, 65% 100%, 35% 100%);
}
.hour-hand .hand-highlight {
position: absolute;
top: 10%;
left: 30%;
width: 40%;
height: 80%;
background: linear-gradient(
90deg,
transparent 0%,
rgba(255, 255, 255, 0.15) 50%,
transparent 100%
);
clip-path: polygon(0% 0%, 100% 0%, 80% 100%, 20% 100%);
}
.minute-hand {
width: calc(var(--size) * 0.03);
height: calc(var(--size) * 0.32);
margin-left: calc(var(--size) * -0.015);
margin-top: calc(var(--size) * -0.32);
}
.minute-hand .hand-body {
width: 100%;
height: 100%;
background: linear-gradient(90deg, #2a2a2a 0%, #4a4a4a 50%, #2a2a2a 100%);
clip-path: polygon(15% 0%, 85% 0%, 60% 100%, 40% 100%);
}
.minute-hand .hand-highlight {
position: absolute;
top: 8%;
left: 25%;
width: 50%;
height: 85%;
background: linear-gradient(
90deg,
transparent 0%,
rgba(255, 255, 255, 0.15) 50%,
transparent 100%
);
clip-path: polygon(0% 0%, 100% 0%, 75% 100%, 25% 100%);
}
.second-hand {
width: calc(var(--size) * 0.012);
height: calc(var(--size) * 0.42);
margin-left: calc(var(--size) * -0.006);
margin-top: calc(var(--size) * -0.34);
background: #f59e0b;
border-radius: 1px;
transform-origin: center 80.95%;
box-shadow: 0 0 8px rgba(245, 158, 11, 0.5);
}
.center-bolt {
position: absolute;
top: 50%;
left: 50%;
width: calc(var(--size) * 0.065);
height: calc(var(--size) * 0.065);
margin: calc(var(--size) * -0.0325);
border-radius: 50%;
background: linear-gradient(135deg, #666 0%, #333 100%);
box-shadow:
0 2px 6px rgba(0, 0, 0, 0.4),
inset 0 1px 2px rgba(255, 255, 255, 0.2);
display: flex;
align-items: center;
justify-content: center;
}
.bolt-slot {
width: 60%;
height: 3px;
background: #1a1a1a;
border-radius: 1px;
}
</style>

View file

@ -0,0 +1,316 @@
<script lang="ts">
interface Props {
hours: number;
minutes: number;
seconds: number;
size?: number;
}
let { hours, minutes, seconds, size = 280 }: Props = $props();
// 7-segment display mapping
// Segments: a(top), b(topRight), c(bottomRight), d(bottom), e(bottomLeft), f(topLeft), g(middle)
const segments: Record<string, string> = {
'0': 'abcdef',
'1': 'bc',
'2': 'abdeg',
'3': 'abcdg',
'4': 'bcfg',
'5': 'acdfg',
'6': 'acdefg',
'7': 'abc',
'8': 'abcdefg',
'9': 'abcdfg',
};
let h1 = $derived(Math.floor(hours / 10).toString());
let h2 = $derived((hours % 10).toString());
let m1 = $derived(Math.floor(minutes / 10).toString());
let m2 = $derived((minutes % 10).toString());
let s1 = $derived(Math.floor(seconds / 10).toString());
let s2 = $derived((seconds % 10).toString());
function isOn(digit: string, segment: string): boolean {
return segments[digit]?.includes(segment) ?? false;
}
</script>
<div class="clock-face-lcd" style="--size: {size}px;">
<div class="lcd-case">
<div class="lcd-screen">
<!-- Screen glare -->
<div class="screen-glare"></div>
<!-- Digits container -->
<div class="digits">
<!-- Hours -->
<div class="digit-group">
<div class="digit">
<div class="segment seg-a" class:on={isOn(h1, 'a')}></div>
<div class="segment seg-b" class:on={isOn(h1, 'b')}></div>
<div class="segment seg-c" class:on={isOn(h1, 'c')}></div>
<div class="segment seg-d" class:on={isOn(h1, 'd')}></div>
<div class="segment seg-e" class:on={isOn(h1, 'e')}></div>
<div class="segment seg-f" class:on={isOn(h1, 'f')}></div>
<div class="segment seg-g" class:on={isOn(h1, 'g')}></div>
</div>
<div class="digit">
<div class="segment seg-a" class:on={isOn(h2, 'a')}></div>
<div class="segment seg-b" class:on={isOn(h2, 'b')}></div>
<div class="segment seg-c" class:on={isOn(h2, 'c')}></div>
<div class="segment seg-d" class:on={isOn(h2, 'd')}></div>
<div class="segment seg-e" class:on={isOn(h2, 'e')}></div>
<div class="segment seg-f" class:on={isOn(h2, 'f')}></div>
<div class="segment seg-g" class:on={isOn(h2, 'g')}></div>
</div>
</div>
<!-- Colon -->
<div class="colon">
<div class="colon-dot"></div>
<div class="colon-dot"></div>
</div>
<!-- Minutes -->
<div class="digit-group">
<div class="digit">
<div class="segment seg-a" class:on={isOn(m1, 'a')}></div>
<div class="segment seg-b" class:on={isOn(m1, 'b')}></div>
<div class="segment seg-c" class:on={isOn(m1, 'c')}></div>
<div class="segment seg-d" class:on={isOn(m1, 'd')}></div>
<div class="segment seg-e" class:on={isOn(m1, 'e')}></div>
<div class="segment seg-f" class:on={isOn(m1, 'f')}></div>
<div class="segment seg-g" class:on={isOn(m1, 'g')}></div>
</div>
<div class="digit">
<div class="segment seg-a" class:on={isOn(m2, 'a')}></div>
<div class="segment seg-b" class:on={isOn(m2, 'b')}></div>
<div class="segment seg-c" class:on={isOn(m2, 'c')}></div>
<div class="segment seg-d" class:on={isOn(m2, 'd')}></div>
<div class="segment seg-e" class:on={isOn(m2, 'e')}></div>
<div class="segment seg-f" class:on={isOn(m2, 'f')}></div>
<div class="segment seg-g" class:on={isOn(m2, 'g')}></div>
</div>
</div>
<!-- Small colon -->
<div class="colon colon-small">
<div class="colon-dot"></div>
<div class="colon-dot"></div>
</div>
<!-- Seconds (smaller) -->
<div class="digit-group digit-group-small">
<div class="digit digit-small">
<div class="segment seg-a" class:on={isOn(s1, 'a')}></div>
<div class="segment seg-b" class:on={isOn(s1, 'b')}></div>
<div class="segment seg-c" class:on={isOn(s1, 'c')}></div>
<div class="segment seg-d" class:on={isOn(s1, 'd')}></div>
<div class="segment seg-e" class:on={isOn(s1, 'e')}></div>
<div class="segment seg-f" class:on={isOn(s1, 'f')}></div>
<div class="segment seg-g" class:on={isOn(s1, 'g')}></div>
</div>
<div class="digit digit-small">
<div class="segment seg-a" class:on={isOn(s2, 'a')}></div>
<div class="segment seg-b" class:on={isOn(s2, 'b')}></div>
<div class="segment seg-c" class:on={isOn(s2, 'c')}></div>
<div class="segment seg-d" class:on={isOn(s2, 'd')}></div>
<div class="segment seg-e" class:on={isOn(s2, 'e')}></div>
<div class="segment seg-f" class:on={isOn(s2, 'f')}></div>
<div class="segment seg-g" class:on={isOn(s2, 'g')}></div>
</div>
</div>
</div>
</div>
</div>
</div>
<style>
.clock-face-lcd {
display: flex;
align-items: center;
justify-content: center;
}
.lcd-case {
background: linear-gradient(180deg, #3a3a3a 0%, #2a2a2a 50%, #1a1a1a 100%);
border-radius: 12px;
padding: 8px;
box-shadow:
0 8px 24px rgba(0, 0, 0, 0.4),
inset 0 1px 1px rgba(255, 255, 255, 0.1);
}
.lcd-screen {
position: relative;
background: linear-gradient(180deg, #7a9a6a 0%, #5a7a4a 100%);
border-radius: 6px;
padding: 16px 20px;
box-shadow: inset 0 2px 8px rgba(0, 0, 0, 0.3);
}
:global(.dark) .lcd-screen {
background: linear-gradient(180deg, #1a2a1a 0%, #0a1a0a 100%);
}
.screen-glare {
position: absolute;
top: 0;
left: 0;
right: 0;
height: 40%;
background: linear-gradient(180deg, rgba(255, 255, 255, 0.15) 0%, transparent 100%);
border-radius: 6px 6px 0 0;
pointer-events: none;
}
.digits {
display: flex;
align-items: center;
gap: 4px;
}
.digit-group {
display: flex;
gap: 4px;
}
.digit {
position: relative;
width: 28px;
height: 48px;
}
.digit-small {
width: 20px;
height: 34px;
}
.segment {
position: absolute;
background: rgba(30, 60, 30, 0.2);
transition:
background-color 50ms,
box-shadow 50ms;
}
:global(.dark) .segment {
background: rgba(0, 50, 0, 0.3);
}
.segment.on {
background: #2a3a20;
}
:global(.dark) .segment.on {
background: #00dd00;
box-shadow: 0 0 6px #00dd00;
}
/* Segment positions */
.seg-a {
top: 0;
left: 4px;
right: 4px;
height: 4px;
clip-path: polygon(15% 0%, 85% 0%, 100% 50%, 85% 100%, 15% 100%, 0% 50%);
}
.seg-b {
top: 3px;
right: 0;
width: 4px;
height: 44%;
clip-path: polygon(50% 0%, 100% 15%, 100% 85%, 50% 100%, 0% 85%, 0% 15%);
}
.seg-c {
top: 53%;
right: 0;
width: 4px;
height: 44%;
clip-path: polygon(50% 0%, 100% 15%, 100% 85%, 50% 100%, 0% 85%, 0% 15%);
}
.seg-d {
bottom: 0;
left: 4px;
right: 4px;
height: 4px;
clip-path: polygon(15% 0%, 85% 0%, 100% 50%, 85% 100%, 15% 100%, 0% 50%);
}
.seg-e {
top: 53%;
left: 0;
width: 4px;
height: 44%;
clip-path: polygon(50% 0%, 100% 15%, 100% 85%, 50% 100%, 0% 85%, 0% 15%);
}
.seg-f {
top: 3px;
left: 0;
width: 4px;
height: 44%;
clip-path: polygon(50% 0%, 100% 15%, 100% 85%, 50% 100%, 0% 85%, 0% 15%);
}
.seg-g {
top: 50%;
left: 4px;
right: 4px;
height: 4px;
margin-top: -2px;
clip-path: polygon(15% 0%, 85% 0%, 100% 50%, 85% 100%, 15% 100%, 0% 50%);
}
.colon {
display: flex;
flex-direction: column;
justify-content: center;
gap: 12px;
padding: 0 4px;
}
.colon-small {
gap: 8px;
padding: 0 3px;
}
.colon-dot {
width: 5px;
height: 5px;
background: #2a3a20;
border-radius: 1px;
}
:global(.dark) .colon-dot {
background: #00dd00;
box-shadow: 0 0 4px #00dd00;
}
.digit-group-small {
align-self: flex-end;
margin-bottom: 2px;
}
.digit-small .seg-a,
.digit-small .seg-d,
.digit-small .seg-g {
left: 3px;
right: 3px;
height: 3px;
}
.digit-small .seg-b,
.digit-small .seg-c,
.digit-small .seg-e,
.digit-small .seg-f {
width: 3px;
}
.digit-small .seg-g {
margin-top: -1.5px;
}
</style>

View file

@ -0,0 +1,297 @@
<script lang="ts">
interface Props {
hours: number;
minutes: number;
seconds: number;
size?: number;
}
let { hours, minutes, seconds, size = 280 }: Props = $props();
// 5x7 dot matrix patterns for digits 0-9
const patterns: Record<string, number[][]> = {
'0': [
[0, 1, 1, 1, 0],
[1, 0, 0, 0, 1],
[1, 0, 0, 1, 1],
[1, 0, 1, 0, 1],
[1, 1, 0, 0, 1],
[1, 0, 0, 0, 1],
[0, 1, 1, 1, 0],
],
'1': [
[0, 0, 1, 0, 0],
[0, 1, 1, 0, 0],
[0, 0, 1, 0, 0],
[0, 0, 1, 0, 0],
[0, 0, 1, 0, 0],
[0, 0, 1, 0, 0],
[0, 1, 1, 1, 0],
],
'2': [
[0, 1, 1, 1, 0],
[1, 0, 0, 0, 1],
[0, 0, 0, 0, 1],
[0, 0, 0, 1, 0],
[0, 0, 1, 0, 0],
[0, 1, 0, 0, 0],
[1, 1, 1, 1, 1],
],
'3': [
[0, 1, 1, 1, 0],
[1, 0, 0, 0, 1],
[0, 0, 0, 0, 1],
[0, 0, 1, 1, 0],
[0, 0, 0, 0, 1],
[1, 0, 0, 0, 1],
[0, 1, 1, 1, 0],
],
'4': [
[0, 0, 0, 1, 0],
[0, 0, 1, 1, 0],
[0, 1, 0, 1, 0],
[1, 0, 0, 1, 0],
[1, 1, 1, 1, 1],
[0, 0, 0, 1, 0],
[0, 0, 0, 1, 0],
],
'5': [
[1, 1, 1, 1, 1],
[1, 0, 0, 0, 0],
[1, 1, 1, 1, 0],
[0, 0, 0, 0, 1],
[0, 0, 0, 0, 1],
[1, 0, 0, 0, 1],
[0, 1, 1, 1, 0],
],
'6': [
[0, 0, 1, 1, 0],
[0, 1, 0, 0, 0],
[1, 0, 0, 0, 0],
[1, 1, 1, 1, 0],
[1, 0, 0, 0, 1],
[1, 0, 0, 0, 1],
[0, 1, 1, 1, 0],
],
'7': [
[1, 1, 1, 1, 1],
[0, 0, 0, 0, 1],
[0, 0, 0, 1, 0],
[0, 0, 1, 0, 0],
[0, 1, 0, 0, 0],
[0, 1, 0, 0, 0],
[0, 1, 0, 0, 0],
],
'8': [
[0, 1, 1, 1, 0],
[1, 0, 0, 0, 1],
[1, 0, 0, 0, 1],
[0, 1, 1, 1, 0],
[1, 0, 0, 0, 1],
[1, 0, 0, 0, 1],
[0, 1, 1, 1, 0],
],
'9': [
[0, 1, 1, 1, 0],
[1, 0, 0, 0, 1],
[1, 0, 0, 0, 1],
[0, 1, 1, 1, 1],
[0, 0, 0, 0, 1],
[0, 0, 0, 1, 0],
[0, 1, 1, 0, 0],
],
};
const colonPattern = [[0], [1], [0], [0], [0], [1], [0]];
let h1 = $derived(Math.floor(hours / 10).toString());
let h2 = $derived((hours % 10).toString());
let m1 = $derived(Math.floor(minutes / 10).toString());
let m2 = $derived((minutes % 10).toString());
let s1 = $derived(Math.floor(seconds / 10).toString());
let s2 = $derived((seconds % 10).toString());
</script>
<div class="clock-face-matrix" style="--size: {size}px;">
<div class="matrix-display">
<div class="matrix-screen">
<div class="screen-overlay"></div>
<div class="digits-container">
<!-- Hours -->
<div class="digit-matrix">
{#each patterns[h1] as row}
<div class="dot-row">
{#each row as dot}
<div class="dot" class:on={dot === 1}></div>
{/each}
</div>
{/each}
</div>
<div class="digit-matrix">
{#each patterns[h2] as row}
<div class="dot-row">
{#each row as dot}
<div class="dot" class:on={dot === 1}></div>
{/each}
</div>
{/each}
</div>
<!-- Colon -->
<div class="colon-matrix">
{#each colonPattern as row}
<div class="dot-row">
{#each row as dot}
<div class="dot dot-colon" class:on={dot === 1}></div>
{/each}
</div>
{/each}
</div>
<!-- Minutes -->
<div class="digit-matrix">
{#each patterns[m1] as row}
<div class="dot-row">
{#each row as dot}
<div class="dot" class:on={dot === 1}></div>
{/each}
</div>
{/each}
</div>
<div class="digit-matrix">
{#each patterns[m2] as row}
<div class="dot-row">
{#each row as dot}
<div class="dot" class:on={dot === 1}></div>
{/each}
</div>
{/each}
</div>
<!-- Colon -->
<div class="colon-matrix">
{#each colonPattern as row}
<div class="dot-row">
{#each row as dot}
<div class="dot dot-colon" class:on={dot === 1}></div>
{/each}
</div>
{/each}
</div>
<!-- Seconds -->
<div class="digit-matrix digit-small">
{#each patterns[s1] as row}
<div class="dot-row">
{#each row as dot}
<div class="dot dot-small" class:on={dot === 1}></div>
{/each}
</div>
{/each}
</div>
<div class="digit-matrix digit-small">
{#each patterns[s2] as row}
<div class="dot-row">
{#each row as dot}
<div class="dot dot-small" class:on={dot === 1}></div>
{/each}
</div>
{/each}
</div>
</div>
</div>
</div>
</div>
<style>
.clock-face-matrix {
display: flex;
align-items: center;
justify-content: center;
}
.matrix-display {
background: linear-gradient(180deg, #2a2a2a 0%, #1a1a1a 100%);
border-radius: 10px;
padding: 6px;
box-shadow:
0 8px 24px rgba(0, 0, 0, 0.4),
inset 0 1px 1px rgba(255, 255, 255, 0.1);
}
.matrix-screen {
position: relative;
background: #0a0a0a;
border-radius: 6px;
padding: 14px 18px;
box-shadow: inset 0 2px 6px rgba(0, 0, 0, 0.5);
}
.screen-overlay {
position: absolute;
inset: 0;
background: linear-gradient(
180deg,
rgba(255, 255, 255, 0.03) 0%,
transparent 50%,
rgba(0, 0, 0, 0.1) 100%
);
border-radius: 6px;
pointer-events: none;
}
.digits-container {
display: flex;
gap: 4px;
align-items: flex-end;
}
.digit-matrix {
display: flex;
flex-direction: column;
gap: 2px;
}
.digit-small {
transform: scale(0.7);
transform-origin: bottom left;
}
.dot-row {
display: flex;
gap: 2px;
}
.dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: #1a1a1a;
transition: all 80ms;
}
.dot.on {
background: #ff3333;
box-shadow:
0 0 4px #ff3333,
0 0 8px #ff3333,
0 0 12px rgba(255, 51, 51, 0.5);
}
.dot-small {
width: 5px;
height: 5px;
}
.dot-colon {
width: 4px;
height: 4px;
}
.colon-matrix {
padding: 0 3px;
display: flex;
flex-direction: column;
gap: 2px;
}
</style>

View file

@ -0,0 +1,158 @@
<script lang="ts">
interface Props {
hours: number;
minutes: number;
seconds: number;
size?: number;
}
let { hours, minutes, seconds, size = 280 }: Props = $props();
// Clock hand rotations
let secondRotation = $derived((seconds / 60) * 360);
let minuteRotation = $derived(((minutes + seconds / 60) / 60) * 360);
let hourRotation = $derived((((hours % 12) + minutes / 60) / 12) * 360);
</script>
<div class="clock-face-minimalist" style="--size: {size}px;">
<!-- Clean background -->
<div class="clock-bg"></div>
<!-- Only 12 subtle hour markers as thin lines -->
{#each Array(12) as _, i}
<div
class="marker"
class:marker-main={i % 3 === 0}
style="transform: rotate({i * 30}deg)"
></div>
{/each}
<!-- Clock hands -->
<div class="hands-container">
<!-- Hour hand - thick, short -->
<div class="hand hour-hand" style="transform: rotate({hourRotation}deg)"></div>
<!-- Minute hand - thinner, longer -->
<div class="hand minute-hand" style="transform: rotate({minuteRotation}deg)"></div>
<!-- Second hand - very thin, accent color -->
<div class="hand second-hand" style="transform: rotate({secondRotation}deg)">
<div class="second-line"></div>
<div class="second-circle"></div>
</div>
</div>
<!-- Minimal center dot -->
<div class="center-dot"></div>
</div>
<style>
.clock-face-minimalist {
position: relative;
width: var(--size);
height: var(--size);
border-radius: 50%;
}
.clock-bg {
position: absolute;
inset: 0;
border-radius: 50%;
background: hsl(var(--color-surface));
box-shadow:
0 4px 20px rgba(0, 0, 0, 0.08),
0 1px 4px rgba(0, 0, 0, 0.04);
}
.marker {
position: absolute;
top: 50%;
left: 50%;
width: 1px;
height: calc(var(--size) * 0.06);
margin-left: -0.5px;
margin-top: calc(var(--size) * -0.47);
background: hsl(var(--color-muted-foreground) / 0.25);
transform-origin: center calc(var(--size) * 0.47);
}
.marker-main {
width: 2px;
margin-left: -1px;
height: calc(var(--size) * 0.08);
margin-top: calc(var(--size) * -0.47);
background: hsl(var(--color-foreground) / 0.6);
border-radius: 1px;
}
.hands-container {
position: absolute;
inset: 0;
}
.hand {
position: absolute;
top: 50%;
left: 50%;
transform-origin: center bottom;
}
.hour-hand {
width: calc(var(--size) * 0.025);
height: calc(var(--size) * 0.24);
margin-left: calc(var(--size) * -0.0125);
margin-top: calc(var(--size) * -0.24);
background: hsl(var(--color-foreground));
border-radius: calc(var(--size) * 0.0125);
}
.minute-hand {
width: calc(var(--size) * 0.015);
height: calc(var(--size) * 0.34);
margin-left: calc(var(--size) * -0.0075);
margin-top: calc(var(--size) * -0.34);
background: hsl(var(--color-foreground));
border-radius: calc(var(--size) * 0.0075);
}
.second-hand {
width: calc(var(--size) * 0.006);
height: calc(var(--size) * 0.42);
margin-left: calc(var(--size) * -0.003);
margin-top: calc(var(--size) * -0.34);
transform-origin: center 80.95%;
}
.second-line {
position: absolute;
top: 20%;
left: 50%;
transform: translateX(-50%);
width: 100%;
height: 80%;
background: hsl(var(--color-primary));
border-radius: 1px;
}
.second-circle {
position: absolute;
top: 10%;
left: 50%;
transform: translateX(-50%);
width: calc(var(--size) * 0.025);
height: calc(var(--size) * 0.025);
background: hsl(var(--color-primary));
border-radius: 50%;
}
.center-dot {
position: absolute;
top: 50%;
left: 50%;
width: calc(var(--size) * 0.035);
height: calc(var(--size) * 0.035);
margin: calc(var(--size) * -0.0175);
border-radius: 50%;
background: hsl(var(--color-foreground));
}
</style>

View file

@ -0,0 +1,195 @@
<script lang="ts">
interface Props {
hours: number;
minutes: number;
seconds: number;
size?: number;
}
let { hours, minutes, seconds, size = 280 }: Props = $props();
// Clock hand rotations
let secondRotation = $derived((seconds / 60) * 360);
let minuteRotation = $derived(((minutes + seconds / 60) / 60) * 360);
let hourRotation = $derived((((hours % 12) + minutes / 60) / 12) * 360);
// Arabic numerals
const numbers = [12, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11];
</script>
<div class="clock-face-modern" style="--size: {size}px;">
<!-- Background with subtle gradient -->
<div class="clock-bg">
<div class="inner-shadow"></div>
</div>
<!-- Minute tick marks -->
{#each Array(60) as _, i}
{#if i % 5 !== 0}
<div class="tick tick-minute" style="transform: rotate({i * 6}deg)"></div>
{/if}
{/each}
<!-- Arabic numbers -->
{#each numbers as num, i}
<span class="number" style="--angle: {i * 30}deg;">
{num}
</span>
{/each}
<!-- Clock hands -->
<div class="hands-container">
<div class="hand hour-hand" style="transform: rotate({hourRotation}deg)"></div>
<div class="hand minute-hand" style="transform: rotate({minuteRotation}deg)"></div>
<div class="hand second-hand" style="transform: rotate({secondRotation}deg)">
<div class="second-body"></div>
<div class="second-tail"></div>
</div>
</div>
<!-- Center dot -->
<div class="center-cap">
<div class="center-inner"></div>
</div>
</div>
<style>
.clock-face-modern {
position: relative;
width: var(--size);
height: var(--size);
border-radius: 50%;
}
.clock-bg {
position: absolute;
inset: 0;
border-radius: 50%;
background: hsl(var(--color-surface));
border: 3px solid hsl(var(--color-border));
box-shadow:
0 8px 32px rgba(0, 0, 0, 0.1),
0 2px 8px rgba(0, 0, 0, 0.05);
overflow: hidden;
}
.inner-shadow {
position: absolute;
inset: 0;
border-radius: 50%;
box-shadow: inset 0 2px 10px rgba(0, 0, 0, 0.06);
}
.tick {
position: absolute;
top: 50%;
left: 50%;
transform-origin: center center;
}
.tick-minute {
width: 2px;
height: calc(var(--size) * 0.025);
margin-left: -1px;
margin-top: calc(var(--size) * -0.46);
background: hsl(var(--color-muted-foreground) / 0.3);
border-radius: 1px;
}
.number {
position: absolute;
top: 50%;
left: 50%;
font-family:
-apple-system, BlinkMacSystemFont, 'SF Pro Display', 'Segoe UI', Roboto, sans-serif;
font-size: calc(var(--size) * 0.075);
font-weight: 600;
color: hsl(var(--color-foreground));
transform: translate(-50%, -50%) rotate(var(--angle)) translateY(calc(var(--size) * -0.36))
rotate(calc(var(--angle) * -1));
}
.hands-container {
position: absolute;
inset: 0;
}
.hand {
position: absolute;
top: 50%;
left: 50%;
transform-origin: center bottom;
}
.hour-hand {
width: calc(var(--size) * 0.028);
height: calc(var(--size) * 0.26);
margin-left: calc(var(--size) * -0.014);
margin-top: calc(var(--size) * -0.26);
background: hsl(var(--color-foreground));
border-radius: calc(var(--size) * 0.014);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.15);
}
.minute-hand {
width: calc(var(--size) * 0.02);
height: calc(var(--size) * 0.36);
margin-left: calc(var(--size) * -0.01);
margin-top: calc(var(--size) * -0.36);
background: hsl(var(--color-foreground));
border-radius: calc(var(--size) * 0.01);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.15);
}
.second-hand {
width: calc(var(--size) * 0.008);
height: calc(var(--size) * 0.46);
margin-left: calc(var(--size) * -0.004);
margin-top: calc(var(--size) * -0.36);
transform-origin: center 78.26%;
}
.second-body {
position: absolute;
top: 22%;
left: 50%;
transform: translateX(-50%);
width: 100%;
height: 78%;
background: hsl(var(--color-primary));
border-radius: 1px;
}
.second-tail {
position: absolute;
top: 0;
left: 50%;
transform: translateX(-50%);
width: 180%;
height: 22%;
background: hsl(var(--color-primary));
border-radius: 2px;
}
.center-cap {
position: absolute;
top: 50%;
left: 50%;
width: calc(var(--size) * 0.055);
height: calc(var(--size) * 0.055);
margin: calc(var(--size) * -0.0275);
border-radius: 50%;
background: hsl(var(--color-foreground));
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
display: flex;
align-items: center;
justify-content: center;
}
.center-inner {
width: 50%;
height: 50%;
border-radius: 50%;
background: hsl(var(--color-primary));
}
</style>

View file

@ -0,0 +1,286 @@
<script lang="ts">
interface Props {
hours: number;
minutes: number;
seconds: number;
size?: number;
}
let { hours, minutes, seconds, size = 280 }: Props = $props();
// Clock hand rotations
let secondRotation = $derived((seconds / 60) * 360);
let minuteRotation = $derived(((minutes + seconds / 60) / 60) * 360);
let hourRotation = $derived((((hours % 12) + minutes / 60) / 12) * 360);
// Numbers
const numbers = [12, 3, 6, 9];
const angles = [0, 90, 180, 270];
</script>
<div class="clock-face-nautical" style="--size: {size}px;">
<!-- Brass bezel -->
<div class="bezel">
<div class="bezel-inner"></div>
</div>
<!-- White enamel dial -->
<div class="clock-bg">
<div class="dial-texture"></div>
</div>
<!-- Compass rose decoration -->
<div class="compass-rose">
{#each Array(8) as _, i}
<div class="compass-point" style="transform: rotate({i * 45}deg)"></div>
{/each}
</div>
<!-- Minute markers -->
{#each Array(60) as _, i}
<div
class="marker"
class:marker-5={i % 5 === 0 && i % 15 !== 0}
class:marker-15={i % 15 === 0}
style="transform: rotate({i * 6}deg)"
></div>
{/each}
<!-- Cardinal numbers -->
{#each numbers as num, i}
<span class="number" style="--angle: {angles[i]}deg;">
{num}
</span>
{/each}
<!-- Ship's wheel decoration -->
<div class="ships-wheel">
{#each Array(8) as _, i}
<div class="spoke" style="transform: rotate({i * 45}deg)"></div>
{/each}
<div class="wheel-hub"></div>
</div>
<!-- Brand text -->
<div class="brand">MARINE</div>
<!-- Clock hands -->
<div class="hands-container">
<div class="hand hour-hand" style="transform: rotate({hourRotation}deg)"></div>
<div class="hand minute-hand" style="transform: rotate({minuteRotation}deg)"></div>
<div class="hand second-hand" style="transform: rotate({secondRotation}deg)"></div>
</div>
<!-- Center cap -->
<div class="center-cap"></div>
</div>
<style>
.clock-face-nautical {
position: relative;
width: var(--size);
height: var(--size);
border-radius: 50%;
}
.bezel {
position: absolute;
inset: 0;
border-radius: 50%;
background: linear-gradient(
135deg,
#d4a84b 0%,
#b8860b 20%,
#cd9b1d 40%,
#b8860b 60%,
#d4a84b 80%,
#b8860b 100%
);
box-shadow:
0 8px 24px rgba(0, 0, 0, 0.3),
inset 0 2px 4px rgba(255, 255, 255, 0.4);
}
.bezel-inner {
position: absolute;
inset: 4px;
border-radius: 50%;
background: linear-gradient(180deg, #cd9b1d 0%, #b8860b 50%, #8b6914 100%);
}
.clock-bg {
position: absolute;
inset: calc(var(--size) * 0.04);
border-radius: 50%;
background: #f8f8f0;
box-shadow: inset 0 2px 8px rgba(0, 0, 0, 0.1);
overflow: hidden;
}
.dial-texture {
position: absolute;
inset: 0;
background: radial-gradient(circle at 50% 50%, transparent 60%, rgba(184, 134, 11, 0.03) 100%);
}
.compass-rose {
position: absolute;
top: 50%;
left: 50%;
width: 70%;
height: 70%;
transform: translate(-50%, -50%);
}
.compass-point {
position: absolute;
top: 50%;
left: 50%;
width: 1px;
height: 35%;
margin-left: -0.5px;
margin-top: -35%;
background: rgba(184, 134, 11, 0.08);
transform-origin: center bottom;
}
.marker {
position: absolute;
top: 50%;
left: 50%;
width: 1px;
height: calc(var(--size) * 0.02);
margin-left: -0.5px;
margin-top: calc(var(--size) * -0.44);
background: #1a365d;
opacity: 0.4;
transform-origin: center calc(var(--size) * 0.44);
}
.marker-5 {
width: 2px;
margin-left: -1px;
height: calc(var(--size) * 0.035);
opacity: 0.6;
}
.marker-15 {
width: 3px;
margin-left: -1.5px;
height: calc(var(--size) * 0.05);
opacity: 0.8;
background: #1a365d;
}
.number {
position: absolute;
top: 50%;
left: 50%;
font-family: 'Cinzel', 'Times New Roman', serif;
font-size: calc(var(--size) * 0.085);
font-weight: 700;
color: #1a365d;
transform: translate(-50%, -50%) rotate(var(--angle)) translateY(calc(var(--size) * -0.32))
rotate(calc(var(--angle) * -1));
}
.ships-wheel {
position: absolute;
top: 62%;
left: 50%;
width: calc(var(--size) * 0.14);
height: calc(var(--size) * 0.14);
transform: translateX(-50%);
}
.spoke {
position: absolute;
top: 50%;
left: 50%;
width: 2px;
height: 50%;
margin-left: -1px;
margin-top: -50%;
background: #b8860b;
opacity: 0.4;
transform-origin: center bottom;
}
.wheel-hub {
position: absolute;
top: 50%;
left: 50%;
width: 30%;
height: 30%;
transform: translate(-50%, -50%);
border-radius: 50%;
border: 2px solid rgba(184, 134, 11, 0.4);
}
.brand {
position: absolute;
top: 28%;
left: 50%;
transform: translateX(-50%);
font-family: 'Cinzel', 'Times New Roman', serif;
font-size: calc(var(--size) * 0.035);
font-weight: 600;
letter-spacing: 0.25em;
color: #1a365d;
opacity: 0.5;
}
.hands-container {
position: absolute;
inset: 0;
}
.hand {
position: absolute;
top: 50%;
left: 50%;
transform-origin: center bottom;
}
.hour-hand {
width: calc(var(--size) * 0.032);
height: calc(var(--size) * 0.24);
margin-left: calc(var(--size) * -0.016);
margin-top: calc(var(--size) * -0.24);
background: #1a365d;
clip-path: polygon(30% 0%, 70% 0%, 55% 100%, 45% 100%);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
.minute-hand {
width: calc(var(--size) * 0.024);
height: calc(var(--size) * 0.34);
margin-left: calc(var(--size) * -0.012);
margin-top: calc(var(--size) * -0.34);
background: #1a365d;
clip-path: polygon(25% 0%, 75% 0%, 55% 100%, 45% 100%);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
.second-hand {
width: calc(var(--size) * 0.008);
height: calc(var(--size) * 0.4);
margin-left: calc(var(--size) * -0.004);
margin-top: calc(var(--size) * -0.32);
background: #b8860b;
border-radius: 1px;
transform-origin: center 80%;
}
.center-cap {
position: absolute;
top: 50%;
left: 50%;
width: calc(var(--size) * 0.05);
height: calc(var(--size) * 0.05);
margin: calc(var(--size) * -0.025);
border-radius: 50%;
background: linear-gradient(135deg, #d4a84b 0%, #b8860b 100%);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
}
</style>

View file

@ -0,0 +1,221 @@
<script lang="ts">
interface Props {
hours: number;
minutes: number;
seconds: number;
size?: number;
}
let { hours, minutes, seconds, size = 280 }: Props = $props();
let timeString = $derived(
`${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}`
);
let secondsString = $derived(seconds.toString().padStart(2, '0'));
</script>
<div class="clock-face-neon" style="--size: {size}px;">
<div class="neon-container">
<!-- Background glow -->
<div class="bg-glow"></div>
<!-- Main time -->
<div class="time-wrapper">
<span class="neon-glow">{timeString}</span>
<span class="neon-blur">{timeString}</span>
<span class="neon-text">{timeString}</span>
</div>
<!-- Seconds (smaller, different color) -->
<div class="seconds-wrapper">
<span class="seconds-glow">{secondsString}</span>
<span class="seconds-blur">{secondsString}</span>
<span class="seconds-text">{secondsString}</span>
</div>
</div>
</div>
<style>
.clock-face-neon {
display: flex;
align-items: center;
justify-content: center;
}
.neon-container {
position: relative;
padding: 20px 32px;
background: linear-gradient(180deg, #0a0a0a 0%, #050510 100%);
border-radius: 12px;
box-shadow:
0 8px 32px rgba(0, 0, 0, 0.5),
inset 0 1px 1px rgba(255, 255, 255, 0.03);
display: flex;
align-items: baseline;
gap: 12px;
}
.bg-glow {
position: absolute;
inset: 0;
background: radial-gradient(ellipse at center, rgba(255, 0, 255, 0.05) 0%, transparent 70%);
border-radius: 12px;
pointer-events: none;
}
.time-wrapper,
.seconds-wrapper {
position: relative;
display: flex;
align-items: center;
justify-content: center;
}
.neon-glow,
.neon-blur,
.neon-text {
font-family: 'Orbitron', 'Audiowide', 'Rajdhani', sans-serif;
font-size: 3rem;
font-weight: 700;
letter-spacing: 0.05em;
white-space: nowrap;
}
.neon-glow {
position: absolute;
color: transparent;
text-shadow:
0 0 60px #ff00ff,
0 0 100px #ff00ff,
0 0 140px #ff00ff;
filter: blur(20px);
opacity: 0.6;
}
.neon-blur {
position: absolute;
color: #ff00ff;
text-shadow:
0 0 10px #ff00ff,
0 0 20px #ff00ff,
0 0 40px #ff00ff;
filter: blur(2px);
}
.neon-text {
position: relative;
color: #fff;
text-shadow:
0 0 5px #fff,
0 0 10px #ff80ff,
0 0 20px #ff00ff;
animation: flicker 4s infinite;
}
.seconds-glow,
.seconds-blur,
.seconds-text {
font-family: 'Orbitron', 'Audiowide', 'Rajdhani', sans-serif;
font-size: 1.5rem;
font-weight: 500;
letter-spacing: 0.05em;
}
.seconds-glow {
position: absolute;
color: transparent;
text-shadow:
0 0 40px #00ffff,
0 0 80px #00ffff;
filter: blur(15px);
opacity: 0.5;
}
.seconds-blur {
position: absolute;
color: #00ffff;
text-shadow:
0 0 8px #00ffff,
0 0 16px #00ffff;
filter: blur(1px);
}
.seconds-text {
position: relative;
color: #fff;
text-shadow:
0 0 3px #fff,
0 0 8px #80ffff,
0 0 15px #00ffff;
animation: flicker 4s infinite 0.5s;
}
@keyframes flicker {
0%,
100% {
opacity: 1;
}
92% {
opacity: 1;
}
93% {
opacity: 0.85;
}
94% {
opacity: 1;
}
95% {
opacity: 0.9;
}
96% {
opacity: 1;
}
}
/* Theme-based color variations */
:global([data-theme='forest']) .neon-glow {
text-shadow:
0 0 60px #00ff66,
0 0 100px #00ff66,
0 0 140px #00ff66;
}
:global([data-theme='forest']) .neon-blur {
color: #00ff66;
text-shadow:
0 0 10px #00ff66,
0 0 20px #00ff66,
0 0 40px #00ff66;
}
:global([data-theme='forest']) .neon-text {
text-shadow:
0 0 5px #fff,
0 0 10px #80ff99,
0 0 20px #00ff66;
}
:global([data-theme='forest']) .bg-glow {
background: radial-gradient(ellipse at center, rgba(0, 255, 102, 0.05) 0%, transparent 70%);
}
:global([data-theme='ocean']) .neon-glow {
text-shadow:
0 0 60px #0099ff,
0 0 100px #0099ff,
0 0 140px #0099ff;
}
:global([data-theme='ocean']) .neon-blur {
color: #0099ff;
text-shadow:
0 0 10px #0099ff,
0 0 20px #0099ff,
0 0 40px #0099ff;
}
:global([data-theme='ocean']) .neon-text {
text-shadow:
0 0 5px #fff,
0 0 10px #80ccff,
0 0 20px #0099ff;
}
:global([data-theme='ocean']) .bg-glow {
background: radial-gradient(ellipse at center, rgba(0, 153, 255, 0.05) 0%, transparent 70%);
}
</style>

View file

@ -0,0 +1,283 @@
<script lang="ts">
interface Props {
hours: number;
minutes: number;
seconds: number;
size?: number;
}
let { hours, minutes, seconds, size = 280 }: Props = $props();
let timeString = $derived(
`${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`
);
// Radar sweep angle based on seconds
let sweepAngle = $derived((seconds / 60) * 360);
</script>
<div class="clock-face-radar" style="--size: {size}px;">
<div class="radar-display">
<!-- Radar screen -->
<div class="radar-screen">
<!-- Grid lines -->
<div class="grid-horizontal"></div>
<div class="grid-vertical"></div>
<div class="grid-circle circle-1"></div>
<div class="grid-circle circle-2"></div>
<div class="grid-circle circle-3"></div>
<!-- Sweep line -->
<div class="sweep-container" style="transform: rotate({sweepAngle}deg)">
<div class="sweep-line"></div>
<div class="sweep-glow"></div>
</div>
<!-- Center dot -->
<div class="center-dot"></div>
<!-- Time blips -->
<div class="blip blip-hours" style="--angle: {(hours % 12) * 30}deg;">
<div class="blip-dot"></div>
</div>
<div class="blip blip-minutes" style="--angle: {minutes * 6}deg;">
<div class="blip-dot"></div>
</div>
</div>
<!-- Digital readout -->
<div class="digital-readout">
<span class="readout-label">TIME</span>
<span class="readout-value">{timeString}</span>
</div>
<!-- Status indicators -->
<div class="status-row">
<div class="status">
<span class="status-dot active"></span>
<span class="status-text">SYNC</span>
</div>
<div class="status">
<span class="status-dot active"></span>
<span class="status-text">TRAC</span>
</div>
</div>
</div>
</div>
<style>
.clock-face-radar {
display: flex;
align-items: center;
justify-content: center;
}
.radar-display {
background: linear-gradient(180deg, #0a1a0a 0%, #051505 100%);
border-radius: 12px;
padding: 16px;
box-shadow:
0 8px 32px rgba(0, 0, 0, 0.5),
inset 0 1px 1px rgba(0, 255, 0, 0.05);
}
.radar-screen {
position: relative;
width: 180px;
height: 180px;
background: radial-gradient(circle at center, #0a2a0a 0%, #051505 70%, #030a03 100%);
border-radius: 50%;
border: 2px solid #0f3f0f;
overflow: hidden;
box-shadow: inset 0 0 30px rgba(0, 255, 0, 0.1);
}
.grid-horizontal,
.grid-vertical {
position: absolute;
background: rgba(0, 255, 0, 0.1);
}
.grid-horizontal {
top: 50%;
left: 10%;
right: 10%;
height: 1px;
transform: translateY(-50%);
}
.grid-vertical {
left: 50%;
top: 10%;
bottom: 10%;
width: 1px;
transform: translateX(-50%);
}
.grid-circle {
position: absolute;
top: 50%;
left: 50%;
border-radius: 50%;
border: 1px solid rgba(0, 255, 0, 0.1);
}
.circle-1 {
width: 30%;
height: 30%;
transform: translate(-50%, -50%);
}
.circle-2 {
width: 60%;
height: 60%;
transform: translate(-50%, -50%);
}
.circle-3 {
width: 90%;
height: 90%;
transform: translate(-50%, -50%);
}
.sweep-container {
position: absolute;
top: 50%;
left: 50%;
width: 50%;
height: 2px;
transform-origin: left center;
transition: transform 1s linear;
}
.sweep-line {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(90deg, rgba(0, 255, 0, 0.8) 0%, transparent 100%);
}
.sweep-glow {
position: absolute;
top: 50%;
left: 0;
width: 100%;
height: 40px;
transform: translateY(-50%);
background: linear-gradient(90deg, rgba(0, 255, 0, 0.2) 0%, transparent 100%);
clip-path: polygon(0% 50%, 100% 0%, 100% 100%);
}
.center-dot {
position: absolute;
top: 50%;
left: 50%;
width: 8px;
height: 8px;
transform: translate(-50%, -50%);
background: #00ff00;
border-radius: 50%;
box-shadow: 0 0 8px #00ff00;
}
.blip {
position: absolute;
top: 50%;
left: 50%;
width: 35%;
height: 2px;
transform-origin: left center;
transform: rotate(var(--angle));
}
.blip-dot {
position: absolute;
right: 0;
top: 50%;
transform: translateY(-50%);
width: 10px;
height: 10px;
background: #00ff00;
border-radius: 50%;
box-shadow: 0 0 6px #00ff00;
animation: pulse 2s ease-in-out infinite;
}
.blip-hours .blip-dot {
width: 12px;
height: 12px;
background: #00ffaa;
box-shadow: 0 0 8px #00ffaa;
}
@keyframes pulse {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.5;
}
}
.digital-readout {
display: flex;
flex-direction: column;
align-items: center;
margin-top: 12px;
padding: 8px 16px;
background: rgba(0, 255, 0, 0.05);
border: 1px solid rgba(0, 255, 0, 0.2);
border-radius: 4px;
}
.readout-label {
font-family: 'Share Tech Mono', monospace;
font-size: 0.6rem;
color: rgba(0, 255, 0, 0.5);
letter-spacing: 0.2em;
}
.readout-value {
font-family: 'Share Tech Mono', monospace;
font-size: 1.2rem;
font-weight: 600;
color: #00ff00;
text-shadow: 0 0 8px rgba(0, 255, 0, 0.5);
letter-spacing: 0.1em;
}
.status-row {
display: flex;
justify-content: center;
gap: 16px;
margin-top: 8px;
}
.status {
display: flex;
align-items: center;
gap: 4px;
}
.status-dot {
width: 6px;
height: 6px;
background: #333;
border-radius: 50%;
}
.status-dot.active {
background: #00ff00;
box-shadow: 0 0 4px #00ff00;
}
.status-text {
font-family: 'Share Tech Mono', monospace;
font-size: 0.6rem;
color: rgba(0, 255, 0, 0.6);
letter-spacing: 0.1em;
}
</style>

View file

@ -0,0 +1,207 @@
<script lang="ts">
interface Props {
hours: number;
minutes: number;
seconds: number;
size?: number;
}
let { hours, minutes, seconds, size = 280 }: Props = $props();
// Clock hand rotations
let secondRotation = $derived((seconds / 60) * 360);
let minuteRotation = $derived(((minutes + seconds / 60) / 60) * 360);
let hourRotation = $derived((((hours % 12) + minutes / 60) / 12) * 360);
const numbers = [12, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11];
</script>
<div class="clock-face-railway" style="--size: {size}px;">
<!-- Black bezel -->
<div class="bezel"></div>
<!-- White dial -->
<div class="clock-bg"></div>
<!-- Minute markers -->
{#each Array(60) as _, i}
{#if i % 5 !== 0}
<div class="tick tick-minute" style="transform: rotate({i * 6}deg)"></div>
{/if}
{/each}
<!-- Hour markers - bold rectangles -->
{#each Array(12) as _, i}
<div class="tick tick-hour" style="transform: rotate({i * 30}deg)"></div>
{/each}
<!-- Numbers -->
{#each numbers as num, i}
<span class="number" style="--angle: {i * 30}deg;">
{num}
</span>
{/each}
<!-- SBB/CFF brand mark -->
<div class="brand">SBB CFF FFS</div>
<!-- Clock hands -->
<div class="hands-container">
<!-- Hour hand - rounded rectangle -->
<div class="hand hour-hand" style="transform: rotate({hourRotation}deg)"></div>
<!-- Minute hand - longer rounded rectangle -->
<div class="hand minute-hand" style="transform: rotate({minuteRotation}deg)"></div>
<!-- Second hand - distinctive red circle (Mondaine style) -->
<div class="hand second-hand" style="transform: rotate({secondRotation}deg)">
<div class="second-shaft"></div>
<div class="second-lollipop"></div>
</div>
</div>
<!-- Center cap -->
<div class="center-cap"></div>
</div>
<style>
.clock-face-railway {
position: relative;
width: var(--size);
height: var(--size);
border-radius: 50%;
}
.bezel {
position: absolute;
inset: 0;
border-radius: 50%;
background: #1a1a1a;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2);
}
.clock-bg {
position: absolute;
inset: calc(var(--size) * 0.03);
border-radius: 50%;
background: #ffffff;
}
.tick {
position: absolute;
top: 50%;
left: 50%;
transform-origin: center center;
}
.tick-minute {
width: 2px;
height: calc(var(--size) * 0.025);
margin-left: -1px;
margin-top: calc(var(--size) * -0.44);
background: #1a1a1a;
}
.tick-hour {
width: calc(var(--size) * 0.025);
height: calc(var(--size) * 0.065);
margin-left: calc(var(--size) * -0.0125);
margin-top: calc(var(--size) * -0.44);
background: #1a1a1a;
border-radius: 2px;
}
.number {
position: absolute;
top: 50%;
left: 50%;
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
font-size: calc(var(--size) * 0.075);
font-weight: 700;
color: #1a1a1a;
transform: translate(-50%, -50%) rotate(var(--angle)) translateY(calc(var(--size) * -0.32))
rotate(calc(var(--angle) * -1));
}
.brand {
position: absolute;
top: 68%;
left: 50%;
transform: translateX(-50%);
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
font-size: calc(var(--size) * 0.025);
font-weight: 500;
letter-spacing: 0.1em;
color: #666;
}
.hands-container {
position: absolute;
inset: 0;
}
.hand {
position: absolute;
top: 50%;
left: 50%;
transform-origin: center bottom;
}
.hour-hand {
width: calc(var(--size) * 0.045);
height: calc(var(--size) * 0.23);
margin-left: calc(var(--size) * -0.0225);
margin-top: calc(var(--size) * -0.23);
background: #1a1a1a;
border-radius: calc(var(--size) * 0.0225);
}
.minute-hand {
width: calc(var(--size) * 0.035);
height: calc(var(--size) * 0.33);
margin-left: calc(var(--size) * -0.0175);
margin-top: calc(var(--size) * -0.33);
background: #1a1a1a;
border-radius: calc(var(--size) * 0.0175);
}
.second-hand {
width: calc(var(--size) * 0.012);
height: calc(var(--size) * 0.42);
margin-left: calc(var(--size) * -0.006);
margin-top: calc(var(--size) * -0.36);
transform-origin: center 85.71%;
}
.second-shaft {
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: calc(var(--size) * 0.008);
height: 85%;
background: #dc2626;
}
.second-lollipop {
position: absolute;
top: 0;
left: 50%;
transform: translateX(-50%);
width: calc(var(--size) * 0.055);
height: calc(var(--size) * 0.055);
background: #dc2626;
border-radius: 50%;
}
.center-cap {
position: absolute;
top: 50%;
left: 50%;
width: calc(var(--size) * 0.04);
height: calc(var(--size) * 0.04);
margin: calc(var(--size) * -0.02);
border-radius: 50%;
background: #1a1a1a;
}
</style>

View file

@ -0,0 +1,294 @@
<script lang="ts">
interface Props {
hours: number;
minutes: number;
seconds: number;
size?: number;
}
let { hours, minutes, seconds, size = 280 }: Props = $props();
// 3x5 pixel font patterns
const pixelPatterns: Record<string, number[][]> = {
'0': [
[1, 1, 1],
[1, 0, 1],
[1, 0, 1],
[1, 0, 1],
[1, 1, 1],
],
'1': [
[0, 1, 0],
[1, 1, 0],
[0, 1, 0],
[0, 1, 0],
[1, 1, 1],
],
'2': [
[1, 1, 1],
[0, 0, 1],
[1, 1, 1],
[1, 0, 0],
[1, 1, 1],
],
'3': [
[1, 1, 1],
[0, 0, 1],
[1, 1, 1],
[0, 0, 1],
[1, 1, 1],
],
'4': [
[1, 0, 1],
[1, 0, 1],
[1, 1, 1],
[0, 0, 1],
[0, 0, 1],
],
'5': [
[1, 1, 1],
[1, 0, 0],
[1, 1, 1],
[0, 0, 1],
[1, 1, 1],
],
'6': [
[1, 1, 1],
[1, 0, 0],
[1, 1, 1],
[1, 0, 1],
[1, 1, 1],
],
'7': [
[1, 1, 1],
[0, 0, 1],
[0, 0, 1],
[0, 0, 1],
[0, 0, 1],
],
'8': [
[1, 1, 1],
[1, 0, 1],
[1, 1, 1],
[1, 0, 1],
[1, 1, 1],
],
'9': [
[1, 1, 1],
[1, 0, 1],
[1, 1, 1],
[0, 0, 1],
[1, 1, 1],
],
};
const colonPattern = [[0], [1], [0], [1], [0]];
let h1 = $derived(Math.floor(hours / 10).toString());
let h2 = $derived((hours % 10).toString());
let m1 = $derived(Math.floor(minutes / 10).toString());
let m2 = $derived((minutes % 10).toString());
let s1 = $derived(Math.floor(seconds / 10).toString());
let s2 = $derived((seconds % 10).toString());
</script>
<div class="clock-face-retro" style="--size: {size}px;">
<div class="retro-case">
<!-- CRT screen effect -->
<div class="crt-screen">
<div class="scanlines"></div>
<div class="crt-glow"></div>
<div class="pixels-container">
<!-- Hours -->
<div class="digit-pixels">
{#each pixelPatterns[h1] as row}
<div class="pixel-row">
{#each row as pixel}
<div class="pixel" class:on={pixel === 1}></div>
{/each}
</div>
{/each}
</div>
<div class="digit-pixels">
{#each pixelPatterns[h2] as row}
<div class="pixel-row">
{#each row as pixel}
<div class="pixel" class:on={pixel === 1}></div>
{/each}
</div>
{/each}
</div>
<!-- Colon -->
<div class="colon-pixels">
{#each colonPattern as row}
<div class="pixel-row">
{#each row as pixel}
<div class="pixel pixel-colon" class:on={pixel === 1}></div>
{/each}
</div>
{/each}
</div>
<!-- Minutes -->
<div class="digit-pixels">
{#each pixelPatterns[m1] as row}
<div class="pixel-row">
{#each row as pixel}
<div class="pixel" class:on={pixel === 1}></div>
{/each}
</div>
{/each}
</div>
<div class="digit-pixels">
{#each pixelPatterns[m2] as row}
<div class="pixel-row">
{#each row as pixel}
<div class="pixel" class:on={pixel === 1}></div>
{/each}
</div>
{/each}
</div>
<!-- Small colon -->
<div class="colon-pixels colon-small">
{#each colonPattern as row}
<div class="pixel-row">
{#each row as pixel}
<div class="pixel pixel-small" class:on={pixel === 1}></div>
{/each}
</div>
{/each}
</div>
<!-- Seconds -->
<div class="digit-pixels digit-small">
{#each pixelPatterns[s1] as row}
<div class="pixel-row">
{#each row as pixel}
<div class="pixel pixel-small" class:on={pixel === 1}></div>
{/each}
</div>
{/each}
</div>
<div class="digit-pixels digit-small">
{#each pixelPatterns[s2] as row}
<div class="pixel-row">
{#each row as pixel}
<div class="pixel pixel-small" class:on={pixel === 1}></div>
{/each}
</div>
{/each}
</div>
</div>
</div>
</div>
</div>
<style>
.clock-face-retro {
display: flex;
align-items: center;
justify-content: center;
}
.retro-case {
background: linear-gradient(180deg, #4a4a4a 0%, #2a2a2a 50%, #1a1a1a 100%);
border-radius: 16px;
padding: 12px;
box-shadow:
0 8px 32px rgba(0, 0, 0, 0.4),
inset 0 2px 4px rgba(255, 255, 255, 0.1);
}
.crt-screen {
position: relative;
background: #0a0a0a;
border-radius: 8px;
padding: 20px 24px;
box-shadow: inset 0 0 30px rgba(0, 0, 0, 0.5);
overflow: hidden;
}
.scanlines {
position: absolute;
inset: 0;
background: repeating-linear-gradient(
0deg,
transparent,
transparent 2px,
rgba(0, 0, 0, 0.15) 2px,
rgba(0, 0, 0, 0.15) 4px
);
pointer-events: none;
}
.crt-glow {
position: absolute;
inset: 0;
background: radial-gradient(ellipse at center, rgba(0, 255, 0, 0.03) 0%, transparent 70%);
pointer-events: none;
}
.pixels-container {
display: flex;
align-items: flex-end;
gap: 6px;
position: relative;
z-index: 1;
}
.digit-pixels {
display: flex;
flex-direction: column;
gap: 3px;
}
.digit-small {
transform: scale(0.7);
transform-origin: bottom left;
}
.pixel-row {
display: flex;
gap: 3px;
}
.pixel {
width: 8px;
height: 8px;
background: #1a2a1a;
border-radius: 1px;
transition: all 80ms;
}
.pixel.on {
background: #00ff00;
box-shadow:
0 0 4px #00ff00,
0 0 8px #00ff00,
0 0 12px rgba(0, 255, 0, 0.4);
}
.pixel-small {
width: 6px;
height: 6px;
}
.pixel-colon {
width: 4px;
height: 4px;
}
.colon-pixels {
display: flex;
flex-direction: column;
gap: 3px;
padding: 0 2px;
}
.colon-small {
padding: 0 1px;
}
</style>

View file

@ -0,0 +1,313 @@
<script lang="ts">
interface Props {
hours: number;
minutes: number;
seconds: number;
size?: number;
}
let { hours, minutes, seconds, size = 280 }: Props = $props();
// Clock hand rotations
let secondRotation = $derived((seconds / 60) * 360);
let minuteRotation = $derived(((minutes + seconds / 60) / 60) * 360);
let hourRotation = $derived((((hours % 12) + minutes / 60) / 12) * 360);
// Main numbers
const mainNumbers = [
{ num: 12, angle: 0 },
{ num: 3, angle: 90 },
{ num: 6, angle: 180 },
{ num: 9, angle: 270 },
];
</script>
<div class="clock-face-sporty" style="--size: {size}px;">
<!-- Textured bezel -->
<div class="bezel">
{#each Array(120) as _, i}
<div class="bezel-notch" style="transform: rotate({i * 3}deg)"></div>
{/each}
</div>
<!-- Dark background -->
<div class="clock-bg">
<div class="inner-bevel"></div>
</div>
<!-- Minute markers -->
{#each Array(60) as _, i}
<div
class="marker"
class:marker-5={i % 5 === 0}
class:marker-15={i % 15 === 0}
style="transform: rotate({i * 6}deg)"
></div>
{/each}
<!-- Main numbers -->
{#each mainNumbers as { num, angle }}
<span class="number" style="--angle: {angle}deg;">
{num}
</span>
{/each}
<!-- Subdial decorations -->
<div class="subdial subdial-top">
<span class="subdial-text">CHRONO</span>
</div>
<div class="subdial subdial-bottom">
<div class="subdial-ring"></div>
</div>
<!-- Clock hands -->
<div class="hands-container">
<div class="hand hour-hand" style="transform: rotate({hourRotation}deg)">
<div class="hand-arrow"></div>
</div>
<div class="hand minute-hand" style="transform: rotate({minuteRotation}deg)">
<div class="hand-arrow"></div>
</div>
<div class="hand second-hand" style="transform: rotate({secondRotation}deg)">
<div class="second-needle"></div>
<div class="second-counter"></div>
<div class="second-circle"></div>
</div>
</div>
<!-- Center hub -->
<div class="center-hub">
<div class="hub-inner"></div>
</div>
</div>
<style>
.clock-face-sporty {
position: relative;
width: var(--size);
height: var(--size);
border-radius: 50%;
}
.bezel {
position: absolute;
inset: 0;
border-radius: 50%;
background: linear-gradient(
180deg,
hsl(var(--color-foreground)) 0%,
hsl(var(--color-muted-foreground)) 100%
);
box-shadow:
0 8px 24px rgba(0, 0, 0, 0.3),
inset 0 1px 2px rgba(255, 255, 255, 0.2);
}
.bezel-notch {
position: absolute;
top: 50%;
left: 50%;
width: 2px;
height: calc(var(--size) * 0.02);
margin-left: -1px;
margin-top: calc(var(--size) * -0.5);
background: hsl(var(--color-background) / 0.3);
transform-origin: center calc(var(--size) * 0.5);
}
.clock-bg {
position: absolute;
inset: calc(var(--size) * 0.04);
border-radius: 50%;
background: hsl(var(--color-background));
box-shadow: inset 0 4px 12px rgba(0, 0, 0, 0.2);
}
.inner-bevel {
position: absolute;
inset: 0;
border-radius: 50%;
border: 2px solid hsl(var(--color-border) / 0.3);
}
.marker {
position: absolute;
top: 50%;
left: 50%;
width: 2px;
height: calc(var(--size) * 0.03);
margin-left: -1px;
margin-top: calc(var(--size) * -0.42);
background: hsl(var(--color-muted-foreground) / 0.5);
transform-origin: center calc(var(--size) * 0.42);
}
.marker-5 {
width: 3px;
margin-left: -1.5px;
height: calc(var(--size) * 0.05);
background: hsl(var(--color-foreground));
}
.marker-15 {
width: 4px;
margin-left: -2px;
height: calc(var(--size) * 0.06);
background: hsl(var(--color-foreground));
border-radius: 1px;
}
.number {
position: absolute;
top: 50%;
left: 50%;
font-family: 'Impact', 'Arial Black', 'Helvetica Neue', sans-serif;
font-size: calc(var(--size) * 0.095);
font-weight: 900;
color: hsl(var(--color-foreground));
letter-spacing: -0.02em;
transform: translate(-50%, -50%) rotate(var(--angle)) translateY(calc(var(--size) * -0.32))
rotate(calc(var(--angle) * -1));
}
.subdial {
position: absolute;
left: 50%;
transform: translateX(-50%);
}
.subdial-top {
top: 26%;
}
.subdial-text {
font-family: 'Arial Narrow', Arial, sans-serif;
font-size: calc(var(--size) * 0.03);
font-weight: 700;
letter-spacing: 0.15em;
color: hsl(var(--color-muted-foreground));
text-transform: uppercase;
}
.subdial-bottom {
top: 62%;
}
.subdial-ring {
width: calc(var(--size) * 0.15);
height: calc(var(--size) * 0.15);
border-radius: 50%;
border: 2px solid hsl(var(--color-border) / 0.4);
background: hsl(var(--color-background) / 0.5);
}
.hands-container {
position: absolute;
inset: 0;
}
.hand {
position: absolute;
top: 50%;
left: 50%;
transform-origin: center bottom;
}
.hour-hand {
width: calc(var(--size) * 0.04);
height: calc(var(--size) * 0.22);
margin-left: calc(var(--size) * -0.02);
margin-top: calc(var(--size) * -0.22);
}
.hour-hand .hand-arrow {
width: 100%;
height: 100%;
background: hsl(var(--color-foreground));
clip-path: polygon(15% 0%, 85% 0%, 100% 20%, 60% 100%, 40% 100%, 0% 20%);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
}
.minute-hand {
width: calc(var(--size) * 0.03);
height: calc(var(--size) * 0.32);
margin-left: calc(var(--size) * -0.015);
margin-top: calc(var(--size) * -0.32);
}
.minute-hand .hand-arrow {
width: 100%;
height: 100%;
background: hsl(var(--color-foreground));
clip-path: polygon(20% 0%, 80% 0%, 100% 15%, 55% 100%, 45% 100%, 0% 15%);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
}
.second-hand {
width: calc(var(--size) * 0.012);
height: calc(var(--size) * 0.48);
margin-left: calc(var(--size) * -0.006);
margin-top: calc(var(--size) * -0.36);
transform-origin: center 75%;
}
.second-needle {
position: absolute;
top: 25%;
left: 50%;
transform: translateX(-50%);
width: 100%;
height: 75%;
background: hsl(var(--color-error));
border-radius: 1px;
}
.second-counter {
position: absolute;
top: 0;
left: 50%;
transform: translateX(-50%);
width: calc(var(--size) * 0.025);
height: 25%;
background: hsl(var(--color-error));
border-radius: 3px;
}
.second-circle {
position: absolute;
top: 20%;
left: 50%;
transform: translateX(-50%);
width: calc(var(--size) * 0.03);
height: calc(var(--size) * 0.03);
background: hsl(var(--color-error));
border-radius: 50%;
}
.center-hub {
position: absolute;
top: 50%;
left: 50%;
width: calc(var(--size) * 0.06);
height: calc(var(--size) * 0.06);
margin: calc(var(--size) * -0.03);
border-radius: 50%;
background: linear-gradient(
135deg,
hsl(var(--color-foreground)) 0%,
hsl(var(--color-muted-foreground)) 100%
);
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.4);
display: flex;
align-items: center;
justify-content: center;
}
.hub-inner {
width: 50%;
height: 50%;
border-radius: 50%;
background: hsl(var(--color-error));
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.3);
}
</style>

View file

@ -0,0 +1,171 @@
<script lang="ts">
interface Props {
hours: number;
minutes: number;
seconds: number;
size?: number;
}
let { hours, minutes, seconds, size = 280 }: Props = $props();
let timeString = $derived(
`${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`
);
// Get current date info
let now = $derived(new Date());
let dateString = $derived(
now.toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric' })
);
</script>
<div class="clock-face-terminal" style="--size: {size}px;">
<div class="terminal-window">
<!-- Title bar -->
<div class="title-bar">
<div class="title-buttons">
<span class="btn btn-close"></span>
<span class="btn btn-minimize"></span>
<span class="btn btn-maximize"></span>
</div>
<span class="title-text">clock@system:~</span>
</div>
<!-- Terminal content -->
<div class="terminal-content">
<div class="line">
<span class="prompt">$</span>
<span class="command">date --format="%T"</span>
</div>
<div class="output time-output">
{timeString}
</div>
<div class="line">
<span class="prompt">$</span>
<span class="command">date --format="%a %b %d"</span>
</div>
<div class="output date-output">
{dateString}
</div>
<div class="line">
<span class="prompt">$</span>
<span class="cursor">_</span>
</div>
</div>
</div>
</div>
<style>
.clock-face-terminal {
display: flex;
align-items: center;
justify-content: center;
}
.terminal-window {
background: #1e1e1e;
border-radius: 8px;
overflow: hidden;
box-shadow:
0 8px 32px rgba(0, 0, 0, 0.4),
0 0 0 1px rgba(255, 255, 255, 0.05);
min-width: 260px;
}
.title-bar {
display: flex;
align-items: center;
gap: 12px;
padding: 8px 12px;
background: #2d2d2d;
border-bottom: 1px solid #3a3a3a;
}
.title-buttons {
display: flex;
gap: 6px;
}
.btn {
width: 12px;
height: 12px;
border-radius: 50%;
}
.btn-close {
background: #ff5f56;
}
.btn-minimize {
background: #ffbd2e;
}
.btn-maximize {
background: #27ca40;
}
.title-text {
font-family: 'SF Mono', 'Monaco', 'Cascadia Code', monospace;
font-size: 0.7rem;
color: #888;
}
.terminal-content {
padding: 16px;
font-family: 'SF Mono', 'Monaco', 'Cascadia Code', monospace;
}
.line {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 4px;
}
.prompt {
color: #27ca40;
font-weight: 600;
}
.command {
color: #888;
font-size: 0.85rem;
}
.output {
margin-bottom: 12px;
padding-left: 16px;
}
.time-output {
font-size: 2rem;
font-weight: 600;
color: #00ff88;
text-shadow: 0 0 10px rgba(0, 255, 136, 0.3);
letter-spacing: 0.05em;
}
.date-output {
font-size: 0.9rem;
color: #61afef;
}
.cursor {
color: #27ca40;
animation: blink 1s step-end infinite;
}
@keyframes blink {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0;
}
}
</style>

View file

@ -0,0 +1,190 @@
<script lang="ts">
interface Props {
hours: number;
minutes: number;
seconds: number;
size?: number;
}
let { hours, minutes, seconds, size = 280 }: Props = $props();
let h1 = $derived(Math.floor(hours / 10));
let h2 = $derived(hours % 10);
let m1 = $derived(Math.floor(minutes / 10));
let m2 = $derived(minutes % 10);
let s1 = $derived(Math.floor(seconds / 10));
let s2 = $derived(seconds % 10);
const digits = $derived([h1, h2, -1, m1, m2, -1, s1, s2]);
</script>
<div class="clock-face-typewriter" style="--size: {size}px;">
<div class="typewriter-case">
<!-- Paper roll effect -->
<div class="paper-top"></div>
<!-- Main display area -->
<div class="paper">
<div class="paper-texture"></div>
<div class="digits-row">
{#each digits as digit, i}
{#if digit === -1}
<div class="separator">:</div>
{:else}
<div class="key-container">
<div class="key" class:key-small={i >= 5}>
<span class="key-text">{digit}</span>
</div>
<div class="key-shadow"></div>
</div>
{/if}
{/each}
</div>
</div>
<!-- Typewriter bar -->
<div class="type-bar">
<div class="bar-detail"></div>
</div>
</div>
</div>
<style>
.clock-face-typewriter {
display: flex;
align-items: center;
justify-content: center;
}
.typewriter-case {
position: relative;
background: linear-gradient(180deg, #3d3d3d 0%, #2a2a2a 50%, #1a1a1a 100%);
border-radius: 12px;
padding: 16px;
box-shadow:
0 8px 32px rgba(0, 0, 0, 0.4),
inset 0 1px 2px rgba(255, 255, 255, 0.1);
}
.paper-top {
position: absolute;
top: 0;
left: 20px;
right: 20px;
height: 8px;
background: #f5f0e6;
border-radius: 0 0 4px 4px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
.paper {
position: relative;
background: #f5f0e6;
border-radius: 4px;
padding: 20px 16px;
margin-top: 8px;
box-shadow: inset 0 2px 8px rgba(0, 0, 0, 0.1);
}
.paper-texture {
position: absolute;
inset: 0;
background: repeating-linear-gradient(
0deg,
transparent,
transparent 24px,
rgba(0, 0, 0, 0.02) 24px,
rgba(0, 0, 0, 0.02) 25px
);
border-radius: 4px;
pointer-events: none;
}
.digits-row {
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
position: relative;
}
.key-container {
position: relative;
}
.key {
position: relative;
width: 36px;
height: 44px;
background: linear-gradient(180deg, #4a4a4a 0%, #2d2d2d 100%);
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
box-shadow:
0 4px 8px rgba(0, 0, 0, 0.3),
inset 0 1px 2px rgba(255, 255, 255, 0.2),
inset 0 -1px 2px rgba(0, 0, 0, 0.3);
transform: translateY(-2px);
transition: transform 100ms;
}
.key-small {
width: 28px;
height: 34px;
}
.key-text {
font-family: 'American Typewriter', 'Courier New', monospace;
font-size: 1.5rem;
font-weight: 700;
color: #f5f0e6;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);
}
.key-small .key-text {
font-size: 1.1rem;
}
.key-shadow {
position: absolute;
bottom: -4px;
left: 2px;
right: 2px;
height: 6px;
background: #1a1a1a;
border-radius: 0 0 4px 4px;
z-index: -1;
}
.separator {
font-family: 'American Typewriter', 'Courier New', monospace;
font-size: 1.5rem;
font-weight: 700;
color: #2d2d2d;
padding: 0 2px;
}
.type-bar {
height: 6px;
background: linear-gradient(90deg, #666 0%, #888 50%, #666 100%);
border-radius: 3px;
margin-top: 12px;
box-shadow:
inset 0 1px 2px rgba(255, 255, 255, 0.3),
0 2px 4px rgba(0, 0, 0, 0.3);
}
.bar-detail {
height: 100%;
background: repeating-linear-gradient(
90deg,
transparent,
transparent 4px,
rgba(0, 0, 0, 0.1) 4px,
rgba(0, 0, 0, 0.1) 5px
);
border-radius: 3px;
}
</style>

View file

@ -0,0 +1,301 @@
<script lang="ts">
interface Props {
hours: number;
minutes: number;
seconds: number;
size?: number;
}
let { hours, minutes, seconds, size = 280 }: Props = $props();
// Clock hand rotations
let secondRotation = $derived((seconds / 60) * 360);
let minuteRotation = $derived(((minutes + seconds / 60) / 60) * 360);
let hourRotation = $derived((((hours % 12) + minutes / 60) / 12) * 360);
// Vintage numbers
const numbers = [12, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11];
</script>
<div class="clock-face-vintage" style="--size: {size}px;">
<!-- Aged outer frame -->
<div class="outer-frame"></div>
<!-- Background with aged texture -->
<div class="clock-bg">
<div class="age-texture"></div>
<div class="stains"></div>
</div>
<!-- Decorative inner border -->
<div class="inner-border"></div>
<!-- Minute tick marks -->
{#each Array(60) as _, i}
{#if i % 5 !== 0}
<div class="tick tick-minute" style="transform: rotate({i * 6}deg)"></div>
{/if}
{/each}
<!-- Hour tick marks -->
{#each Array(12) as _, i}
<div class="tick tick-hour" style="transform: rotate({i * 30}deg)"></div>
{/each}
<!-- Vintage numbers -->
{#each numbers as num, i}
<span class="number" style="--angle: {i * 30}deg;">
{num}
</span>
{/each}
<!-- Brand text -->
<div class="brand">
<span class="brand-name">VINTAGE</span>
<span class="brand-sub">EST. 1920</span>
</div>
<!-- Clock hands -->
<div class="hands-container">
<div class="hand hour-hand" style="transform: rotate({hourRotation}deg)">
<div class="spade-tip"></div>
<div class="hand-body"></div>
</div>
<div class="hand minute-hand" style="transform: rotate({minuteRotation}deg)">
<div class="spade-tip"></div>
<div class="hand-body"></div>
</div>
<div class="hand second-hand" style="transform: rotate({secondRotation}deg)"></div>
</div>
<!-- Center cap -->
<div class="center-cap">
<div class="cap-detail"></div>
</div>
</div>
<style>
.clock-face-vintage {
position: relative;
width: var(--size);
height: var(--size);
border-radius: 50%;
}
.outer-frame {
position: absolute;
inset: 0;
border-radius: 50%;
background: linear-gradient(
135deg,
#8b6914 0%,
#c9a227 25%,
#a67c00 50%,
#c9a227 75%,
#8b6914 100%
);
box-shadow:
0 8px 24px rgba(0, 0, 0, 0.3),
inset 0 2px 4px rgba(255, 255, 255, 0.3),
inset 0 -2px 4px rgba(0, 0, 0, 0.2);
}
.clock-bg {
position: absolute;
inset: calc(var(--size) * 0.035);
border-radius: 50%;
background: linear-gradient(180deg, #f5e6c8 0%, #e8d4a8 50%, #d4c090 100%);
overflow: hidden;
}
.age-texture {
position: absolute;
inset: 0;
background:
radial-gradient(circle at 20% 80%, rgba(139, 90, 43, 0.1) 0%, transparent 30%),
radial-gradient(circle at 80% 20%, rgba(139, 90, 43, 0.08) 0%, transparent 25%),
radial-gradient(circle at 50% 50%, transparent 40%, rgba(0, 0, 0, 0.05) 100%);
}
.stains {
position: absolute;
inset: 0;
background:
radial-gradient(ellipse at 30% 70%, rgba(101, 67, 33, 0.08) 0%, transparent 20%),
radial-gradient(ellipse at 70% 30%, rgba(101, 67, 33, 0.06) 0%, transparent 15%);
}
.inner-border {
position: absolute;
inset: calc(var(--size) * 0.06);
border-radius: 50%;
border: 2px solid rgba(139, 105, 20, 0.3);
}
.tick {
position: absolute;
top: 50%;
left: 50%;
transform-origin: center center;
}
.tick-minute {
width: 1px;
height: calc(var(--size) * 0.025);
margin-left: -0.5px;
margin-top: calc(var(--size) * -0.44);
background: rgba(101, 67, 33, 0.4);
}
.tick-hour {
width: 2px;
height: calc(var(--size) * 0.045);
margin-left: -1px;
margin-top: calc(var(--size) * -0.44);
background: #654321;
border-radius: 1px;
}
.number {
position: absolute;
top: 50%;
left: 50%;
font-family: 'Playfair Display', 'Times New Roman', Georgia, serif;
font-size: calc(var(--size) * 0.075);
font-weight: 700;
color: #3d2914;
text-shadow: 1px 1px 0 rgba(255, 255, 255, 0.5);
transform: translate(-50%, -50%) rotate(var(--angle)) translateY(calc(var(--size) * -0.34))
rotate(calc(var(--angle) * -1));
}
.brand {
position: absolute;
top: 65%;
left: 50%;
transform: translateX(-50%);
display: flex;
flex-direction: column;
align-items: center;
gap: 2px;
}
.brand-name {
font-family: 'Playfair Display', Georgia, serif;
font-size: calc(var(--size) * 0.038);
font-weight: 600;
letter-spacing: 0.2em;
color: #654321;
opacity: 0.6;
}
.brand-sub {
font-family: 'Times New Roman', serif;
font-size: calc(var(--size) * 0.025);
font-style: italic;
color: #8b6914;
opacity: 0.5;
}
.hands-container {
position: absolute;
inset: 0;
}
.hand {
position: absolute;
top: 50%;
left: 50%;
transform-origin: center bottom;
}
.hour-hand {
width: calc(var(--size) * 0.035);
height: calc(var(--size) * 0.25);
margin-left: calc(var(--size) * -0.0175);
margin-top: calc(var(--size) * -0.25);
}
.hour-hand .hand-body {
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 60%;
height: 85%;
background: #1a1a1a;
border-radius: 2px;
}
.hour-hand .spade-tip {
position: absolute;
top: 0;
left: 50%;
transform: translateX(-50%);
width: 100%;
height: 25%;
background: #1a1a1a;
clip-path: polygon(50% 0%, 100% 100%, 80% 100%, 50% 30%, 20% 100%, 0% 100%);
}
.minute-hand {
width: calc(var(--size) * 0.028);
height: calc(var(--size) * 0.35);
margin-left: calc(var(--size) * -0.014);
margin-top: calc(var(--size) * -0.35);
}
.minute-hand .hand-body {
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 60%;
height: 88%;
background: #1a1a1a;
border-radius: 1px;
}
.minute-hand .spade-tip {
position: absolute;
top: 0;
left: 50%;
transform: translateX(-50%);
width: 100%;
height: 20%;
background: #1a1a1a;
clip-path: polygon(50% 0%, 100% 100%, 75% 100%, 50% 35%, 25% 100%, 0% 100%);
}
.second-hand {
width: calc(var(--size) * 0.008);
height: calc(var(--size) * 0.42);
margin-left: calc(var(--size) * -0.004);
margin-top: calc(var(--size) * -0.34);
background: #8b0000;
border-radius: 1px;
transform-origin: center 80.95%;
}
.center-cap {
position: absolute;
top: 50%;
left: 50%;
width: calc(var(--size) * 0.055);
height: calc(var(--size) * 0.055);
margin: calc(var(--size) * -0.0275);
border-radius: 50%;
background: linear-gradient(135deg, #c9a227 0%, #8b6914 100%);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
display: flex;
align-items: center;
justify-content: center;
}
.cap-detail {
width: 50%;
height: 50%;
border-radius: 50%;
background: #1a1a1a;
}
</style>

View file

@ -0,0 +1,23 @@
export { default as ClockFace } from './ClockFace.svelte';
// Analog faces
export { default as ClockFaceClassic } from './ClockFaceClassic.svelte';
export { default as ClockFaceMinimalist } from './ClockFaceMinimalist.svelte';
export { default as ClockFaceModern } from './ClockFaceModern.svelte';
export { default as ClockFaceElegant } from './ClockFaceElegant.svelte';
export { default as ClockFaceSporty } from './ClockFaceSporty.svelte';
export { default as ClockFaceVintage } from './ClockFaceVintage.svelte';
export { default as ClockFaceNautical } from './ClockFaceNautical.svelte';
export { default as ClockFaceIndustrial } from './ClockFaceIndustrial.svelte';
export { default as ClockFaceBauhaus } from './ClockFaceBauhaus.svelte';
export { default as ClockFaceRailway } from './ClockFaceRailway.svelte';
// Digital faces
export { default as ClockFaceLCD } from './ClockFaceLCD.svelte';
export { default as ClockFaceFlip } from './ClockFaceFlip.svelte';
export { default as ClockFaceMatrix } from './ClockFaceMatrix.svelte';
export { default as ClockFaceNeon } from './ClockFaceNeon.svelte';
export { default as ClockFaceBinary } from './ClockFaceBinary.svelte';
export { default as ClockFaceRetro } from './ClockFaceRetro.svelte';
export { default as ClockFaceGradient } from './ClockFaceGradient.svelte';
export { default as ClockFaceTerminal } from './ClockFaceTerminal.svelte';
export { default as ClockFaceTypewriter } from './ClockFaceTypewriter.svelte';
export { default as ClockFaceRadar } from './ClockFaceRadar.svelte';

View file

@ -84,7 +84,13 @@
"noLaps": "Noch keine Runden",
"best": "Beste",
"worst": "Längste",
"total": "Gesamt"
"total": "Gesamt",
"new": "Neu",
"continue": "Weiter",
"noStopwatches": "Keine Stoppuhren",
"noStopwatchesDescription": "Starte deine erste Stoppuhr, um mehrere Zeitmessungen parallel zu verfolgen.",
"startFirst": "Erste Stoppuhr starten",
"otherStopwatches": "Weitere Stoppuhren"
},
"pomodoro": {
"title": "Pomodoro",
@ -144,6 +150,96 @@
"ok": "OK",
"loading": "Laden...",
"error": "Fehler",
"success": "Erfolg"
"success": "Erfolg",
"back": "Zurück"
},
"clockFaces": {
"title": "Zifferblätter",
"subtitle": "Wähle dein bevorzugtes Uhren-Design für die Startseite",
"customize": "Zifferblatt anpassen",
"currentSelection": "Aktuelle Auswahl",
"analog": "Analoge Uhren",
"digital": "Digitale Uhren",
"selected": "Ausgewählt",
"classic": {
"name": "Klassisch",
"description": "Elegante Uhr mit römischen Ziffern"
},
"minimalist": {
"name": "Minimalistisch",
"description": "Schlichtes Design mit Stundenmarkierungen"
},
"modern": {
"name": "Modern",
"description": "Zeitgemäßer Stil mit arabischen Zahlen"
},
"elegant": {
"name": "Elegant",
"description": "Luxuriöse goldene Akzente"
},
"sporty": {
"name": "Sportlich",
"description": "Dynamisches Design wie bei Sportuhren"
},
"lcd": {
"name": "LCD",
"description": "Klassisches 7-Segment LCD-Display"
},
"flip": {
"name": "Fallblatt",
"description": "Retro-Stil wie alte Bahnhofsuhren"
},
"matrix": {
"name": "Matrix",
"description": "Punktmatrix LED-Anzeige"
},
"neon": {
"name": "Neon",
"description": "Leuchtender Neonröhren-Effekt"
},
"binary": {
"name": "Binär",
"description": "Zeit im Binärformat dargestellt"
},
"vintage": {
"name": "Vintage",
"description": "Antike Uhr mit gealterter Patina"
},
"nautical": {
"name": "Nautisch",
"description": "Marine-Stil mit Messing-Schiffsuhr"
},
"industrial": {
"name": "Industrie",
"description": "Fabrik-Stil mit Metall-Akzenten"
},
"bauhaus": {
"name": "Bauhaus",
"description": "Geometrisches Design mit Primärfarben"
},
"railway": {
"name": "Bahnhof",
"description": "Schweizer Bahnhofsuhr-Design"
},
"retro": {
"name": "Retro",
"description": "Pixel-Stil CRT-Anzeige"
},
"gradient": {
"name": "Farbverlauf",
"description": "Moderne Anzeige mit Farbwechsel"
},
"terminal": {
"name": "Terminal",
"description": "Kommandozeilen-Interface-Stil"
},
"typewriter": {
"name": "Schreibmaschine",
"description": "Vintage mechanischer Tastatur-Stil"
},
"radar": {
"name": "Radar",
"description": "Militär-Radarschirm-Anzeige"
}
}
}

View file

@ -84,7 +84,13 @@
"noLaps": "No laps yet",
"best": "Best",
"worst": "Worst",
"total": "Total"
"total": "Total",
"new": "New",
"continue": "Continue",
"noStopwatches": "No Stopwatches",
"noStopwatchesDescription": "Start your first stopwatch to track multiple time measurements simultaneously.",
"startFirst": "Start First Stopwatch",
"otherStopwatches": "Other Stopwatches"
},
"pomodoro": {
"title": "Pomodoro",
@ -144,6 +150,96 @@
"ok": "OK",
"loading": "Loading...",
"error": "Error",
"success": "Success"
"success": "Success",
"back": "Back"
},
"clockFaces": {
"title": "Clock Faces",
"subtitle": "Choose your preferred clock design for the homepage",
"customize": "Customize Clock",
"currentSelection": "Current Selection",
"analog": "Analog Clocks",
"digital": "Digital Clocks",
"selected": "Selected",
"classic": {
"name": "Classic",
"description": "Elegant clock with Roman numerals"
},
"minimalist": {
"name": "Minimalist",
"description": "Clean design with only hour markers"
},
"modern": {
"name": "Modern",
"description": "Contemporary style with Arabic numerals"
},
"elegant": {
"name": "Elegant",
"description": "Luxurious golden accents"
},
"sporty": {
"name": "Sporty",
"description": "Bold design inspired by sports watches"
},
"lcd": {
"name": "LCD",
"description": "Classic 7-segment LCD display"
},
"flip": {
"name": "Flip Clock",
"description": "Retro split-flap display style"
},
"matrix": {
"name": "Matrix",
"description": "Dot matrix LED display"
},
"neon": {
"name": "Neon",
"description": "Glowing neon tube effect"
},
"binary": {
"name": "Binary",
"description": "Time displayed in binary format"
},
"vintage": {
"name": "Vintage",
"description": "Antique clock with aged patina"
},
"nautical": {
"name": "Nautical",
"description": "Marine-inspired brass ship clock"
},
"industrial": {
"name": "Industrial",
"description": "Factory-style with metal accents"
},
"bauhaus": {
"name": "Bauhaus",
"description": "Geometric design with primary colors"
},
"railway": {
"name": "Railway",
"description": "Swiss railway station clock style"
},
"retro": {
"name": "Retro",
"description": "Pixel-style CRT display"
},
"gradient": {
"name": "Gradient",
"description": "Modern display with color shifts"
},
"terminal": {
"name": "Terminal",
"description": "Command-line interface style"
},
"typewriter": {
"name": "Typewriter",
"description": "Vintage mechanical keyboard style"
},
"radar": {
"name": "Radar",
"description": "Military radar screen display"
}
}
}

View file

@ -84,7 +84,13 @@
"noLaps": "Aún no hay vueltas",
"best": "Mejor",
"worst": "Peor",
"total": "Total"
"total": "Total",
"new": "Nuevo",
"continue": "Continuar",
"noStopwatches": "Sin cronómetros",
"noStopwatchesDescription": "Inicia tu primer cronómetro para seguir múltiples mediciones de tiempo simultáneamente.",
"startFirst": "Iniciar primer cronómetro",
"otherStopwatches": "Otros cronómetros"
},
"pomodoro": {
"title": "Pomodoro",
@ -144,6 +150,96 @@
"ok": "OK",
"loading": "Cargando...",
"error": "Error",
"success": "Éxito"
"success": "Éxito",
"back": "Volver"
},
"clockFaces": {
"title": "Esferas de Reloj",
"subtitle": "Elige tu diseño de reloj preferido para la página de inicio",
"customize": "Personalizar reloj",
"currentSelection": "Selección actual",
"analog": "Relojes analógicos",
"digital": "Relojes digitales",
"selected": "Seleccionado",
"classic": {
"name": "Clásico",
"description": "Reloj elegante con números romanos"
},
"minimalist": {
"name": "Minimalista",
"description": "Diseño limpio con marcadores de hora"
},
"modern": {
"name": "Moderno",
"description": "Estilo contemporáneo con números árabes"
},
"elegant": {
"name": "Elegante",
"description": "Acentos dorados de lujo"
},
"sporty": {
"name": "Deportivo",
"description": "Diseño audaz inspirado en relojes deportivos"
},
"lcd": {
"name": "LCD",
"description": "Pantalla LCD clásica de 7 segmentos"
},
"flip": {
"name": "Flip",
"description": "Estilo retro de paneles giratorios"
},
"matrix": {
"name": "Matriz",
"description": "Pantalla LED de matriz de puntos"
},
"neon": {
"name": "Neón",
"description": "Efecto de tubo de neón brillante"
},
"binary": {
"name": "Binario",
"description": "Hora mostrada en formato binario"
},
"vintage": {
"name": "Vintage",
"description": "Reloj antiguo con pátina envejecida"
},
"nautical": {
"name": "Náutico",
"description": "Reloj de barco de latón estilo marino"
},
"industrial": {
"name": "Industrial",
"description": "Estilo fábrica con acentos metálicos"
},
"bauhaus": {
"name": "Bauhaus",
"description": "Diseño geométrico con colores primarios"
},
"railway": {
"name": "Estación",
"description": "Estilo reloj de estación suiza"
},
"retro": {
"name": "Retro",
"description": "Pantalla CRT estilo pixel"
},
"gradient": {
"name": "Degradado",
"description": "Pantalla moderna con cambios de color"
},
"terminal": {
"name": "Terminal",
"description": "Estilo interfaz de línea de comandos"
},
"typewriter": {
"name": "Máquina de escribir",
"description": "Estilo teclado mecánico vintage"
},
"radar": {
"name": "Radar",
"description": "Pantalla de radar militar"
}
}
}

View file

@ -84,7 +84,13 @@
"noLaps": "Pas encore de tours",
"best": "Meilleur",
"worst": "Pire",
"total": "Total"
"total": "Total",
"new": "Nouveau",
"continue": "Continuer",
"noStopwatches": "Aucun chronomètre",
"noStopwatchesDescription": "Démarrez votre premier chronomètre pour suivre plusieurs mesures de temps simultanément.",
"startFirst": "Démarrer le premier chronomètre",
"otherStopwatches": "Autres chronomètres"
},
"pomodoro": {
"title": "Pomodoro",
@ -144,6 +150,96 @@
"ok": "OK",
"loading": "Chargement...",
"error": "Erreur",
"success": "Succès"
"success": "Succès",
"back": "Retour"
},
"clockFaces": {
"title": "Cadrans",
"subtitle": "Choisissez votre design d'horloge préféré pour la page d'accueil",
"customize": "Personnaliser l'horloge",
"currentSelection": "Sélection actuelle",
"analog": "Horloges analogiques",
"digital": "Horloges numériques",
"selected": "Sélectionné",
"classic": {
"name": "Classique",
"description": "Horloge élégante avec chiffres romains"
},
"minimalist": {
"name": "Minimaliste",
"description": "Design épuré avec marqueurs d'heures"
},
"modern": {
"name": "Moderne",
"description": "Style contemporain avec chiffres arabes"
},
"elegant": {
"name": "Élégant",
"description": "Accents dorés luxueux"
},
"sporty": {
"name": "Sportif",
"description": "Design audacieux inspiré des montres de sport"
},
"lcd": {
"name": "LCD",
"description": "Affichage LCD classique à 7 segments"
},
"flip": {
"name": "Flip",
"description": "Style rétro à volets"
},
"matrix": {
"name": "Matrice",
"description": "Affichage LED à matrice de points"
},
"neon": {
"name": "Néon",
"description": "Effet tube néon lumineux"
},
"binary": {
"name": "Binaire",
"description": "Heure affichée en format binaire"
},
"vintage": {
"name": "Vintage",
"description": "Horloge ancienne avec patine vieillie"
},
"nautical": {
"name": "Nautique",
"description": "Horloge de navire en laiton style marin"
},
"industrial": {
"name": "Industriel",
"description": "Style usine avec accents métalliques"
},
"bauhaus": {
"name": "Bauhaus",
"description": "Design géométrique avec couleurs primaires"
},
"railway": {
"name": "Gare",
"description": "Style horloge de gare suisse"
},
"retro": {
"name": "Rétro",
"description": "Affichage CRT style pixel"
},
"gradient": {
"name": "Dégradé",
"description": "Affichage moderne avec changements de couleur"
},
"terminal": {
"name": "Terminal",
"description": "Style interface ligne de commande"
},
"typewriter": {
"name": "Machine à écrire",
"description": "Style clavier mécanique vintage"
},
"radar": {
"name": "Radar",
"description": "Affichage écran radar militaire"
}
}
}

View file

@ -84,7 +84,13 @@
"noLaps": "Nessun giro ancora",
"best": "Migliore",
"worst": "Peggiore",
"total": "Totale"
"total": "Totale",
"new": "Nuovo",
"continue": "Continua",
"noStopwatches": "Nessun cronometro",
"noStopwatchesDescription": "Avvia il tuo primo cronometro per tracciare più misurazioni di tempo contemporaneamente.",
"startFirst": "Avvia primo cronometro",
"otherStopwatches": "Altri cronometri"
},
"pomodoro": {
"title": "Pomodoro",
@ -144,6 +150,96 @@
"ok": "OK",
"loading": "Caricamento...",
"error": "Errore",
"success": "Successo"
"success": "Successo",
"back": "Indietro"
},
"clockFaces": {
"title": "Quadranti",
"subtitle": "Scegli il tuo design di orologio preferito per la home",
"customize": "Personalizza orologio",
"currentSelection": "Selezione attuale",
"analog": "Orologi analogici",
"digital": "Orologi digitali",
"selected": "Selezionato",
"classic": {
"name": "Classico",
"description": "Orologio elegante con numeri romani"
},
"minimalist": {
"name": "Minimalista",
"description": "Design pulito con marcatori delle ore"
},
"modern": {
"name": "Moderno",
"description": "Stile contemporaneo con numeri arabi"
},
"elegant": {
"name": "Elegante",
"description": "Accenti dorati di lusso"
},
"sporty": {
"name": "Sportivo",
"description": "Design audace ispirato agli orologi sportivi"
},
"lcd": {
"name": "LCD",
"description": "Display LCD classico a 7 segmenti"
},
"flip": {
"name": "Flip",
"description": "Stile retrò a palette"
},
"matrix": {
"name": "Matrice",
"description": "Display LED a matrice di punti"
},
"neon": {
"name": "Neon",
"description": "Effetto tubo neon luminoso"
},
"binary": {
"name": "Binario",
"description": "Ora visualizzata in formato binario"
},
"vintage": {
"name": "Vintage",
"description": "Orologio antico con patina invecchiata"
},
"nautical": {
"name": "Nautico",
"description": "Orologio da nave in ottone stile marino"
},
"industrial": {
"name": "Industriale",
"description": "Stile fabbrica con accenti metallici"
},
"bauhaus": {
"name": "Bauhaus",
"description": "Design geometrico con colori primari"
},
"railway": {
"name": "Stazione",
"description": "Stile orologio stazione svizzera"
},
"retro": {
"name": "Retro",
"description": "Display CRT stile pixel"
},
"gradient": {
"name": "Sfumato",
"description": "Display moderno con cambi di colore"
},
"terminal": {
"name": "Terminale",
"description": "Stile interfaccia riga di comando"
},
"typewriter": {
"name": "Macchina da scrivere",
"description": "Stile tastiera meccanica vintage"
},
"radar": {
"name": "Radar",
"description": "Display schermo radar militare"
}
}
}

View file

@ -0,0 +1,280 @@
/**
* Clock Face store for Clock app
* Manages the selected clock face style for the homepage
* SSR-safe implementation with localStorage persistence
*/
import { browser } from '$app/environment';
// Storage key
const CLOCK_FACE_KEY = 'clock-selected-face';
// Available clock face types
export type ClockFaceType =
// Analog faces
| 'classic'
| 'minimalist'
| 'modern'
| 'elegant'
| 'sporty'
| 'vintage'
| 'nautical'
| 'industrial'
| 'bauhaus'
| 'railway'
// Digital faces
| 'lcd'
| 'flip'
| 'matrix'
| 'neon'
| 'binary'
| 'retro'
| 'gradient'
| 'terminal'
| 'typewriter'
| 'radar';
export interface ClockFaceDefinition {
id: ClockFaceType;
name: string;
nameKey: string;
description: string;
descriptionKey: string;
category: 'analog' | 'digital';
}
// All available clock faces
export const CLOCK_FACES: ClockFaceDefinition[] = [
// Analog faces
{
id: 'classic',
name: 'Classic',
nameKey: 'clockFaces.classic.name',
description: 'Elegant clock with Roman numerals',
descriptionKey: 'clockFaces.classic.description',
category: 'analog',
},
{
id: 'minimalist',
name: 'Minimalist',
nameKey: 'clockFaces.minimalist.name',
description: 'Clean design with only hour markers',
descriptionKey: 'clockFaces.minimalist.description',
category: 'analog',
},
{
id: 'modern',
name: 'Modern',
nameKey: 'clockFaces.modern.name',
description: 'Contemporary style with Arabic numerals',
descriptionKey: 'clockFaces.modern.description',
category: 'analog',
},
{
id: 'elegant',
name: 'Elegant',
nameKey: 'clockFaces.elegant.name',
description: 'Luxurious golden accents',
descriptionKey: 'clockFaces.elegant.description',
category: 'analog',
},
{
id: 'sporty',
name: 'Sporty',
nameKey: 'clockFaces.sporty.name',
description: 'Bold design inspired by sports watches',
descriptionKey: 'clockFaces.sporty.description',
category: 'analog',
},
{
id: 'vintage',
name: 'Vintage',
nameKey: 'clockFaces.vintage.name',
description: 'Antique clock with aged patina',
descriptionKey: 'clockFaces.vintage.description',
category: 'analog',
},
{
id: 'nautical',
name: 'Nautical',
nameKey: 'clockFaces.nautical.name',
description: 'Marine-inspired brass ship clock',
descriptionKey: 'clockFaces.nautical.description',
category: 'analog',
},
{
id: 'industrial',
name: 'Industrial',
nameKey: 'clockFaces.industrial.name',
description: 'Factory-style with metal accents',
descriptionKey: 'clockFaces.industrial.description',
category: 'analog',
},
{
id: 'bauhaus',
name: 'Bauhaus',
nameKey: 'clockFaces.bauhaus.name',
description: 'Geometric design with primary colors',
descriptionKey: 'clockFaces.bauhaus.description',
category: 'analog',
},
{
id: 'railway',
name: 'Railway',
nameKey: 'clockFaces.railway.name',
description: 'Swiss railway station clock style',
descriptionKey: 'clockFaces.railway.description',
category: 'analog',
},
// Digital faces
{
id: 'lcd',
name: 'LCD',
nameKey: 'clockFaces.lcd.name',
description: 'Classic 7-segment LCD display',
descriptionKey: 'clockFaces.lcd.description',
category: 'digital',
},
{
id: 'flip',
name: 'Flip Clock',
nameKey: 'clockFaces.flip.name',
description: 'Retro split-flap display style',
descriptionKey: 'clockFaces.flip.description',
category: 'digital',
},
{
id: 'matrix',
name: 'Matrix',
nameKey: 'clockFaces.matrix.name',
description: 'Dot matrix LED display',
descriptionKey: 'clockFaces.matrix.description',
category: 'digital',
},
{
id: 'neon',
name: 'Neon',
nameKey: 'clockFaces.neon.name',
description: 'Glowing neon tube effect',
descriptionKey: 'clockFaces.neon.description',
category: 'digital',
},
{
id: 'binary',
name: 'Binary',
nameKey: 'clockFaces.binary.name',
description: 'Time displayed in binary format',
descriptionKey: 'clockFaces.binary.description',
category: 'digital',
},
{
id: 'retro',
name: 'Retro',
nameKey: 'clockFaces.retro.name',
description: 'Pixel-style CRT display',
descriptionKey: 'clockFaces.retro.description',
category: 'digital',
},
{
id: 'gradient',
name: 'Gradient',
nameKey: 'clockFaces.gradient.name',
description: 'Modern display with color shifts',
descriptionKey: 'clockFaces.gradient.description',
category: 'digital',
},
{
id: 'terminal',
name: 'Terminal',
nameKey: 'clockFaces.terminal.name',
description: 'Command-line interface style',
descriptionKey: 'clockFaces.terminal.description',
category: 'digital',
},
{
id: 'typewriter',
name: 'Typewriter',
nameKey: 'clockFaces.typewriter.name',
description: 'Vintage mechanical keyboard style',
descriptionKey: 'clockFaces.typewriter.description',
category: 'digital',
},
{
id: 'radar',
name: 'Radar',
nameKey: 'clockFaces.radar.name',
description: 'Military radar screen display',
descriptionKey: 'clockFaces.radar.description',
category: 'digital',
},
];
// Default clock face
const DEFAULT_FACE: ClockFaceType = 'modern';
// State
let selectedFace = $state<ClockFaceType>(DEFAULT_FACE);
let initialized = $state(false);
export const clockFaceStore = {
// Getters
get selectedFace(): ClockFaceType {
return selectedFace ?? DEFAULT_FACE;
},
get initialized(): boolean {
return initialized;
},
get faces(): ClockFaceDefinition[] {
return CLOCK_FACES;
},
get analogFaces(): ClockFaceDefinition[] {
return CLOCK_FACES.filter((f) => f.category === 'analog');
},
get digitalFaces(): ClockFaceDefinition[] {
return CLOCK_FACES.filter((f) => f.category === 'digital');
},
get currentFace(): ClockFaceDefinition | undefined {
return CLOCK_FACES.find((f) => f.id === selectedFace);
},
/**
* Initialize from localStorage (client-side only)
*/
initialize() {
if (!browser) return;
if (initialized) return;
const saved = localStorage.getItem(CLOCK_FACE_KEY) as ClockFaceType | null;
if (saved && CLOCK_FACES.some((f) => f.id === saved)) {
selectedFace = saved;
}
initialized = true;
},
/**
* Set the selected clock face
*/
setFace(face: ClockFaceType) {
if (!CLOCK_FACES.some((f) => f.id === face)) return;
selectedFace = face;
if (browser) {
localStorage.setItem(CLOCK_FACE_KEY, face);
}
},
/**
* Check if a face is analog
*/
isAnalog(face: ClockFaceType): boolean {
return CLOCK_FACES.find((f) => f.id === face)?.category === 'analog';
},
/**
* Check if a face is digital
*/
isDigital(face: ClockFaceType): boolean {
return CLOCK_FACES.find((f) => f.id === face)?.category === 'digital';
},
};

View file

@ -0,0 +1,181 @@
<script lang="ts">
import { onMount, onDestroy } from 'svelte';
import { _ } from 'svelte-i18n';
import { PageHeader } from '@manacore/shared-ui';
import { ClockFace } from '$lib/components/clock-faces';
import { clockFaceStore, CLOCK_FACES, type ClockFaceType } from '$lib/stores/clock-face.svelte';
// Current time state for preview
let currentTime = $state(new Date());
let interval: ReturnType<typeof setInterval> | null = null;
// Derived time values
let hours = $derived(currentTime.getHours());
let minutes = $derived(currentTime.getMinutes());
let seconds = $derived(currentTime.getSeconds());
// Group faces by category
let analogFaces = $derived(CLOCK_FACES.filter((f) => f.category === 'analog'));
let digitalFaces = $derived(CLOCK_FACES.filter((f) => f.category === 'digital'));
// Current selection
let selectedFace = $derived(clockFaceStore.selectedFace);
function selectFace(face: ClockFaceType) {
clockFaceStore.setFace(face);
}
onMount(() => {
clockFaceStore.initialize();
interval = setInterval(() => {
currentTime = new Date();
}, 1000);
});
onDestroy(() => {
if (interval) {
clearInterval(interval);
}
});
</script>
<PageHeader
title={$_('clockFaces.title')}
description={$_('clockFaces.subtitle')}
size="md"
centered
backHref="/"
/>
<div class="space-y-8">
<!-- Current Selection Preview -->
<div class="card">
<div class="flex flex-col items-center gap-4">
<h2 class="text-lg font-semibold text-foreground">{$_('clockFaces.currentSelection')}</h2>
<div class="clock-preview">
<ClockFace type={selectedFace} {hours} {minutes} {seconds} size={240} />
</div>
<p class="text-sm text-muted-foreground">
{$_(CLOCK_FACES.find((f) => f.id === selectedFace)?.nameKey || '')}
</p>
</div>
</div>
<!-- Analog Clock Faces -->
<section>
<h2 class="mb-4 text-xl font-semibold text-foreground">{$_('clockFaces.analog')}</h2>
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-5">
{#each analogFaces as face}
<button
type="button"
class="face-card"
class:selected={selectedFace === face.id}
onclick={() => selectFace(face.id)}
>
<div class="face-preview">
<ClockFace type={face.id} {hours} {minutes} {seconds} size={160} />
</div>
<div class="face-info">
<h3 class="font-medium text-foreground">{$_(face.nameKey)}</h3>
<p class="text-xs text-muted-foreground">{$_(face.descriptionKey)}</p>
</div>
{#if selectedFace === face.id}
<div class="selected-badge">
<span class="text-xs font-medium">{$_('clockFaces.selected')}</span>
</div>
{/if}
</button>
{/each}
</div>
</section>
<!-- Digital Clock Faces -->
<section>
<h2 class="mb-4 text-xl font-semibold text-foreground">{$_('clockFaces.digital')}</h2>
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-5">
{#each digitalFaces as face}
<button
type="button"
class="face-card"
class:selected={selectedFace === face.id}
onclick={() => selectFace(face.id)}
>
<div class="face-preview digital-preview">
<ClockFace type={face.id} {hours} {minutes} {seconds} size={180} />
</div>
<div class="face-info">
<h3 class="font-medium text-foreground">{$_(face.nameKey)}</h3>
<p class="text-xs text-muted-foreground">{$_(face.descriptionKey)}</p>
</div>
{#if selectedFace === face.id}
<div class="selected-badge">
<span class="text-xs font-medium">{$_('clockFaces.selected')}</span>
</div>
{/if}
</button>
{/each}
</div>
</section>
</div>
<style>
.clock-preview {
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.face-card {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
padding: 16px;
background: hsl(var(--color-surface));
border: 2px solid hsl(var(--color-border));
border-radius: 12px;
cursor: pointer;
transition: all 200ms;
}
.face-card:hover {
border-color: hsl(var(--color-primary) / 0.5);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.face-card.selected {
border-color: hsl(var(--color-primary));
background: hsl(var(--color-primary) / 0.05);
}
.face-preview {
display: flex;
align-items: center;
justify-content: center;
min-height: 180px;
width: 100%;
overflow: hidden;
}
.digital-preview {
min-height: 120px;
transform: scale(0.85);
}
.face-info {
text-align: center;
margin-top: 12px;
}
.selected-badge {
position: absolute;
top: 8px;
right: 8px;
background: hsl(var(--color-primary));
color: hsl(var(--color-primary-foreground));
padding: 4px 8px;
border-radius: 9999px;
}
</style>