secureweb/sweb/src/components/NavItem.svelte
Mahmoud Emad da0ced9b4a feat: Add Tailwind CSS and navigation generation
- Added Tailwind CSS for styling.
- Implemented automatic navigation data generation from markdown files.
- Created `NavItem` component for rendering navigation items.
- Added scripts for navigation generation and updated build process.
2025-05-12 11:28:10 +03:00

166 lines
4.5 KiB
Svelte

<script lang="ts">
import type { NavItem } from "../types/nav";
import { ChevronRight, ChevronDown } from "lucide-svelte";
import { slide } from "svelte/transition";
export let item: NavItem;
export let level: number = 0;
export let expanded: Record<string, boolean> = {};
export let activePath: string = "";
export let onToggle: (path: string) => void;
export let onNavClick: (path: string, event: MouseEvent) => void;
$: hasChildren = item.children && item.children.length > 0;
$: isExpanded = expanded[item.link];
$: isActive = activePath === item.link;
// Calculate indentation based on nesting level
$: indentation = level * 16; // 16px per level
function handleKeyDown(event: KeyboardEvent, isFolder: boolean) {
// Add keyboard navigation
if (isFolder && (event.key === "Enter" || event.key === " ")) {
event.preventDefault();
onToggle(item.link);
} else if (!isFolder && event.key === "Enter") {
event.preventDefault();
onNavClick(item.link, event as unknown as MouseEvent);
}
}
</script>
<div class="nav-item" style="--indent: {indentation}px;">
{#if hasChildren}
<!-- Folder item -->
<button
class="nav-button folder-button {isActive ? 'active' : ''}"
on:click={() => onToggle(item.link)}
on:keydown={(e) => handleKeyDown(e, true)}
tabindex="0"
aria-expanded={isExpanded}
>
<div class="tree-line" style="width: {indentation}px;"></div>
<div class="icon-container">
{#if isExpanded}
<ChevronDown
class="chevron-icon {isExpanded ? 'expanded' : ''}"
/>
{:else}
<ChevronRight
class="chevron-icon {isExpanded ? 'expanded' : ''}"
/>
{/if}
</div>
<span class="label">{item.label}</span>
</button>
{#if isExpanded && item.children && item.children.length > 0}
<div class="children" transition:slide={{ duration: 200 }}>
{#each item.children as child}
<svelte:self
item={child}
level={level + 1}
{expanded}
{activePath}
{onToggle}
{onNavClick}
/>
{/each}
</div>
{/if}
{:else}
<!-- File item -->
<a
href={item.link}
class="nav-button file-button {isActive ? 'active' : ''}"
on:click={(e) => onNavClick(item.link, e)}
on:keydown={(e) => handleKeyDown(e, false)}
tabindex="0"
>
<div class="tree-line" style="width: {indentation}px;"></div>
<div class="icon-container">
<!-- No file icon -->
</div>
<span class="label">{item.label}</span>
</a>
{/if}
</div>
<style>
.nav-item {
position: relative;
}
.nav-button {
display: flex;
align-items: center;
width: 100%;
text-align: left;
padding: 8px 12px;
padding-left: calc(var(--indent) + 12px);
position: relative;
border: none;
background: transparent;
color: #4b5563;
font-size: 0.9rem;
cursor: pointer;
transition:
background-color 0.2s,
color 0.2s;
}
.nav-button:hover {
background-color: rgba(0, 0, 0, 0.05);
}
.nav-button.active {
background-color: rgba(59, 130, 246, 0.1);
color: #3b82f6;
font-weight: 500;
}
.tree-line {
position: absolute;
left: 0;
top: 0;
bottom: 0;
pointer-events: none;
}
.tree-line::before {
content: "";
position: absolute;
left: 12px;
top: 0;
bottom: 0;
width: 1px;
background-color: #e5e7eb;
}
.icon-container {
display: flex;
align-items: center;
margin-right: 8px;
}
.chevron-icon {
width: 16px;
height: 16px;
transition: transform 0.2s;
}
.chevron-icon.expanded {
transform: rotate(90deg);
}
.label {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.children {
overflow: hidden;
}
</style>