@servicetitan/navigation 14.2.0 → 14.3.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 (89) hide show
  1. package/dist/components/titan-layout/__tests__/layout-sidebar-links-internal.test.d.ts +2 -0
  2. package/dist/components/titan-layout/__tests__/layout-sidebar-links-internal.test.d.ts.map +1 -0
  3. package/dist/components/titan-layout/__tests__/navigation-catalog-reset.test.d.ts +2 -0
  4. package/dist/components/titan-layout/__tests__/navigation-catalog-reset.test.d.ts.map +1 -0
  5. package/dist/components/titan-layout/interface.d.ts +66 -0
  6. package/dist/components/titan-layout/interface.d.ts.map +1 -1
  7. package/dist/components/titan-layout/interface.js.map +1 -1
  8. package/dist/components/titan-layout/layout-sidebar-links-internal.d.ts +3 -0
  9. package/dist/components/titan-layout/layout-sidebar-links-internal.d.ts.map +1 -1
  10. package/dist/components/titan-layout/layout-sidebar-links-internal.js +58 -13
  11. package/dist/components/titan-layout/layout-sidebar-links-internal.js.map +1 -1
  12. package/dist/components/titan-layout/layout-sidebar.d.ts +14 -0
  13. package/dist/components/titan-layout/layout-sidebar.d.ts.map +1 -1
  14. package/dist/components/titan-layout/layout-sidebar.js +169 -97
  15. package/dist/components/titan-layout/layout-sidebar.js.map +1 -1
  16. package/dist/components/titan-layout/layout-sidebar.module.less +230 -144
  17. package/dist/components/titan-layout/layout-sidebar.module.less.d.ts +16 -5
  18. package/dist/components/titan-layout/navigation-catalog/index.d.ts +3 -0
  19. package/dist/components/titan-layout/navigation-catalog/index.d.ts.map +1 -0
  20. package/dist/components/titan-layout/navigation-catalog/index.js +4 -0
  21. package/dist/components/titan-layout/navigation-catalog/index.js.map +1 -0
  22. package/dist/components/titan-layout/navigation-catalog/navigation-catalog-footer.d.ts +8 -0
  23. package/dist/components/titan-layout/navigation-catalog/navigation-catalog-footer.d.ts.map +1 -0
  24. package/dist/components/titan-layout/navigation-catalog/navigation-catalog-footer.js +88 -0
  25. package/dist/components/titan-layout/navigation-catalog/navigation-catalog-footer.js.map +1 -0
  26. package/dist/components/titan-layout/navigation-catalog/navigation-catalog-trigger.d.ts +21 -0
  27. package/dist/components/titan-layout/navigation-catalog/navigation-catalog-trigger.d.ts.map +1 -0
  28. package/dist/components/titan-layout/navigation-catalog/navigation-catalog-trigger.js +138 -0
  29. package/dist/components/titan-layout/navigation-catalog/navigation-catalog-trigger.js.map +1 -0
  30. package/dist/components/titan-layout/navigation-catalog/navigation-catalog-trigger.module.less +89 -0
  31. package/dist/components/titan-layout/navigation-catalog/navigation-catalog-trigger.module.less.d.ts +9 -0
  32. package/dist/components/titan-layout/navigation-catalog/navigation-catalog.d.ts +14 -0
  33. package/dist/components/titan-layout/navigation-catalog/navigation-catalog.d.ts.map +1 -0
  34. package/dist/components/titan-layout/navigation-catalog/navigation-catalog.js +382 -0
  35. package/dist/components/titan-layout/navigation-catalog/navigation-catalog.js.map +1 -0
  36. package/dist/components/titan-layout/navigation-catalog/navigation-catalog.module.less +436 -0
  37. package/dist/components/titan-layout/navigation-catalog/navigation-catalog.module.less.d.ts +24 -0
  38. package/dist/components/titan-layout/navigation-scroll-indicators.d.ts +12 -0
  39. package/dist/components/titan-layout/navigation-scroll-indicators.d.ts.map +1 -0
  40. package/dist/components/titan-layout/navigation-scroll-indicators.js +48 -0
  41. package/dist/components/titan-layout/navigation-scroll-indicators.js.map +1 -0
  42. package/dist/components/titan-layout/titan-layout.d.ts +29 -1
  43. package/dist/components/titan-layout/titan-layout.d.ts.map +1 -1
  44. package/dist/components/titan-layout/titan-layout.js +46 -6
  45. package/dist/components/titan-layout/titan-layout.js.map +1 -1
  46. package/dist/components/titan-layout/titan-layout.module.less +2 -1
  47. package/dist/components/titan-layout/use-navigation-scroll.d.ts +20 -0
  48. package/dist/components/titan-layout/use-navigation-scroll.d.ts.map +1 -0
  49. package/dist/components/titan-layout/use-navigation-scroll.js +140 -0
  50. package/dist/components/titan-layout/use-navigation-scroll.js.map +1 -0
  51. package/dist/utils/__tests__/navigation-notifications.test.d.ts +2 -0
  52. package/dist/utils/__tests__/navigation-notifications.test.d.ts.map +1 -0
  53. package/dist/utils/__tests__/submenu-state.test.d.ts +2 -0
  54. package/dist/utils/__tests__/submenu-state.test.d.ts.map +1 -0
  55. package/dist/utils/navigation-notifications.d.ts +21 -0
  56. package/dist/utils/navigation-notifications.d.ts.map +1 -0
  57. package/dist/utils/navigation-notifications.js +39 -0
  58. package/dist/utils/navigation-notifications.js.map +1 -0
  59. package/dist/utils/side-nav.js +3 -1
  60. package/dist/utils/side-nav.js.map +1 -1
  61. package/dist/utils/submenu-state.d.ts +3 -0
  62. package/dist/utils/submenu-state.d.ts.map +1 -0
  63. package/dist/utils/submenu-state.js +15 -0
  64. package/dist/utils/submenu-state.js.map +1 -0
  65. package/package.json +2 -2
  66. package/src/components/titan-layout/__tests__/layout-sidebar-links-internal.test.tsx +225 -0
  67. package/src/components/titan-layout/__tests__/navigation-catalog-reset.test.tsx +186 -0
  68. package/src/components/titan-layout/interface.ts +73 -0
  69. package/src/components/titan-layout/layout-sidebar-links-internal.tsx +86 -14
  70. package/src/components/titan-layout/layout-sidebar.module.less +230 -144
  71. package/src/components/titan-layout/layout-sidebar.module.less.d.ts +16 -5
  72. package/src/components/titan-layout/layout-sidebar.tsx +193 -117
  73. package/src/components/titan-layout/navigation-catalog/index.ts +2 -0
  74. package/src/components/titan-layout/navigation-catalog/navigation-catalog-footer.tsx +88 -0
  75. package/src/components/titan-layout/navigation-catalog/navigation-catalog-trigger.module.less +89 -0
  76. package/src/components/titan-layout/navigation-catalog/navigation-catalog-trigger.module.less.d.ts +9 -0
  77. package/src/components/titan-layout/navigation-catalog/navigation-catalog-trigger.tsx +170 -0
  78. package/src/components/titan-layout/navigation-catalog/navigation-catalog.module.less +436 -0
  79. package/src/components/titan-layout/navigation-catalog/navigation-catalog.module.less.d.ts +24 -0
  80. package/src/components/titan-layout/navigation-catalog/navigation-catalog.tsx +458 -0
  81. package/src/components/titan-layout/navigation-scroll-indicators.tsx +80 -0
  82. package/src/components/titan-layout/titan-layout.module.less +2 -1
  83. package/src/components/titan-layout/titan-layout.tsx +81 -5
  84. package/src/components/titan-layout/use-navigation-scroll.ts +145 -0
  85. package/src/utils/__tests__/navigation-notifications.test.ts +70 -0
  86. package/src/utils/__tests__/submenu-state.test.ts +39 -0
  87. package/src/utils/navigation-notifications.ts +59 -0
  88. package/src/utils/side-nav.ts +1 -1
  89. package/src/utils/submenu-state.ts +16 -0
@@ -0,0 +1,88 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Button, Dialog, Text } from '@servicetitan/anvil2';
3
+ import IconUndo from '@servicetitan/anvil2/assets/icons/material/round/undo.svg';
4
+ import { Fragment, useEffect, useId, useRef, useState } from 'react';
5
+ import * as styles from './navigation-catalog.module.less';
6
+ export const NavigationCatalogFooter = ({ onRestore })=>{
7
+ const [confirming, setConfirming] = useState(false);
8
+ const cancelRef = useRef(null);
9
+ const restoreRef = useRef(null);
10
+ const returnFocus = useRef(false);
11
+ const confirmationId = useId();
12
+ useEffect(()=>{
13
+ if (confirming) {
14
+ var _cancelRef_current;
15
+ (_cancelRef_current = cancelRef.current) === null || _cancelRef_current === void 0 ? void 0 : _cancelRef_current.focus();
16
+ } else if (returnFocus.current) {
17
+ var _restoreRef_current;
18
+ (_restoreRef_current = restoreRef.current) === null || _restoreRef_current === void 0 ? void 0 : _restoreRef_current.focus();
19
+ returnFocus.current = false;
20
+ }
21
+ }, [
22
+ confirming
23
+ ]);
24
+ useEffect(()=>{
25
+ if (!onRestore) {
26
+ setConfirming(false);
27
+ }
28
+ }, [
29
+ onRestore
30
+ ]);
31
+ return /*#__PURE__*/ _jsx(Dialog.Footer, {
32
+ className: styles.footer,
33
+ "data-cy": "navigation-catalog-footer",
34
+ children: confirming && onRestore ? /*#__PURE__*/ _jsxs(Fragment, {
35
+ children: [
36
+ /*#__PURE__*/ _jsx(Text, {
37
+ size: "small",
38
+ id: confirmationId,
39
+ children: "This action replaces your current favorites. You can favorite items again anytime."
40
+ }),
41
+ /*#__PURE__*/ _jsxs("div", {
42
+ className: styles.footerActions,
43
+ children: [
44
+ /*#__PURE__*/ _jsx(Button, {
45
+ ref: cancelRef,
46
+ appearance: "secondary",
47
+ size: "xsmall",
48
+ "aria-describedby": confirmationId,
49
+ onClick: ()=>{
50
+ returnFocus.current = true;
51
+ setConfirming(false);
52
+ },
53
+ children: "Cancel"
54
+ }),
55
+ /*#__PURE__*/ _jsx(Button, {
56
+ appearance: "primary",
57
+ size: "xsmall",
58
+ "aria-describedby": confirmationId,
59
+ onClick: ()=>{
60
+ setConfirming(false);
61
+ onRestore();
62
+ },
63
+ children: "Restore"
64
+ })
65
+ ]
66
+ })
67
+ ]
68
+ }) : /*#__PURE__*/ _jsxs(Fragment, {
69
+ children: [
70
+ /*#__PURE__*/ _jsx(Text, {
71
+ size: "small",
72
+ subdued: true,
73
+ children: "Add or remove items to your navigation by clicking the star icon to the right"
74
+ }),
75
+ onRestore && /*#__PURE__*/ _jsx(Button, {
76
+ ref: restoreRef,
77
+ appearance: "secondary",
78
+ size: "xsmall",
79
+ icon: IconUndo,
80
+ onClick: ()=>setConfirming(true),
81
+ children: "Restore Default Favorites"
82
+ })
83
+ ]
84
+ })
85
+ });
86
+ };
87
+
88
+ //# sourceMappingURL=navigation-catalog-footer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../src/components/titan-layout/navigation-catalog/navigation-catalog-footer.tsx"],"sourcesContent":["import { Button, Dialog, Text } from '@servicetitan/anvil2';\nimport IconUndo from '@servicetitan/anvil2/assets/icons/material/round/undo.svg';\nimport { FC, Fragment, useEffect, useId, useRef, useState } from 'react';\nimport * as styles from './navigation-catalog.module.less';\n\ninterface NavigationCatalogFooterProps {\n /** Present only when the current favorites differ from the host's defaults. */\n onRestore?: () => void;\n}\n\nexport const NavigationCatalogFooter: FC<NavigationCatalogFooterProps> = ({ onRestore }) => {\n const [confirming, setConfirming] = useState(false);\n const cancelRef = useRef<HTMLButtonElement>(null);\n const restoreRef = useRef<HTMLButtonElement>(null);\n const returnFocus = useRef(false);\n const confirmationId = useId();\n\n useEffect(() => {\n if (confirming) {\n cancelRef.current?.focus();\n } else if (returnFocus.current) {\n restoreRef.current?.focus();\n returnFocus.current = false;\n }\n }, [confirming]);\n\n useEffect(() => {\n if (!onRestore) {\n setConfirming(false);\n }\n }, [onRestore]);\n\n return (\n <Dialog.Footer className={styles.footer} data-cy=\"navigation-catalog-footer\">\n {confirming && onRestore ? (\n <Fragment>\n <Text size=\"small\" id={confirmationId}>\n This action replaces your current favorites. You can favorite items again\n anytime.\n </Text>\n <div className={styles.footerActions}>\n <Button\n ref={cancelRef}\n appearance=\"secondary\"\n size=\"xsmall\"\n aria-describedby={confirmationId}\n onClick={() => {\n returnFocus.current = true;\n setConfirming(false);\n }}\n >\n Cancel\n </Button>\n <Button\n appearance=\"primary\"\n size=\"xsmall\"\n aria-describedby={confirmationId}\n onClick={() => {\n setConfirming(false);\n onRestore();\n }}\n >\n Restore\n </Button>\n </div>\n </Fragment>\n ) : (\n <Fragment>\n <Text size=\"small\" subdued>\n Add or remove items to your navigation by clicking the star icon to the\n right\n </Text>\n {onRestore && (\n <Button\n ref={restoreRef}\n appearance=\"secondary\"\n size=\"xsmall\"\n icon={IconUndo}\n onClick={() => setConfirming(true)}\n >\n Restore Default Favorites\n </Button>\n )}\n </Fragment>\n )}\n </Dialog.Footer>\n );\n};\n"],"names":["Button","Dialog","Text","IconUndo","Fragment","useEffect","useId","useRef","useState","styles","NavigationCatalogFooter","onRestore","confirming","setConfirming","cancelRef","restoreRef","returnFocus","confirmationId","current","focus","Footer","className","footer","data-cy","size","id","div","footerActions","ref","appearance","aria-describedby","onClick","subdued","icon"],"mappings":";AAAA,SAASA,MAAM,EAAEC,MAAM,EAAEC,IAAI,QAAQ,uBAAuB;AAC5D,OAAOC,cAAc,4DAA4D;AACjF,SAAaC,QAAQ,EAAEC,SAAS,EAAEC,KAAK,EAAEC,MAAM,EAAEC,QAAQ,QAAQ,QAAQ;AACzE,YAAYC,YAAY,mCAAmC;AAO3D,OAAO,MAAMC,0BAA4D,CAAC,EAAEC,SAAS,EAAE;IACnF,MAAM,CAACC,YAAYC,cAAc,GAAGL,SAAS;IAC7C,MAAMM,YAAYP,OAA0B;IAC5C,MAAMQ,aAAaR,OAA0B;IAC7C,MAAMS,cAAcT,OAAO;IAC3B,MAAMU,iBAAiBX;IAEvBD,UAAU;QACN,IAAIO,YAAY;gBACZE;aAAAA,qBAAAA,UAAUI,OAAO,cAAjBJ,yCAAAA,mBAAmBK,KAAK;QAC5B,OAAO,IAAIH,YAAYE,OAAO,EAAE;gBAC5BH;aAAAA,sBAAAA,WAAWG,OAAO,cAAlBH,0CAAAA,oBAAoBI,KAAK;YACzBH,YAAYE,OAAO,GAAG;QAC1B;IACJ,GAAG;QAACN;KAAW;IAEfP,UAAU;QACN,IAAI,CAACM,WAAW;YACZE,cAAc;QAClB;IACJ,GAAG;QAACF;KAAU;IAEd,qBACI,KAACV,OAAOmB,MAAM;QAACC,WAAWZ,OAAOa,MAAM;QAAEC,WAAQ;kBAC5CX,cAAcD,0BACX,MAACP;;8BACG,KAACF;oBAAKsB,MAAK;oBAAQC,IAAIR;8BAAgB;;8BAIvC,MAACS;oBAAIL,WAAWZ,OAAOkB,aAAa;;sCAChC,KAAC3B;4BACG4B,KAAKd;4BACLe,YAAW;4BACXL,MAAK;4BACLM,oBAAkBb;4BAClBc,SAAS;gCACLf,YAAYE,OAAO,GAAG;gCACtBL,cAAc;4BAClB;sCACH;;sCAGD,KAACb;4BACG6B,YAAW;4BACXL,MAAK;4BACLM,oBAAkBb;4BAClBc,SAAS;gCACLlB,cAAc;gCACdF;4BACJ;sCACH;;;;;2BAMT,MAACP;;8BACG,KAACF;oBAAKsB,MAAK;oBAAQQ,OAAO;8BAAC;;gBAI1BrB,2BACG,KAACX;oBACG4B,KAAKb;oBACLc,YAAW;oBACXL,MAAK;oBACLS,MAAM9B;oBACN4B,SAAS,IAAMlB,cAAc;8BAChC;;;;;AAQzB,EAAE"}
@@ -0,0 +1,21 @@
1
+ import { FC, ReactNode } from 'react';
2
+ import { PinnedNavigationConfig } from '../interface';
3
+ export interface NavigationCatalogTriggerProps extends PinnedNavigationConfig {
4
+ /** Whether the rail is expanded; the compact trigger keeps its label below the icon. */
5
+ expanded: boolean;
6
+ /** Optional sidebar action beside the expanded search trigger. */
7
+ trailingAction?: ReactNode;
8
+ }
9
+ /**
10
+ * Sidebar entry point for the navigation catalog.
11
+ *
12
+ * Presented as a labelled control rather than a bare icon: the catalog is the only route to an
13
+ * unpinned destination, so the affordance has to read as "search everything" at a glance. It is a
14
+ * button and not an input — the real field lives in the dialog and takes focus on open, and two
15
+ * inputs would mean typing in one and getting results in the other.
16
+ *
17
+ * The catalog is never opened automatically; an unpinned navigation is a deliberate state, and this
18
+ * trigger plus its shortcut are what keep the user from being stranded there.
19
+ */
20
+ export declare const NavigationCatalogTrigger: FC<NavigationCatalogTriggerProps>;
21
+ //# sourceMappingURL=navigation-catalog-trigger.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"navigation-catalog-trigger.d.ts","sourceRoot":"","sources":["../../../../src/components/titan-layout/navigation-catalog/navigation-catalog-trigger.tsx"],"names":[],"mappings":"AAGA,OAAO,EAAE,EAAE,EAAY,SAAS,EAA4C,MAAM,OAAO,CAAC;AAC1F,OAAO,EAAE,sBAAsB,EAAE,MAAM,cAAc,CAAC;AA8BtD,MAAM,WAAW,6BAA8B,SAAQ,sBAAsB;IACzE,wFAAwF;IACxF,QAAQ,EAAE,OAAO,CAAC;IAClB,kEAAkE;IAClE,cAAc,CAAC,EAAE,SAAS,CAAC;CAC9B;AAED;;;;;;;;;;GAUG;AACH,eAAO,MAAM,wBAAwB,EAAE,EAAE,CAAC,6BAA6B,CAqHtE,CAAC"}
@@ -0,0 +1,138 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Button, Chip, Flex, Text, ThemeProvider } from '@servicetitan/anvil2';
3
+ import IconSearch from '@servicetitan/anvil2/assets/icons/material/round/search.svg';
4
+ import classNames from 'classnames';
5
+ import { Fragment, useCallback, useEffect, useRef, useState } from 'react';
6
+ import { NavigationCatalog } from './navigation-catalog';
7
+ import * as styles from './navigation-catalog-trigger.module.less';
8
+ /**
9
+ * True when the keystroke should open the catalog rather than reach the focused control.
10
+ *
11
+ * The shortcut is deliberately a modifier chord: a bare key would be swallowed by, or would steal
12
+ * from, every text field on the page.
13
+ */ const isCatalogShortcut = (event)=>(event.metaKey || event.ctrlKey) && !event.altKey && event.key.toLowerCase() === 'k';
14
+ /**
15
+ * The chord as the user's own keyboard prints it.
16
+ *
17
+ * The listener accepts either modifier on every platform, so this only decides which one to
18
+ * advertise. `platform` is deprecated but is still the only reliable signal here; the user agent is
19
+ * the fallback and a wrong guess costs nothing but a mislabelled badge.
20
+ */ const shortcutLabel = ()=>{
21
+ const platform = typeof window.navigator === 'undefined' ? '' : window.navigator.platform || window.navigator.userAgent;
22
+ return /Mac|iPod|iPhone|iPad/.test(platform) ? '⌘ K' : 'Ctrl K';
23
+ };
24
+ /**
25
+ * Sidebar entry point for the navigation catalog.
26
+ *
27
+ * Presented as a labelled control rather than a bare icon: the catalog is the only route to an
28
+ * unpinned destination, so the affordance has to read as "search everything" at a glance. It is a
29
+ * button and not an input — the real field lives in the dialog and takes focus on open, and two
30
+ * inputs would mean typing in one and getting results in the other.
31
+ *
32
+ * The catalog is never opened automatically; an unpinned navigation is a deliberate state, and this
33
+ * trigger plus its shortcut are what keep the user from being stranded there.
34
+ */ export const NavigationCatalogTrigger = ({ expanded, trailingAction, ...catalog })=>{
35
+ const [isOpen, setIsOpen] = useState(false);
36
+ const triggerRef = useRef(null);
37
+ const wasOpen = useRef(false);
38
+ const close = useCallback(()=>setIsOpen(false), []);
39
+ const toggle = useCallback(()=>setIsOpen((open)=>!open), []);
40
+ useEffect(()=>{
41
+ // The dialog unmounts on close, so restore focus after its modal content is removed.
42
+ if (wasOpen.current && !isOpen) {
43
+ var _triggerRef_current;
44
+ (_triggerRef_current = triggerRef.current) === null || _triggerRef_current === void 0 ? void 0 : _triggerRef_current.focus();
45
+ }
46
+ wasOpen.current = isOpen;
47
+ }, [
48
+ isOpen
49
+ ]);
50
+ useEffect(()=>{
51
+ const handler = (event)=>{
52
+ if (!isCatalogShortcut(event)) {
53
+ return;
54
+ }
55
+ event.preventDefault();
56
+ toggle();
57
+ };
58
+ window.addEventListener('keydown', handler);
59
+ return ()=>window.removeEventListener('keydown', handler);
60
+ }, [
61
+ toggle
62
+ ]);
63
+ /*
64
+ * Labelled explicitly so the name stays "Go to" whether or not the text is rendered, and so the
65
+ * shortcut badge never reads out as part of it.
66
+ */ const buttonProps = {
67
+ /*
68
+ * Not `navigation-item-*`: that prefix belongs to the rail's links, and this is a control
69
+ * that opens a dialog rather than a destination, so sharing the namespace invites a
70
+ * selector written for a link to match it.
71
+ */ 'data-cy': 'navigation-catalog-trigger',
72
+ 'data-pendo': 'navigation-catalog-trigger',
73
+ 'appearance': 'secondary',
74
+ 'size': 'small',
75
+ 'aria-label': 'Go to',
76
+ 'aria-haspopup': 'dialog',
77
+ 'aria-expanded': isOpen,
78
+ 'aria-keyshortcuts': 'Meta+K Control+K',
79
+ 'onClick': toggle
80
+ };
81
+ return /*#__PURE__*/ _jsxs(Fragment, {
82
+ children: [
83
+ /*#__PURE__*/ _jsxs("div", {
84
+ className: classNames(styles.container, !expanded && styles.containerCollapsed),
85
+ children: [
86
+ expanded ? /*#__PURE__*/ _jsx(Button, {
87
+ ...buttonProps,
88
+ ref: triggerRef,
89
+ icon: {
90
+ before: IconSearch
91
+ },
92
+ className: styles.trigger,
93
+ children: /*#__PURE__*/ _jsxs(Flex, {
94
+ alignItems: "center",
95
+ justifyContent: "space-between",
96
+ gap: 1,
97
+ grow: 1,
98
+ children: [
99
+ /*#__PURE__*/ _jsx(Text, {
100
+ inline: true,
101
+ size: "small",
102
+ className: styles.triggerLabel,
103
+ children: "Go to..."
104
+ }),
105
+ /*#__PURE__*/ _jsx(Chip, {
106
+ label: shortcutLabel(),
107
+ size: "small",
108
+ className: styles.shortcut
109
+ })
110
+ ]
111
+ })
112
+ }) : /*#__PURE__*/ _jsx(Button, {
113
+ ...buttonProps,
114
+ ref: triggerRef,
115
+ appearance: "ghost",
116
+ icon: IconSearch,
117
+ className: styles.triggerCollapsed,
118
+ children: /*#__PURE__*/ _jsx("span", {
119
+ className: styles.collapsedLabel,
120
+ children: "Go to..."
121
+ })
122
+ }),
123
+ expanded && trailingAction
124
+ ]
125
+ }),
126
+ isOpen && /*#__PURE__*/ _jsx(ThemeProvider, {
127
+ mode: "dark",
128
+ children: /*#__PURE__*/ _jsx(NavigationCatalog, {
129
+ open: true,
130
+ onClose: close,
131
+ ...catalog
132
+ })
133
+ })
134
+ ]
135
+ });
136
+ };
137
+
138
+ //# sourceMappingURL=navigation-catalog-trigger.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../src/components/titan-layout/navigation-catalog/navigation-catalog-trigger.tsx"],"sourcesContent":["import { Button, Chip, Flex, Text, ThemeProvider } from '@servicetitan/anvil2';\nimport IconSearch from '@servicetitan/anvil2/assets/icons/material/round/search.svg';\nimport classNames from 'classnames';\nimport { FC, Fragment, ReactNode, useCallback, useEffect, useRef, useState } from 'react';\nimport { PinnedNavigationConfig } from '../interface';\nimport { NavigationCatalog } from './navigation-catalog';\n\nimport * as styles from './navigation-catalog-trigger.module.less';\n\n/**\n * True when the keystroke should open the catalog rather than reach the focused control.\n *\n * The shortcut is deliberately a modifier chord: a bare key would be swallowed by, or would steal\n * from, every text field on the page.\n */\nconst isCatalogShortcut = (event: KeyboardEvent): boolean =>\n (event.metaKey || event.ctrlKey) && !event.altKey && event.key.toLowerCase() === 'k';\n\n/**\n * The chord as the user's own keyboard prints it.\n *\n * The listener accepts either modifier on every platform, so this only decides which one to\n * advertise. `platform` is deprecated but is still the only reliable signal here; the user agent is\n * the fallback and a wrong guess costs nothing but a mislabelled badge.\n */\nconst shortcutLabel = (): string => {\n const platform =\n typeof window.navigator === 'undefined'\n ? ''\n : window.navigator.platform || window.navigator.userAgent;\n\n return /Mac|iPod|iPhone|iPad/.test(platform) ? '⌘ K' : 'Ctrl K';\n};\n\nexport interface NavigationCatalogTriggerProps extends PinnedNavigationConfig {\n /** Whether the rail is expanded; the compact trigger keeps its label below the icon. */\n expanded: boolean;\n /** Optional sidebar action beside the expanded search trigger. */\n trailingAction?: ReactNode;\n}\n\n/**\n * Sidebar entry point for the navigation catalog.\n *\n * Presented as a labelled control rather than a bare icon: the catalog is the only route to an\n * unpinned destination, so the affordance has to read as \"search everything\" at a glance. It is a\n * button and not an input — the real field lives in the dialog and takes focus on open, and two\n * inputs would mean typing in one and getting results in the other.\n *\n * The catalog is never opened automatically; an unpinned navigation is a deliberate state, and this\n * trigger plus its shortcut are what keep the user from being stranded there.\n */\nexport const NavigationCatalogTrigger: FC<NavigationCatalogTriggerProps> = ({\n expanded,\n trailingAction,\n ...catalog\n}) => {\n const [isOpen, setIsOpen] = useState(false);\n const triggerRef = useRef<HTMLButtonElement>(null);\n const wasOpen = useRef(false);\n\n const close = useCallback(() => setIsOpen(false), []);\n const toggle = useCallback(() => setIsOpen(open => !open), []);\n\n useEffect(() => {\n // The dialog unmounts on close, so restore focus after its modal content is removed.\n if (wasOpen.current && !isOpen) {\n triggerRef.current?.focus();\n }\n wasOpen.current = isOpen;\n }, [isOpen]);\n\n useEffect(() => {\n const handler = (event: KeyboardEvent) => {\n if (!isCatalogShortcut(event)) {\n return;\n }\n\n event.preventDefault();\n toggle();\n };\n\n window.addEventListener('keydown', handler);\n return () => window.removeEventListener('keydown', handler);\n }, [toggle]);\n\n /*\n * Labelled explicitly so the name stays \"Go to\" whether or not the text is rendered, and so the\n * shortcut badge never reads out as part of it.\n */\n const buttonProps = {\n /*\n * Not `navigation-item-*`: that prefix belongs to the rail's links, and this is a control\n * that opens a dialog rather than a destination, so sharing the namespace invites a\n * selector written for a link to match it.\n */\n 'data-cy': 'navigation-catalog-trigger',\n 'data-pendo': 'navigation-catalog-trigger',\n 'appearance': 'secondary',\n 'size': 'small',\n 'aria-label': 'Go to',\n 'aria-haspopup': 'dialog',\n 'aria-expanded': isOpen,\n 'aria-keyshortcuts': 'Meta+K Control+K',\n 'onClick': toggle,\n } as const;\n\n return (\n <Fragment>\n <div className={classNames(styles.container, !expanded && styles.containerCollapsed)}>\n {expanded ? (\n <Button\n {...buttonProps}\n ref={triggerRef}\n icon={{ before: IconSearch }}\n className={styles.trigger}\n >\n <Flex alignItems=\"center\" justifyContent=\"space-between\" gap={1} grow={1}>\n {/* The ellipsis is part of the label, matching the field in the dialog. */}\n <Text inline size=\"small\" className={styles.triggerLabel}>\n Go to...\n </Text>\n {/* Decorative: the chord is already on the button as aria-keyshortcuts. */}\n <Chip\n label={shortcutLabel()}\n size=\"small\"\n className={styles.shortcut}\n />\n </Flex>\n </Button>\n ) : (\n <Button\n {...buttonProps}\n ref={triggerRef}\n appearance=\"ghost\"\n icon={IconSearch}\n className={styles.triggerCollapsed}\n >\n <span className={styles.collapsedLabel}>Go to...</span>\n </Button>\n )}\n {expanded && trailingAction}\n </div>\n {/*\n * Mounted only while open, so a host that resolves its catalog lazily is not asked for\n * it on every page load, and a second navigation tree is not held inside the sidebar's\n * subtree for a dialog nobody opened.\n *\n * Dark, because the catalog belongs to the rail rather than to the page. The rail paints\n * itself dark whatever the application's mode is, and this is the rail's own menu — the\n * same `ThemeProvider` the rail uses on its own sections.\n *\n * A `ThemeProvider` rather than a second `AnvilProvider`: the latter is the\n * application-level provider, and mounting a nested one alongside the host's meant its\n * unmount tore down state the outer one was still relying on, so closing the catalog\n * left the rail unstyled.\n *\n * This only sets the mode, which is what makes the *text* tokens light. The surface is\n * matched to the rail separately, in `navigation-catalog.module.less` — anvil2's dark is\n * a neutral grey and the rail's is a navy, so the mode alone would leave two visibly\n * different darks.\n */}\n {isOpen && (\n <ThemeProvider mode=\"dark\">\n <NavigationCatalog open onClose={close} {...catalog} />\n </ThemeProvider>\n )}\n </Fragment>\n );\n};\n"],"names":["Button","Chip","Flex","Text","ThemeProvider","IconSearch","classNames","Fragment","useCallback","useEffect","useRef","useState","NavigationCatalog","styles","isCatalogShortcut","event","metaKey","ctrlKey","altKey","key","toLowerCase","shortcutLabel","platform","window","navigator","userAgent","test","NavigationCatalogTrigger","expanded","trailingAction","catalog","isOpen","setIsOpen","triggerRef","wasOpen","close","toggle","open","current","focus","handler","preventDefault","addEventListener","removeEventListener","buttonProps","div","className","container","containerCollapsed","ref","icon","before","trigger","alignItems","justifyContent","gap","grow","inline","size","triggerLabel","label","shortcut","appearance","triggerCollapsed","span","collapsedLabel","mode","onClose"],"mappings":"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"}
@@ -0,0 +1,89 @@
1
+ @import (reference) '@servicetitan/tokens/core/tokens.less';
2
+
3
+ /*
4
+ * Scoped to `.anvil2.anvil2` so these rules outrank anvil2's own.
5
+ *
6
+ * anvil2 nests every component rule under `.anvil2`, giving it two classes of specificity,
7
+ * which a bare class here loses to. Doubling the scope wins outright and leaves the selectors
8
+ * inside it plain, rather than each one having to defend itself with `!important`.
9
+ */
10
+ :global(.anvil2.anvil2) {
11
+ .container {
12
+ display: flex;
13
+ align-items: center;
14
+ gap: 12px;
15
+ padding: @spacing-1 @spacing-1 @spacing-2;
16
+
17
+ > :last-child:not(:first-child) {
18
+ flex-shrink: 0;
19
+ }
20
+ }
21
+
22
+ /*
23
+ * The rail stretches its children to its own width, so the button follows rather than sizing to
24
+ * its label. Everything else about the control — surface, border, radius, focus ring, hover — is
25
+ * the anvil2 secondary button's own.
26
+ */
27
+ .trigger {
28
+ width: 100%;
29
+ min-width: 0;
30
+ flex: 1;
31
+ }
32
+
33
+ // The reference uses the core subdued swatch (#bcbcbd).
34
+ .triggerLabel {
35
+ color: @color-neutral-70;
36
+ line-height: @spacing-2;
37
+ white-space: nowrap;
38
+ }
39
+
40
+ /*
41
+ * Held back from the label so the chord reads as an aside rather than as part of the name, and
42
+ * kept from wrapping when the rail is at its narrowest expanded width.
43
+ */
44
+ .shortcut {
45
+ --a2-mod-chip-min-height: @spacing-2;
46
+ --a2-mod-chip-inner-padding-block: 1px;
47
+
48
+ white-space: nowrap;
49
+ flex-shrink: 0;
50
+ }
51
+
52
+ .containerCollapsed {
53
+ // Together with the button's 6px bottom inset, keep 12px before Dashboard's icon box.
54
+ padding: @spacing-0 @spacing-0 6px;
55
+ }
56
+
57
+ .triggerCollapsed {
58
+ width: 60px;
59
+ min-width: 0;
60
+ min-height: 48px;
61
+ box-sizing: border-box;
62
+ padding: 5px @spacing-half;
63
+ flex-direction: column;
64
+ gap: 2px;
65
+ color: @color-white;
66
+
67
+ [data-anv='icon'] {
68
+ width: 24px;
69
+ height: 24px;
70
+ flex-shrink: 0;
71
+ display: flex;
72
+ align-items: center;
73
+ justify-content: center;
74
+ }
75
+
76
+ svg {
77
+ width: 20px;
78
+ height: 20px;
79
+ }
80
+ }
81
+
82
+ .collapsedLabel {
83
+ font-family: @base-font-family;
84
+ font-size: @typescale-0;
85
+ font-weight: @font-weight-regular;
86
+ line-height: 10px;
87
+ white-space: nowrap;
88
+ }
89
+ }
@@ -0,0 +1,9 @@
1
+ export const __esModule: true;
2
+ export const collapsedLabel: string;
3
+ export const container: string;
4
+ export const containerCollapsed: string;
5
+ export const shortcut: string;
6
+ export const trigger: string;
7
+ export const triggerCollapsed: string;
8
+ export const triggerLabel: string;
9
+
@@ -0,0 +1,14 @@
1
+ import { FC } from 'react';
2
+ import { PinnedNavigationConfig } from '../interface';
3
+ export interface NavigationCatalogProps extends PinnedNavigationConfig {
4
+ open: boolean;
5
+ onClose: () => void;
6
+ }
7
+ /**
8
+ * Every destination the host offers, grouped into sections, with a star on every row.
9
+ *
10
+ * The catalog shows what it is given and nothing more — the host has already resolved what this
11
+ * user may reach, so the catalog cannot offer a destination they do not have.
12
+ */
13
+ export declare const NavigationCatalog: FC<NavigationCatalogProps>;
14
+ //# sourceMappingURL=navigation-catalog.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"navigation-catalog.d.ts","sourceRoot":"","sources":["../../../../src/components/titan-layout/navigation-catalog/navigation-catalog.tsx"],"names":[],"mappings":"AAKA,OAAO,EAAE,EAAE,EAAmD,MAAM,OAAO,CAAC;AAI5E,OAAO,EAA0B,sBAAsB,EAAE,MAAM,cAAc,CAAC;AAyO9E,MAAM,WAAW,sBAAuB,SAAQ,sBAAsB;IAClE,IAAI,EAAE,OAAO,CAAC;IACd,OAAO,EAAE,MAAM,IAAI,CAAC;CACvB;AAED;;;;;GAKG;AACH,eAAO,MAAM,iBAAiB,EAAE,EAAE,CAAC,sBAAsB,CA4MxD,CAAC"}