feat(nav): Breadcrumb-Navigation für Hierarchie-Aufrufkette
Neue BreadcrumbBar-Widget-Komponente (breadcrumb.ts) zeigt die Aufrufkette als anklickbare Leiste. Der Route-Param bc (JSON-String) transportiert die Vorgänger-Ansichten durch alle Listen und Masken. Jede Ansicht hängt beim Weiternavigieren ihren eigenen Crumb an die Kette an; Masken reichen bc beim Speichern/Abbrechen zurück. Erfasst: kunde, projekt, kontaktadresse, call_log, kontakt. Generated by Mistral Vibe. Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
1 parent
f1e5938ae1
commit
fd30049a32
6 files changed
+316
-27
No files matched your search
@@ -0,0 +1,99 @@
|
||||
import m from "mithril"
|
||||
|
||||
/** Ein Breadcrumb-Glied: Route, Filter-Params und Anzeige-Label einer
|
||||
* Vorgänger-Ansicht in der Aufrufkette. */
|
||||
export interface iBreadcrumb {
|
||||
route: string
|
||||
params: any
|
||||
label: string
|
||||
}
|
||||
|
||||
/** Attribute der BreadcrumbBar: die Vorgänger-Kette (ancestors) und das
|
||||
* Label der aktuellen Ansicht (wird als letztes, nicht klickbares Glied
|
||||
* angezeigt). Ist breadcrumbs leer, wird keine Leiste gerendert. */
|
||||
export interface iBreadcrumbBar {
|
||||
breadcrumbs: iBreadcrumb[]
|
||||
current: string
|
||||
}
|
||||
|
||||
/** Parst den Route-Param `bc` (JSON-String) in ein Breadcrumb-Array.
|
||||
* Liefert bei fehlendem/ungültigem `bc` ein leeres Array. */
|
||||
export function parseBc(bc: string | undefined): iBreadcrumb[] {
|
||||
if (!bc) return []
|
||||
try { return JSON.parse(bc) } catch { return [] }
|
||||
}
|
||||
|
||||
/** Serialisiert ein Breadcrumb-Array in den Route-Param `bc`.
|
||||
* Liefert undefined bei leerer Kette (kein bc in der Route). */
|
||||
export function serializeBc(crumbs: iBreadcrumb[]): string | undefined {
|
||||
return crumbs.length > 0 ? JSON.stringify(crumbs) : undefined
|
||||
}
|
||||
|
||||
export const BreadcrumbBar: m.ClosureComponent<iBreadcrumbBar> = () => {
|
||||
return {
|
||||
view: (vnode) => {
|
||||
const crumbs = vnode.attrs.breadcrumbs || []
|
||||
const current = vnode.attrs.current
|
||||
// Ohne Vorgänger und ohne aktuelles Label keine Leiste rendern
|
||||
// (Top-Level-Ansichten ohne Hierarchie-Kontext).
|
||||
if (crumbs.length === 0 && !current) return null
|
||||
|
||||
const items: any[] = []
|
||||
crumbs.forEach((crumb: iBreadcrumb, i: number) => {
|
||||
if (i > 0) {
|
||||
items.push(m("span.w3-small", {
|
||||
style: {margin: '0 2px', opacity: '0.6'}
|
||||
}, "›"))
|
||||
}
|
||||
// Vorgänger sind klickbar: Navigation zur jeweiligen Ansicht
|
||||
// mit deren Filter-Params und der auf dieses Glied gekürzten
|
||||
// Kette als neuem bc.
|
||||
items.push(m("a.w3-button.w3-small.w3-theme-l2", {
|
||||
style: {
|
||||
padding: '2px 8px',
|
||||
'border-radius': '4px',
|
||||
'text-decoration': 'none',
|
||||
'white-space': 'nowrap'
|
||||
},
|
||||
title: "Zurück zu " + crumb.label,
|
||||
onclick: () => {
|
||||
const subChain = crumbs.slice(0, i)
|
||||
const params: any = {...crumb.params}
|
||||
const bc = serializeBc(subChain)
|
||||
if (bc) params.bc = bc
|
||||
m.route.set(crumb.route, params)
|
||||
}
|
||||
}, crumb.label))
|
||||
})
|
||||
// Aktuelle Ansicht als letztes, nicht klickbares Glied (fett).
|
||||
if (current) {
|
||||
if (crumbs.length > 0) {
|
||||
items.push(m("span.w3-small", {
|
||||
style: {margin: '0 2px', opacity: '0.6'}
|
||||
}, "›"))
|
||||
}
|
||||
items.push(m("span.w3-small.w3-theme-d3", {
|
||||
style: {
|
||||
padding: '2px 8px',
|
||||
'border-radius': '4px',
|
||||
'font-weight': 'bold',
|
||||
'white-space': 'nowrap'
|
||||
}
|
||||
}, current))
|
||||
}
|
||||
|
||||
return m("div.w3-theme-l1", {
|
||||
style: {
|
||||
display: 'flex',
|
||||
'flex-wrap': 'nowrap',
|
||||
'align-items': 'center',
|
||||
padding: '4px 8px',
|
||||
'border-radius': '4px',
|
||||
'margin-bottom': '8px',
|
||||
'overflow-x': 'auto',
|
||||
'overflow-y': 'hidden'
|
||||
}
|
||||
}, items)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user