@defra/flood-webchat 0.0.1-beta.52 → 0.0.1-beta.53

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 (42) hide show
  1. package/package.json +1 -1
  2. package/src/client/components/availability/availability.jsx +130 -0
  3. package/src/client/components/availability/availability.scss +77 -0
  4. package/src/client/components/chat/chat.jsx +233 -0
  5. package/src/client/components/chat/chat.scss +259 -0
  6. package/src/client/components/errorSummary/error-summary.jsx +34 -0
  7. package/src/client/components/live-region.jsx +55 -0
  8. package/src/client/components/message/message.jsx +21 -0
  9. package/src/client/components/panel/panel-footer.jsx +9 -0
  10. package/src/client/components/panel/panel-header.jsx +75 -0
  11. package/src/client/components/panel/panel.jsx +163 -0
  12. package/src/client/components/panel/panel.scss +112 -0
  13. package/src/client/components/screens/end-chat.jsx +77 -0
  14. package/src/client/components/screens/feedback.jsx +65 -0
  15. package/src/client/components/screens/pre-chat.jsx +62 -0
  16. package/src/client/components/screens/request-chat.jsx +183 -0
  17. package/src/client/components/screens/settings.jsx +97 -0
  18. package/src/client/components/screens/unavailable.jsx +23 -0
  19. package/src/client/components/skip-link.jsx +42 -0
  20. package/src/client/hooks/useFocusedElements.js +80 -0
  21. package/src/client/hooks/useTextareaAutosize.js +13 -0
  22. package/src/client/index.jsx +33 -0
  23. package/src/client/lib/agent-status-headline.js +17 -0
  24. package/src/client/lib/check-availability.js +8 -0
  25. package/src/client/lib/classnames.js +1 -0
  26. package/src/client/lib/history.js +13 -0
  27. package/src/client/lib/message-notification.js +30 -0
  28. package/src/client/lib/transform-messages.js +47 -0
  29. package/src/client/scss/_objects.scss +33 -0
  30. package/src/client/scss/_settings.scss +101 -0
  31. package/src/client/scss/_tools.scss +33 -0
  32. package/src/client/scss/_utilities.scss +25 -0
  33. package/src/client/store/AppProvider.jsx +183 -0
  34. package/src/client/store/actions-map.js +152 -0
  35. package/src/client/store/constants.js +3 -0
  36. package/src/client/store/reducer.js +31 -0
  37. package/src/client/store/useApp.js +5 -0
  38. package/src/client/store/useChatSdk.js +37 -0
  39. package/src/server/index.js +60 -0
  40. package/src/server/lib/client.js +88 -0
  41. package/src/server/lib/utils.js +23 -0
  42. package/webpack.prod.mjs +0 -7
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@defra/flood-webchat",
3
- "version": "0.0.1-beta.52",
3
+ "version": "0.0.1-beta.53",
4
4
  "description": "",
5
5
  "main": "dist/server.js",
6
6
  "browser": "dist/client.js",
@@ -0,0 +1,130 @@
1
+ import React, { useEffect, useRef } from 'react'
2
+
3
+ import { classnames } from '../../lib/classnames'
4
+ import { Panel } from '../panel/panel.jsx'
5
+ import { SkipLink } from '../skip-link.jsx'
6
+
7
+ import { useApp } from '../../store/useApp'
8
+ import { historyPushState, historyReplaceState } from '../../lib/history.js'
9
+ import { LiveRegion } from '../live-region.jsx'
10
+
11
+ export function Availability () {
12
+ const { availability, isChatOpen, setChatVisibility, unseenCount, threadId, setUnseenCount, setInstigatorId, setLiveRegionText } = useApp()
13
+
14
+ const buttonRef = useRef()
15
+
16
+ const showUnseenCount = unseenCount > 0 && !isChatOpen
17
+
18
+ const messageText = unseenCount > 1 ? 'new messages' : 'new message'
19
+
20
+ const onClick = e => {
21
+ e.preventDefault()
22
+ setUnseenCount(0)
23
+ setChatVisibility(!isChatOpen)
24
+ setInstigatorId(e.target.id)
25
+
26
+ if (!isChatOpen) {
27
+ historyPushState()
28
+ } else {
29
+ historyReplaceState()
30
+ }
31
+ }
32
+
33
+ const onKeyDown = event => {
34
+ if (event.key === ' ') {
35
+ event.preventDefault()
36
+ }
37
+ }
38
+
39
+ const onKeyUp = event => {
40
+ if (event.key === ' ') {
41
+ setUnseenCount(0)
42
+ setChatVisibility(!isChatOpen)
43
+ }
44
+ }
45
+
46
+ useEffect(() => {
47
+ if (showUnseenCount) {
48
+ setLiveRegionText(`Floodline webchat - ${unseenCount} ${messageText}`)
49
+ }
50
+
51
+ return () => {
52
+ setLiveRegionText()
53
+ }
54
+ }, [unseenCount, isChatOpen])
55
+
56
+ useEffect(() => {
57
+ const onScroll = () => {
58
+ if (!threadId) {
59
+ return
60
+ }
61
+
62
+ if (availability !== 'AVAILABLE') {
63
+ return
64
+ }
65
+
66
+ const rect = buttonRef.current.parentElement.getBoundingClientRect()
67
+
68
+ const isBelowFold = rect.top + 35 > (window.innerHeight || document.documentElement.clientHeight)
69
+ const isFixed = !isChatOpen && isBelowFold
70
+
71
+ document.documentElement.classList.toggle('wc-u-scroll-padding', isFixed)
72
+ document.body.classList.toggle('wc-u-scroll-padding', isFixed)
73
+ buttonRef.current.classList.toggle('wc-availability--fixed', isFixed)
74
+ }
75
+
76
+ document.addEventListener('scroll', onScroll)
77
+ onScroll()
78
+
79
+ return () => {
80
+ document.removeEventListener('scroll', onScroll)
81
+ }
82
+ }, [threadId, isChatOpen, availability])
83
+
84
+ switch (availability) {
85
+ case 'AVAILABLE':
86
+ return (
87
+ <>
88
+ <div
89
+ className={classnames('wc-availability', isChatOpen && 'wc-open')}
90
+ ref={buttonRef}
91
+ >
92
+ <div className='wc-availability__inner'>
93
+ <a
94
+ id='webchat-start-chat-link'
95
+ className='wc-availability__link'
96
+ href='#webchat'
97
+ draggable='false'
98
+ onClick={onClick}
99
+ onKeyUp={onKeyUp}
100
+ onKeyDown={onKeyDown}
101
+ role='button'
102
+ data-module='govuk-button'
103
+ >
104
+ {!threadId ? <>Start chat</> : <>Show chat</>}
105
+ {showUnseenCount
106
+ ? (
107
+ <span className='wc-availability__unseen'>
108
+ {unseenCount}
109
+ <span className='govuk-visually-hidden'> {messageText}</span>
110
+ </span>
111
+ )
112
+ : null}
113
+ </a>
114
+ </div>
115
+ </div>
116
+ {!isChatOpen ? <LiveRegion /> : <Panel />}
117
+ <SkipLink />
118
+ </>
119
+ )
120
+ case 'EXISTING':
121
+ case 'UNAVAILABLE':
122
+ return (
123
+ <p className='govuk-body'>When it is available, a 'start chat' link will appear.</p>
124
+ )
125
+ default:
126
+ return (
127
+ <p className='govuk-body'>Checking availability</p>
128
+ )
129
+ }
130
+ }
@@ -0,0 +1,77 @@
1
+ // stylelint-disable selector-max-id
2
+ #wc-availability {
3
+ margin-bottom: govuk-spacing(4);
4
+ }
5
+ // stylelint-enable selector-max-id
6
+
7
+ .wc-availability__inner {
8
+ max-width: 960px;
9
+ margin-left: auto;
10
+ margin-right: auto;
11
+ }
12
+
13
+ .wc-availability--fixed {
14
+ position: fixed;
15
+ bottom: 0;
16
+ left: 0;
17
+ right: 0;
18
+ padding-left: 15px;
19
+ padding-right: 15px;
20
+ border: 1px solid transparent;
21
+ @media (forced-colors: active) {
22
+ border-color: currentColor;
23
+ }
24
+
25
+ @include mq ($from: 'tablet') {
26
+ padding-left: 30px;
27
+ padding-right: 30px;
28
+ }
29
+
30
+ background-color: govuk-colour('light-grey');
31
+
32
+ .wc-availability__link {
33
+ margin-top: 10px;
34
+ margin-bottom: 10px;
35
+ }
36
+ }
37
+
38
+ .wc-availability__link {
39
+ @extend %govuk-link;
40
+ display: inline-block;
41
+ @include govuk-font($size: 19);
42
+ margin-right: 5px;
43
+
44
+ &:visited:not(:hover):not(:focus) {
45
+ color: govuk-colour('blue');
46
+ }
47
+
48
+ .govuk-visually-hidden, .wc-availability__unseen {
49
+ pointer-events: none;
50
+ }
51
+
52
+ &:hover:not(:focus):not(:active) .wc-availability__unseen {
53
+ background-color: govuk-colour('dark-blue');
54
+ }
55
+ }
56
+
57
+ .wc-availability__unseen {
58
+ display: inline-block;
59
+ position: relative;
60
+ border: 1px solid transparent;
61
+ @media (forced-colors: active) {
62
+ border-color: currentColor;
63
+ }
64
+ border-radius: 10px;
65
+ background-color: govuk-colour('black');
66
+ color: govuk-colour('white');
67
+ font-weight: bold;
68
+ font-size: 14px;
69
+ padding: 0 6px;
70
+ margin-left: 5px;
71
+
72
+ @include mq ($from: tablet) {
73
+ top: -1px;
74
+ padding-left: 7px;
75
+ padding-right: 7px;
76
+ }
77
+ }
@@ -0,0 +1,233 @@
1
+ import React, { useEffect, useRef, useState } from 'react'
2
+
3
+ import { PanelHeader } from '../panel/panel-header.jsx'
4
+ import { PanelFooter } from '../panel/panel-footer.jsx'
5
+ import { Message } from '../message/message.jsx'
6
+
7
+ import { useApp } from '../../store/useApp.js'
8
+ import { useTextareaAutosize } from '../../hooks/useTextareaAutosize.js'
9
+ import { formatTranscript } from '../../lib/transform-messages.js'
10
+ import { agentStatusHeadline } from '../../lib/agent-status-headline.js'
11
+
12
+ export function Chat ({ onEndChatScreen, onSettingsScreen }) {
13
+ const { availability, thread, messages, agent, agentStatus, isAgentTyping, isChatOpen, settings, isKeyboard, setLiveRegionText } = useApp()
14
+
15
+ const [userMessage, setUserMessage] = useState('')
16
+ const [focusVisibleWithin, setFocusVisibleWithin] = useState(false)
17
+
18
+ const messageRef = useRef()
19
+
20
+ useTextareaAutosize(messageRef.current, userMessage)
21
+
22
+ useEffect(() => {
23
+ if (settings.scroll && messages.length !== 0) {
24
+ const chatBody = document.querySelector('.wc-body')
25
+ chatBody.scrollTop = chatBody.scrollHeight
26
+ }
27
+ }, [messages, isAgentTyping])
28
+
29
+ useEffect(() => {
30
+ const label = document.querySelector('.wc-form__label')
31
+
32
+ if (userMessage.length === 0) {
33
+ label.classList.remove('govuk-visually-hidden')
34
+ } else {
35
+ label.classList.add('govuk-visually-hidden')
36
+ }
37
+ }, [userMessage])
38
+
39
+ const onChange = e => {
40
+ setUserMessage(e.target?.value)
41
+ }
42
+
43
+ const agentName = agent?.nickname || agent?.firstName
44
+
45
+ const connectionHeadlineText = agentStatusHeadline(availability, agentStatus, agentName)
46
+
47
+ useEffect(() => {
48
+ if (connectionHeadlineText) {
49
+ setLiveRegionText(`Floodline webchat - ${connectionHeadlineText}`)
50
+ }
51
+
52
+ return () => {
53
+ setLiveRegionText()
54
+ }
55
+ }, [connectionHeadlineText])
56
+
57
+ useEffect(() => {
58
+ if (isAgentTyping && isChatOpen) {
59
+ setLiveRegionText(`${agentName} is typing...`)
60
+ }
61
+
62
+ return () => {
63
+ setLiveRegionText()
64
+ }
65
+ }, [isAgentTyping, isChatOpen])
66
+
67
+ useEffect(() => {
68
+ const lastAgentMessage = messages[messages.length - 1]
69
+
70
+ if (agentStatus !== 'closed' && lastAgentMessage?.direction === 'outbound') {
71
+ setLiveRegionText(`${agentName} said: ${lastAgentMessage.text}`)
72
+ }
73
+
74
+ return () => {
75
+ setLiveRegionText()
76
+ }
77
+ }, [messages])
78
+
79
+ const sendMessage = () => {
80
+ if (messageRef.current.value.length === 0 || agentStatus === 'closed') {
81
+ return
82
+ }
83
+
84
+ try {
85
+ const message = messageRef.current.value.trim()
86
+ thread.sendTextMessage(message)
87
+ setLiveRegionText(`You said: ${message}`)
88
+ } catch (err) {
89
+ console.log('[Chat Error] sendMessage', err)
90
+ }
91
+
92
+ setUserMessage('')
93
+ }
94
+
95
+ const handleSubmit = e => {
96
+ e.preventDefault()
97
+ sendMessage()
98
+ }
99
+
100
+ const saveChat = () => {
101
+ const transcript = formatTranscript(messages)
102
+
103
+ const saveChatLink = document.querySelector('#transcript-download')
104
+
105
+ saveChatLink.setAttribute('href', `data:text/plain;charset=utf-8,${transcript}`)
106
+ }
107
+
108
+ const handleKeyPress = e => {
109
+ if (e.key === 'Enter' && !e.shiftKey) {
110
+ switch (e.target.id) {
111
+ case 'text-area':
112
+ e.preventDefault()
113
+ sendMessage()
114
+ break
115
+ case 'end-chat':
116
+ onEndChatScreen(e)
117
+ break
118
+ case 'wc-settings':
119
+ onSettingsScreen(e)
120
+ break
121
+ case 'transcript-download':
122
+ saveChat()
123
+ break
124
+ default:
125
+ break
126
+ }
127
+ }
128
+ if (e.key === ' ') {
129
+ if (e.target.id === 'end-chat') {
130
+ onEndChatScreen(e)
131
+ }
132
+ if (e.target.id === 'wc-settings') {
133
+ onSettingsScreen(e)
134
+ }
135
+ }
136
+ }
137
+
138
+ return (
139
+ <>
140
+ <PanelHeader />
141
+
142
+ <div className='wc-status'>
143
+ <p className='wc-status__availability'>{connectionHeadlineText}</p>
144
+ <a
145
+ id='end-chat'
146
+ className='wc-status__link'
147
+ href='#'
148
+ data-module='govuk-button'
149
+ role='button'
150
+ onClick={onEndChatScreen}
151
+ onKeyDown={handleKeyPress}
152
+ >
153
+ End chat
154
+ </a>
155
+ </div>
156
+
157
+ <div className='wc-body' tabIndex='0'>
158
+ <ul className='wc-chat'>
159
+ {messages.length
160
+ ? messages.map((msg, index) => <Message key={msg.id} message={msg} previousMessage={messages[index - 1]} />)
161
+ : null}
162
+ {isAgentTyping
163
+ ? (
164
+ <li className='wc-chat__message outbound'>
165
+ <div className='wc-chat__from'>{agentName} is typing</div>
166
+ <div className='wc-chat__text outbound'>
167
+ <svg width='28' height='16' x='0px' y='0px' viewBox='0 0 28 16'>
168
+ <circle stroke='none' cx='3' cy='8' r='3' fill='currentColor' />
169
+ <circle stroke='none' cx='14' cy='8' r='3' fill='currentColor' />
170
+ <circle stroke='none' cx='25' cy='8' r='3' fill='currentColor' />
171
+ </svg>
172
+ </div>
173
+ </li>
174
+ )
175
+ : null}
176
+ </ul>
177
+ </div>
178
+
179
+ <PanelFooter>
180
+ <form className={`wc-form${focusVisibleWithin ? ' wc-focus-within' : ''}`} noValidate onSubmit={handleSubmit}>
181
+ <label className='govuk-label wc-form__label' htmlFor='wc-form-textarea'>
182
+ Your message<span className='govuk-visually-hidden'> (enter key submits)</span>
183
+ </label>
184
+
185
+ <textarea
186
+ ref={messageRef}
187
+ rows='1'
188
+ aria-required='true'
189
+ className='wc-form__textarea'
190
+ id='text-area'
191
+ name='message'
192
+ onChange={onChange}
193
+ onKeyDown={handleKeyPress}
194
+ onFocus={() => { setFocusVisibleWithin(isKeyboard) }}
195
+ onBlur={() => { setFocusVisibleWithin(false) }}
196
+ value={userMessage}
197
+ />
198
+
199
+ <input
200
+ type='submit'
201
+ className='wc-form__button govuk-button'
202
+ value='Send'
203
+ data-prevent-double-click='true'
204
+ />
205
+ </form>
206
+
207
+ <div className='wc-footer__settings'>
208
+ <a
209
+ href='#'
210
+ id='wc-settings'
211
+ className='wc-footer__settings-link'
212
+ data-module='govuk-button'
213
+ onKeyDown={handleKeyPress}
214
+ onClick={onSettingsScreen}
215
+ >
216
+ Settings
217
+ </a>
218
+ <a
219
+ href='#'
220
+ id='transcript-download'
221
+ className='wc-footer__settings-link'
222
+ data-module='govuk-button'
223
+ download='floodline-webchat-transcript.txt'
224
+ onKeyDown={handleKeyPress}
225
+ onClick={saveChat}
226
+ >
227
+ Save chat
228
+ </a>
229
+ </div>
230
+ </PanelFooter>
231
+ </>
232
+ )
233
+ }
@@ -0,0 +1,259 @@
1
+ @keyframes blink {
2
+ 50% {
3
+ fill: transparent
4
+ }
5
+ }
6
+
7
+ .wc-status {
8
+ display: flex;
9
+ position: relative;
10
+ padding: 15px;
11
+ @include mq ($from: tablet) {
12
+ padding: 10px;
13
+ }
14
+
15
+ &::after {
16
+ position: absolute;
17
+ content: "";
18
+ bottom: 0;
19
+ left: 15px;
20
+ right: 15px;
21
+ height: 0;
22
+ border-bottom: 1px solid $govuk-border-colour;
23
+ @include mq ($from: tablet) {
24
+ left: 10px;
25
+ right: 10px;
26
+ }
27
+ }
28
+
29
+ &__availability {
30
+ font-size: 16px;
31
+ margin-bottom: 0;
32
+ }
33
+
34
+ &__link{
35
+ @extend %wc-link-button;
36
+ margin-left: auto;
37
+
38
+ &:link {
39
+ color: govuk-colour('black');
40
+ }
41
+ }
42
+ }
43
+
44
+ .wc-body[tabindex="0"]:focus-visible {
45
+ outline: 3px solid transparent;
46
+ outline-offset: -2px;
47
+ box-shadow:
48
+ inset 0 0 0 2px $govuk-focus-colour,
49
+ inset 0 0 0 4px govuk-colour('black'),
50
+ 0 0 0 1px $govuk-focus-colour;
51
+ }
52
+
53
+ .wc-chat {
54
+ @extend %govuk-body-s;
55
+ line-height: 1.25;
56
+ font-size: 14px;
57
+ overflow: auto;
58
+ list-style: none;
59
+ padding: 0;
60
+
61
+ &__message {
62
+ margin: 5px 15px 0;
63
+ max-width: 80%;
64
+ width: fit-content;
65
+
66
+ &.inbound {
67
+ margin-left: auto;
68
+ }
69
+
70
+ @include mq ($from: tablet) {
71
+ margin-left: 10px;
72
+ margin-right: 10px;
73
+ }
74
+
75
+ &:first-child {
76
+ padding-top: 15px;
77
+ }
78
+ }
79
+
80
+ &__from {
81
+ line-height: 1.25;
82
+ color: govuk-colour('dark-grey');
83
+ margin-top: 2px;
84
+ margin-bottom: 2px;
85
+
86
+ .inbound {
87
+ display: block;
88
+ text-align: right;
89
+ }
90
+ }
91
+
92
+ &__text {
93
+ font-size: 16px;
94
+ padding: 10px 12.5px;
95
+ margin-bottom: 0;
96
+ display: inline-block;
97
+ border: 1px solid transparent;
98
+ overflow-wrap: anywhere;
99
+
100
+ @media (forced-colors: active) {
101
+ border-color: currentColor;
102
+ }
103
+
104
+ &.inbound {
105
+ color: govuk-colour('white');
106
+ background-color: govuk-colour('blue');
107
+ border-radius: 10px 0 10px 10px;
108
+
109
+ .govuk-link:link,
110
+ .govuk-link:visited {
111
+ color: govuk-colour('white');
112
+ }
113
+
114
+ .govuk-link:focus {
115
+ color: govuk-colour('black');
116
+ }
117
+ }
118
+
119
+ &.outbound {
120
+ background-color: govuk-colour('light-grey');
121
+ border-radius: 0 10px 10px;
122
+ }
123
+
124
+ svg {
125
+ display: block;
126
+ color: govuk-colour('dark-grey');
127
+ }
128
+
129
+ @media (forced-colors: active) {
130
+ svg {
131
+ color: currentColor;
132
+ }
133
+ }
134
+
135
+
136
+ svg circle {
137
+ animation: 1s blink infinite;
138
+ fill: currentColor;
139
+
140
+ &:nth-child(2) {
141
+ animation-delay: 250ms
142
+ }
143
+
144
+ &:nth-child(3) {
145
+ animation-delay: 500ms
146
+ }
147
+ }
148
+ }
149
+ }
150
+
151
+ .wc-form {
152
+ position: relative;
153
+ display: flex;
154
+ flex-wrap: nowrap;
155
+ align-items: baseline;
156
+ border-top: 1px solid govuk-colour('black');
157
+ border-bottom: 1px solid govuk-colour('black');
158
+
159
+ &__label {
160
+ position: absolute;
161
+ pointer-events: none;
162
+ font-size: 16px;
163
+ line-height: 20px;
164
+ margin: 20px;
165
+ @include mq ($from: tablet) {
166
+ margin: 18px 18px 19px;
167
+ }
168
+ left: 0;
169
+ bottom: 0;
170
+ color: $govuk-secondary-text-colour;
171
+ z-index: 3;
172
+ }
173
+
174
+ &__textarea {
175
+ @extend %govuk-body-s;
176
+ box-sizing: border-box;
177
+ min-height: auto;
178
+ font-size: 16px;
179
+ line-height: 20px;
180
+ max-height: 120px;
181
+ flex: 1 1 auto;
182
+ align-self: flex-end;
183
+ margin: 20px;
184
+ @include mq ($from: tablet) {
185
+ margin: 18px 18px 19px;
186
+ }
187
+ padding: 0;
188
+ border: 0;
189
+ height: 20px;
190
+ resize: none;
191
+ overflow-x: hidden;
192
+ overscroll-behavior: contain;
193
+
194
+ &:focus {
195
+ outline: none;
196
+ box-shadow: none;
197
+ z-index: 2;
198
+ }
199
+ }
200
+
201
+ &__button {
202
+ margin: 10px 15px 12px 0;
203
+ @include mq ($from: tablet) {
204
+ margin-right: 10px;
205
+ }
206
+ font-size: 16px;
207
+ position: relative;
208
+ flex: 0 0 auto;
209
+ align-self: flex-end;
210
+ display: flex;
211
+ flex-direction: row;
212
+ flex-wrap: nowrap;
213
+ align-items: center;
214
+ width: auto;
215
+ cursor: pointer;
216
+ }
217
+
218
+ &.wc-focus-within::after {
219
+ @include focus($glow: 5px, $strong: 2px, $background: 0, $inset: 0);
220
+ left: 4px;
221
+ right: 4px;
222
+ top: 4px;
223
+ bottom: 4px;
224
+ }
225
+ }
226
+
227
+ .wc-body:focus-visible + .wc-footer .wc-form {
228
+ border-top: 1px solid transparent;
229
+ }
230
+
231
+ .wc-footer {
232
+ position: relative;
233
+ flex: 0 0 0;
234
+
235
+ &__settings {
236
+ @include govuk-responsive-padding(2);
237
+
238
+ display: flex;
239
+ flex-direction: row;
240
+
241
+ &-link {
242
+ @extend %wc-link-button;
243
+
244
+ &:first-child {
245
+ margin-right: govuk-spacing(3);
246
+ }
247
+
248
+ &:link {
249
+ color: govuk-colour('black');
250
+ }
251
+ }
252
+ }
253
+
254
+ &__input {
255
+ @include govuk-responsive-padding(2);
256
+
257
+ border-top: 1px solid govuk-colour('black');
258
+ }
259
+ }