@servicetitan/navigation 14.2.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.
- package/dist/components/titan-layout/__tests__/layout-sidebar-links-internal.test.d.ts +2 -0
- package/dist/components/titan-layout/__tests__/layout-sidebar-links-internal.test.d.ts.map +1 -0
- package/dist/components/titan-layout/__tests__/navigation-catalog-reset.test.d.ts +2 -0
- package/dist/components/titan-layout/__tests__/navigation-catalog-reset.test.d.ts.map +1 -0
- package/dist/components/titan-layout/interface.d.ts +66 -0
- package/dist/components/titan-layout/interface.d.ts.map +1 -1
- package/dist/components/titan-layout/interface.js.map +1 -1
- package/dist/components/titan-layout/layout-sidebar-links-internal.d.ts +3 -0
- package/dist/components/titan-layout/layout-sidebar-links-internal.d.ts.map +1 -1
- package/dist/components/titan-layout/layout-sidebar-links-internal.js +58 -13
- package/dist/components/titan-layout/layout-sidebar-links-internal.js.map +1 -1
- package/dist/components/titan-layout/layout-sidebar.d.ts +14 -0
- package/dist/components/titan-layout/layout-sidebar.d.ts.map +1 -1
- package/dist/components/titan-layout/layout-sidebar.js +169 -97
- package/dist/components/titan-layout/layout-sidebar.js.map +1 -1
- package/dist/components/titan-layout/layout-sidebar.module.less +230 -144
- package/dist/components/titan-layout/layout-sidebar.module.less.d.ts +16 -5
- package/dist/components/titan-layout/navigation-catalog/index.d.ts +3 -0
- package/dist/components/titan-layout/navigation-catalog/index.d.ts.map +1 -0
- package/dist/components/titan-layout/navigation-catalog/index.js +4 -0
- package/dist/components/titan-layout/navigation-catalog/index.js.map +1 -0
- package/dist/components/titan-layout/navigation-catalog/navigation-catalog-footer.d.ts +8 -0
- package/dist/components/titan-layout/navigation-catalog/navigation-catalog-footer.d.ts.map +1 -0
- package/dist/components/titan-layout/navigation-catalog/navigation-catalog-footer.js +88 -0
- package/dist/components/titan-layout/navigation-catalog/navigation-catalog-footer.js.map +1 -0
- package/dist/components/titan-layout/navigation-catalog/navigation-catalog-trigger.d.ts +21 -0
- package/dist/components/titan-layout/navigation-catalog/navigation-catalog-trigger.d.ts.map +1 -0
- package/dist/components/titan-layout/navigation-catalog/navigation-catalog-trigger.js +138 -0
- package/dist/components/titan-layout/navigation-catalog/navigation-catalog-trigger.js.map +1 -0
- package/dist/components/titan-layout/navigation-catalog/navigation-catalog-trigger.module.less +89 -0
- package/dist/components/titan-layout/navigation-catalog/navigation-catalog-trigger.module.less.d.ts +9 -0
- package/dist/components/titan-layout/navigation-catalog/navigation-catalog.d.ts +14 -0
- package/dist/components/titan-layout/navigation-catalog/navigation-catalog.d.ts.map +1 -0
- package/dist/components/titan-layout/navigation-catalog/navigation-catalog.js +343 -0
- package/dist/components/titan-layout/navigation-catalog/navigation-catalog.js.map +1 -0
- package/dist/components/titan-layout/navigation-catalog/navigation-catalog.module.less +379 -0
- package/dist/components/titan-layout/navigation-catalog/navigation-catalog.module.less.d.ts +26 -0
- package/dist/components/titan-layout/navigation-scroll-indicators.d.ts +12 -0
- package/dist/components/titan-layout/navigation-scroll-indicators.d.ts.map +1 -0
- package/dist/components/titan-layout/navigation-scroll-indicators.js +48 -0
- package/dist/components/titan-layout/navigation-scroll-indicators.js.map +1 -0
- package/dist/components/titan-layout/titan-layout.d.ts +29 -1
- package/dist/components/titan-layout/titan-layout.d.ts.map +1 -1
- package/dist/components/titan-layout/titan-layout.js +46 -6
- package/dist/components/titan-layout/titan-layout.js.map +1 -1
- package/dist/components/titan-layout/titan-layout.module.less +2 -1
- package/dist/components/titan-layout/use-navigation-scroll.d.ts +20 -0
- package/dist/components/titan-layout/use-navigation-scroll.d.ts.map +1 -0
- package/dist/components/titan-layout/use-navigation-scroll.js +140 -0
- package/dist/components/titan-layout/use-navigation-scroll.js.map +1 -0
- package/dist/utils/__tests__/catalog-groups.test.d.ts +2 -0
- package/dist/utils/__tests__/catalog-groups.test.d.ts.map +1 -0
- package/dist/utils/__tests__/navigation-notifications.test.d.ts +2 -0
- package/dist/utils/__tests__/navigation-notifications.test.d.ts.map +1 -0
- package/dist/utils/__tests__/submenu-state.test.d.ts +2 -0
- package/dist/utils/__tests__/submenu-state.test.d.ts.map +1 -0
- package/dist/utils/catalog-groups.d.ts +11 -0
- package/dist/utils/catalog-groups.d.ts.map +1 -0
- package/dist/utils/catalog-groups.js +66 -0
- package/dist/utils/catalog-groups.js.map +1 -0
- package/dist/utils/navigation-notifications.d.ts +21 -0
- package/dist/utils/navigation-notifications.d.ts.map +1 -0
- package/dist/utils/navigation-notifications.js +39 -0
- package/dist/utils/navigation-notifications.js.map +1 -0
- package/dist/utils/side-nav.js +3 -1
- package/dist/utils/side-nav.js.map +1 -1
- package/dist/utils/submenu-state.d.ts +3 -0
- package/dist/utils/submenu-state.d.ts.map +1 -0
- package/dist/utils/submenu-state.js +15 -0
- package/dist/utils/submenu-state.js.map +1 -0
- package/package.json +2 -2
- package/src/components/titan-layout/__tests__/layout-sidebar-links-internal.test.tsx +225 -0
- package/src/components/titan-layout/__tests__/navigation-catalog-reset.test.tsx +190 -0
- package/src/components/titan-layout/interface.ts +73 -0
- package/src/components/titan-layout/layout-sidebar-links-internal.tsx +86 -14
- package/src/components/titan-layout/layout-sidebar.module.less +230 -144
- package/src/components/titan-layout/layout-sidebar.module.less.d.ts +16 -5
- package/src/components/titan-layout/layout-sidebar.tsx +193 -117
- package/src/components/titan-layout/navigation-catalog/index.ts +2 -0
- package/src/components/titan-layout/navigation-catalog/navigation-catalog-footer.tsx +88 -0
- package/src/components/titan-layout/navigation-catalog/navigation-catalog-trigger.module.less +89 -0
- package/src/components/titan-layout/navigation-catalog/navigation-catalog-trigger.module.less.d.ts +9 -0
- package/src/components/titan-layout/navigation-catalog/navigation-catalog-trigger.tsx +170 -0
- package/src/components/titan-layout/navigation-catalog/navigation-catalog.module.less +379 -0
- package/src/components/titan-layout/navigation-catalog/navigation-catalog.module.less.d.ts +26 -0
- package/src/components/titan-layout/navigation-catalog/navigation-catalog.tsx +430 -0
- package/src/components/titan-layout/navigation-scroll-indicators.tsx +80 -0
- package/src/components/titan-layout/titan-layout.module.less +2 -1
- package/src/components/titan-layout/titan-layout.tsx +81 -5
- package/src/components/titan-layout/use-navigation-scroll.ts +145 -0
- package/src/utils/__tests__/catalog-groups.test.ts +97 -0
- package/src/utils/__tests__/navigation-notifications.test.ts +70 -0
- package/src/utils/__tests__/submenu-state.test.ts +39 -0
- package/src/utils/catalog-groups.ts +57 -0
- package/src/utils/navigation-notifications.ts +59 -0
- package/src/utils/side-nav.ts +1 -1
- package/src/utils/submenu-state.ts +16 -0
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { Button, Chip, Flex, Text, ThemeProvider } from '@servicetitan/anvil2';
|
|
2
|
+
import IconSearch from '@servicetitan/anvil2/assets/icons/material/round/search.svg';
|
|
3
|
+
import classNames from 'classnames';
|
|
4
|
+
import { FC, Fragment, ReactNode, useCallback, useEffect, useRef, useState } from 'react';
|
|
5
|
+
import { PinnedNavigationConfig } from '../interface';
|
|
6
|
+
import { NavigationCatalog } from './navigation-catalog';
|
|
7
|
+
|
|
8
|
+
import * as styles from './navigation-catalog-trigger.module.less';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* True when the keystroke should open the catalog rather than reach the focused control.
|
|
12
|
+
*
|
|
13
|
+
* The shortcut is deliberately a modifier chord: a bare key would be swallowed by, or would steal
|
|
14
|
+
* from, every text field on the page.
|
|
15
|
+
*/
|
|
16
|
+
const isCatalogShortcut = (event: KeyboardEvent): boolean =>
|
|
17
|
+
(event.metaKey || event.ctrlKey) && !event.altKey && event.key.toLowerCase() === 'k';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The chord as the user's own keyboard prints it.
|
|
21
|
+
*
|
|
22
|
+
* The listener accepts either modifier on every platform, so this only decides which one to
|
|
23
|
+
* advertise. `platform` is deprecated but is still the only reliable signal here; the user agent is
|
|
24
|
+
* the fallback and a wrong guess costs nothing but a mislabelled badge.
|
|
25
|
+
*/
|
|
26
|
+
const shortcutLabel = (): string => {
|
|
27
|
+
const platform =
|
|
28
|
+
typeof window.navigator === 'undefined'
|
|
29
|
+
? ''
|
|
30
|
+
: window.navigator.platform || window.navigator.userAgent;
|
|
31
|
+
|
|
32
|
+
return /Mac|iPod|iPhone|iPad/.test(platform) ? '⌘ K' : 'Ctrl K';
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export interface NavigationCatalogTriggerProps extends PinnedNavigationConfig {
|
|
36
|
+
/** Whether the rail is expanded; the compact trigger keeps its label below the icon. */
|
|
37
|
+
expanded: boolean;
|
|
38
|
+
/** Optional sidebar action beside the expanded search trigger. */
|
|
39
|
+
trailingAction?: ReactNode;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Sidebar entry point for the navigation catalog.
|
|
44
|
+
*
|
|
45
|
+
* Presented as a labelled control rather than a bare icon: the catalog is the only route to an
|
|
46
|
+
* unpinned destination, so the affordance has to read as "search everything" at a glance. It is a
|
|
47
|
+
* button and not an input — the real field lives in the dialog and takes focus on open, and two
|
|
48
|
+
* inputs would mean typing in one and getting results in the other.
|
|
49
|
+
*
|
|
50
|
+
* The catalog is never opened automatically; an unpinned navigation is a deliberate state, and this
|
|
51
|
+
* trigger plus its shortcut are what keep the user from being stranded there.
|
|
52
|
+
*/
|
|
53
|
+
export const NavigationCatalogTrigger: FC<NavigationCatalogTriggerProps> = ({
|
|
54
|
+
expanded,
|
|
55
|
+
trailingAction,
|
|
56
|
+
...catalog
|
|
57
|
+
}) => {
|
|
58
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
59
|
+
const triggerRef = useRef<HTMLButtonElement>(null);
|
|
60
|
+
const wasOpen = useRef(false);
|
|
61
|
+
|
|
62
|
+
const close = useCallback(() => setIsOpen(false), []);
|
|
63
|
+
const toggle = useCallback(() => setIsOpen(open => !open), []);
|
|
64
|
+
|
|
65
|
+
useEffect(() => {
|
|
66
|
+
// The dialog unmounts on close, so restore focus after its modal content is removed.
|
|
67
|
+
if (wasOpen.current && !isOpen) {
|
|
68
|
+
triggerRef.current?.focus();
|
|
69
|
+
}
|
|
70
|
+
wasOpen.current = isOpen;
|
|
71
|
+
}, [isOpen]);
|
|
72
|
+
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
const handler = (event: KeyboardEvent) => {
|
|
75
|
+
if (!isCatalogShortcut(event)) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
event.preventDefault();
|
|
80
|
+
toggle();
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
window.addEventListener('keydown', handler);
|
|
84
|
+
return () => window.removeEventListener('keydown', handler);
|
|
85
|
+
}, [toggle]);
|
|
86
|
+
|
|
87
|
+
/*
|
|
88
|
+
* Labelled explicitly so the name stays "Go to" whether or not the text is rendered, and so the
|
|
89
|
+
* shortcut badge never reads out as part of it.
|
|
90
|
+
*/
|
|
91
|
+
const buttonProps = {
|
|
92
|
+
/*
|
|
93
|
+
* Not `navigation-item-*`: that prefix belongs to the rail's links, and this is a control
|
|
94
|
+
* that opens a dialog rather than a destination, so sharing the namespace invites a
|
|
95
|
+
* selector written for a link to match it.
|
|
96
|
+
*/
|
|
97
|
+
'data-cy': 'navigation-catalog-trigger',
|
|
98
|
+
'data-pendo': 'navigation-catalog-trigger',
|
|
99
|
+
'appearance': 'secondary',
|
|
100
|
+
'size': 'small',
|
|
101
|
+
'aria-label': 'Go to',
|
|
102
|
+
'aria-haspopup': 'dialog',
|
|
103
|
+
'aria-expanded': isOpen,
|
|
104
|
+
'aria-keyshortcuts': 'Meta+K Control+K',
|
|
105
|
+
'onClick': toggle,
|
|
106
|
+
} as const;
|
|
107
|
+
|
|
108
|
+
return (
|
|
109
|
+
<Fragment>
|
|
110
|
+
<div className={classNames(styles.container, !expanded && styles.containerCollapsed)}>
|
|
111
|
+
{expanded ? (
|
|
112
|
+
<Button
|
|
113
|
+
{...buttonProps}
|
|
114
|
+
ref={triggerRef}
|
|
115
|
+
icon={{ before: IconSearch }}
|
|
116
|
+
className={styles.trigger}
|
|
117
|
+
>
|
|
118
|
+
<Flex alignItems="center" justifyContent="space-between" gap={1} grow={1}>
|
|
119
|
+
{/* The ellipsis is part of the label, matching the field in the dialog. */}
|
|
120
|
+
<Text inline size="small" className={styles.triggerLabel}>
|
|
121
|
+
Go to...
|
|
122
|
+
</Text>
|
|
123
|
+
{/* Decorative: the chord is already on the button as aria-keyshortcuts. */}
|
|
124
|
+
<Chip
|
|
125
|
+
label={shortcutLabel()}
|
|
126
|
+
size="small"
|
|
127
|
+
className={styles.shortcut}
|
|
128
|
+
/>
|
|
129
|
+
</Flex>
|
|
130
|
+
</Button>
|
|
131
|
+
) : (
|
|
132
|
+
<Button
|
|
133
|
+
{...buttonProps}
|
|
134
|
+
ref={triggerRef}
|
|
135
|
+
appearance="ghost"
|
|
136
|
+
icon={IconSearch}
|
|
137
|
+
className={styles.triggerCollapsed}
|
|
138
|
+
>
|
|
139
|
+
<span className={styles.collapsedLabel}>Go to...</span>
|
|
140
|
+
</Button>
|
|
141
|
+
)}
|
|
142
|
+
{expanded && trailingAction}
|
|
143
|
+
</div>
|
|
144
|
+
{/*
|
|
145
|
+
* Mounted only while open, so a host that resolves its catalog lazily is not asked for
|
|
146
|
+
* it on every page load, and a second navigation tree is not held inside the sidebar's
|
|
147
|
+
* subtree for a dialog nobody opened.
|
|
148
|
+
*
|
|
149
|
+
* Dark, because the catalog belongs to the rail rather than to the page. The rail paints
|
|
150
|
+
* itself dark whatever the application's mode is, and this is the rail's own menu — the
|
|
151
|
+
* same `ThemeProvider` the rail uses on its own sections.
|
|
152
|
+
*
|
|
153
|
+
* A `ThemeProvider` rather than a second `AnvilProvider`: the latter is the
|
|
154
|
+
* application-level provider, and mounting a nested one alongside the host's meant its
|
|
155
|
+
* unmount tore down state the outer one was still relying on, so closing the catalog
|
|
156
|
+
* left the rail unstyled.
|
|
157
|
+
*
|
|
158
|
+
* This only sets the mode, which is what makes the *text* tokens light. The surface is
|
|
159
|
+
* matched to the rail separately, in `navigation-catalog.module.less` — anvil2's dark is
|
|
160
|
+
* a neutral grey and the rail's is a navy, so the mode alone would leave two visibly
|
|
161
|
+
* different darks.
|
|
162
|
+
*/}
|
|
163
|
+
{isOpen && (
|
|
164
|
+
<ThemeProvider mode="dark">
|
|
165
|
+
<NavigationCatalog open onClose={close} {...catalog} />
|
|
166
|
+
</ThemeProvider>
|
|
167
|
+
)}
|
|
168
|
+
</Fragment>
|
|
169
|
+
);
|
|
170
|
+
};
|
|
@@ -0,0 +1,379 @@
|
|
|
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
|
+
// Four readable columns fit the desktop dialog; narrower dialogs wrap whole groups.
|
|
12
|
+
@catalog-column: 15rem;
|
|
13
|
+
@catalog-gap: 1.5rem;
|
|
14
|
+
@catalog-padding: @spacing-2;
|
|
15
|
+
|
|
16
|
+
/*
|
|
17
|
+
* The rail's own background, from `@bg-color` in `@servicetitan/navigation`'s
|
|
18
|
+
* `layout-sidebar.module.less`, its hover on top of it, and the trigger chip this catalog opens
|
|
19
|
+
* from.
|
|
20
|
+
*
|
|
21
|
+
* Repeated as literals because they are LESS variables private to that package, not custom
|
|
22
|
+
* properties, so there is nothing to reference at runtime. The rail paints them unconditionally and
|
|
23
|
+
* has done since it was written, so the risk of drift is low and the failure is cosmetic.
|
|
24
|
+
*
|
|
25
|
+
* They are here because the catalog is the rail's menu and should read as part of it. anvil2's dark
|
|
26
|
+
* mode is a neutral grey (#1a1a1a) and the rail is a navy, so taking the mode alone would put two
|
|
27
|
+
* different darks side by side on screen.
|
|
28
|
+
*
|
|
29
|
+
* Translucent white rather than a mixed-down navy throughout: a translucent layer tints whatever it
|
|
30
|
+
* covers, so these keep the surface's hue and only lift it, which is what makes them read as part of
|
|
31
|
+
* the same surface rather than as patches on it.
|
|
32
|
+
*/
|
|
33
|
+
@rail-surface: #0f1d26;
|
|
34
|
+
@rail-hover: rgba(255, 255, 255, 0.08);
|
|
35
|
+
@rail-field: rgba(255, 255, 255, 0.06);
|
|
36
|
+
@rail-field-border: rgba(255, 255, 255, 0.16);
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Repaints the dialog in the rail's navy instead of anvil2's neutral dark, and lightens its
|
|
40
|
+
* backdrop instead of anvil2's default near-black scrim.
|
|
41
|
+
*
|
|
42
|
+
* `--a2-dialog-background-color`, `--a2-dialog-border-color` and `--a2-dialog-backdrop-color` are
|
|
43
|
+
* the hooks anvil2's Dialog reads inside its own `var()` chains, ahead of the tokens they otherwise
|
|
44
|
+
* fall back to. Setting those rather than `background-color`/`::backdrop` directly means no
|
|
45
|
+
* specificity contest — nothing else defines these properties, they are only read — and the
|
|
46
|
+
* background one reaches the header and content panels too, which paint from the same variable.
|
|
47
|
+
*
|
|
48
|
+
* The border goes with the background. Left alone it resolves to a flat `#404040`, which is the
|
|
49
|
+
* last neutral grey on the surface and outlines the whole dialog in it. The same translucent white
|
|
50
|
+
* the field's edge uses puts the outline back in the surface's own colour.
|
|
51
|
+
*
|
|
52
|
+
* `--a2-color-neutral-0` for the backdrop is the same override anvil2's own `fullScreen` variant
|
|
53
|
+
* uses in place of its default `neutral-900` — a supported swap, not a hack.
|
|
54
|
+
*/
|
|
55
|
+
.dialog {
|
|
56
|
+
--a2-dialog-background-color: @rail-surface;
|
|
57
|
+
--a2-dialog-border-color: @rail-field-border;
|
|
58
|
+
--a2-dialog-backdrop-color: var(--a2-color-neutral-0, #ffffff);
|
|
59
|
+
|
|
60
|
+
/*
|
|
61
|
+
* Pins the dialog's size to the viewport instead of its content, so opening it, searching, and
|
|
62
|
+
* clearing the search all leave it exactly where it was.
|
|
63
|
+
*
|
|
64
|
+
* anvil2 centres the dialog by giving it `inset: 0` and `margin: auto`; overriding `top` and
|
|
65
|
+
* `height` here (with the matching margins zeroed so they cannot re-introduce the auto-centring
|
|
66
|
+
* they replace) fixes the vertical position and size, while `left`/`right` stay on the anvil2
|
|
67
|
+
* default and keep centring horizontally. `--nav-offset-top` is the rail's own top-nav height
|
|
68
|
+
* custom property, already threaded through this package for the same purpose.
|
|
69
|
+
*
|
|
70
|
+
* `> *` reaches anvil2's own content wrapper, which otherwise sizes to its content rather than
|
|
71
|
+
* to this now-fixed height — there is no class hook for it, but the dialog only ever has that
|
|
72
|
+
* one child.
|
|
73
|
+
*/
|
|
74
|
+
top: calc(var(--nav-offset-top) + 10px);
|
|
75
|
+
bottom: auto;
|
|
76
|
+
height: calc(100vh - 112px);
|
|
77
|
+
margin-top: @spacing-0;
|
|
78
|
+
margin-bottom: @spacing-0;
|
|
79
|
+
|
|
80
|
+
> * {
|
|
81
|
+
height: 100%;
|
|
82
|
+
padding-bottom: @spacing-0;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Stacks the search above the columns.
|
|
88
|
+
*
|
|
89
|
+
* `Dialog.Content` is `display: flex` with the default row direction, so its children line up
|
|
90
|
+
* side by side. Without this the search field became a flex item next to the column block and sat
|
|
91
|
+
* in the left third of the dialog, reading as a fourth column.
|
|
92
|
+
*/
|
|
93
|
+
.body {
|
|
94
|
+
container-type: inline-size;
|
|
95
|
+
display: flex;
|
|
96
|
+
flex: 1;
|
|
97
|
+
min-width: 0;
|
|
98
|
+
flex-direction: column;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Lets the results area, rather than the dialog's own scroller, take the overflow.
|
|
103
|
+
*
|
|
104
|
+
* `Dialog.Content` is a flex item of that scroller, and a flex item will not shrink below its
|
|
105
|
+
* content by default. Without this the scroller is the thing that overflows, which scrolls the
|
|
106
|
+
* search field off the top along with the results.
|
|
107
|
+
*/
|
|
108
|
+
.content {
|
|
109
|
+
min-height: 0;
|
|
110
|
+
padding-inline: @catalog-padding;
|
|
111
|
+
padding-bottom: @spacing-0;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Results scroll above the full-width footer; allow its content to wrap on narrow screens.
|
|
115
|
+
.footer {
|
|
116
|
+
box-sizing: border-box;
|
|
117
|
+
min-height: 56px;
|
|
118
|
+
flex: none;
|
|
119
|
+
flex-wrap: wrap;
|
|
120
|
+
align-items: center;
|
|
121
|
+
justify-content: center;
|
|
122
|
+
gap: @spacing-1 @spacing-3;
|
|
123
|
+
padding: 12px @catalog-padding;
|
|
124
|
+
background-color: var(--a2-background-color-secondary);
|
|
125
|
+
text-align: center;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.footerActions {
|
|
129
|
+
display: flex;
|
|
130
|
+
flex: none;
|
|
131
|
+
gap: @spacing-1;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/*
|
|
135
|
+
* Matched to the rail's own "Go to" field, which is what the user clicked to get here — the same
|
|
136
|
+
* control, so it should not change colour on the way.
|
|
137
|
+
*
|
|
138
|
+
* anvil2's field is an opaque neutral (#1a1a1a on a #545454 border) which, like the row hover, is
|
|
139
|
+
* the right lightness but the wrong hue against the navy. The rail's translucent white gives the
|
|
140
|
+
* same lift while keeping the surface's colour, and these are its exact values.
|
|
141
|
+
*/
|
|
142
|
+
.search {
|
|
143
|
+
--a2-text-field-input-wrapper-background-color: @rail-field;
|
|
144
|
+
--a2-text-field-input-wrapper-border-color-default: @rail-field-border;
|
|
145
|
+
|
|
146
|
+
width: 100%;
|
|
147
|
+
margin-bottom: 1.5rem;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/*
|
|
151
|
+
* Fills whatever the now fixed-height dialog leaves under the search field, instead of sizing to
|
|
152
|
+
* its own content, so browse, a narrowed search, and the empty state all hold the same height.
|
|
153
|
+
*/
|
|
154
|
+
.results {
|
|
155
|
+
flex: 1;
|
|
156
|
+
min-height: 0;
|
|
157
|
+
// Overlay scrollbars ignore scrollbar-gutter; keep the last column's stars clear of them.
|
|
158
|
+
padding-right: @spacing-2;
|
|
159
|
+
overflow-y: auto;
|
|
160
|
+
|
|
161
|
+
/*
|
|
162
|
+
* Reserve the scrollbar's width whether or not it is showing, so a query that stops the results
|
|
163
|
+
* overflowing does not shift the columns sideways as the scrollbar disappears.
|
|
164
|
+
*/
|
|
165
|
+
scrollbar-gutter: stable;
|
|
166
|
+
}
|
|
167
|
+
|
|
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;
|
|
173
|
+
|
|
174
|
+
> div {
|
|
175
|
+
width: 100cqw;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/*
|
|
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.
|
|
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
|
+
}
|
|
190
|
+
|
|
191
|
+
/*
|
|
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.
|
|
194
|
+
*/
|
|
195
|
+
.group {
|
|
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
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Keep eyebrow medium typography and use the catalog's specified subdued colour.
|
|
211
|
+
.groupTitle {
|
|
212
|
+
--a2-text-foreground-color-subdued: @color-neutral-70;
|
|
213
|
+
|
|
214
|
+
display: block;
|
|
215
|
+
padding: @spacing-0 0.5rem;
|
|
216
|
+
margin-bottom: 0.5rem;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
.module-group {
|
|
220
|
+
margin-bottom: 0.75rem;
|
|
221
|
+
}
|
|
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
|
+
|
|
232
|
+
.row {
|
|
233
|
+
display: flex;
|
|
234
|
+
align-items: center;
|
|
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
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
.link {
|
|
246
|
+
display: flex;
|
|
247
|
+
flex: 1;
|
|
248
|
+
min-width: 0;
|
|
249
|
+
align-items: center;
|
|
250
|
+
gap: 0.5rem;
|
|
251
|
+
padding: 0.25rem 0.5rem;
|
|
252
|
+
border-radius: var(--a2-border-radius-medium, 4px);
|
|
253
|
+
text-decoration: none;
|
|
254
|
+
|
|
255
|
+
&:hover,
|
|
256
|
+
&:focus-visible {
|
|
257
|
+
text-decoration: none;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
.moduleIcon {
|
|
262
|
+
flex: none;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/*
|
|
266
|
+
* Level 2 of three, and the anchor of the scale: full-strength foreground at the body size, which
|
|
267
|
+
* everything else is set against. The destination is the usable part of the row, so it carries the
|
|
268
|
+
* weight and the brighter colour; the category heading above it is deliberately quieter.
|
|
269
|
+
*/
|
|
270
|
+
.moduleTitle {
|
|
271
|
+
flex: 1;
|
|
272
|
+
min-width: 0;
|
|
273
|
+
overflow: hidden;
|
|
274
|
+
text-overflow: ellipsis;
|
|
275
|
+
white-space: nowrap;
|
|
276
|
+
color: var(--a2-foreground-color-default, light-dark(#040404, #ffffff));
|
|
277
|
+
font-weight: var(--a2-font-weight-semibold, 600);
|
|
278
|
+
line-height: 1rem;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// As in the sidebar, page text sits 8px farther in than its group heading.
|
|
282
|
+
.childLink {
|
|
283
|
+
padding: @spacing-1 @spacing-1 @spacing-1 2.5rem;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Match the expanded sidebar's child-page typography.
|
|
287
|
+
.childTitle {
|
|
288
|
+
flex: 1;
|
|
289
|
+
min-width: 0;
|
|
290
|
+
overflow: hidden;
|
|
291
|
+
text-overflow: ellipsis;
|
|
292
|
+
white-space: nowrap;
|
|
293
|
+
color: var(--a2-foreground-color-default, light-dark(#040404, #ffffff));
|
|
294
|
+
font-size: @typescale-2;
|
|
295
|
+
font-weight: @font-weight-semibold;
|
|
296
|
+
line-height: 20px;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/*
|
|
300
|
+
* anvil2 positions a Badge absolutely with a translate, because its usual job is to overlay the
|
|
301
|
+
* corner of an icon. Here it is a sibling in a row and has to sit in the flow, so the position
|
|
302
|
+
* and the transform are both undone — the same override the rail applies to its own submenu
|
|
303
|
+
* counters in `layout-sidebar.module.less`.
|
|
304
|
+
*
|
|
305
|
+
* No `!important` needed: anvil2 nests its rule as `.anvil2 ._badge_x`, and the enclosing
|
|
306
|
+
* `.anvil2.anvil2` scope already outranks that.
|
|
307
|
+
*
|
|
308
|
+
* `flex: none` because the title beside it takes the slack — without it a long name would
|
|
309
|
+
* squeeze the count instead of truncating itself, and a truncated count is worse than a
|
|
310
|
+
* truncated name.
|
|
311
|
+
*/
|
|
312
|
+
.badge[data-anv][data-anv] {
|
|
313
|
+
position: relative;
|
|
314
|
+
transform: none;
|
|
315
|
+
flex: none;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// 2rem to match the star button's box, so both align down the column. Always visible, unlike
|
|
319
|
+
// the star, and #707070 to match the Pantheon frame.
|
|
320
|
+
.lock {
|
|
321
|
+
display: inline-flex;
|
|
322
|
+
width: 2rem;
|
|
323
|
+
height: 2rem;
|
|
324
|
+
flex: none;
|
|
325
|
+
align-items: center;
|
|
326
|
+
justify-content: center;
|
|
327
|
+
color: var(--a2-color-neutral-500, #707070);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
.star {
|
|
331
|
+
flex: none;
|
|
332
|
+
opacity: 0;
|
|
333
|
+
}
|
|
334
|
+
|
|
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 {
|
|
338
|
+
opacity: 0.5;
|
|
339
|
+
|
|
340
|
+
&:hover,
|
|
341
|
+
&:focus-visible {
|
|
342
|
+
opacity: 1;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/*
|
|
347
|
+
* After the hover rules, which match at the same specificity, so a pinned star stays lit.
|
|
348
|
+
*
|
|
349
|
+
* The token's dark value, `yellow-300`, is a pale gold that clears the surface below at 13.5:1.
|
|
350
|
+
* Note that the light value on the other side of this token is `yellow-500` (#ffbe00), which only
|
|
351
|
+
* manages 1.66:1 on white — under the 3:1 WCAG 1.4.11 asks of a graphical control. That branch is
|
|
352
|
+
* unreachable while the catalog follows the rail, but it is the thing to fix first if this surface
|
|
353
|
+
* is ever made light.
|
|
354
|
+
*/
|
|
355
|
+
.starActive,
|
|
356
|
+
.row:hover .starActive,
|
|
357
|
+
.row:has(:focus-visible) .starActive {
|
|
358
|
+
color: var(--a2-status-color-warning, light-dark(#ffbe00, #ffe278));
|
|
359
|
+
opacity: 1;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/*
|
|
363
|
+
* Centred in the fixed-height results area rather than left stranded against its top edge.
|
|
364
|
+
*
|
|
365
|
+
* `border-box` so the padding is counted inside the `100%`. Under the default `content-box` the
|
|
366
|
+
* vertical padding is added to it, and the message ended up 64px taller than the area it sits in —
|
|
367
|
+
* enough to raise a scrollbar next to a single line of text.
|
|
368
|
+
*/
|
|
369
|
+
.empty {
|
|
370
|
+
display: flex;
|
|
371
|
+
box-sizing: border-box;
|
|
372
|
+
height: 100%;
|
|
373
|
+
align-items: center;
|
|
374
|
+
justify-content: center;
|
|
375
|
+
padding: 2rem 0.5rem;
|
|
376
|
+
text-align: center;
|
|
377
|
+
color: var(--a2-foreground-color-subdued, light-dark(#707070, #c4c4c4));
|
|
378
|
+
}
|
|
379
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export const __esModule: true;
|
|
2
|
+
export const badge: string;
|
|
3
|
+
export const body: string;
|
|
4
|
+
export const childLink: string;
|
|
5
|
+
export const childTitle: string;
|
|
6
|
+
export const columns: string;
|
|
7
|
+
export const content: string;
|
|
8
|
+
export const dialog: string;
|
|
9
|
+
export const empty: string;
|
|
10
|
+
export const footer: string;
|
|
11
|
+
export const footerActions: string;
|
|
12
|
+
export const group: string;
|
|
13
|
+
export const groupTitle: string;
|
|
14
|
+
export const link: string;
|
|
15
|
+
export const lock: string;
|
|
16
|
+
export const moduleGroup: string;
|
|
17
|
+
export const moduleIcon: string;
|
|
18
|
+
export const moduleTitle: string;
|
|
19
|
+
export const results: string;
|
|
20
|
+
export const row: string;
|
|
21
|
+
export const search: string;
|
|
22
|
+
export const searchResults: string;
|
|
23
|
+
export const star: string;
|
|
24
|
+
export const starActive: string;
|
|
25
|
+
export const submenuTitle: string;
|
|
26
|
+
|