@seamly/web-ui 22.1.0 → 22.3.0-beta.1

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 (66) hide show
  1. package/build/dist/lib/components.js +698 -318
  2. package/build/dist/lib/components.js.map +1 -1
  3. package/build/dist/lib/components.min.js +1 -1
  4. package/build/dist/lib/components.min.js.LICENSE.txt +2 -2
  5. package/build/dist/lib/components.min.js.map +1 -1
  6. package/build/dist/lib/hooks.js +301 -60
  7. package/build/dist/lib/hooks.js.map +1 -1
  8. package/build/dist/lib/hooks.min.js +1 -1
  9. package/build/dist/lib/hooks.min.js.map +1 -1
  10. package/build/dist/lib/index.debug.js +80 -58
  11. package/build/dist/lib/index.debug.js.map +1 -1
  12. package/build/dist/lib/index.debug.min.js +1 -1
  13. package/build/dist/lib/index.debug.min.js.LICENSE.txt +12 -4
  14. package/build/dist/lib/index.debug.min.js.map +1 -1
  15. package/build/dist/lib/index.js +718 -325
  16. package/build/dist/lib/index.js.map +1 -1
  17. package/build/dist/lib/index.min.js +1 -1
  18. package/build/dist/lib/index.min.js.LICENSE.txt +2 -2
  19. package/build/dist/lib/index.min.js.map +1 -1
  20. package/build/dist/lib/standalone.js +803 -348
  21. package/build/dist/lib/standalone.js.map +1 -1
  22. package/build/dist/lib/standalone.min.js +1 -1
  23. package/build/dist/lib/standalone.min.js.LICENSE.txt +1 -1
  24. package/build/dist/lib/standalone.min.js.map +1 -1
  25. package/build/dist/lib/style-guide.js +830 -323
  26. package/build/dist/lib/style-guide.js.map +1 -1
  27. package/build/dist/lib/style-guide.min.js +1 -1
  28. package/build/dist/lib/style-guide.min.js.LICENSE.txt +2 -2
  29. package/build/dist/lib/style-guide.min.js.map +1 -1
  30. package/build/dist/lib/styles-default-implementation.js +1 -1
  31. package/build/dist/lib/styles.css +1 -1
  32. package/build/dist/lib/styles.js +1 -1
  33. package/build/dist/lib/utils.js +783 -360
  34. package/build/dist/lib/utils.js.map +1 -1
  35. package/build/dist/lib/utils.min.js +1 -1
  36. package/build/dist/lib/utils.min.js.LICENSE.txt +1 -1
  37. package/build/dist/lib/utils.min.js.map +1 -1
  38. package/package.json +28 -28
  39. package/src/javascripts/api/errors/seamly-api-error.ts +0 -1
  40. package/src/javascripts/api/index.ts +29 -9
  41. package/src/javascripts/domains/app/actions.ts +8 -3
  42. package/src/javascripts/domains/config/slice.ts +2 -1
  43. package/src/javascripts/domains/forms/selectors.ts +6 -8
  44. package/src/javascripts/domains/forms/slice.ts +1 -1
  45. package/src/javascripts/domains/interrupt/selectors.ts +3 -2
  46. package/src/javascripts/domains/interrupt/slice.ts +2 -0
  47. package/src/javascripts/domains/redux/create-debounced-async-thunk.ts +109 -0
  48. package/src/javascripts/domains/redux/redux.types.ts +2 -1
  49. package/src/javascripts/domains/store/actions.ts +38 -0
  50. package/src/javascripts/domains/translations/components/options-dialog/translation-option.tsx +3 -1
  51. package/src/javascripts/domains/translations/components/options-dialog/translation-options.tsx +62 -35
  52. package/src/javascripts/domains/translations/slice.ts +8 -1
  53. package/src/javascripts/domains/visibility/actions.ts +4 -1
  54. package/src/javascripts/lib/engine/index.tsx +3 -1
  55. package/src/javascripts/style-guide/states.js +65 -1
  56. package/src/javascripts/ui/components/conversation/event/{card-component.js → card-component.tsx} +6 -4
  57. package/src/javascripts/ui/components/conversation/event/event-participant.js +1 -1
  58. package/src/javascripts/ui/components/core/seamly-event-subscriber.ts +14 -30
  59. package/src/javascripts/ui/components/entry/text-entry/hooks.ts +2 -2
  60. package/src/javascripts/ui/components/form-controls/wrapper.tsx +13 -3
  61. package/src/javascripts/ui/components/view/window-view/window-open-button.js +8 -3
  62. package/src/javascripts/ui/hooks/use-session-expired-command.ts +31 -2
  63. package/src/stylesheets/5-components/_input.scss +0 -5
  64. package/src/stylesheets/5-components/_message-count.scss +11 -9
  65. package/src/stylesheets/5-components/_options.scss +2 -2
  66. package/src/stylesheets/5-components/_translation-options.scss +23 -3
@@ -100,7 +100,14 @@ export const translationSlice = createSlice({
100
100
  if (!feature) return
101
101
 
102
102
  state.isAvailable = feature.enabled === true
103
- state.languages = feature.languages
103
+ state.languages = [...feature.languages].sort((a, b) => {
104
+ if (a.locale === payload.locale) return -1
105
+ if (b.locale === payload.locale) return 1
106
+
107
+ return a.nativeName.localeCompare(b.nativeName, undefined, {
108
+ sensitivity: 'base',
109
+ })
110
+ })
104
111
  })
105
112
  .addCase(setHistory, (state, { payload }) => {
106
113
  state.translationProposal = payload.translationProposal
@@ -44,9 +44,12 @@ export const setVisibility = createAsyncThunk<
44
44
  if (previousVisibility === calculatedVisibility) {
45
45
  return undefined
46
46
  }
47
+
48
+ const visibility = api.store.get(StoreKey) as object | undefined
49
+
47
50
  // Store the user-requested visibility in order to reinitialize after refresh
48
51
  api.store.set(StoreKey, {
49
- ...(api.store.get(StoreKey) || {}),
52
+ ...(visibility || {}),
50
53
  [layoutMode]: requestedVisibility,
51
54
  })
52
55
  if (requestedVisibility) {
@@ -85,7 +85,9 @@ export default class Engine {
85
85
  await store.dispatch(initializeConfig())
86
86
  try {
87
87
  const { locale } = await store.dispatch(initializeApp()).unwrap()
88
- await store.dispatch(setLocale(locale))
88
+ if (locale) {
89
+ await store.dispatch(setLocale(locale))
90
+ }
89
91
  } catch (rejectedValueOrSerializedError) {
90
92
  // nothing to do
91
93
  }
@@ -941,6 +941,23 @@ const cardTopic = {
941
941
  },
942
942
  },
943
943
  }
944
+ const cardNoImage = {
945
+ type: 'message',
946
+ payload: {
947
+ type: 'card',
948
+ id: randomId(),
949
+ body: {
950
+ action: {
951
+ ask: '',
952
+ type: 'ask',
953
+ },
954
+ buttonText: 'Ask about pizzas!',
955
+ description:
956
+ 'Pizza Margherita is a <strong>typical Neapolitan pizza</strong>.\n\nIt is made with San Marzano tomatoes, mozzarella cheese, fresh basil, salt, and extra-virgin olive oil.',
957
+ title: 'Pizza Margherita',
958
+ },
959
+ },
960
+ }
944
961
 
945
962
  const standardState = {
946
963
  base: {
@@ -1242,7 +1259,7 @@ const standardState = {
1242
1259
  serviceInfo: {
1243
1260
  activeServiceSessionId: '3942159e-9878-469e-9120-f44fd6be0f35',
1244
1261
  },
1245
- events: [cardAskText, cardNavigate, cardTopic],
1262
+ events: [cardAskText, cardNavigate, cardTopic, cardNoImage],
1246
1263
  },
1247
1264
  carousel: {
1248
1265
  category: categoryKeys.messages,
@@ -1644,6 +1661,53 @@ const standardState = {
1644
1661
  ],
1645
1662
  },
1646
1663
  },
1664
+ translationsActiveLarge: {
1665
+ category: categoryKeys.translations,
1666
+ headingText: 'Show translations active (large list)',
1667
+ description: '',
1668
+ ...baseState,
1669
+ config: {
1670
+ ...baseState.config,
1671
+ context: {
1672
+ ...baseState.context,
1673
+ locale: 'nl',
1674
+ },
1675
+ },
1676
+ translations: {
1677
+ ...translationsSlice,
1678
+ currentLocale: 'lv',
1679
+ isActive: true,
1680
+ isAvailable: true,
1681
+ languages: [
1682
+ { locale: 'nl', nativeName: 'Dutch' },
1683
+ { locale: 'en', nativeName: 'English' },
1684
+ { locale: 'ar', nativeName: 'Arabic' },
1685
+ { locale: 'bg', nativeName: 'Bulgarian' },
1686
+ { locale: 'zh', nativeName: 'Chinese' },
1687
+ { locale: 'cs', nativeName: 'Czech' },
1688
+ { locale: 'da', nativeName: 'Danish' },
1689
+ { locale: 'et', nativeName: 'Estonian' },
1690
+ { locale: 'fi', nativeName: 'Finnish' },
1691
+ { locale: 'fr', nativeName: 'French' },
1692
+ { locale: 'de-informal', nativeName: 'German' },
1693
+ { locale: 'el', nativeName: 'Greek' },
1694
+ { locale: 'hu', nativeName: 'Hungarian' },
1695
+ { locale: 'it', nativeName: 'Italian' },
1696
+ { locale: 'ja', nativeName: 'Japanese' },
1697
+ { locale: 'lv', nativeName: 'Latvian' },
1698
+ { locale: 'pl', nativeName: 'Polish' },
1699
+ { locale: 'ro', nativeName: 'Romanian' },
1700
+ { locale: 'ru', nativeName: 'Russian' },
1701
+ { locale: 'sk', nativeName: 'Slovak' },
1702
+ { locale: 'sl', nativeName: 'Slovenian' },
1703
+ { locale: 'es-informal', nativeName: 'Spanish' },
1704
+ { locale: 'sv', nativeName: 'Swedish' },
1705
+ { locale: 'ti', nativeName: 'Tigrinya' },
1706
+ { locale: 'tr', nativeName: 'Turkish' },
1707
+ { locale: 'uk', nativeName: 'Ukrainian' },
1708
+ ],
1709
+ },
1710
+ },
1647
1711
  translationsFullConversation: {
1648
1712
  category: categoryKeys.translations,
1649
1713
  headingText: 'Show translated messages',
@@ -16,7 +16,7 @@ const CardComponent = ({
16
16
  const cardRef = useRef(null)
17
17
  const { sendMessage, sendAction, emitEvent } = useSeamlyCommands()
18
18
  const descriptionId = useGeneratedId()
19
- const isMounted = useRef()
19
+ const isMounted = useRef(false)
20
20
 
21
21
  const CardActionComponent =
22
22
  action.type === cardTypes.navigate ? 'a' : 'button'
@@ -74,10 +74,12 @@ const CardComponent = ({
74
74
  <div
75
75
  className={className('card__wrapper')}
76
76
  id={id}
77
- tabIndex="-1" // set tabIndex of -1 so card can be focussed
77
+ tabIndex={-1} // set tabIndex of -1 so card can be focussed
78
78
  ref={cardRef}
79
79
  >
80
- <img className={className('card__image')} src={image} alt="" />
80
+ {image ? (
81
+ <img className={className('card__image')} src={image} alt="" />
82
+ ) : null}
81
83
  <div className={className('card__content')} id={id}>
82
84
  {title && <h2 className={className('card__title')}>{title}</h2>}
83
85
  {description && (
@@ -87,7 +89,7 @@ const CardComponent = ({
87
89
  />
88
90
  )}
89
91
  <CardActionComponent
90
- tabIndex={isCarouselItem && !hasFocus ? '-1' : undefined} // disable to prevent tabbing through cards
92
+ tabIndex={isCarouselItem && !hasFocus ? -1 : undefined} // disable to prevent tabbing through cards
91
93
  className={className('button', 'button--primary')}
92
94
  aria-describedby={descriptionId}
93
95
  {...actionProps}
@@ -28,7 +28,7 @@ const EventParticipant = ({ eventPayload }) => {
28
28
  )
29
29
  }
30
30
 
31
- if (showName) {
31
+ if (showName && participantName) {
32
32
  authorInfo.push(
33
33
  <span className={className('message__author-name')}>
34
34
  {participantName}
@@ -1,11 +1,9 @@
1
1
  import { useContext, useEffect, useRef } from 'preact/hooks'
2
- import { useDispatch } from 'react-redux'
3
2
  import SeamlyGeneralError from 'api/errors/seamly-general-error'
4
3
  import SeamlyOfflineError from 'api/errors/seamly-offline-error'
5
4
  import SeamlySessionExpiredError from 'api/errors/seamly-session-expired-error'
6
5
  import { SeamlyEventBusContext } from 'ui/components/core/seamly-api-context'
7
6
  import {
8
- useEvents,
9
7
  useSeamlyApiContext,
10
8
  useSeamlyCommands,
11
9
  useSeamlyIdleDetachCountdown,
@@ -13,6 +11,8 @@ import {
13
11
  import { featureKeys } from 'ui/utils/seamly-utils'
14
12
  import { setHasResponded } from 'domains/app/slice'
15
13
  import { clearInterrupt, setInterrupt } from 'domains/interrupt/slice'
14
+ import { useAppDispatch } from 'domains/store'
15
+ import { getConversation } from 'domains/store/actions'
16
16
  import {
17
17
  ackEvent,
18
18
  addEvent,
@@ -41,8 +41,7 @@ const SeamlyEventSubscriber = () => {
41
41
  const api = useSeamlyApiContext()
42
42
  const syncChannelRef = useRef<number>()
43
43
  const messageChannelRef = useRef<number>()
44
- const dispatch = useDispatch()
45
- const events = useEvents()
44
+ const dispatch = useAppDispatch()
46
45
  const eventBus = useContext(SeamlyEventBusContext)
47
46
  const prevEmittedEventId = useRef(null)
48
47
  const { initCountdown, endCountdown } = useSeamlyIdleDetachCountdown()
@@ -314,39 +313,24 @@ const SeamlyEventSubscriber = () => {
314
313
 
315
314
  syncChannelRef.current = api.conversation.channel.on(
316
315
  'sync',
317
- (payload) => {
318
- const lastEvent = events[events.length - 1]
319
- const payloadLastEventId = payload?.lastEvent?.id
320
-
321
- if (lastEvent && payloadLastEventId === lastEvent.payload.id) {
322
- return payload
316
+ async (payload: {
317
+ lastEvent: { id: string; occurredAt: number }
318
+ }) => {
319
+ try {
320
+ const history = await dispatch(getConversation(payload)).unwrap()
321
+ if (!history) return
322
+
323
+ dispatch(setHistory(history))
324
+ } catch (_e) {
325
+ // nothing to do, the error is handled in the thunk
323
326
  }
324
-
325
- return api
326
- .getConversation()
327
- .then((history) => {
328
- if (!history) return
329
- dispatch(setHistory(history))
330
- })
331
- .catch((error) => {
332
- dispatch(
333
- setInterrupt({
334
- name: error?.name,
335
- message: error?.message,
336
- langKey: error?.langKey,
337
- action: error?.action,
338
- originalEvent: error?.originalEvent,
339
- originalError: error?.originalError,
340
- }),
341
- )
342
- })
343
327
  },
344
328
  )
345
329
 
346
330
  return true
347
331
  })
348
332
  }
349
- }, [api, api.connectionInfo, api.conversation.channel, events, dispatch])
333
+ }, [api, api.connectionInfo, api.conversation.channel, dispatch])
350
334
 
351
335
  return null
352
336
  }
@@ -82,8 +82,8 @@ export const useEntryTextTranslation = (controlName: ControlState['name']) => {
82
82
  [t, hasCharacterLimit, characterLimit, text?.label],
83
83
  )
84
84
 
85
- const labelClass: string = useMemo(
86
- () => (text?.label ? 'input__label' : 'visually-hidden'),
85
+ const labelClass = useMemo(
86
+ () => (text?.label ? 'label' : 'visually-hidden'),
87
87
  [text?.label],
88
88
  )
89
89
 
@@ -1,11 +1,21 @@
1
+ import { FC } from 'preact/compat'
1
2
  import { className } from 'lib/css'
2
3
  import Error from './error'
3
4
 
4
- const FormControlWrapper = ({
5
+ type FormControlWrapperProps = {
6
+ contentHint: string
7
+ id: string
8
+ labelText: string
9
+ labelClass: string
10
+ validity: boolean
11
+ errorText: unknown
12
+ }
13
+
14
+ const FormControlWrapper: FC<FormControlWrapperProps> = ({
5
15
  contentHint,
6
16
  id,
7
17
  labelText,
8
- labelClass = className('label'),
18
+ labelClass,
9
19
  validity,
10
20
  errorText,
11
21
  children,
@@ -24,7 +34,7 @@ const FormControlWrapper = ({
24
34
  <Error id={`${id}-error`} error={!validity && errorText} />
25
35
 
26
36
  <div className={className('form-control__wrapper')}>
27
- <label htmlFor={id} className={labelClass}>
37
+ <label htmlFor={id} className={className(labelClass)}>
28
38
  {labelText}
29
39
  </label>
30
40
  {children}
@@ -57,9 +57,14 @@ const WindowOpenButton = ({ onClick }) => {
57
57
  aria-hidden={isOpen}
58
58
  onClick={handleClick}
59
59
  >
60
- <span className={className('message-count')} aria-hidden="true">
61
- {!!count && count}
62
- </span>
60
+ <InOutTransition
61
+ isActive={!!count}
62
+ transitionStartState={transitionStartStates.notRendered}
63
+ >
64
+ <span className={className('message-count')} aria-hidden="true">
65
+ {count}
66
+ </span>
67
+ </InOutTransition>
63
68
  <ButtonIcon />
64
69
  </button>
65
70
  </InOutTransition>
@@ -1,17 +1,46 @@
1
- import { useEffect } from 'preact/hooks'
1
+ import { useEffect, useRef } from 'preact/hooks'
2
+ import SeamlyGeneralError from 'api/errors/seamly-general-error'
2
3
  import { useInterrupt } from 'domains/interrupt/hooks'
4
+ import { setInterrupt } from 'domains/interrupt/slice'
5
+ import { useAppDispatch } from 'domains/store'
3
6
  import useSeamlyCommands from './use-seamly-commands'
4
7
 
5
8
  export default function useSessionExpiredCommand() {
6
9
  const {
7
10
  meta: { originalError, action },
8
11
  } = useInterrupt()
12
+ const dispatch = useAppDispatch()
9
13
  const seamlyCommands = useSeamlyCommands()
10
14
  const isExpiredError = originalError?.name === 'SeamlySessionExpiredError'
15
+ const limit = useRef(0)
16
+ const limitTimer = useRef<ReturnType<typeof setTimeout>>(null)
11
17
 
12
18
  useEffect(() => {
13
19
  if (isExpiredError && seamlyCommands[action]) {
20
+ if (limit.current >= 10) {
21
+ limitTimer.current = setTimeout(() => {
22
+ limit.current = 0
23
+ }, 10000)
24
+
25
+ const error = new SeamlyGeneralError()
26
+ dispatch(
27
+ setInterrupt({
28
+ name: error.name,
29
+ message: error.message,
30
+ langKey: error.langKey,
31
+ originalEvent: error.originalEvent,
32
+ originalError: error.originalError,
33
+ action: error.action,
34
+ }),
35
+ )
36
+ return () => {}
37
+ }
38
+ limit.current += 1
14
39
  seamlyCommands[action]()
15
40
  }
16
- }, [action, seamlyCommands, isExpiredError])
41
+
42
+ return () => {
43
+ if (limitTimer.current) clearTimeout(limitTimer.current)
44
+ }
45
+ }, [action, seamlyCommands, isExpiredError, dispatch])
17
46
  }
@@ -25,11 +25,6 @@
25
25
  width: 100%;
26
26
  }
27
27
 
28
- .#{$n}-input__label {
29
- font-size: $fontsize-medium;
30
- font-weight: $fontweight-bold;
31
- }
32
-
33
28
  .#{$n}-input__text {
34
29
  appearance: none;
35
30
  flex-grow: 4;
@@ -1,5 +1,5 @@
1
1
  .#{$n}-message-count {
2
- display: flex;
2
+ display: none;
3
3
  position: absolute;
4
4
  z-index: 1;
5
5
  top: $spacer * -0.5;
@@ -8,21 +8,23 @@
8
8
  justify-content: center;
9
9
  width: $messagecountsize;
10
10
  height: $messagecountsize;
11
- transform: scale(1);
11
+ transform: scale(0);
12
12
  transform-origin: 50% 50%;
13
- transition: transform 0.3s 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275),
14
- opacity $transition;
13
+ transition: transform $transition, opacity $transition;
15
14
  border-radius: 50%;
16
- opacity: 1;
15
+ opacity: 0;
17
16
  background-color: $negative;
18
17
  color: $white;
19
18
  font-size: $fontsize-small;
20
19
  font-weight: $fontweight-bold;
21
20
  line-height: 1;
22
21
 
23
- &:empty {
24
- transform: scale(0);
25
- transition: transform $transition, opacity $transition;
26
- opacity: 0;
22
+ &.#{$n}-transition--visible {
23
+ display: flex;
24
+ }
25
+
26
+ &.#{$n}-transition--in {
27
+ transform: scale(1);
28
+ opacity: 1;
27
29
  }
28
30
  }
@@ -215,9 +215,9 @@
215
215
  &:last-child::after {
216
216
  content: '';
217
217
  display: block;
218
- flex: 0 0 $spacer;
218
+ flex: 0 0 $spacer * 0.5;
219
219
  width: 100%;
220
- height: $spacer;
220
+ height: $spacer * 0.5;
221
221
  }
222
222
  }
223
223
 
@@ -1,7 +1,6 @@
1
1
  .#{$n}-translation-options {
2
2
  display: flex;
3
3
  flex-direction: column;
4
- gap: $spacer * 0.25;
5
4
  width: 100%;
6
5
  margin: 0;
7
6
 
@@ -10,12 +9,33 @@
10
9
  list-style: none;
11
10
  }
12
11
 
12
+ .#{$n}-translation-options__item--selected {
13
+ margin-bottom: $spacer * 0.5;
14
+
15
+ &::after {
16
+ content: '';
17
+ display: block;
18
+ position: absolute;
19
+ bottom: $spacer * -0.5;
20
+ flex: 0 0 100%;
21
+ width: 100%;
22
+ height: 1px;
23
+ border-bottom: 1px solid $grey-b;
24
+ }
25
+
26
+ + .#{$n}-translation-options__item {
27
+ margin-top: $spacer * 0.5;
28
+ }
29
+ }
30
+
13
31
  .#{$n}-translation-options__item {
14
32
  display: flex;
33
+ position: relative;
34
+ flex-flow: row wrap;
15
35
  align-items: center;
16
36
  gap: $spacer * 0.5;
17
37
  width: 100%;
18
- padding: calc($spacer * 0.25) 0;
38
+ padding: $spacer * 0.5 0;
19
39
  color: $brand3;
20
40
  font-size: $fontsize-small;
21
41
  font-weight: $fontweight-bold;
@@ -25,7 +45,7 @@
25
45
  outline: -webkit-focus-ring-color auto 1px;
26
46
  }
27
47
 
28
- > span {
48
+ span {
29
49
  margin-left: -0.5em;
30
50
  }
31
51