@vuecs/navigation 3.0.2 → 4.0.0

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.
Files changed (54) hide show
  1. package/README.md +2 -2
  2. package/dist/components/index.d.ts +1 -0
  3. package/dist/components/index.d.ts.map +1 -1
  4. package/dist/components/item/module.d.ts +115 -2
  5. package/dist/components/item/module.d.ts.map +1 -1
  6. package/dist/components/items/module.d.ts +234 -19
  7. package/dist/components/items/module.d.ts.map +1 -1
  8. package/dist/components/items/theme.d.ts.map +1 -1
  9. package/dist/components/select-context.d.ts +30 -0
  10. package/dist/components/select-context.d.ts.map +1 -0
  11. package/dist/components/stepper/Stepper.vue.d.ts +1 -1
  12. package/dist/components/stepper/StepperDescription.vue.d.ts +1 -1
  13. package/dist/components/stepper/StepperIndicator.vue.d.ts +1 -1
  14. package/dist/components/stepper/StepperSeparator.vue.d.ts +1 -1
  15. package/dist/components/stepper/StepperTitle.vue.d.ts +1 -1
  16. package/dist/components/stepper/StepperTrigger.vue.d.ts +1 -1
  17. package/dist/components/type.d.ts +12 -5
  18. package/dist/components/type.d.ts.map +1 -1
  19. package/dist/helpers/component/types.d.ts +6 -0
  20. package/dist/helpers/component/types.d.ts.map +1 -1
  21. package/dist/helpers/index.d.ts +2 -1
  22. package/dist/helpers/index.d.ts.map +1 -1
  23. package/dist/helpers/normalize.d.ts +2 -6
  24. package/dist/helpers/normalize.d.ts.map +1 -1
  25. package/dist/helpers/reset.d.ts.map +1 -1
  26. package/dist/helpers/submenu.d.ts +9 -0
  27. package/dist/helpers/submenu.d.ts.map +1 -0
  28. package/dist/helpers/trail.d.ts +12 -0
  29. package/dist/helpers/trail.d.ts.map +1 -0
  30. package/dist/index.d.ts +1 -1
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.mjs +521 -272
  33. package/dist/index.mjs.map +1 -1
  34. package/dist/registry/index.d.ts.map +1 -0
  35. package/dist/registry/module.d.ts +42 -0
  36. package/dist/registry/module.d.ts.map +1 -0
  37. package/dist/registry/singleton.d.ts +6 -0
  38. package/dist/registry/singleton.d.ts.map +1 -0
  39. package/dist/registry/types.d.ts +22 -0
  40. package/dist/registry/types.d.ts.map +1 -0
  41. package/dist/style.css +83 -0
  42. package/dist/types.d.ts +30 -15
  43. package/dist/types.d.ts.map +1 -1
  44. package/package.json +6 -2
  45. package/dist/helpers/level.d.ts +0 -11
  46. package/dist/helpers/level.d.ts.map +0 -1
  47. package/dist/manager/index.d.ts.map +0 -1
  48. package/dist/manager/module.d.ts +0 -23
  49. package/dist/manager/module.d.ts.map +0 -1
  50. package/dist/manager/singleton.d.ts +0 -5
  51. package/dist/manager/singleton.d.ts.map +0 -1
  52. package/dist/manager/types.d.ts +0 -8
  53. package/dist/manager/types.d.ts.map +0 -1
  54. /package/dist/{manager → registry}/index.d.ts +0 -0
package/dist/index.mjs CHANGED
@@ -1,8 +1,87 @@
1
- import { hasNormalizedSlot, inject, installDefaultsManager, installThemeManager, normalizeSlot, provide, useArrowNavigation, useComponentTheme } from "@vuecs/core";
2
- import { EventEmitter } from "@posva/event-emitter";
1
+ import { hasNormalizedSlot, inject, installDefaultsManager, installThemeManager, isPromise, normalizeSlot, provide, useArrowNavigation, useComponentTheme } from "@vuecs/core";
2
+ import { computed, defineComponent, getCurrentInstance, h, inject as inject$1, mergeProps, onMounted, onUnmounted, provide as provide$1, ref, resolveComponent, shallowReactive, toRef, watch, watchEffect } from "vue";
3
3
  import { VCLink } from "@vuecs/link";
4
- import { computed, defineComponent, h, inject as inject$1, mergeProps, onMounted, onUnmounted, provide as provide$1, ref, resolveComponent, toRef } from "vue";
5
- import { StepperDescription, StepperIndicator, StepperItem, StepperRoot, StepperSeparator, StepperTitle, StepperTrigger } from "reka-ui";
4
+ import { CollapsibleContent, CollapsibleRoot, CollapsibleTrigger, NavigationMenuContent, NavigationMenuItem, NavigationMenuLink, NavigationMenuList, NavigationMenuRoot, NavigationMenuTrigger, StepperDescription, StepperIndicator, StepperItem, StepperRoot, StepperSeparator, StepperTitle, StepperTrigger } from "reka-ui";
5
+ //#region src/registry/module.ts
6
+ function createEmptyEntry() {
7
+ return {
8
+ items: ref([]),
9
+ active: computed(() => []),
10
+ activeTrail: computed(() => [])
11
+ };
12
+ }
13
+ /**
14
+ * Reactive, app-wide navigation registry. `<VCNavItems registry>`
15
+ * publishes its resolved output here under a `registry-id`; other navs
16
+ * read it reactively + empty-safe via the resolver context's
17
+ * `registry(id)`.
18
+ *
19
+ * The backing map is `shallowReactive`, so membership changes
20
+ * (register + the returned unregister closure) are tracked dependencies
21
+ * — a consumer reading `get(id)` inside a `computed` / `watchEffect`
22
+ * re-runs when the id's occupancy flips.
23
+ */
24
+ var NavigationRegistry = class {
25
+ map = shallowReactive(/* @__PURE__ */ new Map());
26
+ /**
27
+ * Stable empty entries handed out for absent ids, memoized per id so
28
+ * the SAME reactive handle is returned every call — a consumer
29
+ * subscribed to an absent id keeps its dependency and lights up the
30
+ * moment an occupant registers.
31
+ */
32
+ empties = /* @__PURE__ */ new Map();
33
+ /**
34
+ * Claim `id`. Last-wins: a newer occupant replaces the current one
35
+ * (dev warning on collision). Returns a token-guarded unregister
36
+ * closure: it releases `id` ONLY if this registration is still the
37
+ * occupant. During a route handoff (Vue mounts the new page before
38
+ * unmounting the old) the departing nav's closure holds a stale token
39
+ * and cannot evict the incoming occupant.
40
+ */
41
+ register(id, entry) {
42
+ if (this.map.has(id)) console.warn(`[vuecs] navigation registry id "${id}" reassigned to a new occupant.`);
43
+ const token = Symbol("vc-nav-registry-token");
44
+ this.map.set(id, {
45
+ token,
46
+ entry
47
+ });
48
+ return () => {
49
+ const occupant = this.map.get(id);
50
+ if (occupant && occupant.token === token) this.map.delete(id);
51
+ };
52
+ }
53
+ /** Reactive, empty-safe read. Never returns `undefined`. */
54
+ get(id) {
55
+ const occupant = this.map.get(id);
56
+ if (occupant) return occupant.entry;
57
+ let empty = this.empties.get(id);
58
+ if (!empty) {
59
+ empty = createEmptyEntry();
60
+ this.empties.set(id, empty);
61
+ }
62
+ return empty;
63
+ }
64
+ /** True when an occupant currently holds `id`. */
65
+ has(id) {
66
+ return this.map.has(id);
67
+ }
68
+ };
69
+ //#endregion
70
+ //#region src/registry/singleton.ts
71
+ const sym = Symbol.for("VCNavigationRegistry");
72
+ function tryInjectNavigationRegistry(app) {
73
+ return inject(sym, app);
74
+ }
75
+ function injectNavigationRegistry(app) {
76
+ const instance = tryInjectNavigationRegistry(app);
77
+ if (!instance) throw new Error("A navigation registry has not been provided.");
78
+ return instance;
79
+ }
80
+ function provideNavigationRegistry(registry = new NavigationRegistry(), app) {
81
+ provide(sym, registry, app);
82
+ return registry;
83
+ }
84
+ //#endregion
6
85
  //#region src/helpers/match.ts
7
86
  function calculateItemScoreForPath(item, currentPath) {
8
87
  if (item.url === "/") return 1;
@@ -41,23 +120,22 @@ function findBestItemMatches(items, options = {}) {
41
120
  }
42
121
  //#endregion
43
122
  //#region src/helpers/normalize.ts
44
- function normalizeItemIF(item, defaults, trace) {
123
+ function normalizeItemIF(item, trace) {
45
124
  const output = {
46
125
  ...item,
47
- level: defaults.level,
48
126
  children: [],
49
127
  trace: [...trace, item.name],
50
128
  meta: item.meta || {}
51
129
  };
52
130
  if (!item.children) return output;
53
- for (let i = 0; i < item.children.length; i++) output.children.push(normalizeItemIF(item.children[i], defaults, output.trace));
131
+ for (let i = 0; i < item.children.length; i++) output.children.push(normalizeItemIF(item.children[i], output.trace));
54
132
  return output;
55
133
  }
56
- function normalizeItem(item, defaults) {
57
- return normalizeItemIF(item, defaults, []);
134
+ function normalizeItem(item) {
135
+ return normalizeItemIF(item, []);
58
136
  }
59
- function normalizeItems(items, options) {
60
- return items.map((item) => normalizeItem(item, options));
137
+ function normalizeItems(items) {
138
+ return items.map((item) => normalizeItem(item));
61
139
  }
62
140
  //#endregion
63
141
  //#region src/helpers/trace.ts
@@ -77,8 +155,14 @@ function resetItemsByTraceIF(items, trace) {
77
155
  const isEqual = isTraceEqual(item.trace, trace);
78
156
  item.active = isEqual;
79
157
  item.display = true;
80
- if (isEqual) item.displayChildren = true;
81
- else item.displayChildren = isTracePartOf(item.trace, trace);
158
+ if (isEqual) {
159
+ item.activeWithin = false;
160
+ item.displayChildren = true;
161
+ } else {
162
+ const isAncestor = isTracePartOf(item.trace, trace);
163
+ item.activeWithin = isAncestor;
164
+ item.displayChildren = isAncestor;
165
+ }
82
166
  item.children = resetItemsByTraceIF(item.children, trace);
83
167
  }
84
168
  return items;
@@ -87,27 +171,45 @@ function resetItemsByTrace(items, trace) {
87
171
  return resetItemsByTraceIF(items, trace);
88
172
  }
89
173
  //#endregion
90
- //#region src/helpers/level.ts
91
- function findItemsWithLevel(items, tier) {
92
- return items.filter((item) => item.level === tier);
93
- }
94
- function findItemWithLevel(tier, items) {
95
- const data = findItemsWithLevel(items, tier);
96
- if (data.length >= 1) return data[0];
97
- }
98
- function removeItemsWithLevel(tier, items) {
99
- return items.filter((item) => item.level !== tier);
174
+ //#region src/helpers/submenu.ts
175
+ /**
176
+ * Resolve the effective submenu presentation. An explicit `collapse` /
177
+ * `dropdown` wins; `auto` derives from orientation — only an explicit
178
+ * `horizontal` opts into the dropdown (NavigationMenu) path, everything
179
+ * else collapses (Collapsible).
180
+ */
181
+ function resolveSubmenuMode(submenu, orientation) {
182
+ if (submenu === "collapse" || submenu === "dropdown") return submenu;
183
+ return orientation === "horizontal" ? "dropdown" : "collapse";
100
184
  }
101
- function replaceLevelItem(tier, input, next) {
102
- const output = removeItemsWithLevel(tier, input);
103
- if (next) {
104
- next.level = tier;
105
- return [...output, next];
185
+ //#endregion
186
+ //#region src/helpers/trail.ts
187
+ /**
188
+ * Walk a normalized tree along `trace` (an ordered list of item names,
189
+ * root → leaf) and collect the item at each depth. Returns the ordered
190
+ * active trail: `[0]` is the top-level section, `.at(-1)` is the leaf.
191
+ */
192
+ function collectTrail(items, trace) {
193
+ const output = [];
194
+ let level = items;
195
+ for (const name of trace) {
196
+ const found = level.find((item) => item.name === name);
197
+ if (!found) break;
198
+ output.push(found);
199
+ level = found.children;
106
200
  }
107
201
  return output;
108
202
  }
109
- function replaceLevelItems(tier, src, next) {
110
- return [...removeItemsWithLevel(tier, src), ...next];
203
+ /**
204
+ * Depth-first collect of every item in the tree matching `predicate`.
205
+ */
206
+ function flattenWhere(items, predicate) {
207
+ const output = [];
208
+ for (const item of items) {
209
+ if (predicate(item)) output.push(item);
210
+ if (item.children.length > 0) output.push(...flattenWhere(item.children, predicate));
211
+ }
212
+ return output;
111
213
  }
112
214
  //#endregion
113
215
  //#region src/helpers/url.ts
@@ -115,119 +217,6 @@ function isAbsoluteURL(str) {
115
217
  return str.substring(0, 7) === "http://" || str.substring(0, 8) === "https://";
116
218
  }
117
219
  //#endregion
118
- //#region src/manager/module.ts
119
- var NavigationManager = class extends EventEmitter {
120
- itemsActive;
121
- items;
122
- itemsFn;
123
- built;
124
- building;
125
- constructor(options) {
126
- super();
127
- let itemsFn;
128
- if (typeof options.items === "function") itemsFn = options.items;
129
- else itemsFn = async ({ level }) => {
130
- if (level > 0) return [];
131
- return options.items;
132
- };
133
- this.itemsFn = itemsFn;
134
- this.items = [];
135
- this.itemsActive = [];
136
- this.built = false;
137
- this.building = false;
138
- }
139
- getItems(tier) {
140
- if (typeof tier === "undefined") return this.items;
141
- return this.items.filter((item) => item.level === tier);
142
- }
143
- reset() {
144
- this.built = false;
145
- this.items = [];
146
- this.itemsActive = [];
147
- }
148
- async build(options) {
149
- if (this.built || this.building) return;
150
- this.building = true;
151
- this.emit("building");
152
- let parent;
153
- let level = 0;
154
- while (true) {
155
- const raw = await this.itemsFn({
156
- level,
157
- parent
158
- });
159
- if (!raw || raw.length === 0) break;
160
- const [match] = findBestItemMatches(normalizeItems(raw, { level }), { path: options.path });
161
- if (!match) break;
162
- this.itemsActive.push(match);
163
- await this.buildLevel(level);
164
- parent = match;
165
- level++;
166
- }
167
- this.building = false;
168
- this.built = true;
169
- this.emit("built");
170
- this.emit("updated", this.items);
171
- }
172
- async select(level, itemNew) {
173
- const itemOld = findItemWithLevel(level, this.itemsActive);
174
- if (itemOld && isTraceEqual(itemOld.trace, itemNew.trace)) return;
175
- this.itemsActive = this.itemsActive.filter((el) => el.level < level);
176
- this.itemsActive.push(itemNew);
177
- const startLevel = level;
178
- while (true) {
179
- if (!await this.buildLevel(level, startLevel === level)) break;
180
- level++;
181
- }
182
- }
183
- async toggle(level, item) {
184
- let isMatch;
185
- if (item.displayChildren) isMatch = true;
186
- else {
187
- const itemOld = findItemWithLevel(level, this.itemsActive);
188
- isMatch = !!itemOld && isTraceEqual(item.trace, itemOld.trace);
189
- }
190
- if (isMatch) this.itemsActive = removeItemsWithLevel(level, this.itemsActive);
191
- else this.itemsActive = replaceLevelItem(level, this.itemsActive, item);
192
- await this.buildLevel(level, true);
193
- }
194
- async buildLevel(level, useCache) {
195
- let items;
196
- if (useCache) items = findItemsWithLevel(this.items, level);
197
- else {
198
- const parent = findItemWithLevel(level - 1, this.itemsActive);
199
- const raw = await this.itemsFn({
200
- level,
201
- parent
202
- });
203
- items = raw && raw.length > 0 ? normalizeItems(raw, { level }) : [];
204
- }
205
- if (!items || items.length === 0) {
206
- this.items = this.items.filter((item) => item.level < level);
207
- this.emit("levelUpdated", level, []);
208
- return false;
209
- }
210
- let trace = [];
211
- const item = findItemWithLevel(level, this.itemsActive);
212
- if (item) trace = item.trace;
213
- resetItemsByTrace(items, trace);
214
- this.items = replaceLevelItems(level, this.items, items);
215
- this.emit("levelUpdated", level, items);
216
- return true;
217
- }
218
- };
219
- //#endregion
220
- //#region src/manager/singleton.ts
221
- const sym = Symbol.for("VCNavigationManager");
222
- function injectNavigationManager(app) {
223
- const instance = inject(sym, app);
224
- if (!instance) throw new Error("A navigation provider has not been provided.");
225
- return instance;
226
- }
227
- function provideNavigationManager(manager, app) {
228
- provide(sym, manager, app);
229
- }
230
- //#endregion
231
220
  //#region src/components/items/theme.ts
232
221
  /**
233
222
  * Default classes for the `navigation` theme entry. Shared between
@@ -235,16 +224,55 @@ function provideNavigationManager(manager, app) {
235
224
  * component) — both call `useComponentTheme('navigation', …)` with
236
225
  * the same slot defaults, so the source of truth lives here.
237
226
  */
238
- const navigationThemeDefaults = { classes: {
239
- group: "vc-nav-items",
240
- item: "vc-nav-item",
241
- itemNested: "vc-nav-item-nested",
242
- separator: "vc-nav-separator",
243
- link: "vc-nav-link",
244
- linkRoot: "vc-nav-link-root",
245
- linkIcon: "vc-nav-link-icon",
246
- linkText: "vc-nav-link-text"
247
- } };
227
+ const navigationThemeDefaults = {
228
+ classes: {
229
+ group: "vc-nav-items",
230
+ item: "vc-nav-item",
231
+ itemNested: "vc-nav-item-nested",
232
+ separator: "vc-nav-separator",
233
+ link: "vc-nav-link",
234
+ linkRoot: "vc-nav-link-root",
235
+ linkIcon: "vc-nav-link-icon",
236
+ linkText: "vc-nav-link-text",
237
+ trigger: "vc-nav-trigger",
238
+ content: "vc-nav-content",
239
+ viewport: "vc-nav-viewport"
240
+ },
241
+ variants: {
242
+ variant: {
243
+ list: {},
244
+ pills: {
245
+ group: "vc-nav-items--pills",
246
+ item: "vc-nav-item--pills",
247
+ link: "vc-nav-link--pills"
248
+ }
249
+ },
250
+ orientation: {
251
+ horizontal: {},
252
+ vertical: { group: "vc-nav-items--vertical" }
253
+ }
254
+ },
255
+ defaultVariants: {
256
+ variant: "list",
257
+ orientation: "horizontal"
258
+ }
259
+ };
260
+ //#endregion
261
+ //#region src/components/select-context.ts
262
+ /**
263
+ * Channels a `<VCNavItem>`'s already-normalized + scored `children` down
264
+ * to the nested `<VCNavItems>` that renders its submenu.
265
+ *
266
+ * The top-level nav scores the WHOLE tree once; nested lists must render
267
+ * those results as-is rather than re-resolving / re-scoring a subtree
268
+ * (which would clobber traces and lose whole-tree active context). The
269
+ * nested `<VCNavItems>` reads this when it has no own `data` prop —
270
+ * presence of the injected nodes is what marks it as a nested renderer
271
+ * rather than a resolving root. Each `<VCNavItem>` re-provides its own
272
+ * children, so the value is correctly scoped per nesting level.
273
+ */
274
+ const NAVIGATION_NODES_KEY = Symbol("vc-navigation-nodes");
275
+ const NAVIGATION_SELECT_KEY = Symbol("vc-navigation-select");
248
276
  const VCNavItem = defineComponent({
249
277
  name: "VCNavItem",
250
278
  props: {
@@ -252,6 +280,42 @@ const VCNavItem = defineComponent({
252
280
  type: Object,
253
281
  required: true
254
282
  },
283
+ variant: {
284
+ type: String,
285
+ default: void 0
286
+ },
287
+ orientation: {
288
+ type: String,
289
+ default: void 0
290
+ },
291
+ /**
292
+ * Resolved submenu presentation handed down by the parent
293
+ * `<VCNavItems>`. `collapse` renders groups as an inline
294
+ * Reka `Collapsible`; `dropdown` renders them as Reka
295
+ * `NavigationMenu` flyouts.
296
+ */
297
+ submenu: {
298
+ type: String,
299
+ default: "collapse"
300
+ },
301
+ /**
302
+ * The tag (or component) this item renders as — its own wrapper
303
+ * (`<li>` by default). Receives `<VCNavItems>`' `itemAs`. Honored in
304
+ * collapse mode only.
305
+ */
306
+ as: {
307
+ type: [String, Object],
308
+ default: "li"
309
+ },
310
+ /**
311
+ * The list-container tag for this item's nested submenu
312
+ * `<VCNavItems>` (`<ul>` by default). Receives `<VCNavItems>`' `as`.
313
+ * Honored in collapse mode only.
314
+ */
315
+ itemsAs: {
316
+ type: [String, Object],
317
+ default: "ul"
318
+ },
255
319
  themeClass: {
256
320
  type: Object,
257
321
  default: void 0
@@ -264,120 +328,229 @@ const VCNavItem = defineComponent({
264
328
  slots: Object,
265
329
  setup(props, { slots }) {
266
330
  const itemsNode = resolveComponent("VCNavItems");
267
- const theme = useComponentTheme("navigation", props, navigationThemeDefaults);
268
- const manager = injectNavigationManager();
331
+ const theme = useComponentTheme("navigation", {
332
+ get themeClass() {
333
+ return props.themeClass;
334
+ },
335
+ get themeVariant() {
336
+ return {
337
+ ...props.themeVariant ?? {},
338
+ ...props.variant !== void 0 ? { variant: props.variant } : {}
339
+ };
340
+ }
341
+ }, navigationThemeDefaults);
269
342
  const data = toRef(props, "data");
270
343
  const hasChildren = computed(() => data.value.children && data.value.children.length > 0);
271
- const select = async (value) => {
272
- await manager.select(data.value.level, value);
344
+ provide$1(NAVIGATION_NODES_KEY, computed(() => data.value.children));
345
+ const open = ref(!!data.value.displayChildren);
346
+ watch(() => data.value.displayChildren, (value) => {
347
+ open.value = !!value;
348
+ });
349
+ const selectContext = inject$1(NAVIGATION_SELECT_KEY, null);
350
+ const select = () => {
351
+ selectContext?.select(data.value);
352
+ };
353
+ const toggle = () => {
354
+ open.value = !open.value;
273
355
  };
274
356
  const renderIcon = (icon) => {
275
357
  if (icon.includes(":")) return h(resolveComponent("VCIcon"), { name: icon });
276
358
  return h("i", { class: icon });
277
359
  };
278
- const toggle = async (value) => {
279
- await manager.toggle(data.value.level, value);
360
+ const renderTitleInner = (resolved) => [...data.value.icon ? [h("div", { class: resolved.linkIcon || void 0 }, [renderIcon(data.value.icon)])] : [], h("div", { class: resolved.linkText || void 0 }, [data.value.name])];
361
+ const renderLeaf = (resolved) => {
362
+ if (hasNormalizedSlot("link", slots)) return normalizeSlot("link", {
363
+ data: data.value,
364
+ select,
365
+ isActive: data.value.active
366
+ }, slots);
367
+ const linkProps = {
368
+ active: data.value.active,
369
+ disabled: false,
370
+ prefetch: true
371
+ };
372
+ if (data.value.url) if (isAbsoluteURL(data.value.url) || data.value.url.startsWith("#")) {
373
+ linkProps.href = data.value.url;
374
+ if (data.value.urlTarget) linkProps.target = data.value.urlTarget;
375
+ } else linkProps.to = data.value.url;
376
+ return h(VCLink, {
377
+ class: [resolved.link],
378
+ "data-vc-collection-item": "",
379
+ ...linkProps,
380
+ onClicked: select
381
+ }, { default: () => renderTitleInner(resolved) });
382
+ };
383
+ const renderChildren = () => {
384
+ if (hasNormalizedSlot("sub-items", slots)) return normalizeSlot("sub-items", {
385
+ data: data.value,
386
+ select,
387
+ toggle
388
+ });
389
+ return h(itemsNode, {
390
+ variant: props.variant,
391
+ orientation: props.orientation,
392
+ submenu: props.submenu === "dropdown" ? "collapse" : props.submenu,
393
+ as: props.itemsAs,
394
+ itemAs: props.as,
395
+ themeClass: props.themeClass,
396
+ themeVariant: props.themeVariant
397
+ });
280
398
  };
281
399
  return () => {
282
400
  const resolved = theme.value;
283
- const buildItem = () => {
284
- if (data.value.type === "separator") {
285
- if (hasNormalizedSlot("separator", slots)) return normalizeSlot("separator", { data: data.value }, slots);
286
- return h("div", { class: resolved.separator || void 0 }, data.value.name);
287
- }
288
- if (!hasChildren.value) {
289
- if (hasNormalizedSlot("link", slots)) return normalizeSlot("link", {
290
- data: data.value,
291
- select,
292
- isActive: data.value.active
293
- }, slots);
294
- const linkProps = {
295
- active: data.value.active,
296
- disabled: false,
297
- prefetch: true
298
- };
299
- if (data.value.url) if (isAbsoluteURL(data.value.url) || data.value.url.startsWith("#")) {
300
- linkProps.href = data.value.url;
301
- if (data.value.urlTarget) linkProps.target = data.value.urlTarget;
302
- } else linkProps.to = data.value.url;
303
- return h(VCLink, {
304
- class: [resolved.link],
305
- "data-vc-collection-item": "",
306
- ...linkProps,
307
- onClicked() {
308
- if (!data.value.url) return select.call(null, data.value);
309
- },
310
- onClick() {
311
- return select.call(null, data.value);
312
- }
313
- }, { default: () => [...data.value.icon ? [h("div", { class: resolved.linkIcon || void 0 }, [renderIcon(data.value.icon)])] : [], h("div", { class: resolved.linkText || void 0 }, [data.value.name])] });
314
- }
315
- if (hasNormalizedSlot("sub", slots)) return normalizeSlot("sub", {
401
+ const isDropdown = props.submenu === "dropdown";
402
+ const isActive = data.value.active || data.value.activeWithin;
403
+ if (data.value.type === "separator") {
404
+ const body = hasNormalizedSlot("separator", slots) ? normalizeSlot("separator", { data: data.value }, slots) : h("div", { class: resolved.separator || void 0 }, data.value.name);
405
+ if (isDropdown) return h(NavigationMenuItem, { class: [resolved.item] }, { default: () => body });
406
+ return h(props.as, { class: [resolved.item] }, [body]);
407
+ }
408
+ if (!hasChildren.value) {
409
+ const leaf = renderLeaf(resolved);
410
+ if (isDropdown) return h(NavigationMenuItem, {
411
+ class: [resolved.item],
412
+ "data-active": data.value.active ? "" : void 0
413
+ }, { default: () => h(NavigationMenuLink, {
414
+ active: data.value.active,
415
+ asChild: true
416
+ }, { default: () => leaf }) });
417
+ return h(props.as, {
418
+ class: [resolved.item, { active: data.value.active }],
419
+ "data-active": data.value.active ? "" : void 0
420
+ }, [leaf]);
421
+ }
422
+ if (hasNormalizedSlot("sub", slots)) {
423
+ const body = normalizeSlot("sub", {
316
424
  data: data.value,
317
425
  select,
318
426
  toggle
319
427
  }, slots);
320
- let title;
321
- if (hasNormalizedSlot("sub-title", slots)) title = normalizeSlot("sub-title", {
322
- data: data.value,
323
- select,
324
- toggle
325
- });
326
- else title = h("div", {
327
- class: resolved.link,
328
- "data-vc-collection-item": "",
329
- "data-state": data.value.displayChildren ? "open" : "closed",
330
- "data-active": data.value.active ? "" : void 0,
331
- tabindex: 0,
332
- role: "button",
333
- "aria-expanded": data.value.displayChildren ? "true" : "false",
334
- onClick($event) {
335
- $event.preventDefault();
336
- return toggle(data.value);
337
- },
338
- onKeydown($event) {
339
- if ($event.key === "Enter" || $event.key === " ") {
340
- $event.preventDefault();
341
- return toggle(data.value);
342
- }
343
- }
344
- }, [...data.value.icon ? [h("div", { class: resolved.linkIcon || void 0 }, [renderIcon(data.value.icon)])] : [], h("div", { class: resolved.linkText || void 0 }, [data.value.name])]);
345
- if (!hasChildren.value) return title;
346
- let vNodes;
347
- if (hasNormalizedSlot("sub-items", slots)) vNodes = normalizeSlot("sub-items", {
348
- data: data.value,
349
- select,
350
- toggle
351
- });
352
- else vNodes = h(itemsNode, {
353
- level: data.value.level,
354
- data: data.value.children
355
- });
356
- return [title, vNodes];
357
- };
358
- return h("li", {
428
+ if (isDropdown) return h(NavigationMenuItem, {
429
+ class: [resolved.item, resolved.itemNested],
430
+ "data-active": isActive ? "" : void 0
431
+ }, { default: () => body });
432
+ return h(props.as, {
433
+ class: [
434
+ resolved.item,
435
+ resolved.itemNested,
436
+ { active: isActive }
437
+ ],
438
+ "data-active": isActive ? "" : void 0
439
+ }, [body]);
440
+ }
441
+ const title = hasNormalizedSlot("sub-title", slots) ? normalizeSlot("sub-title", {
442
+ data: data.value,
443
+ select,
444
+ toggle
445
+ }) : renderTitleInner(resolved);
446
+ if (isDropdown) return h(NavigationMenuItem, {
447
+ class: [resolved.item, resolved.itemNested],
448
+ "data-active": isActive ? "" : void 0
449
+ }, { default: () => [h(NavigationMenuTrigger, {
450
+ class: resolved.trigger || void 0,
451
+ "data-vc-collection-item": "",
452
+ "data-active": isActive ? "" : void 0
453
+ }, { default: () => title }), h(NavigationMenuContent, { class: resolved.content || void 0 }, { default: () => renderChildren() })] });
454
+ return h(CollapsibleRoot, {
455
+ as: props.as,
359
456
  class: [
360
457
  resolved.item,
361
- ...hasChildren.value ? [resolved.itemNested] : [],
362
- { active: data.value.active || data.value.displayChildren }
458
+ resolved.itemNested,
459
+ { active: data.value.active || open.value }
363
460
  ],
364
- "data-active": data.value.active || data.value.displayChildren ? "" : void 0,
365
- ...hasChildren.value ? { "data-state": data.value.displayChildren ? "open" : "closed" } : {}
366
- }, [buildItem()]);
461
+ "data-active": isActive ? "" : void 0,
462
+ open: open.value,
463
+ "onUpdate:open": (value) => {
464
+ open.value = value;
465
+ }
466
+ }, { default: () => [h(CollapsibleTrigger, {
467
+ class: resolved.trigger || void 0,
468
+ "data-vc-collection-item": "",
469
+ "data-active": isActive ? "" : void 0
470
+ }, { default: () => title }), h(CollapsibleContent, { class: resolved.content || void 0 }, { default: () => renderChildren() })] });
367
471
  };
368
472
  }
369
473
  });
370
474
  const VCNavItems = defineComponent({
371
475
  name: "VCNavItems",
372
476
  props: {
373
- level: {
374
- type: Number,
375
- default: 0
376
- },
477
+ /**
478
+ * The source of this nav's items. Plain array, sync fn, or async fn.
479
+ * A fn receives a NavigationResolverContext and may read reactive
480
+ * state freely — the nav re-runs it automatically when that state
481
+ * changes.
482
+ *
483
+ * When omitted, the nav checks whether it is a nested submenu of a
484
+ * parent `<VCNavItem>` (via the {@link NAVIGATION_NODES_KEY} inject)
485
+ * and, if so, renders that parent's already-scored children as-is.
486
+ */
377
487
  data: {
488
+ type: [Array, Function],
489
+ default: void 0
490
+ },
491
+ /** Opt in to publishing this nav's resolved output into the registry. */
492
+ registry: {
493
+ type: Boolean,
494
+ default: false
495
+ },
496
+ /** The key under which to publish. Required when `registry` is true. */
497
+ registryId: {
498
+ type: String,
499
+ default: void 0
500
+ },
501
+ /**
502
+ * Current path for active-state matching. When omitted, the nav softly
503
+ * reads vue-router's current route (via the `$route` global property)
504
+ * if a router is installed; router-free apps simply get `undefined`.
505
+ */
506
+ path: {
507
+ type: String,
508
+ default: void 0
509
+ },
510
+ /**
511
+ * Extra reactive deps that should retrigger the resolver — for state
512
+ * read only AFTER the first `await` in an async resolver (auto-track
513
+ * can't see past an await).
514
+ */
515
+ watch: {
378
516
  type: Array,
379
517
  default: void 0
380
518
  },
519
+ variant: {
520
+ type: String,
521
+ default: void 0
522
+ },
523
+ orientation: {
524
+ type: String,
525
+ default: void 0
526
+ },
527
+ /**
528
+ * How items with children render their submenu. `auto` derives from
529
+ * orientation (horizontal → dropdown, otherwise collapse).
530
+ */
531
+ submenu: {
532
+ type: String,
533
+ default: "auto"
534
+ },
535
+ /**
536
+ * The tag (or component) for this nav's list container. Defaults to
537
+ * `'ul'`. Forwarded unchanged to every nesting level so the whole tree
538
+ * renders the same container tag. Honored in collapse mode only —
539
+ * dropdown mode keeps Reka's NavigationMenu primitives.
540
+ */
541
+ as: {
542
+ type: [String, Object],
543
+ default: "ul"
544
+ },
545
+ /**
546
+ * The tag (or component) for each item wrapper. Defaults to `'li'`.
547
+ * Forwarded unchanged to every nesting level. Honored in collapse mode
548
+ * only — dropdown mode keeps Reka's NavigationMenu primitives.
549
+ */
550
+ itemAs: {
551
+ type: [String, Object],
552
+ default: "li"
553
+ },
381
554
  themeClass: {
382
555
  type: Object,
383
556
  default: void 0
@@ -388,8 +561,19 @@ const VCNavItems = defineComponent({
388
561
  }
389
562
  },
390
563
  slots: Object,
391
- setup(props, { slots }) {
392
- const theme = useComponentTheme("navigation", props, navigationThemeDefaults);
564
+ setup(props, { slots, expose }) {
565
+ const theme = useComponentTheme("navigation", {
566
+ get themeClass() {
567
+ return props.themeClass;
568
+ },
569
+ get themeVariant() {
570
+ return {
571
+ ...props.themeVariant ?? {},
572
+ ...props.variant !== void 0 ? { variant: props.variant } : {},
573
+ ...props.orientation !== void 0 ? { orientation: props.orientation } : {}
574
+ };
575
+ }
576
+ }, navigationThemeDefaults);
393
577
  const rootRef = ref(null);
394
578
  const onKeyDown = (event) => {
395
579
  useArrowNavigation(event, event.target, rootRef.value, {
@@ -398,44 +582,109 @@ const VCNavItems = defineComponent({
398
582
  loop: true
399
583
  });
400
584
  };
401
- const manager = injectNavigationManager();
402
- const managerItems = ref([]);
403
- if (!props.data) managerItems.value = manager.getItems(props.level);
404
- const counter = ref(0);
405
- let removeListener;
406
- onMounted(() => {
407
- removeListener = manager.on("levelUpdated", (level, items) => {
408
- if (level !== props.level) return;
409
- managerItems.value = items;
410
- counter.value++;
411
- });
585
+ const registry = tryInjectNavigationRegistry() ?? new NavigationRegistry();
586
+ const globals = getCurrentInstance()?.appContext.config.globalProperties;
587
+ const currentPath = computed(() => {
588
+ if (typeof props.path !== "undefined") return props.path;
589
+ return (globals?.$route)?.path;
412
590
  });
413
- onUnmounted(() => {
414
- if (typeof removeListener === "function") {
415
- removeListener();
416
- removeListener = void 0;
591
+ const injectedNodes = inject$1(NAVIGATION_NODES_KEY, null);
592
+ const isNested = computed(() => typeof props.data === "undefined" && injectedNodes !== null);
593
+ const selectedTrace = ref(null);
594
+ if (!isNested.value) {
595
+ provide$1(NAVIGATION_SELECT_KEY, { select: (item) => {
596
+ selectedTrace.value = item.trace;
597
+ } });
598
+ watch(currentPath, () => {
599
+ selectedTrace.value = null;
600
+ });
601
+ }
602
+ const raw = ref([]);
603
+ let runToken = 0;
604
+ async function run() {
605
+ const token = ++runToken;
606
+ const value = typeof props.data === "function" ? props.data({
607
+ path: currentPath.value,
608
+ registry: (id) => registry.get(id)
609
+ }) : props.data ?? [];
610
+ if (!isPromise(value)) {
611
+ raw.value = value ?? [];
612
+ return;
417
613
  }
614
+ try {
615
+ const awaited = await value ?? [];
616
+ if (token === runToken) raw.value = awaited;
617
+ } catch (error) {
618
+ if (token === runToken) console.error("[vuecs] <VCNavItems> resolver rejected:", error);
619
+ }
620
+ }
621
+ if (!isNested.value) {
622
+ watchEffect(run);
623
+ if (props.watch) watch(props.watch, run);
624
+ }
625
+ expose({ refresh: run });
626
+ const resolved = computed(() => {
627
+ if (isNested.value && injectedNodes) return {
628
+ items: injectedNodes.value,
629
+ trace: []
630
+ };
631
+ const normalized = normalizeItems(raw.value);
632
+ const [match] = findBestItemMatches(normalized, { path: currentPath.value });
633
+ const trace = selectedTrace.value ?? (match ? match.trace : []);
634
+ resetItemsByTrace(normalized, trace);
635
+ return {
636
+ items: normalized,
637
+ trace
638
+ };
418
639
  });
419
- const items = computed(() => {
420
- if (typeof props.data !== "undefined") return props.data;
421
- return managerItems.value;
422
- });
640
+ const tree = computed(() => resolved.value.items);
641
+ const active = computed(() => flattenWhere(tree.value, (item) => !!item.active));
642
+ const activeTrail = computed(() => collectTrail(tree.value, resolved.value.trace));
643
+ if (props.registry) {
644
+ let unsubscribeFn;
645
+ const entry = {
646
+ items: tree,
647
+ active,
648
+ activeTrail
649
+ };
650
+ onMounted(() => {
651
+ if (!props.registryId) {
652
+ console.warn("[vuecs] <VCNavItems registry> requires a `registry-id`.");
653
+ return;
654
+ }
655
+ unsubscribeFn = registry.register(props.registryId, entry);
656
+ });
657
+ onUnmounted(() => {
658
+ if (!props.registryId || !unsubscribeFn) return;
659
+ unsubscribeFn();
660
+ });
661
+ }
662
+ const submenuMode = computed(() => resolveSubmenuMode(props.submenu, props.orientation));
423
663
  return () => {
424
- const resolved = theme.value;
664
+ const resolvedTheme = theme.value;
425
665
  const vNodes = [];
426
- for (let i = 0; i < items.value.length; i++) {
427
- if (!items.value[i].display && !items.value[i].displayChildren) continue;
666
+ for (let i = 0; i < tree.value.length; i++) {
667
+ const item = tree.value[i];
668
+ if (!item.display && !item.displayChildren) continue;
428
669
  let vNode;
429
- if (hasNormalizedSlot("item", slots)) vNode = normalizeSlot("item", { data: items.value[i] }, slots);
670
+ if (hasNormalizedSlot("item", slots)) vNode = normalizeSlot("item", { data: item }, slots);
430
671
  else vNode = h(VCNavItem, {
431
- key: `${i}:${counter.value}`,
432
- data: items.value[i]
672
+ key: item.trace.join("/") || i,
673
+ data: item,
674
+ variant: props.variant,
675
+ orientation: props.orientation,
676
+ submenu: submenuMode.value,
677
+ as: props.itemAs,
678
+ itemsAs: props.as,
679
+ themeClass: props.themeClass,
680
+ themeVariant: props.themeVariant
433
681
  });
434
682
  vNodes.push(vNode);
435
683
  }
436
- const isRoot = props.level === 0;
437
- return h("ul", {
438
- class: resolved.group || void 0,
684
+ if (submenuMode.value === "dropdown") return h(NavigationMenuRoot, { orientation: "horizontal" }, { default: () => h(NavigationMenuList, { class: resolvedTheme.group || void 0 }, { default: () => vNodes }) });
685
+ const isRoot = !isNested.value;
686
+ return h(props.as, {
687
+ class: resolvedTheme.group || void 0,
439
688
  ...isRoot ? {
440
689
  ref: rootRef,
441
690
  onKeydown: onKeyDown
@@ -834,7 +1083,7 @@ var StepperSeparator_default = defineComponent({
834
1083
  //#endregion
835
1084
  //#region src/index.ts
836
1085
  function install(instance, options = {}) {
837
- provideNavigationManager(new NavigationManager({ items: options.items ?? [] }), instance);
1086
+ provideNavigationRegistry(new NavigationRegistry(), instance);
838
1087
  installThemeManager(instance, options);
839
1088
  installDefaultsManager(instance, options);
840
1089
  Object.entries({
@@ -853,6 +1102,6 @@ function install(instance, options = {}) {
853
1102
  }
854
1103
  var src_default = { install };
855
1104
  //#endregion
856
- export { NavigationManager, VCNavItem, VCNavItems, Stepper_default as VCStepper, StepperDescription_default as VCStepperDescription, StepperIndicator_default as VCStepperIndicator, StepperItem_default as VCStepperItem, StepperSeparator_default as VCStepperSeparator, StepperTitle_default as VCStepperTitle, StepperTrigger_default as VCStepperTrigger, src_default as default, injectNavigationManager, install, navigationThemeDefaults, provideNavigationManager, provideStepperContext, stepperThemeDefaults, useStepperContext };
1105
+ export { NAVIGATION_NODES_KEY, NAVIGATION_SELECT_KEY, NavigationRegistry, VCNavItem, VCNavItems, Stepper_default as VCStepper, StepperDescription_default as VCStepperDescription, StepperIndicator_default as VCStepperIndicator, StepperItem_default as VCStepperItem, StepperSeparator_default as VCStepperSeparator, StepperTitle_default as VCStepperTitle, StepperTrigger_default as VCStepperTrigger, src_default as default, injectNavigationRegistry, install, navigationThemeDefaults, provideNavigationRegistry, provideStepperContext, stepperThemeDefaults, tryInjectNavigationRegistry, useStepperContext };
857
1106
 
858
1107
  //# sourceMappingURL=index.mjs.map