feat: navigation

This commit is contained in:
2026-04-01 16:04:06 +02:00
parent a0a5f18128
commit a95594f446
7 changed files with 312 additions and 6 deletions

View File

@@ -19,7 +19,7 @@ from uframe.ir import (
Gauge, Sparkline, Status, Table, TextSpan,
Form, Field, Password, Radio, Checkbox, FormButton,
Let, Source, IfBlock, ForLoop, CacheControl, OnSubmit, StateDecl,
BigTitle, ImageNode,
BigTitle, ImageNode, HNav, VNav, NavItem,
ComponentDef, ComponentUse,
SourceType,
BorderWeight, HeadingLevel, DividerStyle, ListStyle, Align, Style,
@@ -222,9 +222,19 @@ def _parse_line(keyword: str, args: list[str], line_num: int) -> IRNode:
return ListNode(list_style=style, source_line=line_num)
elif keyword == "item":
# Could be a list item or a nav item — disambiguated by parent in tree builder
if len(args) >= 2 and ("/" in args[1] or ":" in args[1]):
# Nav item: item "Label" "/dest.mu" [active]
label = args[0]
dest = args[1]
active = "active" in args[2:] if len(args) > 2 else False
return _NavItemNode("item", label, dest, active, line_num)
content = args[0] if args else ""
return ListItem(content=content, source_line=line_num)
elif keyword == "separator":
return _NavItemNode("separator", "", "", False, line_num)
# Style modifiers (applied to parent)
elif keyword == "align":
val = args[0].lower() if args else "left"
@@ -427,6 +437,15 @@ def _parse_line(keyword: str, args: list[str], line_num: int) -> IRNode:
content = " ".join([keyword] + args)
return Text(content=content, source_line=line_num)
elif keyword == "hnav":
nav_style = args[0] if args else "bar"
return HNav(nav_style=nav_style, source_line=line_num)
elif keyword == "vnav":
nav_style = args[0] if args else "list"
nav_width = int(args[1]) if len(args) > 1 and args[1].isdigit() else 0
return VNav(nav_style=nav_style, nav_width=nav_width, source_line=line_num)
elif keyword == "image":
path = args[0] if args else ""
mode = args[1] if len(args) > 1 else "braille"
@@ -477,6 +496,16 @@ def _parse_line(keyword: str, args: list[str], line_num: int) -> IRNode:
return ComponentUse(comp_name=keyword, args=args, source_line=line_num)
class _NavItemNode(IRNode):
"""Temporary node — absorbed by parent HNav/VNav during tree building."""
def __init__(self, kind: str, label: str, dest: str, active: bool, line_num: int):
super().__init__(source_line=line_num)
self.kind = kind
self.label = label
self.dest = dest
self.active = active
class _ThemeDirective(IRNode):
"""Temporary node — sets theme_name on the Page during tree building."""
def __init__(self, theme_name: str, line_num: int):
@@ -840,6 +869,30 @@ def parse(source: str, components: dict[str, ComponentDef] | None = None) -> Pag
break
continue
# Nav items — absorbed by parent HNav/VNav
if isinstance(node, _NavItemNode):
for si in range(len(stack) - 1, -1, -1):
parent = stack[si][1]
if isinstance(parent, (HNav, VNav)):
parent.items.append(NavItem(
kind=node.kind, label=node.label,
dest=node.dest, active=node.active))
break
continue
# Headings inside vnav become nav headings
if isinstance(node, Heading):
for si in range(len(stack) - 1, -1, -1):
if isinstance(stack[si][1], VNav):
stack[si][1].items.append(NavItem(
kind="heading", label=node.text))
break
else:
# Not inside a vnav — proceed as normal heading
pass
if any(isinstance(stack[si][1], VNav) for si in range(len(stack))):
continue
# Use directive — load standard library components
if isinstance(node, _UseDirective):
lib_comps = _load_library(node.lib_path)