@canonical/utils 0.29.0 → 0.30.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.
@@ -20,7 +20,7 @@ export function annotateTree(root, parentUrl = null, depth = 0) {
20
20
  };
21
21
  if (items) {
22
22
  // The tree is homogeneous: every node is the same item type `T`. The base
23
- // `Item<C>.items` is typed `Item<C>[]`, so narrow children back to `T`
23
+ // `Item.items` is typed `Item[]`, so narrow children back to `T`
24
24
  // (enhanced item types like `NavItem` already declare `items?: NavItem[]`).
25
25
  annotated.items = items.map((child) => annotateTree(child, getItemId(root), depth + 1));
26
26
  }
@@ -1 +1 @@
1
- {"version":3,"file":"annotateTree.js","sourceRoot":"","sources":["../../../../src/lib/navigation/annotateTree.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAE3C;;;;;;;;;GASG;AACH,MAAM,UAAU,YAAY,CAC1B,IAAO,EACP,YAA2B,IAAI,EAC/B,KAAK,GAAG,CAAC;IAET,MAAM,EAAE,KAAK,EAAE,GAAG,IAAI,EAAE,GAAG,IAAI,CAAC;IAChC,6EAA6E;IAC7E,gFAAgF;IAChF,MAAM,SAAS,GAAG;QAChB,GAAG,IAAI;QACP,SAAS;QACT,KAAK;KACM,CAAC;IACd,IAAI,KAAK,EAAE,CAAC;QACV,0EAA0E;QAC1E,uEAAuE;QACvE,4EAA4E;QAC5E,SAAS,CAAC,KAAK,GAAI,KAAa,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAC7C,YAAY,CAAI,KAAK,EAAE,SAAS,CAAC,IAAI,CAAC,EAAE,KAAK,GAAG,CAAC,CAAC,CACnD,CAAC;IACJ,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC"}
1
+ {"version":3,"file":"annotateTree.js","sourceRoot":"","sources":["../../../../src/lib/navigation/annotateTree.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAE3C;;;;;;;;;GASG;AACH,MAAM,UAAU,YAAY,CAC1B,IAAO,EACP,YAA2B,IAAI,EAC/B,KAAK,GAAG,CAAC;IAET,MAAM,EAAE,KAAK,EAAE,GAAG,IAAI,EAAE,GAAG,IAAI,CAAC;IAChC,6EAA6E;IAC7E,gFAAgF;IAChF,MAAM,SAAS,GAAG;QAChB,GAAG,IAAI;QACP,SAAS;QACT,KAAK;KACM,CAAC;IACd,IAAI,KAAK,EAAE,CAAC;QACV,0EAA0E;QAC1E,iEAAiE;QACjE,4EAA4E;QAC5E,SAAS,CAAC,KAAK,GAAI,KAAa,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAC7C,YAAY,CAAI,KAAK,EAAE,SAAS,CAAC,IAAI,CAAC,EAAE,KAAK,GAAG,CAAC,CAAC,CACnD,CAAC;IACJ,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC"}
@@ -0,0 +1,128 @@
1
+ import getFirstEnabledChild from "./getFirstEnabledChild.js";
2
+ import getLastEnabledChild from "./getLastEnabledChild.js";
3
+ import { NavigationActionType, } from "./navigationTypes.js";
4
+ /**
5
+ * Scan the groups adjacent to `group` (skipping disabled or empty ones) and
6
+ * return the group to land in, in the given direction.
7
+ * @param groups The sibling groups (children of the menu root).
8
+ * @param group The current group being left.
9
+ * @param direction 1 for the next group, -1 for the previous.
10
+ * @returns [adjacentGroup, landingItem] or undefined at the tree's edge.
11
+ */
12
+ const findAdjacentLanding = (groups, group, direction) => {
13
+ let cursor = groups.indexOf(group) + direction;
14
+ while (cursor >= 0 && cursor < groups.length) {
15
+ const candidate = groups.at(cursor);
16
+ if (candidate && !candidate.disabled) {
17
+ // Enter a forward group at its first item, a backward group at its last.
18
+ const landing = direction === 1
19
+ ? getFirstEnabledChild(candidate)
20
+ : getLastEnabledChild(candidate);
21
+ if (landing)
22
+ return [candidate, landing];
23
+ }
24
+ cursor += direction;
25
+ }
26
+ return undefined;
27
+ };
28
+ /**
29
+ * Create a `stateReducer` for `useNavigationTree` that lets vertical arrow keys
30
+ * cross group boundaries. Within a group, {@link createNavigationReducer}
31
+ * already moves between items; at a group edge its `getSibling` finds no sibling
32
+ * and leaves the highlight unchanged. This reducer detects that edge structurally
33
+ * (the highlighted item is the first/last enabled child of its group) and moves
34
+ * the highlight to the adjacent group's boundary item.
35
+ *
36
+ * It operates purely on `state.highlightedItems` — the ancestor path
37
+ * `[root, group, item]` — so it uses the same annotated item instances the
38
+ * navigation tree holds internally. It needs no external index, which avoids the
39
+ * instance-mismatch that would arise from re-annotating the tree separately.
40
+ *
41
+ * The group boundary logic lives entirely here, so the base navigation reducer
42
+ * stays group-agnostic. It suits any grouped, vertically-navigated widget
43
+ * (contextual menus, sectioned side navigation) built on a
44
+ * root -> group -> item tree.
45
+ *
46
+ * @returns A `(state, action) => state` reducer to pass as `stateReducer`.
47
+ */
48
+ /**
49
+ * Type-ahead across group boundaries. The base reducer only searches the
50
+ * current item's siblings (one group), so typing a letter that matches an item
51
+ * in ANOTHER group finds nothing. This searches every group's items — from just
52
+ * after the current item, wrapping — for the first enabled item whose label
53
+ * matches `keysSoFar`, and moves the highlight there.
54
+ * @returns The updated state, or the input state if there is no match.
55
+ */
56
+ const handleCrossGroupTypeAhead = (state) => {
57
+ const path = state.highlightedItems;
58
+ const current = path.at(-1);
59
+ const group = path.at(-2);
60
+ const root = path.at(-3);
61
+ if (!current || !group || !root?.items)
62
+ return state;
63
+ const search = state.keysSoFar.toLowerCase();
64
+ if (!search)
65
+ return state;
66
+ // If the current item already matches, the base reducer handled it in-group.
67
+ if (current.label?.toLowerCase().startsWith(search))
68
+ return state;
69
+ // Flatten every group's enabled items into one ordered list, remembering each
70
+ // item's group so the highlight path can be rebuilt from the tree's instances.
71
+ const entries = [];
72
+ for (const g of root.items) {
73
+ if (g.disabled)
74
+ continue;
75
+ for (const it of g.items ?? []) {
76
+ if (!it.disabled)
77
+ entries.push({ group: g, item: it });
78
+ }
79
+ }
80
+ const currentIndex = entries.findIndex((e) => e.item === current);
81
+ const start = currentIndex + 1;
82
+ for (let i = 0; i < entries.length; i++) {
83
+ const entry = entries[(start + i) % entries.length];
84
+ if (entry.item.label?.toLowerCase().startsWith(search)) {
85
+ return {
86
+ ...state,
87
+ highlightedItems: [...path.slice(0, -2), entry.group, entry.item],
88
+ currentDepth: entry.item.depth,
89
+ };
90
+ }
91
+ }
92
+ return state;
93
+ };
94
+ const createCrossGroupStateReducer = () => (state, action) => {
95
+ if (action.type === NavigationActionType.TYPE_AHEAD) {
96
+ return handleCrossGroupTypeAhead(state);
97
+ }
98
+ const isDown = action.type === NavigationActionType.ARROW_DOWN;
99
+ const isUp = action.type === NavigationActionType.ARROW_UP;
100
+ if (!isDown && !isUp)
101
+ return state;
102
+ // The highlighted path is [root, group, item] for a grouped menu.
103
+ const path = state.highlightedItems;
104
+ const current = path.at(-1);
105
+ const group = path.at(-2);
106
+ const root = path.at(-3);
107
+ // Only items nested inside a group (root -> group -> item) can cross groups.
108
+ if (!current || !group || !root?.items)
109
+ return state;
110
+ const boundaryChild = isDown
111
+ ? getLastEnabledChild(group)
112
+ : getFirstEnabledChild(group);
113
+ // Not at the group edge — the base reducer already moved within the group.
114
+ if (boundaryChild !== current)
115
+ return state;
116
+ const landing = findAdjacentLanding(root.items, group, isDown ? 1 : -1);
117
+ if (!landing)
118
+ return state;
119
+ const [adjacentGroup, target] = landing;
120
+ return {
121
+ ...state,
122
+ // Build the path from the known chain — same instances the tree holds.
123
+ highlightedItems: [...path.slice(0, -2), adjacentGroup, target],
124
+ currentDepth: target.depth,
125
+ };
126
+ };
127
+ export default createCrossGroupStateReducer;
128
+ //# sourceMappingURL=createCrossGroupStateReducer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"createCrossGroupStateReducer.js","sourceRoot":"","sources":["../../../../src/lib/navigation/createCrossGroupStateReducer.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Descend to the first enabled LEAF of a subtree — the first enabled node that
3
+ * has no enabled children of its own.
4
+ *
5
+ * The point of difference from {@link getFirstEnabledChild} is the structural
6
+ * group layer of a contextual menu. A menu is a `root -> group -> item` tree:
7
+ * the root's direct children are GROUPS (rendered `role="group"`, never
8
+ * focusable), and the real `role="menuitem"` nodes live one level deeper.
9
+ * Highlighting the first *child* of the root lands on a group, so no item
10
+ * becomes the roving tab stop and arrow keys have no current item to move from.
11
+ * Descending to the first leaf lands on the first real menuitem instead.
12
+ *
13
+ * For a flat `root -> item` tree (e.g. a simple menu with no groups) the first
14
+ * child is already a leaf, so this returns exactly what
15
+ * {@link getFirstEnabledChild} would — no behaviour change.
16
+ *
17
+ * @param item The subtree root to descend from (typically the menu root).
18
+ * @returns The first enabled leaf, or `undefined` if the subtree has none.
19
+ */
20
+ export default function getFirstEnabledLeaf(item) {
21
+ // Walk the enabled children in order; recurse into any that themselves have
22
+ // children (a group), and backtrack to the next sibling if a branch dead-ends
23
+ // (e.g. a group whose items are all disabled). An enabled child with no
24
+ // children of its own IS the leaf we want.
25
+ for (const child of item.items ?? []) {
26
+ if (child.disabled)
27
+ continue;
28
+ if (child.items?.length) {
29
+ // A container (group / submenu parent): descend, but keep scanning
30
+ // siblings if this branch has no enabled leaf (e.g. all items disabled).
31
+ const leaf = getFirstEnabledLeaf(child);
32
+ if (leaf)
33
+ return leaf;
34
+ }
35
+ else {
36
+ // An enabled node with no children of its own — the first real leaf.
37
+ return child;
38
+ }
39
+ }
40
+ return undefined;
41
+ }
42
+ //# sourceMappingURL=getFirstEnabledLeaf.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"getFirstEnabledLeaf.js","sourceRoot":"","sources":["../../../../src/lib/navigation/getFirstEnabledLeaf.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,CAAC,OAAO,UAAU,mBAAmB,CACzC,IAAc;IAEd,4EAA4E;IAC5E,8EAA8E;IAC9E,wEAAwE;IACxE,2CAA2C;IAC3C,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,KAAK,IAAI,EAAE,EAAE,CAAC;QACrC,IAAI,KAAK,CAAC,QAAQ;YAAE,SAAS;QAC7B,IAAI,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,CAAC;YACxB,mEAAmE;YACnE,yEAAyE;YACzE,MAAM,IAAI,GAAG,mBAAmB,CAAC,KAAK,CAAC,CAAC;YACxC,IAAI,IAAI;gBAAE,OAAO,IAAI,CAAC;QACxB,CAAC;aAAM,CAAC;YACN,qEAAqE;YACrE,OAAO,KAAK,CAAC;QACf,CAAC;IACH,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC"}
@@ -1,7 +1,9 @@
1
1
  export { annotateTree } from "./annotateTree.js";
2
+ export { default as createCrossGroupStateReducer } from "./createCrossGroupStateReducer.js";
2
3
  export { default as createNavigationReducer } from "./createNavigationReducer.js";
3
4
  export { default as findAncestorPath } from "./findAncestorPath.js";
4
5
  export { default as getFirstEnabledChild } from "./getFirstEnabledChild.js";
6
+ export { default as getFirstEnabledLeaf } from "./getFirstEnabledLeaf.js";
5
7
  export { getItemId } from "./getItemId.js";
6
8
  export { default as getLastEnabledChild } from "./getLastEnabledChild.js";
7
9
  export { default as getParentItem } from "./getParentItem.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../src/lib/navigation/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAEjD,OAAO,EAAE,OAAO,IAAI,uBAAuB,EAAE,MAAM,8BAA8B,CAAC;AAClF,OAAO,EAAE,OAAO,IAAI,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AACpE,OAAO,EAAE,OAAO,IAAI,oBAAoB,EAAE,MAAM,2BAA2B,CAAC;AAC5E,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAC3C,OAAO,EAAE,OAAO,IAAI,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AAC1E,OAAO,EAAE,OAAO,IAAI,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAQ9D,OAAO,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAC5D,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACjD,OAAO,EAAE,OAAO,IAAI,kBAAkB,EAAE,MAAM,yBAAyB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../src/lib/navigation/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACjD,OAAO,EAAE,OAAO,IAAI,4BAA4B,EAAE,MAAM,mCAAmC,CAAC;AAE5F,OAAO,EAAE,OAAO,IAAI,uBAAuB,EAAE,MAAM,8BAA8B,CAAC;AAClF,OAAO,EAAE,OAAO,IAAI,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AACpE,OAAO,EAAE,OAAO,IAAI,oBAAoB,EAAE,MAAM,2BAA2B,CAAC;AAC5E,OAAO,EAAE,OAAO,IAAI,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AAC1E,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAC3C,OAAO,EAAE,OAAO,IAAI,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AAC1E,OAAO,EAAE,OAAO,IAAI,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAQ9D,OAAO,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAC5D,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACjD,OAAO,EAAE,OAAO,IAAI,kBAAkB,EAAE,MAAM,yBAAyB,CAAC"}
@@ -8,8 +8,10 @@ import { getItemId } from "./getItemId.js";
8
8
  export function prepareIndex(root) {
9
9
  const index = {};
10
10
  const stack = [root];
11
- while (stack.length > 0) {
12
- const item = stack.pop();
11
+ // Drain the stack by popping into the loop condition: `item` narrows to a
12
+ // defined `_Item<T>` inside the loop with no non-null assertion, and the
13
+ // only exit is the empty-stack `undefined` — no unreachable guard branch.
14
+ for (let item = stack.pop(); item !== undefined; item = stack.pop()) {
13
15
  const id = getItemId(item);
14
16
  index[id] = item;
15
17
  if (item.items) {
@@ -1 +1 @@
1
- {"version":3,"file":"prepareIndex.js","sourceRoot":"","sources":["../../../../src/lib/navigation/prepareIndex.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAE3C;;;;;GAKG;AACH,MAAM,UAAU,YAAY,CAAwB,IAAc;IAChE,MAAM,KAAK,GAAc,EAAE,CAAC;IAC5B,MAAM,KAAK,GAAe,CAAC,IAAI,CAAC,CAAC;IACjC,OAAO,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACxB,MAAM,IAAI,GAAG,KAAK,CAAC,GAAG,EAAG,CAAC;QAC1B,MAAM,EAAE,GAAG,SAAS,CAAC,IAAI,CAAC,CAAC;QAC3B,KAAK,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC;QACjB,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;YACf,KAAK,IAAI,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;gBAChD,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;YAC5B,CAAC;QACH,CAAC;IACH,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC"}
1
+ {"version":3,"file":"prepareIndex.js","sourceRoot":"","sources":["../../../../src/lib/navigation/prepareIndex.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAE3C;;;;;GAKG;AACH,MAAM,UAAU,YAAY,CAAwB,IAAc;IAChE,MAAM,KAAK,GAAc,EAAE,CAAC;IAC5B,MAAM,KAAK,GAAe,CAAC,IAAI,CAAC,CAAC;IACjC,0EAA0E;IAC1E,yEAAyE;IACzE,0EAA0E;IAC1E,KAAK,IAAI,IAAI,GAAG,KAAK,CAAC,GAAG,EAAE,EAAE,IAAI,KAAK,SAAS,EAAE,IAAI,GAAG,KAAK,CAAC,GAAG,EAAE,EAAE,CAAC;QACpE,MAAM,EAAE,GAAG,SAAS,CAAC,IAAI,CAAC,CAAC;QAC3B,KAAK,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC;QACjB,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;YACf,KAAK,IAAI,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;gBAChD,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;YAC5B,CAAC;QACH,CAAC;IACH,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC"}
@@ -0,0 +1,5 @@
1
+ import type { Item } from "@canonical/ds-types";
2
+ import { type NavigationAction, type NavigationState } from "./navigationTypes.js";
3
+ declare const createCrossGroupStateReducer: <T extends Item = Item>() => (state: NavigationState<T>, action: NavigationAction<T>) => NavigationState<T>;
4
+ export default createCrossGroupStateReducer;
5
+ //# sourceMappingURL=createCrossGroupStateReducer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"createCrossGroupStateReducer.d.ts","sourceRoot":"","sources":["../../../../src/lib/navigation/createCrossGroupStateReducer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAS,IAAI,EAAE,MAAM,qBAAqB,CAAC;AAGvD,OAAO,EACL,KAAK,gBAAgB,EAErB,KAAK,eAAe,EACrB,MAAM,sBAAsB,CAAC;AAmG9B,QAAA,MAAM,4BAA4B,GAC/B,CAAC,SAAS,IAAI,GAAG,IAAI,QAEpB,OAAO,eAAe,CAAC,CAAC,CAAC,EACzB,QAAQ,gBAAgB,CAAC,CAAC,CAAC,KAC1B,eAAe,CAAC,CAAC,CAiCnB,CAAC;AAEJ,eAAe,4BAA4B,CAAC"}
@@ -0,0 +1,22 @@
1
+ import type { _Item, Item } from "@canonical/ds-types";
2
+ /**
3
+ * Descend to the first enabled LEAF of a subtree — the first enabled node that
4
+ * has no enabled children of its own.
5
+ *
6
+ * The point of difference from {@link getFirstEnabledChild} is the structural
7
+ * group layer of a contextual menu. A menu is a `root -> group -> item` tree:
8
+ * the root's direct children are GROUPS (rendered `role="group"`, never
9
+ * focusable), and the real `role="menuitem"` nodes live one level deeper.
10
+ * Highlighting the first *child* of the root lands on a group, so no item
11
+ * becomes the roving tab stop and arrow keys have no current item to move from.
12
+ * Descending to the first leaf lands on the first real menuitem instead.
13
+ *
14
+ * For a flat `root -> item` tree (e.g. a simple menu with no groups) the first
15
+ * child is already a leaf, so this returns exactly what
16
+ * {@link getFirstEnabledChild} would — no behaviour change.
17
+ *
18
+ * @param item The subtree root to descend from (typically the menu root).
19
+ * @returns The first enabled leaf, or `undefined` if the subtree has none.
20
+ */
21
+ export default function getFirstEnabledLeaf<T extends Item = Item>(item: _Item<T>): _Item<T> | undefined;
22
+ //# sourceMappingURL=getFirstEnabledLeaf.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"getFirstEnabledLeaf.d.ts","sourceRoot":"","sources":["../../../../src/lib/navigation/getFirstEnabledLeaf.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,qBAAqB,CAAC;AAEvD;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,CAAC,OAAO,UAAU,mBAAmB,CAAC,CAAC,SAAS,IAAI,GAAG,IAAI,EAC/D,IAAI,EAAE,KAAK,CAAC,CAAC,CAAC,GACb,KAAK,CAAC,CAAC,CAAC,GAAG,SAAS,CAkBtB"}
@@ -1,8 +1,10 @@
1
1
  export { annotateTree } from "./annotateTree.js";
2
+ export { default as createCrossGroupStateReducer } from "./createCrossGroupStateReducer.js";
2
3
  export type { NavigationReducerOptions } from "./createNavigationReducer.js";
3
4
  export { default as createNavigationReducer } from "./createNavigationReducer.js";
4
5
  export { default as findAncestorPath } from "./findAncestorPath.js";
5
6
  export { default as getFirstEnabledChild } from "./getFirstEnabledChild.js";
7
+ export { default as getFirstEnabledLeaf } from "./getFirstEnabledLeaf.js";
6
8
  export { getItemId } from "./getItemId.js";
7
9
  export { default as getLastEnabledChild } from "./getLastEnabledChild.js";
8
10
  export { default as getParentItem } from "./getParentItem.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/lib/navigation/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACjD,YAAY,EAAE,wBAAwB,EAAE,MAAM,8BAA8B,CAAC;AAC7E,OAAO,EAAE,OAAO,IAAI,uBAAuB,EAAE,MAAM,8BAA8B,CAAC;AAClF,OAAO,EAAE,OAAO,IAAI,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AACpE,OAAO,EAAE,OAAO,IAAI,oBAAoB,EAAE,MAAM,2BAA2B,CAAC;AAC5E,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAC3C,OAAO,EAAE,OAAO,IAAI,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AAC1E,OAAO,EAAE,OAAO,IAAI,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAC9D,YAAY,EACV,gBAAgB,EAChB,eAAe,EACf,UAAU,EACV,WAAW,EACX,iBAAiB,GAClB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAC5D,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACjD,OAAO,EAAE,OAAO,IAAI,kBAAkB,EAAE,MAAM,yBAAyB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/lib/navigation/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACjD,OAAO,EAAE,OAAO,IAAI,4BAA4B,EAAE,MAAM,mCAAmC,CAAC;AAC5F,YAAY,EAAE,wBAAwB,EAAE,MAAM,8BAA8B,CAAC;AAC7E,OAAO,EAAE,OAAO,IAAI,uBAAuB,EAAE,MAAM,8BAA8B,CAAC;AAClF,OAAO,EAAE,OAAO,IAAI,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AACpE,OAAO,EAAE,OAAO,IAAI,oBAAoB,EAAE,MAAM,2BAA2B,CAAC;AAC5E,OAAO,EAAE,OAAO,IAAI,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AAC1E,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAC3C,OAAO,EAAE,OAAO,IAAI,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AAC1E,OAAO,EAAE,OAAO,IAAI,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAC9D,YAAY,EACV,gBAAgB,EAChB,eAAe,EACf,UAAU,EACV,WAAW,EACX,iBAAiB,GAClB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAC5D,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACjD,OAAO,EAAE,OAAO,IAAI,kBAAkB,EAAE,MAAM,yBAAyB,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"prepareIndex.d.ts","sourceRoot":"","sources":["../../../../src/lib/navigation/prepareIndex.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,qBAAqB,CAAC;AAG/D;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,CAAC,SAAS,IAAI,GAAG,IAAI,EAAE,IAAI,EAAE,KAAK,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC,CAc7E"}
1
+ {"version":3,"file":"prepareIndex.d.ts","sourceRoot":"","sources":["../../../../src/lib/navigation/prepareIndex.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,qBAAqB,CAAC;AAG/D;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,CAAC,SAAS,IAAI,GAAG,IAAI,EAAE,IAAI,EAAE,KAAK,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC,CAgB7E"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@canonical/utils",
3
3
  "description": "Standard utility functions for Canonical's Web Engineering team",
4
- "version": "0.29.0",
4
+ "version": "0.30.0",
5
5
  "type": "module",
6
6
  "module": "dist/esm/index.js",
7
7
  "types": "dist/types/index.d.ts",
@@ -45,18 +45,18 @@
45
45
  "test:vitest:watch": "vitest"
46
46
  },
47
47
  "dependencies": {
48
- "@canonical/ds-types": "^0.29.0"
48
+ "@canonical/ds-types": "^0.30.0"
49
49
  },
50
50
  "devDependencies": {
51
51
  "@biomejs/biome": "2.4.9",
52
- "@canonical/biome-config": "^0.29.0",
53
- "@canonical/typescript-config": "^0.29.0",
54
- "@canonical/webarchitect": "^0.29.0",
52
+ "@canonical/biome-config": "^0.30.0",
53
+ "@canonical/typescript-config": "^0.30.0",
54
+ "@canonical/webarchitect": "^0.30.0",
55
55
  "@types/node": "^24.12.0",
56
56
  "globals": "^17.4.0",
57
57
  "typescript": "^5.9.3",
58
58
  "vite": "^8.0.1",
59
59
  "vitest": "^4.0.18"
60
60
  },
61
- "gitHead": "535302c1e1e5dd8fa9bc2da4144de57ec4e63005"
61
+ "gitHead": "40fe86ac955473536ac14f074812a9821f5b65c0"
62
62
  }