docling-studio/frontend/src/shared/breadcrumb/store.ts
Pier-Jean Malandrino be51105aaa feat(#208): doc workspace breadcrumb (Studio > <doc> > <mode>)
Adds the breadcrumb anchoring the user across modes on the doc
workspace. Empty / hidden on routes that don't opt in.

Components
- shared/breadcrumb/AppBreadcrumb.vue: data-driven, accessible
  (<nav aria-label>, <ol>, aria-current=page on the leaf).
  Renders nothing when crumbs.length === 0.
- shared/breadcrumb/types.ts: Crumb = LinkCrumb | LeafCrumb
  discriminated union.
- shared/breadcrumb/store.ts: Pinia store + useCrumbs(source)
  composable that auto-clears on unmount. Accepts a static array
  OR a reactive ref/computed so pages with async doc fetches can
  rebuild crumbs as data lands.
- shared/breadcrumb/text.ts: truncate(text, max) helper.

Wiring
- App.vue main outlet now sits below <AppBreadcrumb>; the shell
  reads from useBreadcrumbStore so pages don't need teleports.
- DocWorkspacePage provides Studio > <id-truncated> > <mode-label>;
  once the doc is fetched (#216 / E4), the id placeholder will
  swap for the truncated filename.

i18n
- breadcrumb.aria, breadcrumb.studio, breadcrumb.mode.{ask,inspect,
  chunks} added in fr + en.

Tests
- shared/breadcrumb/text.test.ts: 5 cases on truncate.
- shared/breadcrumb/store.test.ts: store actions + useCrumbs reactive/
  static seeding (the lifecycle-clear path is covered end-to-end by
  the integration tests landing in #211 / #216 — the project doesn't
  use @vue/test-utils so a unit test for onBeforeUnmount is overkill).

Refs #208
2026-04-29 17:52:59 +02:00

53 lines
1.3 KiB
TypeScript

import { defineStore } from 'pinia'
import { onBeforeUnmount, watch, type Ref, type ComputedRef } from 'vue'
import type { Crumb } from './types'
/**
* Store the current breadcrumb segments. Pages set them on mount via
* `useCrumbs(crumbs)` and clear them on unmount automatically.
*
* The shell (`App.vue`) reads `crumbs` and renders `<AppBreadcrumb>` —
* empty array → component renders nothing.
*/
export const useBreadcrumbStore = defineStore('breadcrumb', {
state: () => ({
crumbs: [] as Crumb[],
}),
actions: {
setCrumbs(crumbs: Crumb[]) {
this.crumbs = crumbs
},
clear() {
this.crumbs = []
},
},
})
/**
* Composable for pages to declare their breadcrumb. Auto-clears on
* unmount so a stale breadcrumb never leaks to the next route.
*
* Accepts a static array OR a reactive ref / computed so pages with
* async data (e.g. doc workspace fetching the doc title) can rebuild
* crumbs as the data lands.
*/
export function useCrumbs(source: Crumb[] | Ref<Crumb[]> | ComputedRef<Crumb[]>) {
const store = useBreadcrumbStore()
if (Array.isArray(source)) {
store.setCrumbs(source)
} else {
watch(
source,
(next) => {
store.setCrumbs(next)
},
{ immediate: true },
)
}
onBeforeUnmount(() => {
store.clear()
})
}