@ulu/frontend-vue 0.1.0-beta.19 → 0.1.0-beta.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,47 @@
1
+ import { computed } from 'vue';
2
+ import { useHead as defaultUseHead } from '@unhead/vue';
3
+ import { useRoute as defaultUseRoute } from 'vue-router';
4
+ import { pageTitles } from './usePageTitle.js';
5
+
6
+ /**
7
+ * Manages the document's <title> tag based on the current route's title.
8
+ * It pulls titles from the `usePageTitle` system, falling back to `meta.title`,
9
+ * and formats it with a template.
10
+ *
11
+ * This should be called once in the root App.vue component.
12
+ *
13
+ * @param {object} options
14
+ * @param {string} [options.titleTemplate='%s | My Awesome Site'] - The template for the title.
15
+ * @param {Function} [options.useRoute=defaultUseRoute] - The `useRoute` function, injectable for testing.
16
+ * @param {Function} [options.useHead=defaultUseHead] - The `useHead` function, injectable for testing.
17
+ */
18
+ export function useDocumentTitle(options = {}) {
19
+ const {
20
+ titleTemplate = '%s | My Awesome Site',
21
+ useRoute = defaultUseRoute,
22
+ useHead = defaultUseHead
23
+ } = options;
24
+
25
+ const route = useRoute();
26
+
27
+ const documentTitle = computed(() => {
28
+ // Get the title from our reactive state, or fall back to the route's meta.
29
+ const titleFromState = pageTitles[route.path];
30
+ const titleFromMeta = route.meta.title;
31
+
32
+ let title = titleFromState || titleFromMeta;
33
+
34
+ // If the title from meta is a function, resolve it.
35
+ if (typeof title === 'function') {
36
+ title = title(route);
37
+ }
38
+
39
+ // Format the title with the template, or provide a default.
40
+ return title ? titleTemplate.replace('%s', title) : 'My Awesome Site';
41
+ });
42
+
43
+ // useHead is reactive, so it will automatically update when documentTitle changes.
44
+ useHead({
45
+ title: documentTitle,
46
+ });
47
+ }
@@ -0,0 +1,37 @@
1
+ import { reactive, watchEffect, onUnmounted, unref } from "vue";
2
+ import { useRoute as defaultUseRoute } from "vue-router";
3
+
4
+ // A reactive map to store component-defined titles for the current route.
5
+ // Key: route.path, Value: title string
6
+ export const pageTitles = reactive({});
7
+
8
+ /**
9
+ * A composable to set the title for the current page/route from within its component.
10
+ * This provides a single source of truth for a page's title, which can be
11
+ * consumed by various parts of the application (e.g., breadcrumbs, document title).
12
+ * @param {import('vue').Ref<string> | string} title The title to set for the current page. Can be a ref, computed, or a plain string.
13
+ * @param {{ useRoute: Function }} options For dependency injection in tests/stories.
14
+ */
15
+ export function usePageTitle(title, { useRoute = defaultUseRoute } = {}) {
16
+ const route = useRoute();
17
+ const path = route.path;
18
+
19
+ watchEffect(() => {
20
+ pageTitles[path] = unref(title);
21
+ });
22
+
23
+ // Clean up when the component is unmounted to prevent memory leaks
24
+ onUnmounted(() => {
25
+ delete pageTitles[path];
26
+ });
27
+ }
28
+
29
+ /**
30
+ * Gets the dynamically set page title for a given path.
31
+ * For internal use by consumers like breadcrumb or document title utilities.
32
+ * @param {string} path The route path to look up.
33
+ * @returns {string | undefined}
34
+ */
35
+ export function getPageTitle(path) {
36
+ return pageTitles[path];
37
+ }
@@ -1,3 +1,4 @@
1
+ import { getPageTitle } from "../composables/usePageTitle.js";
1
2
  /**
2
3
  * This Module Creates Menus from route or router config
3
4
  * - Note: Functions prefixed with "$" work with $route objects (running application, provided by vue-router ie $router, useRoute, etc),
@@ -251,4 +252,63 @@ export function $createSectionMenu(route, options) {
251
252
  .filter(includeIndex(opts.includeIndex))
252
253
  .map(r => createMenuItem(r, `${ parent.path }/${ r.path }`, opts.item))
253
254
  .sort(opts.sort);
255
+ }
256
+
257
+ /**
258
+ * For a given $route, this will generate a breadcrumb trail.
259
+ * It iterates through `route.matched` to build the trail.
260
+ * - Prioritizes titles set via the `usePageTitle` composable for the current page.
261
+ * - Falls back to `meta.title` (string or function).
262
+ * - Skips routes where `meta.breadcrumb` is set to `false`.
263
+ * - Avoids duplicate crumbs for nested routes with empty paths.
264
+ * @param {Object} route The Vue Router `$route` object.
265
+ * @returns {Array.<{title: String, to: Object, current: Boolean}>} An array of breadcrumb items.
266
+ */
267
+ export function $createBreadcrumb(route) {
268
+ const { matched, path: currentPath } = route;
269
+ let prevPath;
270
+
271
+ const crumbs = matched.reduce((arr, match, index) => {
272
+ // Skip routes configured to be hidden from breadcrumbs
273
+ if (match.meta?.breadcrumb === false) {
274
+ return arr;
275
+ }
276
+
277
+ // Avoid duplicates from child routes with empty paths
278
+ if (match.path === prevPath) {
279
+ return arr;
280
+ }
281
+
282
+ const isLast = index === matched.length - 1;
283
+ let title;
284
+
285
+ // 1. Prioritize component-defined title for the current page
286
+ if (isLast) {
287
+ title = getPageTitle(currentPath);
288
+ }
289
+
290
+ // 2. Fallback to meta.title (function or string)
291
+ if (!title) {
292
+ const metaTitle = match.meta?.title;
293
+ if (typeof metaTitle === 'function') {
294
+ title = metaTitle(route);
295
+ } else {
296
+ title = metaTitle;
297
+ }
298
+ }
299
+
300
+ // 3. Final fallback
301
+ title = title || 'Missing Title';
302
+
303
+ arr.push({
304
+ title,
305
+ to: { path: isLast ? currentPath : match.path },
306
+ current: isLast,
307
+ });
308
+
309
+ prevPath = match.path;
310
+ return arr;
311
+ }, []);
312
+
313
+ return crumbs;
254
314
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ulu/frontend-vue",
3
- "version": "0.1.0-beta.19",
3
+ "version": "0.1.0-beta.20",
4
4
  "description": "A modular and tree-shakeable Vue 3 component library for the Ulu frontend",
5
5
  "type": "module",
6
6
  "files": [
@@ -59,7 +59,8 @@
59
59
  "@headlessui/vue": "^1.7.23",
60
60
  "@ulu/frontend": "^0.1.0-beta.102",
61
61
  "vue": "^3.5.17",
62
- "vue-router": "^4.5.1"
62
+ "vue-router": "^4.5.1",
63
+ "@unhead/vue": "^2.0.11"
63
64
  },
64
65
  "optionalDependencies": {
65
66
  "fuse.js": "^6.6.2",
@@ -89,7 +90,8 @@
89
90
  "storybook-addon-vue-mdx": "^2.0.2",
90
91
  "typescript": "^5.3.3",
91
92
  "vite": "^7.0.0",
92
- "vue-router": "^4.5.1"
93
+ "vue-router": "^4.5.1",
94
+ "@unhead/vue": "^2.0.11"
93
95
  },
94
96
  "volta": {
95
97
  "node": "22.17.0"
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Manages the document's <title> tag based on the current route's title.
3
+ * It pulls titles from the `usePageTitle` system, falling back to `meta.title`,
4
+ * and formats it with a template.
5
+ *
6
+ * This should be called once in the root App.vue component.
7
+ *
8
+ * @param {object} options
9
+ * @param {string} [options.titleTemplate='%s | My Awesome Site'] - The template for the title.
10
+ * @param {Function} [options.useRoute=defaultUseRoute] - The `useRoute` function, injectable for testing.
11
+ * @param {Function} [options.useHead=defaultUseHead] - The `useHead` function, injectable for testing.
12
+ */
13
+ export function useDocumentTitle(options?: {
14
+ titleTemplate?: string;
15
+ useRoute?: Function;
16
+ useHead?: Function;
17
+ }): void;
18
+ //# sourceMappingURL=useDocumentTitle.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useDocumentTitle.d.ts","sourceRoot":"","sources":["../../lib/composables/useDocumentTitle.js"],"names":[],"mappings":"AAKA;;;;;;;;;;;GAWG;AACH,2CAJG;IAAyB,aAAa,GAA9B,MAAM;IACa,QAAQ;IACR,OAAO;CACpC,QA8BA"}
@@ -0,0 +1,19 @@
1
+ /**
2
+ * A composable to set the title for the current page/route from within its component.
3
+ * This provides a single source of truth for a page's title, which can be
4
+ * consumed by various parts of the application (e.g., breadcrumbs, document title).
5
+ * @param {import('vue').Ref<string> | string} title The title to set for the current page. Can be a ref, computed, or a plain string.
6
+ * @param {{ useRoute: Function }} options For dependency injection in tests/stories.
7
+ */
8
+ export function usePageTitle(title: import("vue").Ref<string> | string, { useRoute }?: {
9
+ useRoute: Function;
10
+ }): void;
11
+ /**
12
+ * Gets the dynamically set page title for a given path.
13
+ * For internal use by consumers like breadcrumb or document title utilities.
14
+ * @param {string} path The route path to look up.
15
+ * @returns {string | undefined}
16
+ */
17
+ export function getPageTitle(path: string): string | undefined;
18
+ export const pageTitles: {};
19
+ //# sourceMappingURL=usePageTitle.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"usePageTitle.d.ts","sourceRoot":"","sources":["../../lib/composables/usePageTitle.js"],"names":[],"mappings":"AAOA;;;;;;GAMG;AACH,oCAHW,OAAO,KAAK,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,MAAM,iBAClC;IAAE,QAAQ,WAAU;CAAE,QAchC;AAED;;;;;GAKG;AACH,mCAHW,MAAM,GACJ,MAAM,GAAG,SAAS,CAI9B;AA/BD,4BAAuC"}
@@ -110,6 +110,21 @@ export function $createSectionMenu(route: any, options: {
110
110
  includeIndex: boolean;
111
111
  item: any;
112
112
  }): Array<RouteMenuItem>;
113
+ /**
114
+ * For a given $route, this will generate a breadcrumb trail.
115
+ * It iterates through `route.matched` to build the trail.
116
+ * - Prioritizes titles set via the `usePageTitle` composable for the current page.
117
+ * - Falls back to `meta.title` (string or function).
118
+ * - Skips routes where `meta.breadcrumb` is set to `false`.
119
+ * - Avoids duplicate crumbs for nested routes with empty paths.
120
+ * @param {Object} route The Vue Router `$route` object.
121
+ * @returns {Array.<{title: String, to: Object, current: Boolean}>} An array of breadcrumb items.
122
+ */
123
+ export function $createBreadcrumb(route: any): Array<{
124
+ title: string;
125
+ to: any;
126
+ current: boolean;
127
+ }>;
113
128
  /**
114
129
  * Route Menu Item
115
130
  */
@@ -1 +1 @@
1
- {"version":3,"file":"router.d.ts","sourceRoot":"","sources":["../../lib/utils/router.js"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH;;;;;GAKG;AAEH;;;;;;;GAOG;AACH,uCANW,GAAC,WAET;IAAwB,SAAS;IACT,IAAI;CAC5B,GAAU,KAAK,CAAE,aAAa,CAAC,CAiCjC;AAED;;GAEG;AACH,4CAcC;AAED;;;;;;;;GAQG;AACH,0CAPW,GAAC,eACD,GAAC,WAET;IAAyB,YAAY;IACb,IAAI;CAC5B,GAAU,KAAK,CAAE,aAAa,CAAC,CA6BjC;AAED;;;;;GAKG;AASH;;;;GAIG;AACH,yDAEC;AACD;;;;;;;;GAQG;AACH,oEAJG;IAA0B,MAAM;IACN,SAAS;CACnC,GAAU,aAAa,CAsBzB;AACD;;;;GAIG;AACH,mDAEC;AACD;;;;GAIG;AACH,uDAGC;AACD;;;;GAIG;AACH,gEAUC;AACD;;;;GAIG;AACH,iEAEC;AACD;;;;;GAKG;AACH,2DAFY,UAAW,CAUtB;AAUD;;;;;;;;;;GAUG;AACH,wDALG;IAAwB,MAAM;IACL,YAAY;IACb,IAAI;CAC5B,GAAU,KAAK,CAAE,aAAa,CAAC,CAgBjC"}
1
+ {"version":3,"file":"router.d.ts","sourceRoot":"","sources":["../../lib/utils/router.js"],"names":[],"mappings":"AACA;;;;GAIG;AAEH;;;;;GAKG;AAEH;;;;;;;GAOG;AACH,uCANW,GAAC,WAET;IAAwB,SAAS;IACT,IAAI;CAC5B,GAAU,KAAK,CAAE,aAAa,CAAC,CAiCjC;AAED;;GAEG;AACH,4CAcC;AAED;;;;;;;;GAQG;AACH,0CAPW,GAAC,eACD,GAAC,WAET;IAAyB,YAAY;IACb,IAAI;CAC5B,GAAU,KAAK,CAAE,aAAa,CAAC,CA6BjC;AAED;;;;;GAKG;AASH;;;;GAIG;AACH,yDAEC;AACD;;;;;;;;GAQG;AACH,oEAJG;IAA0B,MAAM;IACN,SAAS;CACnC,GAAU,aAAa,CAsBzB;AACD;;;;GAIG;AACH,mDAEC;AACD;;;;GAIG;AACH,uDAGC;AACD;;;;GAIG;AACH,gEAUC;AACD;;;;GAIG;AACH,iEAEC;AACD;;;;;GAKG;AACH,2DAFY,UAAW,CAUtB;AAUD;;;;;;;;;;GAUG;AACH,wDALG;IAAwB,MAAM;IACL,YAAY;IACb,IAAI;CAC5B,GAAU,KAAK,CAAE,aAAa,CAAC,CAgBjC;AAED;;;;;;;;;GASG;AACH,+CAFa,KAAK,CAAE;IAAC,KAAK,SAAS;IAAC,EAAE,MAAS;IAAC,OAAO,UAAS;CAAC,CAAC,CAiDjE"}