mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 23:21:08 +02:00
✨ 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:
parent
39b8a5682f
commit
9eb0b51c4c
29 changed files with 5937 additions and 10 deletions
|
|
@ -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} />
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
23
apps/clock/apps/web/src/lib/components/clock-faces/index.ts
Normal file
23
apps/clock/apps/web/src/lib/components/clock-faces/index.ts
Normal 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';
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
280
apps/clock/apps/web/src/lib/stores/clock-face.svelte.ts
Normal file
280
apps/clock/apps/web/src/lib/stores/clock-face.svelte.ts
Normal 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';
|
||||
},
|
||||
};
|
||||
181
apps/clock/apps/web/src/routes/clock-faces/+page.svelte
Normal file
181
apps/clock/apps/web/src/routes/clock-faces/+page.svelte
Normal 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>
|
||||
Loading…
Add table
Add a link
Reference in a new issue