@servicetitan/navigation 14.3.0 → 14.4.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.
@@ -8,23 +8,9 @@
8
8
  * inside it plain, rather than each one having to defend itself with `!important`.
9
9
  */
10
10
  :global(.anvil2.anvil2) {
11
- /*
12
- * The catalog's column geometry.
13
- *
14
- * `@catalog-column` is the *minimum* a column may be, not its width — the columns share out the
15
- * dialog evenly and end up wider than this. Its job is to set how many there are: at `xlarge`
16
- * (70rem) four of these plus their gaps fit and a fifth does not. The columns land at 248px, which
17
- * still clears the widest row the catalog renders (229px including its star), so nothing clips.
18
- *
19
- * Width rather than `column-count: 4`, so a dialog narrowed by a small viewport drops to three
20
- * columns and then two instead of squeezing four ever thinner. A fixed count gave 208px per column
21
- * at a 768px viewport, under that 229px, which turned the longest labels into ellipses —
22
- * "Batch/Export Transactions" and "Recurring Service Events" among them.
23
- */
11
+ // Four readable columns fit the desktop dialog; narrower dialogs wrap whole groups.
24
12
  @catalog-column: 15rem;
25
13
  @catalog-gap: 1.5rem;
26
-
27
- // The dialog's own horizontal inset, shared by the search field's row and the footer.
28
14
  @catalog-padding: @spacing-2;
29
15
 
30
16
  /*
@@ -93,6 +79,7 @@
93
79
 
94
80
  > * {
95
81
  height: 100%;
82
+ padding-bottom: @spacing-0;
96
83
  }
97
84
  }
98
85
 
@@ -104,6 +91,7 @@
104
91
  * in the left third of the dialog, reading as a fourth column.
105
92
  */
106
93
  .body {
94
+ container-type: inline-size;
107
95
  display: flex;
108
96
  flex: 1;
109
97
  min-width: 0;
@@ -119,6 +107,8 @@
119
107
  */
120
108
  .content {
121
109
  min-height: 0;
110
+ padding-inline: @catalog-padding;
111
+ padding-bottom: @spacing-0;
122
112
  }
123
113
 
124
114
  // Results scroll above the full-width footer; allow its content to wrap on narrow screens.
@@ -160,14 +150,12 @@
160
150
  /*
161
151
  * Fills whatever the now fixed-height dialog leaves under the search field, instead of sizing to
162
152
  * its own content, so browse, a narrowed search, and the empty state all hold the same height.
163
- *
164
- * `.columns` stays at `height: auto` regardless — a multi-column box given a fixed height becomes a
165
- * fragmentainer and spills overflow sideways into further columns instead of growing downwards, which
166
- * is a horizontal scrollbar rather than the vertical one wanted here.
167
153
  */
168
154
  .results {
169
155
  flex: 1;
170
156
  min-height: 0;
157
+ // Overlay scrollbars ignore scrollbar-gutter; keep the last column's stars clear of them.
158
+ padding-right: @spacing-2;
171
159
  overflow-y: auto;
172
160
 
173
161
  /*
@@ -177,107 +165,81 @@
177
165
  scrollbar-gutter: stable;
178
166
  }
179
167
 
180
- /**
181
- * Column width rather than column count, so a narrow dialog drops a column instead of squeezing all
182
- * three.
183
- *
184
- * The dialog tracks the viewport below its cap, so `column-count: 3` had to divide whatever it was
185
- * given by three. At a 768px viewport that was 208px per column, narrower than the 229px the widest
186
- * category wants, and `max-width: 100%` on the category turned the overflow into ellipses — "Batch/
187
- * Export Transactions" and "Recurring Service Events" among them.
188
- *
189
- * Giving the width instead lets the count fall out of it: three columns at the cap, then two, then
190
- * one, and never a clipped label at any width. It cannot reach four, because a fourth column and its
191
- * gap need more room than the cap ever offers, which is what keeps the reference's three-column
192
- * layout at every desktop size.
193
- *
194
- * Multi-column rather than grid so categories pack against one another and flow into the next column,
195
- * instead of leaving a ragged row of half-empty grid cells whenever one category is much taller than
196
- * its neighbours.
197
- */
168
+ // Match the input's width; let either scrollbar style occupy the dialog's right padding.
169
+ .searchResults {
170
+ padding-right: @spacing-0;
171
+ margin-right: -@catalog-padding;
172
+ scrollbar-gutter: auto;
198
173
 
199
- /*
200
- * `column-rule` rather than a border on the categories, because the line belongs to the boundary
201
- * between columns and not to any category. Multi-column draws it once per gap, only where a gap
202
- * exists, and at the full height of the column block — so it disappears on its own when the dialog
203
- * narrows to a single column, and a short last column still gets the full-height line to its left
204
- * rather than one that stops where its content does.
205
- *
206
- * A translucent white rather than the `subdued` border token, for the same reason as the row hover:
207
- * the token is a flat neutral grey, and a grey line on a navy surface reads as a slightly dirty
208
- * seam. At 12% it lands about where the token's grey would in lightness, but tinted by the navy
209
- * underneath it, so it belongs to the surface it divides.
210
- */
211
- .columns {
212
- columns: @catalog-column;
213
- column-gap: @catalog-gap;
214
- column-rule: 1px solid rgba(255, 255, 255, 0.12);
174
+ > div {
175
+ width: 100cqw;
176
+ }
215
177
  }
216
178
 
217
179
  /*
218
- * A category and its heading stay in the same column.
219
- *
220
- * Without this a tall category flowed across a column boundary and its remainder arrived at the top
221
- * of the next column with no heading over it, reading as though those destinations belonged to
222
- * whatever was above them. `break-after` on the heading only keeps the heading with its first row;
223
- * it says nothing about the rest.
224
- *
225
- * This is a preference, not a guarantee: a category taller than the column still has to break
226
- * somewhere, and the browser will split it rather than overflow. That degrades to today's behaviour
227
- * for the one or two largest categories and fixes it for the rest.
180
+ * Each group owns a column, regardless of its length. In particular, Projects & Field starts
181
+ * beside Demand rather than flowing beneath it. The minimum shrinks to the available width
182
+ * on small screens. Search uses a plain single-column list instead of this grid.
228
183
  */
184
+ .columns {
185
+ display: grid;
186
+ grid-template-columns: repeat(auto-fill, minmax(min(100%, @catalog-column), 1fr));
187
+ gap: @catalog-gap;
188
+ overflow: hidden;
189
+ }
229
190
 
230
191
  /*
231
- * Full column width, so every star in the catalog lands on the column's right edge just inside the
232
- * divider. The rows are flex containers with `flex: 1` on the link, so the star is pushed to
233
- * whatever the category's right edge is; letting that edge be the column's puts all of them on one
234
- * line down the whole column rather than one line per category.
192
+ * Grid items stretch to the tallest group so dividers span the row. Place each divider in the
193
+ * preceding gap; the grid clips the one before its first column, including after wrapping.
235
194
  */
236
195
  .group {
237
- break-inside: avoid;
238
- margin-bottom: 1.5rem;
196
+ position: relative;
197
+ min-width: 0;
198
+
199
+ &::before {
200
+ position: absolute;
201
+ top: 0;
202
+ bottom: 0;
203
+ left: -(@catalog-gap / 2);
204
+ width: 1px;
205
+ background: rgba(255, 255, 255, 0.12);
206
+ content: '';
207
+ }
239
208
  }
240
209
 
241
- /*
242
- * Level 1 of three. It shares its size with the module rows below it and is told apart by case,
243
- * tracking and colour rather than by being bigger or smaller — the standard eyebrow treatment, and
244
- * the one place a plain grey belongs.
245
- *
246
- * `break-after` keeps a heading from being stranded at the foot of a column.
247
- *
248
- * The colour is spelled out rather than taken from `--a2-foreground-color-subdued`, which resolves
249
- * to `#c4c4c4` in dark mode: close enough to white that the heading competed with the destinations
250
- * under it. This is `--a2-color-neutral-400`, a genuine mid grey, and it is the same value the rail
251
- * headings use.
252
- *
253
- * Note that anvil2's colour tokens are spelled `foreground`, not `text`; a `--a2-text-color-*` name
254
- * resolves to nothing and drops silently to the fallback. That matters here because this
255
- * repository's LESS `@color-neutral` ramp runs dark-to-light, opposite to anvil2's, so a wrong
256
- * guess lands on near-black text against a near-black dialog.
257
- */
210
+ // Keep eyebrow medium typography and use the catalog's specified subdued colour.
258
211
  .groupTitle {
212
+ --a2-text-foreground-color-subdued: @color-neutral-70;
213
+
259
214
  display: block;
260
- break-after: avoid;
261
215
  padding: @spacing-0 0.5rem;
262
216
  margin-bottom: 0.5rem;
263
- color: light-dark(
264
- var(--a2-color-neutral-500, #707070),
265
- var(--a2-color-neutral-400, #8b8b8b)
266
- );
267
- text-transform: uppercase;
268
- letter-spacing: 0.06em;
269
217
  }
270
218
 
271
- /* A module and its pages are one unit and stay in the same column. */
272
219
  .module-group {
273
- break-inside: avoid;
274
220
  margin-bottom: 0.75rem;
275
221
  }
276
222
 
223
+ // Match the sidebar's small, subdued headings and spacing above each page group.
224
+ .submenuTitle {
225
+ --a2-text-foreground-color-subdued: @color-neutral-70;
226
+
227
+ display: block;
228
+ padding: 12px @spacing-1 @spacing-half 2rem;
229
+ margin: @spacing-0;
230
+ }
231
+
277
232
  .row {
278
233
  display: flex;
279
234
  align-items: center;
280
235
  gap: 0.25rem;
236
+ border-radius: var(--a2-border-radius-medium, 4px);
237
+
238
+ // Share the highlight while keeping the link and favorite button independently focusable.
239
+ &:hover,
240
+ &:has(:focus-visible) {
241
+ background-color: @rail-hover;
242
+ }
281
243
  }
282
244
 
283
245
  .link {
@@ -290,15 +252,8 @@
290
252
  border-radius: var(--a2-border-radius-medium, 4px);
291
253
  text-decoration: none;
292
254
 
293
- /*
294
- * The rail's hover, not anvil2's. The token resolves to a flat neutral grey in dark mode, which
295
- * on this navy replaces the surface's hue instead of lifting it. A translucent white tints
296
- * whatever it covers, so the hovered row stays the same colour, just brighter — and it is the
297
- * same value the rail uses on its own rows, which is what the user is comparing it against.
298
- */
299
255
  &:hover,
300
256
  &:focus-visible {
301
- background-color: @rail-hover;
302
257
  text-decoration: none;
303
258
  }
304
259
  }
@@ -320,24 +275,15 @@
320
275
  white-space: nowrap;
321
276
  color: var(--a2-foreground-color-default, light-dark(#040404, #ffffff));
322
277
  font-weight: var(--a2-font-weight-semibold, 600);
278
+ line-height: 1rem;
323
279
  }
324
280
 
325
- /* Indented to sit under the module's title rather than its icon. */
281
+ // As in the sidebar, page text sits 8px farther in than its group heading.
326
282
  .childLink {
327
- padding-left: 2rem;
283
+ padding: @spacing-1 @spacing-1 @spacing-1 2.5rem;
328
284
  }
329
285
 
330
- /*
331
- * Level 3 of three: below the module row, above the category heading, at regular weight and at the
332
- * same full-strength foreground. It is still a destination, so it is not dimmed — size and weight
333
- * are what place it under its module. Only the category heading is greyed.
334
- *
335
- * 13px is deliberately off the token scale, which jumps 0.75rem → 0.875rem with nothing in between.
336
- * At 0.75rem this tied with the category heading and read as another label rather than as somewhere
337
- * to go. It has to clear the heading and stay under the module row, and the gap between those two is
338
- * one token wide, so the size is spelled out. It matches the rail's pinned pages, which are sized
339
- * the same way and for the same reason.
340
- */
286
+ // Match the expanded sidebar's child-page typography.
341
287
  .childTitle {
342
288
  flex: 1;
343
289
  min-width: 0;
@@ -345,7 +291,9 @@
345
291
  text-overflow: ellipsis;
346
292
  white-space: nowrap;
347
293
  color: var(--a2-foreground-color-default, light-dark(#040404, #ffffff));
348
- font-size: 0.8125rem;
294
+ font-size: @typescale-2;
295
+ font-weight: @font-weight-semibold;
296
+ line-height: 20px;
349
297
  }
350
298
 
351
299
  /*
@@ -382,24 +330,19 @@
382
330
  .star {
383
331
  flex: none;
384
332
  opacity: 0;
385
-
386
- &:focus-visible {
387
- opacity: 1;
388
- }
389
333
  }
390
334
 
391
- .row:hover .star {
335
+ // Mouse clicks retain focus after leaving the row; only keyboard focus should reveal a star.
336
+ .row:hover .star,
337
+ .row:has(:focus-visible) .star {
392
338
  opacity: 0.5;
393
339
 
394
- &:hover {
340
+ &:hover,
341
+ &:focus-visible {
395
342
  opacity: 1;
396
343
  }
397
344
  }
398
345
 
399
- .row:focus-within .star {
400
- opacity: 0.5;
401
- }
402
-
403
346
  /*
404
347
  * After the hover rules, which match at the same specificity, so a pinned star stays lit.
405
348
  *
@@ -411,7 +354,7 @@
411
354
  */
412
355
  .starActive,
413
356
  .row:hover .starActive,
414
- .row:focus-within .starActive {
357
+ .row:has(:focus-visible) .starActive {
415
358
  color: var(--a2-status-color-warning, light-dark(#ffbe00, #ffe278));
416
359
  opacity: 1;
417
360
  }
@@ -19,6 +19,8 @@ export const moduleTitle: string;
19
19
  export const results: string;
20
20
  export const row: string;
21
21
  export const search: string;
22
+ export const searchResults: string;
22
23
  export const star: string;
23
24
  export const starActive: string;
25
+ export const submenuTitle: string;
24
26
 
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=catalog-groups.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"catalog-groups.test.d.ts","sourceRoot":"","sources":["../../../src/utils/__tests__/catalog-groups.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,11 @@
1
+ import { NavigationCatalogGroup } from '../components/titan-layout/interface';
2
+ import { NavigationItemData } from './navigation';
3
+ /** Preserve submenu headings while offering each page only once within its module. */
4
+ export declare const getCatalogSubmenuGroups: (item: NavigationItemData) => {
5
+ title: string;
6
+ links: import("./navigation").NavigationSubmenuItemData[];
7
+ }[];
8
+ export declare const getCatalogChildren: (item: NavigationItemData) => import("./navigation").NavigationSubmenuItemData[];
9
+ /** Match destination titles literally, retaining the headings of matching child pages. */
10
+ export declare const filterCatalogGroups: (groups: NavigationCatalogGroup[], query: string) => NavigationCatalogGroup[];
11
+ //# sourceMappingURL=catalog-groups.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"catalog-groups.d.ts","sourceRoot":"","sources":["../../src/utils/catalog-groups.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,MAAM,sCAAsC,CAAC;AAC9E,OAAO,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAElD,sFAAsF;AACtF,eAAO,MAAM,uBAAuB,GAAI,MAAM,kBAAkB;;;GAe/D,CAAC;AAEF,eAAO,MAAM,kBAAkB,GAAI,MAAM,kBAAkB,uDACI,CAAC;AAEhE,0FAA0F;AAC1F,eAAO,MAAM,mBAAmB,GAAI,QAAQ,sBAAsB,EAAE,EAAE,OAAO,MAAM,6BA+BlF,CAAC"}
@@ -0,0 +1,66 @@
1
+ /** Preserve submenu headings while offering each page only once within its module. */ export const getCatalogSubmenuGroups = (item)=>{
2
+ var _ref;
3
+ var _item_submenu;
4
+ const seen = new Set();
5
+ return ((_ref = (_item_submenu = item.submenu) === null || _item_submenu === void 0 ? void 0 : _item_submenu.groups) !== null && _ref !== void 0 ? _ref : []).flatMap((group)=>{
6
+ var _group_links, _ref;
7
+ var _group_title;
8
+ const links = ((_group_links = group.links) !== null && _group_links !== void 0 ? _group_links : []).filter((link)=>{
9
+ if (seen.has(link.id)) {
10
+ return false;
11
+ }
12
+ seen.add(link.id);
13
+ return true;
14
+ });
15
+ return links.length ? [
16
+ {
17
+ ...group,
18
+ title: (_ref = (_group_title = group.title) === null || _group_title === void 0 ? void 0 : _group_title.trim()) !== null && _ref !== void 0 ? _ref : '',
19
+ links
20
+ }
21
+ ] : [];
22
+ });
23
+ };
24
+ export const getCatalogChildren = (item)=>getCatalogSubmenuGroups(item).flatMap((group)=>group.links);
25
+ /** Match destination titles literally, retaining the headings of matching child pages. */ export const filterCatalogGroups = (groups, query)=>{
26
+ const needle = query.trim().toLowerCase();
27
+ if (!needle) {
28
+ return groups;
29
+ }
30
+ return groups.reduce((out, group)=>{
31
+ const items = group.items.reduce((kept, item)=>{
32
+ const matchingGroups = getCatalogSubmenuGroups(item).flatMap((submenuGroup)=>{
33
+ const links = submenuGroup.links.filter((child)=>child.title.toLowerCase().includes(needle));
34
+ return links.length ? [
35
+ {
36
+ ...submenuGroup,
37
+ links
38
+ }
39
+ ] : [];
40
+ });
41
+ if (matchingGroups.length > 0) {
42
+ kept.push({
43
+ ...item,
44
+ submenu: {
45
+ groups: matchingGroups
46
+ }
47
+ });
48
+ } else if (item.title.toLowerCase().includes(needle)) {
49
+ kept.push({
50
+ ...item,
51
+ submenu: undefined
52
+ });
53
+ }
54
+ return kept;
55
+ }, []);
56
+ if (items.length > 0) {
57
+ out.push({
58
+ ...group,
59
+ items
60
+ });
61
+ }
62
+ return out;
63
+ }, []);
64
+ };
65
+
66
+ //# sourceMappingURL=catalog-groups.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/utils/catalog-groups.ts"],"sourcesContent":["import { NavigationCatalogGroup } from '../components/titan-layout/interface';\nimport { NavigationItemData } from './navigation';\n\n/** Preserve submenu headings while offering each page only once within its module. */\nexport const getCatalogSubmenuGroups = (item: NavigationItemData) => {\n const seen = new Set<string>();\n\n return (item.submenu?.groups ?? []).flatMap(group => {\n const links = (group.links ?? []).filter(link => {\n if (seen.has(link.id)) {\n return false;\n }\n\n seen.add(link.id);\n return true;\n });\n\n return links.length ? [{ ...group, title: group.title?.trim() ?? '', links }] : [];\n });\n};\n\nexport const getCatalogChildren = (item: NavigationItemData) =>\n getCatalogSubmenuGroups(item).flatMap(group => group.links);\n\n/** Match destination titles literally, retaining the headings of matching child pages. */\nexport const filterCatalogGroups = (groups: NavigationCatalogGroup[], query: string) => {\n const needle = query.trim().toLowerCase();\n\n if (!needle) {\n return groups;\n }\n\n return groups.reduce<NavigationCatalogGroup[]>((out, group) => {\n const items = group.items.reduce<NavigationItemData[]>((kept, item) => {\n const matchingGroups = getCatalogSubmenuGroups(item).flatMap(submenuGroup => {\n const links = submenuGroup.links.filter(child =>\n child.title.toLowerCase().includes(needle)\n );\n return links.length ? [{ ...submenuGroup, links }] : [];\n });\n\n if (matchingGroups.length > 0) {\n kept.push({ ...item, submenu: { groups: matchingGroups } });\n } else if (item.title.toLowerCase().includes(needle)) {\n kept.push({ ...item, submenu: undefined });\n }\n\n return kept;\n }, []);\n\n if (items.length > 0) {\n out.push({ ...group, items });\n }\n\n return out;\n }, []);\n};\n"],"names":["getCatalogSubmenuGroups","item","seen","Set","submenu","groups","flatMap","group","links","filter","link","has","id","add","length","title","trim","getCatalogChildren","filterCatalogGroups","query","needle","toLowerCase","reduce","out","items","kept","matchingGroups","submenuGroup","child","includes","push","undefined"],"mappings":"AAGA,oFAAoF,GACpF,OAAO,MAAMA,0BAA0B,CAACC;;QAG5BA;IAFR,MAAMC,OAAO,IAAIC;IAEjB,OAAO,UAACF,gBAAAA,KAAKG,OAAO,cAAZH,oCAAAA,cAAcI,MAAM,uCAAI,EAAE,EAAEC,OAAO,CAACC,CAAAA;YACzBA;YAS2BA;QAT1C,MAAMC,QAAQ,EAACD,eAAAA,MAAMC,KAAK,cAAXD,0BAAAA,eAAe,EAAE,EAAEE,MAAM,CAACC,CAAAA;YACrC,IAAIR,KAAKS,GAAG,CAACD,KAAKE,EAAE,GAAG;gBACnB,OAAO;YACX;YAEAV,KAAKW,GAAG,CAACH,KAAKE,EAAE;YAChB,OAAO;QACX;QAEA,OAAOJ,MAAMM,MAAM,GAAG;YAAC;gBAAE,GAAGP,KAAK;gBAAEQ,KAAK,WAAER,eAAAA,MAAMQ,KAAK,cAAXR,mCAAAA,aAAaS,IAAI,yCAAM;gBAAIR;YAAM;SAAE,GAAG,EAAE;IACtF;AACJ,EAAE;AAEF,OAAO,MAAMS,qBAAqB,CAAChB,OAC/BD,wBAAwBC,MAAMK,OAAO,CAACC,CAAAA,QAASA,MAAMC,KAAK,EAAE;AAEhE,wFAAwF,GACxF,OAAO,MAAMU,sBAAsB,CAACb,QAAkCc;IAClE,MAAMC,SAASD,MAAMH,IAAI,GAAGK,WAAW;IAEvC,IAAI,CAACD,QAAQ;QACT,OAAOf;IACX;IAEA,OAAOA,OAAOiB,MAAM,CAA2B,CAACC,KAAKhB;QACjD,MAAMiB,QAAQjB,MAAMiB,KAAK,CAACF,MAAM,CAAuB,CAACG,MAAMxB;YAC1D,MAAMyB,iBAAiB1B,wBAAwBC,MAAMK,OAAO,CAACqB,CAAAA;gBACzD,MAAMnB,QAAQmB,aAAanB,KAAK,CAACC,MAAM,CAACmB,CAAAA,QACpCA,MAAMb,KAAK,CAACM,WAAW,GAAGQ,QAAQ,CAACT;gBAEvC,OAAOZ,MAAMM,MAAM,GAAG;oBAAC;wBAAE,GAAGa,YAAY;wBAAEnB;oBAAM;iBAAE,GAAG,EAAE;YAC3D;YAEA,IAAIkB,eAAeZ,MAAM,GAAG,GAAG;gBAC3BW,KAAKK,IAAI,CAAC;oBAAE,GAAG7B,IAAI;oBAAEG,SAAS;wBAAEC,QAAQqB;oBAAe;gBAAE;YAC7D,OAAO,IAAIzB,KAAKc,KAAK,CAACM,WAAW,GAAGQ,QAAQ,CAACT,SAAS;gBAClDK,KAAKK,IAAI,CAAC;oBAAE,GAAG7B,IAAI;oBAAEG,SAAS2B;gBAAU;YAC5C;YAEA,OAAON;QACX,GAAG,EAAE;QAEL,IAAID,MAAMV,MAAM,GAAG,GAAG;YAClBS,IAAIO,IAAI,CAAC;gBAAE,GAAGvB,KAAK;gBAAEiB;YAAM;QAC/B;QAEA,OAAOD;IACX,GAAG,EAAE;AACT,EAAE"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@servicetitan/navigation",
3
- "version": "14.3.0",
3
+ "version": "14.4.0",
4
4
  "description": "Navigation components",
5
5
  "repository": {
6
6
  "type": "git",
@@ -35,5 +35,5 @@
35
35
  "less": true,
36
36
  "webpack": false
37
37
  },
38
- "gitHead": "e3bffa6cdb9f250d97a5559bd20fb08a30819906"
38
+ "gitHead": "b64111bc6e95671da2480fa57b27a09c85520c4b"
39
39
  }
@@ -133,15 +133,19 @@ it('leaves favorites untouched until the host applies the reset', () => {
133
133
  const props = renderCatalog();
134
134
  click(button('Restore Default Favorites')!);
135
135
  click(button('Restore')!);
136
- // Fully controlled: the catalog has proposed nothing, so the rows still read as they did.
137
- expect(marketingStar().getAttribute('aria-pressed')).toBe('true');
136
+ /*
137
+ * Fully controlled: the catalog has proposed nothing, so the rows still read as they did —
138
+ * `mixed` here, because two of Marketing's three ids are favourited and not the third.
139
+ */
140
+ expect(marketingStar().getAttribute('aria-pressed')).toBe('mixed');
138
141
  expect(props.onPinnedIdsChange).not.toHaveBeenCalled();
139
142
  });
140
143
 
141
144
  it('keeps star edits on onPinnedIdsChange without calling the reset handler', () => {
142
145
  const props = renderCatalog();
146
+ // A partly favourited module completes itself rather than clearing: campaigns joins the list.
143
147
  click(marketingStar());
144
- expect(props.onPinnedIdsChange).toHaveBeenCalledWith([]);
148
+ expect(props.onPinnedIdsChange).toHaveBeenCalledWith(['marketing', 'overview', 'campaigns']);
145
149
  expect(props.onResetToDefaults).not.toHaveBeenCalled();
146
150
  });
147
151