added Kano-Bar
This commit is contained in:
parent
a7f60960f6
commit
fae8385e99
|
|
@ -0,0 +1,141 @@
|
|||
# Feature: Command Bar (Kano-Bar)
|
||||
|
||||
## Motivation
|
||||
|
||||
Die Sidebar in Ka-Note ist der primäre Navigationspfad. Das funktioniert gut, wird aber zum Engpass sobald man schnell zwischen Kontexten wechselt, eine Notiz erfassen will ohne den aktuellen Blick zu verlassen, oder auf dem Desktop tastaturgesteuert arbeitet.
|
||||
|
||||
Ein **Cmd+K Command Bar** (nach Slack-Vorbild) löst das:
|
||||
- Navigation ohne Maus in < 3 Tastendrücken
|
||||
- Schnelle Erfassung (Gedanke → Notiz in 2 Sekunden)
|
||||
- Sidebar kann geschlossen bleiben → mehr Platz für Inhalte
|
||||
|
||||
---
|
||||
|
||||
## Konzept: Die "Kano-Bar"
|
||||
|
||||
Ein dezentes, zentriertes Modal das sich über den aktuellen Content legt.
|
||||
|
||||
**Trigger:** `Ctrl+K` (Windows/Linux) / `Cmd+K` (Mac)
|
||||
|
||||
**Drei Modi** — abhängig vom Input:
|
||||
|
||||
| Input | Modus | Beschreibung |
|
||||
|-------|-------|--------------|
|
||||
| *(leer)* | Recent | Zuletzt besuchte Kontexte |
|
||||
| Text | Navigate | Kontexte + Wiki-Seiten durchsuchen |
|
||||
| `/befehl` | Action | Slash-Commands ausführen |
|
||||
|
||||
---
|
||||
|
||||
## Navigation
|
||||
|
||||
Beim Tippen werden **Contexts** (Jour Fixes, Projekte, Firmen, Personen) und **Wiki-Seiten** gefiltert.
|
||||
|
||||
```
|
||||
┌────────────────────────────────────────┐
|
||||
│ 🔍 tis... │
|
||||
├────────────────────────────────────────┤
|
||||
│ 📋 TISAX PROJEKT │
|
||||
│ 📋 IT-Sicherheit / TISAX WIKI ↵ │
|
||||
│ 👤 Tischler, Martin PERSON │
|
||||
└────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
- Links: Icon nach Typ (📅 Meeting, 📋 Projekt, 👤 Person, 🏢 Firma, 📄 Wiki)
|
||||
- Rechts: Typ-Badge + Scope-Punkt (blau = Business, grau = Privat)
|
||||
- Markierter Eintrag: `↵` Hint rechts
|
||||
- `Enter` → Navigation zu `/context/{id}` oder `/wiki/{id}`
|
||||
|
||||
---
|
||||
|
||||
## Slash-Commands
|
||||
|
||||
| Command | Funktion |
|
||||
|---------|----------|
|
||||
| `/note [Text]` | Neues Topic im Daily Log erstellen |
|
||||
| `/todo [Text]` | Topic mit Wiedervorlage (heute) im Daily Log |
|
||||
| `/jf [Name]` | Zu passendem Jour-Fix-Kontext springen |
|
||||
|
||||
Beispiel: `/note Anruf bei Steffen wegen Angebot` → sofort als Topic in `daily-log` gespeichert, ohne den aktuellen Screen zu verlassen.
|
||||
|
||||
---
|
||||
|
||||
## Visuelles Design
|
||||
|
||||
Dark-Mode, passend zum bestehenden Ka-Note-Design:
|
||||
|
||||
```
|
||||
Overlay: bg-black/50 backdrop-blur-sm (z-200)
|
||||
Modal: bg-zinc-800, border border-amber-500/30, rounded-xl, shadow-2xl
|
||||
Breite: max-w-lg, mx-4 (responsiv)
|
||||
Input: text-lg, placeholder "Suchen oder Befehl eingeben..."
|
||||
Liste: max-h-80, overflow-y-auto
|
||||
Auswahl: bg-zinc-700 rounded
|
||||
```
|
||||
|
||||
Goldener Rand (`amber-500/30`) greift das Ka-Note-Logo-Motiv auf.
|
||||
|
||||
---
|
||||
|
||||
## Tastatursteuerung
|
||||
|
||||
| Taste | Funktion |
|
||||
|-------|----------|
|
||||
| `Ctrl/Cmd+K` | Bar öffnen |
|
||||
| `Escape` | Bar schließen |
|
||||
| `↑ / ↓` | Auswahl navigieren |
|
||||
| `Enter` | Ausgewählte Aktion ausführen |
|
||||
|
||||
---
|
||||
|
||||
## Recent Items
|
||||
|
||||
Wenn die Bar ohne Eingabe geöffnet wird, zeigt sie die **5 zuletzt besuchten Kontexte** (sessionStorage `recentContexts`). Aktualisierung in `+layout.svelte` nach jeder Navigation via `afterNavigate()`.
|
||||
|
||||
---
|
||||
|
||||
## Technische Umsetzung
|
||||
|
||||
### Neue Datei
|
||||
`ka-note/client/src/lib/components/CommandBar.svelte`
|
||||
|
||||
### Änderungen
|
||||
`ka-note/client/src/routes/+layout.svelte`:
|
||||
- `CommandBar` mounten
|
||||
- Globaler `keydown`-Listener: `Ctrl/Cmd+K` → öffnen
|
||||
- `afterNavigate` → recentContexts aktualisieren
|
||||
|
||||
### Datenquellen (bestehende Stores/Queries)
|
||||
- `allActiveContexts()` aus `lib/stores/agenda.ts` — Contexts
|
||||
- `allPages()` aus `lib/stores/wiki.ts` — Wiki-Seiten
|
||||
- Kein neuer Backend-Endpunkt nötig (alles Dexie/IndexedDB)
|
||||
|
||||
### Filterung
|
||||
In-Memory: `label.toLowerCase().includes(query)` — reicht für typische Datenmengen (< 200 Contexts).
|
||||
|
||||
---
|
||||
|
||||
## Scope
|
||||
|
||||
**In Scope:**
|
||||
- Navigation zu Contexts und Wiki-Seiten
|
||||
- Slash-Commands: `/note`, `/todo`, `/jf`
|
||||
- Recent-Items (sessionStorage)
|
||||
- Tastatursteuerung
|
||||
- Responsive (Modal auf Mobile funktioniert)
|
||||
|
||||
**Out of Scope (v1):**
|
||||
- Fuzzy-Suche / Treffergewichtung
|
||||
- @-Mention-Suche (Personen-Aktivität)
|
||||
- Server-seitige Volltextsuche
|
||||
- Persistente "Favourites" in der Bar
|
||||
- Konfigurierbarer Shortcut
|
||||
|
||||
---
|
||||
|
||||
## Offene Fragen
|
||||
|
||||
1. **Mobile:** Lupe-Icon in `BottomTabBar` öffnet die Bar. ✓
|
||||
2. **`/todo`:** Wiedervorlage auf **heute** setzen. ✓
|
||||
3. **Ergebnis-Limit:** 3 Contexts + 3 Wiki-Seiten + alle Slash-Commands. ✓
|
||||
4. **Archivierte Contexts:** Ausblenden. ✓
|
||||
|
|
@ -1 +1 @@
|
|||
1.1.57
|
||||
1.1.58
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
<script lang="ts">
|
||||
import { page } from "$app/state";
|
||||
import { goto } from "$app/navigation";
|
||||
import { commandBarOpen } from "$lib/stores/commandBar";
|
||||
|
||||
interface Props {
|
||||
onsidebaropen: () => void;
|
||||
|
|
@ -46,6 +47,13 @@
|
|||
action: () => goto("/wiki"),
|
||||
active: currentPath.startsWith("/wiki"),
|
||||
},
|
||||
{
|
||||
id: "search",
|
||||
label: "Suche",
|
||||
icon: "🔍",
|
||||
action: () => ($commandBarOpen = true),
|
||||
active: false,
|
||||
},
|
||||
{
|
||||
id: "more",
|
||||
label: "Mehr",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,375 @@
|
|||
<script lang="ts">
|
||||
import { onMount, tick, onDestroy } from "svelte";
|
||||
import { goto } from "$app/navigation";
|
||||
import { commandBarOpen } from "$lib/stores/commandBar";
|
||||
import { allActiveContexts } from "$lib/stores/agenda";
|
||||
import { allPages } from "$lib/stores/wiki";
|
||||
import {
|
||||
getOrCreateJournalTopic,
|
||||
createHistoryEntry,
|
||||
} from "$lib/db/repositories";
|
||||
import { today } from "$lib/db/helpers";
|
||||
|
||||
const contextsQuery = allActiveContexts();
|
||||
const pagesQuery = allPages();
|
||||
|
||||
let query = $state("");
|
||||
let inputEl: HTMLInputElement;
|
||||
let selectedIndex = $state(0);
|
||||
let recentContextIds = $state<string[]>([]);
|
||||
let isMac = $state(false);
|
||||
|
||||
onMount(() => {
|
||||
isMac = navigator.platform.toUpperCase().indexOf("MAC") >= 0;
|
||||
});
|
||||
|
||||
// Subscribe to the store to handle focus
|
||||
$effect(() => {
|
||||
if ($commandBarOpen) {
|
||||
query = "";
|
||||
selectedIndex = 0;
|
||||
loadRecentContexts();
|
||||
tick().then(() => inputEl?.focus());
|
||||
// Prevent body scroll when open
|
||||
document.body.style.overflow = "hidden";
|
||||
} else {
|
||||
document.body.style.overflow = "";
|
||||
}
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
if (typeof document !== "undefined") {
|
||||
document.body.style.overflow = "";
|
||||
}
|
||||
});
|
||||
|
||||
function loadRecentContexts() {
|
||||
try {
|
||||
const stored = sessionStorage.getItem("recentContexts");
|
||||
if (stored) {
|
||||
recentContextIds = JSON.parse(stored);
|
||||
} else {
|
||||
recentContextIds = [];
|
||||
}
|
||||
} catch {
|
||||
recentContextIds = [];
|
||||
}
|
||||
}
|
||||
|
||||
function getContextIcon(type: string) {
|
||||
switch (type) {
|
||||
case "meeting":
|
||||
return "📅";
|
||||
case "project":
|
||||
return "📋";
|
||||
case "person":
|
||||
return "👤";
|
||||
case "company":
|
||||
return "🏢";
|
||||
default:
|
||||
return "📁";
|
||||
}
|
||||
}
|
||||
|
||||
function getSubtype(type: string): string {
|
||||
switch (type) {
|
||||
case "meeting":
|
||||
return "MEETING";
|
||||
case "project":
|
||||
return "PROJEKT";
|
||||
case "person":
|
||||
return "PERSON";
|
||||
case "company":
|
||||
return "FIRMA";
|
||||
default:
|
||||
return "KONTEXT";
|
||||
}
|
||||
}
|
||||
|
||||
const results = $derived.by(() => {
|
||||
const q = query.trim().toLowerCase();
|
||||
|
||||
if (q.startsWith("/")) {
|
||||
// Action mode
|
||||
const parts = q.split(" ");
|
||||
const cmd = parts[0];
|
||||
const text = parts.slice(1).join(" ").trim();
|
||||
|
||||
const actions = [];
|
||||
|
||||
if ("/note".startsWith(cmd)) {
|
||||
actions.push({
|
||||
id: "cmd-note",
|
||||
type: "action",
|
||||
icon: "📝",
|
||||
label: text
|
||||
? `Notiz im Daily Log: "${text}"`
|
||||
: "Neu: Notiz (Daily Log)",
|
||||
badge: "BEFEHL",
|
||||
action: async () => {
|
||||
if (!text) return;
|
||||
await executeNoteCommand(text);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if ("/todo".startsWith(cmd)) {
|
||||
actions.push({
|
||||
id: "cmd-todo",
|
||||
type: "action",
|
||||
icon: "⏰",
|
||||
label: text
|
||||
? `Todo (heute) im Daily Log: "${text}"`
|
||||
: "Neu: Todo (Daily Log)",
|
||||
badge: "BEFEHL",
|
||||
action: async () => {
|
||||
if (!text) return;
|
||||
await executeTodoCommand(text);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if ("/jf".startsWith(cmd)) {
|
||||
actions.push({
|
||||
id: "cmd-jf",
|
||||
type: "action",
|
||||
icon: "📅",
|
||||
label: "Zu Jour-Fix springen...",
|
||||
badge: "BEFEHL",
|
||||
action: () => {}, // Managed by the next items
|
||||
});
|
||||
|
||||
// Add matching JFs
|
||||
if (text && $contextsQuery) {
|
||||
const jfs = $contextsQuery.filter(
|
||||
(c) =>
|
||||
c.type === "meeting" &&
|
||||
c.name.toLowerCase().includes(text),
|
||||
);
|
||||
jfs.forEach((jf) => {
|
||||
actions.push({
|
||||
id: `jf-${jf.id}`,
|
||||
type: "nav-context",
|
||||
icon: "📅",
|
||||
label: jf.name,
|
||||
badge: "MEETING",
|
||||
action: () => {
|
||||
closeBar();
|
||||
goto(`/context/${jf.id}`);
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return actions;
|
||||
}
|
||||
|
||||
if (!q) {
|
||||
// Recent mode
|
||||
const recents = [];
|
||||
if ($contextsQuery) {
|
||||
for (const id of recentContextIds) {
|
||||
const ctx = $contextsQuery.find((c) => c.id === id);
|
||||
if (ctx) {
|
||||
recents.push({
|
||||
id: `ctx-${ctx.id}`,
|
||||
type: "nav-context",
|
||||
icon: getContextIcon(ctx.type),
|
||||
label: ctx.name,
|
||||
badge: getSubtype(ctx.type),
|
||||
action: () => {
|
||||
closeBar();
|
||||
goto(`/context/${ctx.id}`);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return recents.slice(0, 5);
|
||||
}
|
||||
|
||||
// Navigate mode: Search contexts and wiki pages
|
||||
const searchResults = [];
|
||||
let ctxCount = 0;
|
||||
if ($contextsQuery) {
|
||||
for (const ctx of $contextsQuery) {
|
||||
if (ctx.name.toLowerCase().includes(q) && ctxCount < 3) {
|
||||
searchResults.push({
|
||||
id: `ctx-${ctx.id}`,
|
||||
type: "nav-context",
|
||||
icon: getContextIcon(ctx.type),
|
||||
label: ctx.name,
|
||||
badge: getSubtype(ctx.type),
|
||||
action: () => {
|
||||
closeBar();
|
||||
goto(`/context/${ctx.id}`);
|
||||
},
|
||||
});
|
||||
ctxCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let pageCount = 0;
|
||||
if ($pagesQuery) {
|
||||
for (const p of $pagesQuery) {
|
||||
if (p.title.toLowerCase().includes(q) && pageCount < 3) {
|
||||
searchResults.push({
|
||||
id: `page-${p.id}`,
|
||||
type: "nav-wiki",
|
||||
icon: "📄",
|
||||
label: p.title,
|
||||
badge: "WIKI",
|
||||
action: () => {
|
||||
closeBar();
|
||||
goto(`/wiki/${p.id}`);
|
||||
},
|
||||
});
|
||||
pageCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return searchResults;
|
||||
});
|
||||
|
||||
// Reset selection when query changes
|
||||
$effect(() => {
|
||||
query; // reference to trigger
|
||||
selectedIndex = 0;
|
||||
});
|
||||
|
||||
function closeBar() {
|
||||
$commandBarOpen = false;
|
||||
}
|
||||
|
||||
function handleKeydown(e: KeyboardEvent) {
|
||||
if (e.key === "Escape") {
|
||||
e.preventDefault();
|
||||
closeBar();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!results || results.length === 0) return;
|
||||
|
||||
if (e.key === "ArrowDown") {
|
||||
e.preventDefault();
|
||||
selectedIndex = (selectedIndex + 1) % results.length;
|
||||
ensureVisible();
|
||||
} else if (e.key === "ArrowUp") {
|
||||
e.preventDefault();
|
||||
selectedIndex =
|
||||
(selectedIndex - 1 + results.length) % results.length;
|
||||
ensureVisible();
|
||||
} else if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
const selected = results[selectedIndex];
|
||||
if (selected && selected.action) {
|
||||
selected.action();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function ensureVisible() {
|
||||
tick().then(() => {
|
||||
const el = document.getElementById(`kano-item-${selectedIndex}`);
|
||||
if (el) el.scrollIntoView({ block: "nearest" });
|
||||
});
|
||||
}
|
||||
|
||||
async function executeNoteCommand(text: string) {
|
||||
const topic = await getOrCreateJournalTopic();
|
||||
await createHistoryEntry(topic.id, today(), text, null, false, false);
|
||||
closeBar();
|
||||
if (window.location.pathname === "/context/daily-log") {
|
||||
// Trigger a reload or just let dexie liveQuery handle it
|
||||
} else {
|
||||
goto("/context/daily-log");
|
||||
}
|
||||
}
|
||||
|
||||
async function executeTodoCommand(text: string) {
|
||||
const topic = await getOrCreateJournalTopic();
|
||||
await createHistoryEntry(topic.id, today(), text, null, true, false);
|
||||
closeBar();
|
||||
if (window.location.pathname !== "/context/daily-log") {
|
||||
goto("/context/daily-log");
|
||||
}
|
||||
}
|
||||
|
||||
function handleBackdropClick(e: MouseEvent) {
|
||||
if (e.target === e.currentTarget) {
|
||||
closeBar();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if $commandBarOpen}
|
||||
<!-- Backdrop -->
|
||||
<div
|
||||
class="fixed inset-0 z-[200] flex items-start justify-center bg-black/50 backdrop-blur-sm p-4 pt-[20vh]"
|
||||
onclick={handleBackdropClick}
|
||||
role="dialog"
|
||||
aria-label="Command Bar"
|
||||
>
|
||||
<!-- Modal -->
|
||||
<div
|
||||
class="w-full max-w-lg rounded-xl border border-amber-500/30 bg-zinc-800 shadow-2xl overflow-hidden flex flex-col"
|
||||
>
|
||||
<div class="flex items-center border-b border-[#444] px-4 py-3">
|
||||
<span class="mr-3 text-lg text-muted">🔍</span>
|
||||
<input
|
||||
bind:this={inputEl}
|
||||
bind:value={query}
|
||||
onkeydown={handleKeydown}
|
||||
type="text"
|
||||
class="w-full bg-transparent text-lg text-white placeholder-muted outline-none"
|
||||
placeholder="Suchen oder Befehl eingeben..."
|
||||
/>
|
||||
<span
|
||||
class="ml-2 text-xs text-muted font-mono rounded bg-zinc-700 px-1.5 py-0.5"
|
||||
>ESC</span
|
||||
>
|
||||
</div>
|
||||
|
||||
{#if results && results.length > 0}
|
||||
<div class="max-h-80 overflow-y-auto py-2">
|
||||
{#each results as item, i (item.id)}
|
||||
<button
|
||||
id={`kano-item-${i}`}
|
||||
class="w-full flex items-center justify-between px-4 py-3 text-left transition-colors {i ===
|
||||
selectedIndex
|
||||
? 'bg-zinc-700'
|
||||
: 'hover:bg-zinc-700/50'}"
|
||||
onmouseover={() => (selectedIndex = i)}
|
||||
onclick={() => item.action()}
|
||||
>
|
||||
<div class="flex items-center truncate">
|
||||
<span class="mr-3 text-lg">{item.icon}</span>
|
||||
<span class="truncate text-white"
|
||||
>{item.label}</span
|
||||
>
|
||||
</div>
|
||||
<div class="ml-4 flex items-center shrink-0">
|
||||
<span
|
||||
class="text-[10px] font-bold tracking-wider text-muted opacity-80"
|
||||
>{item.badge}</span
|
||||
>
|
||||
{#if i === selectedIndex}
|
||||
<span class="ml-3 text-sm text-accent"
|
||||
>↵</span
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
{:else if query}
|
||||
<div class="px-4 py-8 text-center text-muted">
|
||||
Keine Ergebnisse für "{query}"
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
import { writable } from 'svelte/store';
|
||||
|
||||
export const commandBarOpen = writable(false);
|
||||
|
|
@ -3,6 +3,8 @@
|
|||
import Sidebar from "$lib/components/Sidebar.svelte";
|
||||
import BottomTabBar from "$lib/components/BottomTabBar.svelte";
|
||||
import AiLockBanner from "$lib/components/AiLockBanner.svelte";
|
||||
import CommandBar from "$lib/components/CommandBar.svelte";
|
||||
import { commandBarOpen } from "$lib/stores/commandBar";
|
||||
import { seedIfEmpty } from "$lib/db/seed";
|
||||
import { sync } from "$lib/sync/syncService";
|
||||
import { refreshLockStatus } from "$lib/stores/aiLock";
|
||||
|
|
@ -74,13 +76,47 @@
|
|||
sidebarOpen = false;
|
||||
}
|
||||
|
||||
afterNavigate(() => closeSidebar());
|
||||
function handleGlobalKeydown(e: KeyboardEvent) {
|
||||
if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "k") {
|
||||
e.preventDefault();
|
||||
$commandBarOpen = !$commandBarOpen;
|
||||
}
|
||||
}
|
||||
|
||||
afterNavigate(({ to }) => {
|
||||
closeSidebar();
|
||||
if (to?.url.pathname.startsWith("/context/")) {
|
||||
const id = to.url.pathname.split("/").pop();
|
||||
if (id && id !== "daily-log") {
|
||||
// Exclude daily-log from recents
|
||||
try {
|
||||
const stored = sessionStorage.getItem("recentContexts");
|
||||
let recent = stored ? JSON.parse(stored) : [];
|
||||
recent = [
|
||||
id,
|
||||
...recent.filter((x: string) => x !== id),
|
||||
].slice(0, 5);
|
||||
sessionStorage.setItem(
|
||||
"recentContexts",
|
||||
JSON.stringify(recent),
|
||||
);
|
||||
} catch (e) {
|
||||
console.error("Failed to save recent context", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
document.documentElement.style.setProperty('--scope-color', $scopeColor);
|
||||
document.documentElement.style.setProperty(
|
||||
"--scope-color",
|
||||
$scopeColor,
|
||||
);
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:window onkeydown={handleGlobalKeydown} />
|
||||
|
||||
{#if !authReady}
|
||||
<div class="flex h-screen items-center justify-center bg-bg">
|
||||
<span class="text-muted">Loading...</span>
|
||||
|
|
@ -103,27 +139,43 @@
|
|||
<!-- Sidebar: fullscreen on mobile, static on desktop -->
|
||||
{#if sidebarOpen}
|
||||
<!-- Backdrop -->
|
||||
<div class="fixed inset-0 z-[59] bg-black/50 backdrop-blur-sm md:hidden" onclick={closeSidebar}></div>
|
||||
<div
|
||||
class="fixed inset-0 z-[59] bg-black/50 backdrop-blur-sm md:hidden"
|
||||
onclick={closeSidebar}
|
||||
></div>
|
||||
<!-- Mobile: full-screen overlay -->
|
||||
<div class="fixed inset-0 z-[60] flex flex-col bg-sidebar md:hidden">
|
||||
<div
|
||||
class="fixed inset-0 z-[60] flex flex-col bg-sidebar md:hidden"
|
||||
>
|
||||
<!-- Header with close button -->
|
||||
<div class="flex items-center justify-between border-b border-border px-5 py-4 shrink-0" style="padding-top: max(1rem, env(safe-area-inset-top));">
|
||||
<span class="text-lg font-bold text-accent uppercase tracking-wider">KaNote</span>
|
||||
<div
|
||||
class="flex items-center justify-between border-b border-border px-5 py-4 shrink-0"
|
||||
style="padding-top: max(1rem, env(safe-area-inset-top));"
|
||||
>
|
||||
<span
|
||||
class="text-lg font-bold text-accent uppercase tracking-wider"
|
||||
>KaNote</span
|
||||
>
|
||||
<button
|
||||
class="text-2xl text-muted hover:text-white leading-none"
|
||||
onclick={closeSidebar}
|
||||
aria-label="Close"
|
||||
>×</button>
|
||||
aria-label="Close">×</button
|
||||
>
|
||||
</div>
|
||||
<!-- Scrollable content -->
|
||||
<div class="flex-1 overflow-y-auto px-5 pb-8" style="-webkit-overflow-scrolling: touch;">
|
||||
<div
|
||||
class="flex-1 overflow-y-auto px-5 pb-8"
|
||||
style="-webkit-overflow-scrolling: touch;"
|
||||
>
|
||||
<Sidebar onnavigate={closeSidebar} hideLogo />
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Desktop sidebar -->
|
||||
<aside class="hidden md:flex md:w-[250px] md:flex-col md:border-r md:border-border md:bg-sidebar md:p-5 md:overflow-y-auto">
|
||||
<aside
|
||||
class="hidden md:flex md:w-[250px] md:flex-col md:border-r md:border-border md:bg-sidebar md:p-5 md:overflow-y-auto"
|
||||
>
|
||||
<Sidebar onnavigate={closeSidebar} />
|
||||
</aside>
|
||||
|
||||
|
|
@ -145,3 +197,5 @@
|
|||
<span class="text-muted">Loading...</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<CommandBar />
|
||||
|
|
|
|||
Loading…
Reference in New Issue