mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 21:21:10 +02:00
feat(observatory): add tabbed gallery views for plants and lakes
Three tabs on the observatory page: - Seenplatte: the main interactive landscape (existing) - Pflanzen: horizontal scroll gallery of all 20 apps as plant cards, grouped by status (Mature/Production/Beta/Alpha), each with SVG preview, score, type label, trend indicator. Click opens detail panel. - Seen: horizontal scroll of all 6 infrastructure lakes with gradient preview, description, clarity and fill level stats. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
a432c77286
commit
80beef252c
3 changed files with 647 additions and 3 deletions
|
|
@ -0,0 +1,177 @@
|
|||
<script lang="ts">
|
||||
import type { LakeData } from '../data/types';
|
||||
|
||||
let { lake }: { lake: LakeData } = $props();
|
||||
|
||||
// Normalize the lake path to fit inside the card SVG
|
||||
// We'll render it at its original scale but crop via viewBox
|
||||
let viewBox = $derived(() => {
|
||||
// Parse path to get rough bounds
|
||||
const nums = lake.path.match(/[\d.]+/g)?.map(Number) || [];
|
||||
let minX = Infinity,
|
||||
minY = Infinity,
|
||||
maxX = -Infinity,
|
||||
maxY = -Infinity;
|
||||
for (let i = 0; i < nums.length; i += 2) {
|
||||
if (nums[i] < minX) minX = nums[i];
|
||||
if (nums[i] > maxX) maxX = nums[i];
|
||||
}
|
||||
for (let i = 1; i < nums.length; i += 2) {
|
||||
if (nums[i] < minY) minY = nums[i];
|
||||
if (nums[i] > maxY) maxY = nums[i];
|
||||
}
|
||||
const pad = 20;
|
||||
return `${minX - pad} ${minY - pad} ${maxX - minX + pad * 2} ${maxY - minY + pad * 2}`;
|
||||
});
|
||||
|
||||
const lakeDescriptions: Record<string, string> = {
|
||||
auth: 'Zentraler Authentifizierungs-Hub. Alle Services fliessen durch diesen See.',
|
||||
redis: 'Schneller Cache-Speicher. Klein, kristallklar, sofort verfugbar.',
|
||||
minio: 'Objekt-Speicher fur Dateien, Bilder und Medien aller Apps.',
|
||||
'db-left': 'PostgreSQL-Datenbanken fur Calendar, Todo, Contacts, Storage.',
|
||||
'db-center': 'PostgreSQL-Datenbanken fur Zitare, Mukke, Clock, NutriPhi.',
|
||||
'db-right': 'PostgreSQL-Datenbanken fur Photos, SkillTree, Context, Planta.',
|
||||
};
|
||||
|
||||
const lakeIcons: Record<string, string> = {
|
||||
auth: 'Mana Core Auth',
|
||||
redis: 'Redis Cache',
|
||||
minio: 'MinIO Storage',
|
||||
'db-left': 'PostgreSQL West',
|
||||
'db-center': 'PostgreSQL Mitte',
|
||||
'db-right': 'PostgreSQL Ost',
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="lake-card">
|
||||
<!-- Lake SVG preview -->
|
||||
<div class="lake-preview">
|
||||
<svg
|
||||
width="220"
|
||||
height="120"
|
||||
viewBox={viewBox()}
|
||||
preserveAspectRatio="xMidYMid meet"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<defs>
|
||||
<radialGradient id="lc-{lake.id}" cx="50%" cy="40%" r="60%">
|
||||
<stop offset="0%" stop-color={lake.color} />
|
||||
<stop offset="100%" stop-color={lake.colorDeep} />
|
||||
</radialGradient>
|
||||
</defs>
|
||||
<path d={lake.path} fill="url(#lc-{lake.id})" />
|
||||
<!-- Shimmer -->
|
||||
<path d={lake.path} fill="white" opacity="0.08" />
|
||||
<!-- Waves -->
|
||||
<path
|
||||
d={lake.path}
|
||||
fill="none"
|
||||
stroke="white"
|
||||
stroke-width="0.8"
|
||||
stroke-dasharray="4 12"
|
||||
opacity="0.2"
|
||||
>
|
||||
<animate
|
||||
attributeName="stroke-dashoffset"
|
||||
values="0;16"
|
||||
dur="4s"
|
||||
repeatCount="indefinite"
|
||||
/>
|
||||
</path>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<!-- Info -->
|
||||
<div class="lake-info">
|
||||
<h3 class="lake-name">{lake.label}</h3>
|
||||
<span class="lake-service">{lakeIcons[lake.id] || lake.name}</span>
|
||||
<p class="lake-desc">{lakeDescriptions[lake.id] || ''}</p>
|
||||
<div class="lake-stats">
|
||||
<span class="lake-stat">
|
||||
<span class="stat-label">Klarheit</span>
|
||||
<span class="stat-value">{Math.round(lake.clarity * 100)}%</span>
|
||||
</span>
|
||||
<span class="lake-stat">
|
||||
<span class="stat-label">Fullstand</span>
|
||||
<span class="stat-value">{Math.round(lake.level * 100)}%</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.lake-card {
|
||||
flex-shrink: 0;
|
||||
width: 260px;
|
||||
background: rgba(15, 23, 42, 0.6);
|
||||
backdrop-filter: blur(8px);
|
||||
border: 1px solid rgba(148, 163, 184, 0.1);
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
transition:
|
||||
transform 0.2s,
|
||||
border-color 0.2s;
|
||||
}
|
||||
|
||||
.lake-card:hover {
|
||||
transform: translateY(-3px);
|
||||
border-color: rgba(59, 130, 246, 0.2);
|
||||
}
|
||||
|
||||
.lake-preview {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background: linear-gradient(180deg, #1e293b 0%, #0f172a 100%);
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.lake-info {
|
||||
padding: 12px 14px;
|
||||
}
|
||||
|
||||
.lake-name {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #f1f5f9;
|
||||
margin: 0 0 2px 0;
|
||||
}
|
||||
|
||||
.lake-service {
|
||||
font-size: 11px;
|
||||
color: #60a5fa;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.lake-desc {
|
||||
font-size: 11px;
|
||||
color: #94a3b8;
|
||||
margin: 8px 0;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.lake-stats {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.lake-stat {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1px;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 9px;
|
||||
color: #64748b;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #cbd5e1;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,185 @@
|
|||
<script lang="ts">
|
||||
import type { AppData } from '../data/types';
|
||||
import { getHealthColor } from '../data/colors';
|
||||
import PlantFactory from '../plants/PlantFactory.svelte';
|
||||
|
||||
let { app, onclick }: { app: AppData; onclick?: () => void } = $props();
|
||||
|
||||
const statusLabels: Record<string, string> = {
|
||||
mature: 'Mature',
|
||||
production: 'Production',
|
||||
beta: 'Beta',
|
||||
alpha: 'Alpha',
|
||||
prototype: 'Prototype',
|
||||
};
|
||||
|
||||
const statusColors: Record<string, string> = {
|
||||
mature: '#34d399',
|
||||
production: '#60a5fa',
|
||||
beta: '#fbbf24',
|
||||
alpha: '#f97316',
|
||||
prototype: '#94a3b8',
|
||||
};
|
||||
|
||||
const plantTypeLabels: Record<string, string> = {
|
||||
oak: 'Eiche',
|
||||
birch: 'Birke',
|
||||
youngTree: 'Junger Baum',
|
||||
reed: 'Schilf',
|
||||
waterLily: 'Seerose',
|
||||
moss: 'Moos',
|
||||
shrub: 'Busch',
|
||||
sprout: 'Sprossling',
|
||||
stump: 'Baumstumpf',
|
||||
swampCluster: 'Sumpfpflanze',
|
||||
};
|
||||
|
||||
// Create a centered version of the app for the card SVG
|
||||
let cardApp = $derived({
|
||||
...app,
|
||||
position: { x: 80, y: 120 },
|
||||
});
|
||||
|
||||
function scoreColor(score: number): string {
|
||||
if (score >= 85) return '#34d399';
|
||||
if (score >= 70) return '#60a5fa';
|
||||
if (score >= 55) return '#fbbf24';
|
||||
if (score >= 40) return '#f97316';
|
||||
return '#ef4444';
|
||||
}
|
||||
</script>
|
||||
|
||||
<button type="button" class="plant-card" {onclick}>
|
||||
<!-- Plant SVG preview -->
|
||||
<div class="plant-preview">
|
||||
<svg width="160" height="160" viewBox="0 0 160 160" xmlns="http://www.w3.org/2000/svg">
|
||||
<!-- Ground line -->
|
||||
<ellipse cx="80" cy="125" rx="50" ry="6" fill="#7DB86A" opacity="0.3" />
|
||||
<PlantFactory app={cardApp} />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<!-- Info -->
|
||||
<div class="plant-info">
|
||||
<div class="plant-header">
|
||||
<span class="plant-name">{app.displayName}</span>
|
||||
<span class="plant-score" style="color: {scoreColor(app.score)}">{app.score}</span>
|
||||
</div>
|
||||
<div class="plant-meta">
|
||||
<span class="plant-type">{plantTypeLabels[app.plantType] || app.plantType}</span>
|
||||
<span class="plant-status" style="color: {statusColors[app.status]}">
|
||||
{statusLabels[app.status]}
|
||||
</span>
|
||||
</div>
|
||||
<!-- Mini score bar -->
|
||||
<div class="plant-bar">
|
||||
<div
|
||||
class="plant-bar-fill"
|
||||
style="width: {app.score}%; background: {scoreColor(app.score)}"
|
||||
></div>
|
||||
</div>
|
||||
{#if app.trend !== 0}
|
||||
<div class="plant-trend" class:positive={app.trend > 0} class:negative={app.trend < 0}>
|
||||
{app.trend > 0 ? '+' : ''}{app.trend} seit letztem Audit
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<style>
|
||||
.plant-card {
|
||||
flex-shrink: 0;
|
||||
width: 200px;
|
||||
background: rgba(15, 23, 42, 0.6);
|
||||
backdrop-filter: blur(8px);
|
||||
border: 1px solid rgba(148, 163, 184, 0.1);
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
transform 0.2s,
|
||||
border-color 0.2s,
|
||||
box-shadow 0.2s;
|
||||
text-align: left;
|
||||
color: inherit;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.plant-card:hover {
|
||||
transform: translateY(-4px);
|
||||
border-color: rgba(59, 130, 246, 0.3);
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.plant-preview {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background: linear-gradient(180deg, #e0f2fe 0%, #bae6fd 40%, #7db86a 100%);
|
||||
padding: 8px 0 0;
|
||||
}
|
||||
|
||||
.plant-info {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.plant-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.plant-name {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #f1f5f9;
|
||||
}
|
||||
|
||||
.plant-score {
|
||||
font-size: 20px;
|
||||
font-weight: 800;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.plant-meta {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 11px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.plant-type {
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.plant-status {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.plant-bar {
|
||||
height: 3px;
|
||||
background: rgba(148, 163, 184, 0.15);
|
||||
border-radius: 2px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.plant-bar-fill {
|
||||
height: 100%;
|
||||
border-radius: 2px;
|
||||
transition: width 0.4s ease;
|
||||
}
|
||||
|
||||
.plant-trend {
|
||||
font-size: 10px;
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.plant-trend.positive {
|
||||
color: #34d399;
|
||||
}
|
||||
|
||||
.plant-trend.negative {
|
||||
color: #ef4444;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,9 +1,291 @@
|
|||
<script lang="ts">
|
||||
import SeenplatteScene from '$lib/components/observatory/SeenplatteScene.svelte';
|
||||
import PlantCard from '$lib/components/observatory/ui/PlantCard.svelte';
|
||||
import LakeCard from '$lib/components/observatory/ui/LakeCard.svelte';
|
||||
import DetailPanel from '$lib/components/observatory/ui/DetailPanel.svelte';
|
||||
import { createMockEcosystem } from '$lib/components/observatory/data/mockData';
|
||||
import { LAKES } from '$lib/components/observatory/data/layout';
|
||||
import type { AppData } from '$lib/components/observatory/data/types';
|
||||
|
||||
type Tab = 'scene' | 'plants' | 'lakes';
|
||||
let activeTab = $state<Tab>('scene');
|
||||
let selectedApp = $state<AppData | null>(null);
|
||||
|
||||
const apps = createMockEcosystem();
|
||||
const lakes = LAKES;
|
||||
|
||||
// Sort apps by score descending for gallery
|
||||
const sortedApps = apps.toSorted((a, b) => b.score - a.score);
|
||||
|
||||
// Group by status
|
||||
const matureApps = sortedApps.filter((a) => a.status === 'mature');
|
||||
const productionApps = sortedApps.filter((a) => a.status === 'production');
|
||||
const betaApps = sortedApps.filter((a) => a.status === 'beta');
|
||||
const alphaApps = sortedApps.filter((a) => a.status === 'alpha');
|
||||
|
||||
const tabs: { id: Tab; label: string; count?: number }[] = [
|
||||
{ id: 'scene', label: 'Seenplatte' },
|
||||
{ id: 'plants', label: 'Pflanzen', count: apps.length },
|
||||
{ id: 'lakes', label: 'Seen', count: lakes.length },
|
||||
];
|
||||
</script>
|
||||
|
||||
<div style="display: flex; flex-direction: column; gap: 1rem;">
|
||||
<div style="height: calc(100vh - 12rem); min-height: 500px; width: 100%;">
|
||||
<SeenplatteScene />
|
||||
<div class="observatory-page">
|
||||
<!-- Tab bar -->
|
||||
<div class="tab-bar">
|
||||
{#each tabs as tab}
|
||||
<button
|
||||
type="button"
|
||||
class="tab-btn"
|
||||
class:active={activeTab === tab.id}
|
||||
onclick={() => (activeTab = tab.id)}
|
||||
>
|
||||
{tab.label}
|
||||
{#if tab.count}
|
||||
<span class="tab-count">{tab.count}</span>
|
||||
{/if}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<!-- Tab content -->
|
||||
{#if activeTab === 'scene'}
|
||||
<div class="scene-container">
|
||||
<SeenplatteScene />
|
||||
</div>
|
||||
{:else if activeTab === 'plants'}
|
||||
<div class="gallery-section">
|
||||
<div class="gallery-header">
|
||||
<h2 class="gallery-title">Alle Pflanzen</h2>
|
||||
<p class="gallery-subtitle">
|
||||
Jede App im Mana-Okosystem als Pflanze — Grosse und Art spiegeln den ManaScore wider
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{#if matureApps.length}
|
||||
<div class="gallery-group">
|
||||
<h3 class="group-label">
|
||||
<span class="group-dot" style="background: #34d399"></span>
|
||||
Mature
|
||||
</h3>
|
||||
<div class="gallery-scroll">
|
||||
{#each matureApps as app (app.id)}
|
||||
<PlantCard {app} onclick={() => (selectedApp = app)} />
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if productionApps.length}
|
||||
<div class="gallery-group">
|
||||
<h3 class="group-label">
|
||||
<span class="group-dot" style="background: #60a5fa"></span>
|
||||
Production
|
||||
</h3>
|
||||
<div class="gallery-scroll">
|
||||
{#each productionApps as app (app.id)}
|
||||
<PlantCard {app} onclick={() => (selectedApp = app)} />
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if betaApps.length}
|
||||
<div class="gallery-group">
|
||||
<h3 class="group-label">
|
||||
<span class="group-dot" style="background: #fbbf24"></span>
|
||||
Beta
|
||||
</h3>
|
||||
<div class="gallery-scroll">
|
||||
{#each betaApps as app (app.id)}
|
||||
<PlantCard {app} onclick={() => (selectedApp = app)} />
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if alphaApps.length}
|
||||
<div class="gallery-group">
|
||||
<h3 class="group-label">
|
||||
<span class="group-dot" style="background: #f97316"></span>
|
||||
Alpha
|
||||
</h3>
|
||||
<div class="gallery-scroll">
|
||||
{#each alphaApps as app (app.id)}
|
||||
<PlantCard {app} onclick={() => (selectedApp = app)} />
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{:else if activeTab === 'lakes'}
|
||||
<div class="gallery-section">
|
||||
<div class="gallery-header">
|
||||
<h2 class="gallery-title">Alle Seen</h2>
|
||||
<p class="gallery-subtitle">
|
||||
Infrastruktur-Services als Gewasser — Klarheit und Fullstand zeigen den Systemzustand
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="gallery-scroll lakes-scroll">
|
||||
{#each lakes as lake (lake.id)}
|
||||
<LakeCard {lake} />
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Detail panel (shared across tabs) -->
|
||||
<DetailPanel app={selectedApp} onclose={() => (selectedApp = null)} />
|
||||
|
||||
<style>
|
||||
.observatory-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
/* Tab bar */
|
||||
.tab-bar {
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
background: rgba(15, 23, 42, 0.4);
|
||||
padding: 4px;
|
||||
border-radius: 10px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.tab-btn {
|
||||
flex: 1;
|
||||
padding: 8px 16px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: #94a3b8;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
border-radius: 7px;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
background 0.2s,
|
||||
color 0.2s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.tab-btn:hover {
|
||||
color: #cbd5e1;
|
||||
background: rgba(148, 163, 184, 0.08);
|
||||
}
|
||||
|
||||
.tab-btn.active {
|
||||
background: rgba(59, 130, 246, 0.15);
|
||||
color: #60a5fa;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.tab-count {
|
||||
font-size: 10px;
|
||||
background: rgba(148, 163, 184, 0.15);
|
||||
padding: 1px 6px;
|
||||
border-radius: 8px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.tab-btn.active .tab-count {
|
||||
background: rgba(59, 130, 246, 0.2);
|
||||
color: #93c5fd;
|
||||
}
|
||||
|
||||
/* Scene tab */
|
||||
.scene-container {
|
||||
height: calc(100vh - 14rem);
|
||||
min-height: 500px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Gallery sections */
|
||||
.gallery-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.gallery-header {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.gallery-title {
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
color: var(--color-foreground, #f1f5f9);
|
||||
margin: 0 0 4px 0;
|
||||
}
|
||||
|
||||
.gallery-subtitle {
|
||||
font-size: 13px;
|
||||
color: var(--color-muted-foreground, #94a3b8);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Horizontal scroll container */
|
||||
.gallery-scroll {
|
||||
display: flex;
|
||||
gap: 14px;
|
||||
overflow-x: auto;
|
||||
padding-bottom: 12px;
|
||||
scroll-snap-type: x mandatory;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
.gallery-scroll > :global(*) {
|
||||
scroll-snap-align: start;
|
||||
}
|
||||
|
||||
/* Hide scrollbar but keep functionality */
|
||||
.gallery-scroll::-webkit-scrollbar {
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
.gallery-scroll::-webkit-scrollbar-track {
|
||||
background: rgba(148, 163, 184, 0.05);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.gallery-scroll::-webkit-scrollbar-thumb {
|
||||
background: rgba(148, 163, 184, 0.15);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.gallery-scroll::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(148, 163, 184, 0.25);
|
||||
}
|
||||
|
||||
/* Group headers */
|
||||
.gallery-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.group-label {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--color-muted-foreground, #94a3b8);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.group-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue