feat: Phases 4-7 — Full Feature Expansion (26 features)
Phase 4 — New Widget Types: - Clock/Weather, System Stats, RSS/Feed, Calendar, Markdown, Metric/Counter, Link Group, Camera/Stream widgets - Backend services with caching for each data source - Full creation form with dynamic config fields per type Phase 5 — Visual & Styling Enhancements: - Glassmorphism card style (solid/glass/outline) - Board-level themes with per-board hue/saturation - Animated SVG status rings replacing static dots - Card size options (compact/medium/large) - Custom CSS injection (admin + per-board, sanitized) - Wallpaper backgrounds with blur/overlay/parallax Phase 6 — Functional Features: - Favorites bar with drag-and-drop reordering - Recent apps tracking with privacy toggle - Uptime dashboard page (/status, guest-accessible) - Notifications system (Discord/Slack/Telegram/HTTP webhooks) - App tags with filtering in board view - Multi-URL app cards with expandable sub-links - Personal API tokens with scoped permissions - Audit log with retention and admin viewer Phase 7 — Quality of Life: - Onboarding wizard (5-step first-launch setup) - App URL health preview with favicon/title detection - Board templates (4 built-in + custom import/export) - Keyboard shortcut overlay (j/k nav, 1-9 boards, ? help) 212 files changed, 15641 insertions, 980 deletions. Build, lint, type check, and 222 tests all pass.
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
<script lang="ts">
|
||||
interface Props {
|
||||
status: string;
|
||||
size: number;
|
||||
animated?: boolean;
|
||||
}
|
||||
|
||||
let { status, size, animated = true }: Props = $props();
|
||||
|
||||
const strokeWidth = $derived(Math.max(2, size * 0.06));
|
||||
const radius = $derived((size - strokeWidth) / 2);
|
||||
const circumference = $derived(2 * Math.PI * radius);
|
||||
const center = $derived(size / 2);
|
||||
|
||||
const ringConfig = $derived.by(() => {
|
||||
switch (status) {
|
||||
case 'online':
|
||||
return {
|
||||
color: 'var(--status-online, #22c55e)',
|
||||
dashArray: `${circumference}`,
|
||||
dashOffset: '0',
|
||||
animationClass: animated ? 'status-ring-online' : '',
|
||||
opacity: 1
|
||||
};
|
||||
case 'offline':
|
||||
return {
|
||||
color: 'var(--status-offline, #ef4444)',
|
||||
dashArray: `${circumference}`,
|
||||
dashOffset: '0',
|
||||
animationClass: animated ? 'status-ring-offline' : '',
|
||||
opacity: 1
|
||||
};
|
||||
case 'degraded':
|
||||
return {
|
||||
color: 'var(--status-degraded, #eab308)',
|
||||
dashArray: `${circumference * 0.75} ${circumference * 0.25}`,
|
||||
dashOffset: '0',
|
||||
animationClass: animated ? 'status-ring-degraded' : '',
|
||||
opacity: 1
|
||||
};
|
||||
default:
|
||||
return {
|
||||
color: 'var(--status-unknown, #6b7280)',
|
||||
dashArray: `${circumference * 0.1} ${circumference * 0.1}`,
|
||||
dashOffset: '0',
|
||||
animationClass: animated ? 'status-ring-unknown' : '',
|
||||
opacity: 0.6
|
||||
};
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<svg
|
||||
class="pointer-events-none absolute inset-0"
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 {size} {size}"
|
||||
fill="none"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<circle
|
||||
cx={center}
|
||||
cy={center}
|
||||
r={radius}
|
||||
stroke={ringConfig.color}
|
||||
stroke-width={strokeWidth}
|
||||
fill="none"
|
||||
stroke-dasharray={ringConfig.dashArray}
|
||||
stroke-dashoffset={ringConfig.dashOffset}
|
||||
stroke-linecap="round"
|
||||
opacity={ringConfig.opacity}
|
||||
class={ringConfig.animationClass}
|
||||
style="transform-origin: {center}px {center}px;"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
<style>
|
||||
@keyframes ring-fill-sweep {
|
||||
0% {
|
||||
stroke-dashoffset: 100%;
|
||||
}
|
||||
100% {
|
||||
stroke-dashoffset: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes ring-pulse-opacity {
|
||||
0%, 100% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.3;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes ring-degraded-pulse {
|
||||
0%, 100% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes ring-rotate-dash {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.status-ring-online {
|
||||
animation: ring-fill-sweep 1.5s ease-out forwards;
|
||||
}
|
||||
|
||||
.status-ring-offline {
|
||||
animation: ring-pulse-opacity 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.status-ring-degraded {
|
||||
animation: ring-degraded-pulse 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.status-ring-unknown {
|
||||
animation: ring-rotate-dash 8s linear infinite;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user