@frigear-nu/menu 0.0.7 → 0.0.9

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.
package/dist/module.d.mts CHANGED
@@ -16,7 +16,15 @@ type MenuItem = {
16
16
  * entries, so content-sourced and module-sourced items share the exact same shape.
17
17
  */
18
18
  label: string;
19
+ /**
20
+ * Optional i18n key (e.g. `users.menu.logout.label`). When `@nuxtjs/i18n` is installed and the
21
+ * key exists for the active locale, `useMenu` returns its translation as `label`; otherwise the
22
+ * plain `label` above is the fallback. Ids are always derived from the untranslated `label`.
23
+ */
24
+ labelKey?: string;
19
25
  description?: string;
26
+ /** Optional i18n key for `description`, resolved the same way as `labelKey`. */
27
+ descriptionKey?: string;
20
28
  icon?: string;
21
29
  to?: string;
22
30
  external?: boolean;
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@frigear-nu/menu",
3
3
  "configKey": "menus",
4
- "version": "0.0.7",
4
+ "version": "0.0.9",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.3",
7
7
  "unbuild": "unknown"
package/dist/module.mjs CHANGED
@@ -1,5 +1,5 @@
1
- import { defineNuxtModule, createResolver, addImports, hasNuxtModule, addTypeTemplate, addTemplate, addPlugin, addServerImports } from '@nuxt/kit';
2
- import { registerModule } from '@frigear-nu/module-kit/utils';
1
+ import { defineNuxtModule, createResolver, addImports, hasNuxtModule, addTypeTemplate, addPlugin, addServerImports } from '@nuxt/kit';
2
+ import { registerModule, defineVirtualRegistry } from '@frigear-nu/module-kit/utils';
3
3
 
4
4
  const DEFAULT_CONTENT_COLLECTION = "config";
5
5
  const DEFAULT_CONTENT_FIELD = "navigation";
@@ -39,20 +39,32 @@ const module$1 = defineNuxtModule({
39
39
  }
40
40
  `
41
41
  });
42
- nuxt.hook("modules:done", () => {
43
- const staticItems = nuxt.options.menus?._extensions ?? {};
44
- const sources = Array.isArray(nuxt.options.menus?._sources) ? nuxt.options.menus._sources : [];
45
- const sourcesTemplate = addTemplate({
46
- filename: "#menus/sources.mjs",
47
- write: true,
48
- getContents: () => `
42
+ defineVirtualRegistry(nuxt, {
43
+ filename: "#menus/sources.mjs",
44
+ alias: "#menus/sources",
45
+ collect: () => ({
46
+ staticItems: nuxt.options.menus?._extensions ?? {},
47
+ sources: Array.isArray(nuxt.options.menus?._sources) ? nuxt.options.menus._sources : []
48
+ }),
49
+ contents: ({ staticItems, sources }) => `
50
+ export const staticItems = ${JSON.stringify(staticItems)}
51
+ ${sources.map((source, index) => `import source${index} from ${JSON.stringify(source.file)}`).join("\n")}
52
+ export const sources = [${sources.map((source, index) => `{ menu: ${JSON.stringify(source.menu)}, source: source${index} }`).join(", ")}]
53
+ `,
54
+ resolved: ({ staticItems }) => nuxt.callHook("menus:resolved", staticItems)
55
+ });
56
+ defineVirtualRegistry(nuxt, {
57
+ filename: "#menus/server-sources",
58
+ target: "server",
59
+ collect: () => ({
60
+ staticItems: nuxt.options.menus?._extensions ?? {},
61
+ sources: Array.isArray(nuxt.options.menus?._sources) ? nuxt.options.menus._sources : []
62
+ }),
63
+ contents: ({ staticItems, sources }) => `
49
64
  export const staticItems = ${JSON.stringify(staticItems)}
50
65
  ${sources.map((source, index) => `import source${index} from ${JSON.stringify(source.file)}`).join("\n")}
51
66
  export const sources = [${sources.map((source, index) => `{ menu: ${JSON.stringify(source.menu)}, source: source${index} }`).join(", ")}]
52
67
  `
53
- });
54
- nuxt.options.alias["#menus/sources"] = sourcesTemplate.dst;
55
- nuxt.callHook("menus:resolved", staticItems);
56
68
  });
57
69
  addPlugin(resolver.resolve("./runtime/app/plugins/menus"));
58
70
  addServerImports({
@@ -1,6 +1,6 @@
1
1
  import { computed, unref } from "vue";
2
2
  import { useAsyncData, useNuxtApp, useRuntimeConfig } from "#imports";
3
- import { collectMenuItems, matchesMenuGroup, parseMenuPath, sortByOrder } from "../../utils/menu.js";
3
+ import { collectMenuItems, matchesMenuGroup, parseMenuPath, sortByOrder, translateMenuItem } from "../../utils/menu.js";
4
4
  import { menus } from "../utils/state.js";
5
5
  function useMenuLocale() {
6
6
  const nuxtApp = useNuxtApp();
@@ -29,7 +29,8 @@ export const useMenu = (path) => {
29
29
  return useAsyncData(
30
30
  () => `menu:${path}:${locale.value ?? ""}`,
31
31
  async () => {
32
- const registered = collectMenuItems(menus[menu], parsed);
32
+ const i18n = nuxtApp.$i18n?.t && nuxtApp.$i18n.te ? nuxtApp.$i18n : void 0;
33
+ const registered = collectMenuItems(menus[menu], parsed).map((item) => translateMenuItem(item, i18n));
33
34
  const content = await loadContentItems(menu, parsed, unref(locale));
34
35
  const result = sortByOrder([...content, ...registered]);
35
36
  await nuxtApp.callHook("menus:visible", menu);
@@ -1,3 +1,15 @@
1
- import { type MenuItem } from '../utils/menu.js';
2
1
  import type { H3Event } from 'h3';
3
- export declare const useMenu: (event: H3Event, path: string) => Promise<MenuItem[]>;
2
+ import { type MenuItem } from '../utils/menu.js';
3
+ /**
4
+ * Server counterpart of the client `useMenu`: resolves a menu's (or menu group's) items merged from
5
+ * every source - `extendMenu` items, `runtime/menus.ts` sources and the `@nuxt/content` source -
6
+ * with content items first, sorted by `order`. Accepts the same paths, wildcards included.
7
+ * Labels are not translated; pass `locale` to read a localized content collection.
8
+ *
9
+ * ```ts
10
+ * export default defineEventHandler(event => useMenu(event, 'dashboard.*'))
11
+ * ```
12
+ */
13
+ export declare const useMenu: (event: H3Event, path: string, options?: {
14
+ locale?: string;
15
+ }) => Promise<MenuItem[]>;
@@ -1,27 +1,45 @@
1
- import { collectMenuItems, matchesMenuGroup, parseMenuPath, sortByOrder } from "../utils/menu.js";
2
1
  import { useRuntimeConfig } from "nitropack/runtime";
3
- export const useMenu = async (event, path) => {
4
- const parsed = parseMenuPath(path);
5
- const { menu } = parsed;
6
- const config = useRuntimeConfig(event);
7
- const menus = config.public.menus;
8
- const registered = collectMenuItems(menus[menu], parsed);
9
- const content = await loadContentItems(menu, parsed, "");
10
- const result = sortByOrder([...registered, ...content]);
11
- return result;
12
- };
2
+ import { staticItems, sources } from "#menus/server-sources";
3
+ import { collectMenuItems, matchesMenuGroup, parseMenuPath, sortByOrder } from "../utils/menu.js";
4
+ import { menus, pushMenuItems, resetMenus } from "../app/utils/state.js";
5
+ let registered;
6
+ function registerItems() {
7
+ registered ??= (async () => {
8
+ resetMenus();
9
+ for (const [menu, groups] of Object.entries(staticItems)) {
10
+ for (const [group, items] of Object.entries(groups)) {
11
+ if (items.length > 0) pushMenuItems(menu, group, items);
12
+ }
13
+ }
14
+ for (const { menu, source } of sources) {
15
+ const items = [];
16
+ await source(items);
17
+ if (items.length > 0) pushMenuItems(menu, "", items);
18
+ }
19
+ })().catch((error) => {
20
+ registered = void 0;
21
+ throw error;
22
+ });
23
+ return registered;
24
+ }
13
25
  function localizedCollectionName(name, locale) {
14
26
  return locale ? `${name}_${locale.replace(/-/g, "_")}` : name;
15
27
  }
16
- async function loadContentItems(menu, parsed, locale) {
17
- const config = useRuntimeConfig().public.menus ?? {};
28
+ async function loadContentItems(event, menu, parsed, locale) {
29
+ const config = useRuntimeConfig(event).public.menus ?? {};
18
30
  const source = config.content?.[menu];
19
31
  if (!source) return [];
20
- const collection = localizedCollectionName(source.collection, locale);
21
- const entry = await queryCollection(collection).first();
32
+ const { queryCollection } = await import("@nuxt/content/server");
33
+ const entry = await queryCollection(event, localizedCollectionName(source.collection, locale)).first();
22
34
  const items = entry?.[source.field] ?? [];
23
35
  return items.filter((item) => {
24
36
  const group = item.group ?? "";
25
37
  return parsed.wildcard ? matchesMenuGroup(group, parsed.group) : group === parsed.group;
26
38
  });
27
39
  }
40
+ export const useMenu = async (event, path, options = {}) => {
41
+ const parsed = parseMenuPath(path);
42
+ await registerItems();
43
+ const content = await loadContentItems(event, parsed.menu, parsed, options.locale);
44
+ return sortByOrder([...content, ...collectMenuItems(menus[parsed.menu], parsed)]);
45
+ };
@@ -37,6 +37,17 @@ export declare function collectMenuItems(groups: Record<string, MenuItem[]> | un
37
37
  export declare function sortByOrder(items: MenuItem[]): MenuItem[];
38
38
  /** Deterministic id for an item missing one: its `to` if present, else a slug of its `label`. */
39
39
  export declare function deriveMenuItemId(item: MenuItem): string;
40
+ /** The slice of `@nuxtjs/i18n`'s `$i18n` that `translateMenuItem` needs. */
41
+ export type MenuTranslator = {
42
+ t: (key: string) => string;
43
+ te: (key: string) => boolean;
44
+ };
45
+ /**
46
+ * Resolves an item's `labelKey`/`descriptionKey` against the active locale, falling back to the
47
+ * plain `label`/`description` when no translator is available or the key has no message. The
48
+ * `id` is derived first, so it stays identical across locales.
49
+ */
50
+ export declare function translateMenuItem(item: MenuItem, i18n?: MenuTranslator): MenuItem;
40
51
  /**
41
52
  * Build-time API for other modules to contribute static items to a menu, without the host app
42
53
  * wiring anything. Call it once during a module's own `setup()`:
@@ -46,6 +46,14 @@ function slugify(value) {
46
46
  export function deriveMenuItemId(item) {
47
47
  return item.id ?? item.to ?? slugify(item.label);
48
48
  }
49
+ export function translateMenuItem(item, i18n) {
50
+ if (!i18n || !item.labelKey && !item.descriptionKey) return item;
51
+ const id = deriveMenuItemId(item);
52
+ const translated = { ...item, id };
53
+ if (item.labelKey && i18n.te(item.labelKey)) translated.label = i18n.t(item.labelKey);
54
+ if (item.descriptionKey && i18n.te(item.descriptionKey)) translated.description = i18n.t(item.descriptionKey);
55
+ return translated;
56
+ }
49
57
  function normalizeItems(group, items) {
50
58
  const list = Array.isArray(items) ? items : [items];
51
59
  const seen = /* @__PURE__ */ new Set();
package/package.json CHANGED
@@ -1,7 +1,8 @@
1
1
  {
2
2
  "name": "@frigear-nu/menu",
3
- "version": "0.0.7",
3
+ "version": "0.0.9",
4
4
  "description": "Named, shareable navigation menus - modules contribute items (extendMenu / a runtime/menus.ts source) and/or Nuxt Content, consumers read them via useMenu and render them however they like.",
5
+ "homepage": "https://docs.frigear.dev/modules/menu",
5
6
  "repository": {
6
7
  "type": "git",
7
8
  "url": "git+https://github.com/frigear-nu/frigear-nu.git",
@@ -30,7 +31,7 @@
30
31
  ],
31
32
  "dependencies": {
32
33
  "@nuxt/kit": "^4.6.0",
33
- "@frigear-nu/module-kit": "0.1.5",
34
+ "@frigear-nu/module-kit": "0.1.7",
34
35
  "zod": "^4.6.5"
35
36
  },
36
37
  "devDependencies": {