This is an automated email from the ASF dual-hosted git repository.

wu-sheng pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/skywalking-horizon-ui.git

commit 760a1006e7012676320477c12662f898ffcb7131
Author: Wu Sheng <[email protected]>
AuthorDate: Tue May 12 10:16:49 2026 +0800

    ui: appshell with sidebar topbar and design tokens
---
 apps/ui/src/App.vue                         |  20 +++-
 apps/ui/src/components/icons/Icon.vue       | 177 ++++++++++++++++++++++++++++
 apps/ui/src/components/shell/AppShell.vue   |  31 +++++
 apps/ui/src/components/shell/AppSidebar.vue | 171 +++++++++++++++++++++++++++
 apps/ui/src/components/shell/AppTopbar.vue  |  62 ++++++++++
 apps/ui/src/views/landing/LandingView.vue   |  47 +++++---
 6 files changed, 489 insertions(+), 19 deletions(-)

diff --git a/apps/ui/src/App.vue b/apps/ui/src/App.vue
index 69dd87a..944e2c4 100644
--- a/apps/ui/src/App.vue
+++ b/apps/ui/src/App.vue
@@ -1,7 +1,23 @@
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one or more
+  contributor license agreements.  See the NOTICE file distributed with
+  this work for additional information regarding copyright ownership.
+  The ASF licenses this file to You under the Apache License, Version 2.0
+  (the "License"); you may not use this file except in compliance with
+  the License.  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+-->
 <script setup lang="ts">
-import { RouterView } from 'vue-router';
+import AppShell from '@/components/shell/AppShell.vue';
 </script>
 
 <template>
-  <RouterView />
+  <AppShell />
 </template>
diff --git a/apps/ui/src/components/icons/Icon.vue 
b/apps/ui/src/components/icons/Icon.vue
new file mode 100644
index 0000000..4c905e0
--- /dev/null
+++ b/apps/ui/src/components/icons/Icon.vue
@@ -0,0 +1,177 @@
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one or more
+  contributor license agreements.  See the NOTICE file distributed with
+  this work for additional information regarding copyright ownership.
+  The ASF licenses this file to You under the Apache License, Version 2.0
+  (the "License"); you may not use this file except in compliance with
+  the License.  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+-->
+<script setup lang="ts">
+defineProps<{ name: IconName; size?: number }>();
+
+export type IconName =
+  | 'dash'
+  | 'topo'
+  | 'metric'
+  | 'trace'
+  | 'log'
+  | 'alert'
+  | 'prof'
+  | 'svc'
+  | 'ep'
+  | 'event'
+  | 'set'
+  | 'search'
+  | 'chev'
+  | 'caret'
+  | 'bell'
+  | 'plus'
+  | 'share'
+  | 'refresh'
+  | 'more'
+  | 'clock'
+  | 'filter'
+  | 'expand'
+  | 'download'
+  | 'star'
+  | 'flame'
+  | 'sky'
+  | 'user';
+</script>
+
+<template>
+  <svg
+    viewBox="0 0 24 24"
+    :width="size ?? 14"
+    :height="size ?? 14"
+    fill="none"
+    stroke="currentColor"
+    stroke-width="1.6"
+  >
+    <template v-if="name === 'dash'">
+      <rect x="3" y="3" width="7" height="9" rx="1.5" />
+      <rect x="14" y="3" width="7" height="5" rx="1.5" />
+      <rect x="14" y="12" width="7" height="9" rx="1.5" />
+      <rect x="3" y="16" width="7" height="5" rx="1.5" />
+    </template>
+    <template v-else-if="name === 'topo'">
+      <circle cx="5" cy="6" r="2" />
+      <circle cx="19" cy="6" r="2" />
+      <circle cx="12" cy="14" r="2" />
+      <circle cx="5" cy="20" r="2" />
+      <circle cx="19" cy="20" r="2" />
+      <path d="M6.5 7.5L10.5 13M17.5 7.5L13.5 13M10.5 15L6.5 19M13.5 15L17.5 
19" />
+    </template>
+    <template v-else-if="name === 'metric'">
+      <path d="M3 17l5-7 4 4 4-8 5 11" />
+    </template>
+    <template v-else-if="name === 'trace'">
+      <path d="M3 6h10M3 10h14M3 14h7M3 18h12" />
+    </template>
+    <template v-else-if="name === 'log'">
+      <rect x="4" y="3" width="16" height="18" rx="2" />
+      <path d="M8 8h8M8 12h8M8 16h5" />
+    </template>
+    <template v-else-if="name === 'alert'">
+      <path d="M12 3l9 16H3z" />
+      <path d="M12 10v4M12 17v.5" />
+    </template>
+    <template v-else-if="name === 'prof'">
+      <path d="M3 20h18M5 20v-6M9 20v-9M13 20v-4M17 20v-12M21 20v-7" />
+    </template>
+    <template v-else-if="name === 'svc'">
+      <rect x="3" y="4" width="18" height="6" rx="1.5" />
+      <rect x="3" y="14" width="18" height="6" rx="1.5" />
+      <circle cx="7" cy="7" r="0.8" fill="currentColor" />
+      <circle cx="7" cy="17" r="0.8" fill="currentColor" />
+    </template>
+    <template v-else-if="name === 'ep'">
+      <path d="M4 12h6M14 12h6" />
+      <circle cx="12" cy="12" r="2" />
+      <path d="M4 12a8 8 0 0116 0M4 12a8 8 0 0016 0" opacity=".4" />
+    </template>
+    <template v-else-if="name === 'event'">
+      <circle cx="12" cy="12" r="9" />
+      <path d="M12 7v5l3 2" />
+    </template>
+    <template v-else-if="name === 'set'">
+      <circle cx="12" cy="12" r="3" />
+      <path
+        d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 
12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41"
+      />
+    </template>
+    <template v-else-if="name === 'search'">
+      <circle cx="11" cy="11" r="6" />
+      <path d="M20 20l-3.5-3.5" />
+    </template>
+    <template v-else-if="name === 'chev'">
+      <path stroke-width="2" d="M9 6l6 6-6 6" />
+    </template>
+    <template v-else-if="name === 'caret'">
+      <path stroke-width="2" d="M6 9l6 6 6-6" />
+    </template>
+    <template v-else-if="name === 'bell'">
+      <path d="M6 8a6 6 0 0112 0v5l1.5 3H4.5L6 13z" />
+      <path d="M10 19a2 2 0 004 0" />
+    </template>
+    <template v-else-if="name === 'plus'">
+      <path stroke-width="2" d="M12 5v14M5 12h14" />
+    </template>
+    <template v-else-if="name === 'share'">
+      <path d="M4 12v6a2 2 0 002 2h12a2 2 0 002-2v-6M16 6l-4-4-4 4M12 2v13" />
+    </template>
+    <template v-else-if="name === 'refresh'">
+      <path d="M20 11A8 8 0 006 5l-2 2M4 13a8 8 0 0014 6l2-2M4 4v5h5M20 
20v-5h-5" />
+    </template>
+    <template v-else-if="name === 'more'">
+      <circle cx="6" cy="12" r="1.4" fill="currentColor" stroke="none" />
+      <circle cx="12" cy="12" r="1.4" fill="currentColor" stroke="none" />
+      <circle cx="18" cy="12" r="1.4" fill="currentColor" stroke="none" />
+    </template>
+    <template v-else-if="name === 'clock'">
+      <circle cx="12" cy="12" r="9" />
+      <path d="M12 7v5l3 2" />
+    </template>
+    <template v-else-if="name === 'filter'">
+      <path d="M3 5h18l-7 9v6l-4-2v-4z" />
+    </template>
+    <template v-else-if="name === 'expand'">
+      <path d="M9 3H3v6M21 9V3h-6M15 21h6v-6M3 15v6h6" />
+    </template>
+    <template v-else-if="name === 'download'">
+      <path d="M12 3v12M7 10l5 5 5-5M4 21h16" />
+    </template>
+    <template v-else-if="name === 'star'">
+      <path d="M12 3l2.8 6 6.5.9-4.7 4.6 1.1 6.5L12 18l-5.7 3 1.1-6.5L2.7 9.9 
9.2 9z" />
+    </template>
+    <template v-else-if="name === 'flame'">
+      <path d="M12 3c1 4 6 5 6 11a6 6 0 11-12 0c0-3 2-4 2-7 2 2 3 3 4 0z" />
+    </template>
+    <template v-else-if="name === 'sky'">
+      <path
+        fill="currentColor"
+        stroke="none"
+        d="M3 14c4-3 8-3 12-1 3 1.4 5 .5 6-1-1 5-4 8-9 8-4 0-7-2-9-6z"
+        opacity=".95"
+      />
+      <path
+        fill="#fff"
+        stroke="none"
+        d="M5 10c3-2 7-2 11 0 3 1.3 5 .6 6-1-1 3.6-4 6-8 6-4 0-7-1.6-9-5z"
+        opacity=".22"
+      />
+    </template>
+    <template v-else-if="name === 'user'">
+      <circle cx="12" cy="8" r="4" />
+      <path d="M4 21c0-4 4-6 8-6s8 2 8 6" />
+    </template>
+  </svg>
+</template>
diff --git a/apps/ui/src/components/shell/AppShell.vue 
b/apps/ui/src/components/shell/AppShell.vue
new file mode 100644
index 0000000..6482234
--- /dev/null
+++ b/apps/ui/src/components/shell/AppShell.vue
@@ -0,0 +1,31 @@
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one or more
+  contributor license agreements.  See the NOTICE file distributed with
+  this work for additional information regarding copyright ownership.
+  The ASF licenses this file to You under the Apache License, Version 2.0
+  (the "License"); you may not use this file except in compliance with
+  the License.  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+-->
+<script setup lang="ts">
+import { RouterView } from 'vue-router';
+import AppSidebar from './AppSidebar.vue';
+import AppTopbar from './AppTopbar.vue';
+</script>
+
+<template>
+  <div class="sw">
+    <AppSidebar />
+    <AppTopbar />
+    <main class="sw-main">
+      <RouterView />
+    </main>
+  </div>
+</template>
diff --git a/apps/ui/src/components/shell/AppSidebar.vue 
b/apps/ui/src/components/shell/AppSidebar.vue
new file mode 100644
index 0000000..0d63822
--- /dev/null
+++ b/apps/ui/src/components/shell/AppSidebar.vue
@@ -0,0 +1,171 @@
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one or more
+  contributor license agreements.  See the NOTICE file distributed with
+  this work for additional information regarding copyright ownership.
+  The ASF licenses this file to You under the Apache License, Version 2.0
+  (the "License"); you may not use this file except in compliance with
+  the License.  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+-->
+<script setup lang="ts">
+import { ref } from 'vue';
+import { RouterLink, useRoute } from 'vue-router';
+import Icon, { type IconName } from '@/components/icons/Icon.vue';
+
+// Phase 2 will replace this stub with real getMenuItems / listLayers data.
+const layers = ref([
+  { key: 'general', name: 'General Service', svc: 84, color: 
'var(--sw-accent)' },
+  { key: 'mesh', name: 'Service Mesh', svc: 22, color: 'var(--sw-info)' },
+  { key: 'k8s', name: 'Kubernetes', svc: 62, color: 'var(--sw-purple)' },
+  { key: 'rum', name: 'Browser (RUM)', svc: 8, color: 'var(--sw-cyan)' },
+  { key: 'mq', name: 'Virtual MQ', svc: 6, color: 'var(--sw-ok)' },
+  { key: 'db', name: 'Virtual Database', svc: 6, color: 'var(--sw-warn)' },
+  { key: 'otel', name: 'OpenTelemetry', svc: 18, color: 'var(--sw-purple)' },
+  { key: 'faas', name: 'FaaS', svc: 3, color: 'var(--sw-err)' },
+]);
+const expandedLayer = ref<string | null>('general');
+
+const route = useRoute();
+function isActive(path: string): boolean {
+  return route.path === path || route.path.startsWith(path + '/');
+}
+
+interface NavRow {
+  icon: IconName;
+  label: string;
+  to: string;
+  badge?: { text: string; kind?: 'ok' | 'warn' | 'err' | 'info' };
+}
+
+const telemetry: NavRow[] = [
+  { icon: 'metric', label: 'Dashboards', to: '/dashboards' },
+  { icon: 'trace', label: 'Traces', to: '/operate/traces' },
+  { icon: 'log', label: 'Logs', to: '/operate/logs' },
+  { icon: 'prof', label: 'Profiling', to: '/profiling' },
+  { icon: 'event', label: 'Events', to: '/operate/events' },
+];
+const operate: NavRow[] = [
+  { icon: 'alert', label: 'Alarms', to: '/operate/alarms', badge: { text: '7', 
kind: 'err' } },
+];
+const admin: NavRow[] = [
+  { icon: 'svc', label: 'Cluster status', to: '/cluster' },
+  { icon: 'user', label: 'Users', to: '/admin/users' },
+  { icon: 'set', label: 'Roles', to: '/admin/roles' },
+  { icon: 'log', label: 'Audit log', to: '/admin/audit' },
+];
+</script>
+
+<template>
+  <aside class="sw-side">
+    <RouterLink to="/" class="sw-brand">
+      <div class="sw-brand-mark"><Icon name="sky" :size="13" /></div>
+      <span>SkyWalking</span>
+      <small>Horizon</small>
+    </RouterLink>
+
+    <nav class="sw-nav">
+      <div class="sw-nav-section sw-row" style="justify-content: 
space-between">
+        <span>Layers</span>
+        <span style="color: var(--sw-fg-3); font-weight: 400">{{ layers.length 
}} layers</span>
+      </div>
+      <template v-for="L in layers" :key="L.key">
+        <div
+          class="sw-nav-item"
+          :class="{ 'is-active': expandedLayer === L.key }"
+          @click="expandedLayer = expandedLayer === L.key ? null : L.key"
+        >
+          <span class="layer-dot" :style="{ background: L.color }" />
+          <span :style="{ fontWeight: expandedLayer === L.key ? 600 : 500 
}">{{ L.name }}</span>
+          <span class="sw-badge" style="margin-left: auto">{{ L.svc }}</span>
+          <span class="caret" :class="{ open: expandedLayer === L.key }"><Icon 
name="caret" :size="10" /></span>
+        </div>
+        <div v-if="expandedLayer === L.key" class="layer-children">
+          <RouterLink :to="`/layer/${L.key}`" class="sw-nav-item" :class="{ 
'is-active': isActive(`/layer/${L.key}`) }">
+            <Icon name="dash" /><span>Layer overview</span>
+          </RouterLink>
+          <RouterLink :to="`/layer/${L.key}/services`" class="sw-nav-item" 
:class="{ 'is-active': isActive(`/layer/${L.key}/services`) }">
+            <Icon name="svc" /><span>Services</span><span class="sw-badge" 
style="margin-left: auto">{{ L.svc }}</span>
+          </RouterLink>
+          <RouterLink :to="`/layer/${L.key}/instances`" class="sw-nav-item" 
:class="{ 'is-active': isActive(`/layer/${L.key}/instances`) }">
+            <Icon name="prof" /><span>Instances</span>
+          </RouterLink>
+          <RouterLink :to="`/layer/${L.key}/endpoints`" class="sw-nav-item" 
:class="{ 'is-active': isActive(`/layer/${L.key}/endpoints`) }">
+            <Icon name="ep" /><span>Endpoints</span>
+          </RouterLink>
+          <RouterLink :to="`/layer/${L.key}/topology`" class="sw-nav-item" 
:class="{ 'is-active': isActive(`/layer/${L.key}/topology`) }">
+            <Icon name="topo" /><span>Topology</span>
+          </RouterLink>
+        </div>
+      </template>
+
+      <div class="sw-nav-section">Telemetry</div>
+      <RouterLink v-for="row in telemetry" :key="row.to" :to="row.to" 
class="sw-nav-item" :class="{ 'is-active': isActive(row.to) }">
+        <Icon :name="row.icon" /><span>{{ row.label }}</span>
+        <span v-if="row.badge" class="sw-badge" :class="row.badge.kind" 
style="margin-left: auto">{{ row.badge.text }}</span>
+      </RouterLink>
+
+      <div class="sw-nav-section">Operate</div>
+      <RouterLink v-for="row in operate" :key="row.to" :to="row.to" 
class="sw-nav-item" :class="{ 'is-active': isActive(row.to) }">
+        <Icon :name="row.icon" /><span>{{ row.label }}</span>
+        <span v-if="row.badge" class="sw-badge" :class="row.badge.kind" 
style="margin-left: auto">{{ row.badge.text }}</span>
+      </RouterLink>
+
+      <div class="sw-nav-section">Admin</div>
+      <RouterLink v-for="row in admin" :key="row.to" :to="row.to" 
class="sw-nav-item" :class="{ 'is-active': isActive(row.to) }">
+        <Icon :name="row.icon" /><span>{{ row.label }}</span>
+      </RouterLink>
+    </nav>
+
+    <div class="sw-side-foot">
+      <div class="sw-avatar">SW</div>
+      <div style="line-height: 1.2">
+        <div style="color: var(--sw-fg-0); font-weight: 600">guest</div>
+        <div>not signed in</div>
+      </div>
+    </div>
+  </aside>
+</template>
+
+<style scoped>
+.sw-brand,
+.sw-brand:hover {
+  text-decoration: none;
+  color: inherit;
+}
+.layer-dot {
+  width: 7px;
+  height: 7px;
+  border-radius: 50%;
+  flex: 0 0 7px;
+}
+.caret {
+  color: var(--sw-fg-3);
+  margin-left: 4px;
+  transition: transform 0.15s;
+  display: inline-flex;
+  width: 10px;
+  transform: rotate(-90deg);
+}
+.caret.open {
+  transform: rotate(0);
+}
+.layer-children {
+  padding-left: 12px;
+  margin-left: 18px;
+  margin-bottom: 4px;
+  border-left: 1px dashed var(--sw-line-2);
+}
+.layer-children .sw-nav-item {
+  text-decoration: none;
+}
+.sw-nav-item {
+  text-decoration: none;
+}
+</style>
diff --git a/apps/ui/src/components/shell/AppTopbar.vue 
b/apps/ui/src/components/shell/AppTopbar.vue
new file mode 100644
index 0000000..d4f456a
--- /dev/null
+++ b/apps/ui/src/components/shell/AppTopbar.vue
@@ -0,0 +1,62 @@
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one or more
+  contributor license agreements.  See the NOTICE file distributed with
+  this work for additional information regarding copyright ownership.
+  The ASF licenses this file to You under the Apache License, Version 2.0
+  (the "License"); you may not use this file except in compliance with
+  the License.  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+-->
+<script setup lang="ts">
+import { computed } from 'vue';
+import { useRoute } from 'vue-router';
+import Icon from '@/components/icons/Icon.vue';
+
+const route = useRoute();
+
+// Trivial breadcrumb derivation from the path. Real breadcrumb metadata
+// lands when individual views start setting `route.meta.breadcrumbs`.
+const crumbs = computed<string[]>(() => {
+  const segs = route.path.split('/').filter(Boolean);
+  if (segs.length === 0) return ['Home'];
+  return segs.map((s) => s.replace(/-/g, ' ').replace(/^./, (c) => 
c.toUpperCase()));
+});
+</script>
+
+<template>
+  <header class="sw-top">
+    <div class="sw-crumbs">
+      <template v-for="(c, i) in crumbs" :key="i">
+        <Icon v-if="i > 0" name="chev" :size="10" />
+        <b v-if="i === crumbs.length - 1">{{ c }}</b>
+        <span v-else>{{ c }}</span>
+      </template>
+    </div>
+    <div class="sw-top-search">
+      <Icon name="search" :size="12" />
+      <span>Search services, endpoints, traceId&hellip;</span>
+      <kbd>⌘K</kbd>
+    </div>
+    <div class="sw-top-actions">
+      <div class="sw-btn">
+        <span style="color: var(--sw-fg-2)">env</span>
+        <b style="color: var(--sw-fg-0)">production</b>
+        <Icon name="caret" :size="10" />
+      </div>
+      <div class="sw-btn">
+        <Icon name="clock" :size="12" />
+        <span>Last 30 minutes</span>
+        <Icon name="caret" :size="10" />
+      </div>
+      <div class="sw-btn is-icon"><Icon name="refresh" :size="12" /></div>
+      <div class="sw-btn is-icon"><Icon name="bell" :size="12" /></div>
+    </div>
+  </header>
+</template>
diff --git a/apps/ui/src/views/landing/LandingView.vue 
b/apps/ui/src/views/landing/LandingView.vue
index d8acc74..c772fa5 100644
--- a/apps/ui/src/views/landing/LandingView.vue
+++ b/apps/ui/src/views/landing/LandingView.vue
@@ -1,30 +1,43 @@
-<script setup lang="ts">
-// Placeholder landing view. Implementation pending.
-</script>
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one or more
+  contributor license agreements.  See the NOTICE file distributed with
+  this work for additional information regarding copyright ownership.
+  The ASF licenses this file to You under the Apache License, Version 2.0
+  (the "License"); you may not use this file except in compliance with
+  the License.  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+-->
+<script setup lang="ts"></script>
 
 <template>
-  <main class="placeholder">
-    <h1>SkyWalking Horizon UI</h1>
-    <p>Scaffold ready.</p>
-  </main>
+  <div class="placeholder">
+    <h1>Horizon UI</h1>
+    <p>Shell ready. Routed views land in Phase 2 onward.</p>
+  </div>
 </template>
 
 <style scoped>
 .placeholder {
-  min-height: 100vh;
-  display: grid;
-  place-items: center;
-  background: var(--sw-bg-0);
-  color: var(--sw-fg-0);
-  font-family: var(--sw-sans);
-  text-align: center;
-  padding: 24px;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  min-height: 60vh;
+  gap: 8px;
+  color: var(--sw-fg-1);
 }
 .placeholder h1 {
   font-size: 22px;
   font-weight: 600;
-  letter-spacing: -0.01em;
-  margin-bottom: 8px;
+  letter-spacing: -0.02em;
+  color: var(--sw-fg-0);
 }
 .placeholder p {
   color: var(--sw-fg-2);

Reply via email to