@telefonica/mistica 16.62.0-beta.1 → 16.63.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 (217) hide show
  1. package/css/mistica.css +1 -1
  2. package/dist/accordion.css-mistica.js +16 -16
  3. package/dist/align.css-mistica.js +2 -2
  4. package/dist/autocomplete.css-mistica.js +6 -6
  5. package/dist/avatar.css-mistica.js +3 -3
  6. package/dist/badge.css-mistica.js +7 -7
  7. package/dist/box.css-mistica.js +15 -15
  8. package/dist/boxed.css-mistica.js +31 -31
  9. package/dist/button-group.css-mistica.js +10 -10
  10. package/dist/button-layout.css-mistica.js +21 -21
  11. package/dist/button.css-mistica.js +51 -51
  12. package/dist/callout.css-mistica.js +16 -16
  13. package/dist/card-internal.css-mistica.js +38 -38
  14. package/dist/carousel.css-mistica.js +18 -18
  15. package/dist/checkbox.css-mistica.js +21 -21
  16. package/dist/chip.css-mistica.js +30 -30
  17. package/dist/circle.css-mistica.js +2 -2
  18. package/dist/community/advanced-data-card.css-mistica.js +26 -26
  19. package/dist/community/ai-card.css-mistica.js +48 -0
  20. package/dist/community/ai-card.css.d.ts +13 -0
  21. package/dist/community/ai-card.d.ts +19 -0
  22. package/dist/community/ai-card.js +312 -0
  23. package/dist/community/blocks.css-mistica.js +3 -3
  24. package/dist/community/example-component.css-mistica.js +2 -2
  25. package/dist/community/index.d.ts +1 -0
  26. package/dist/community/index.js +4 -0
  27. package/dist/counter.css-mistica.js +12 -12
  28. package/dist/cover-hero.css-mistica.js +16 -16
  29. package/dist/credit-card-number-field.css-mistica.js +6 -6
  30. package/dist/date-field.css-mistica.js +1 -1
  31. package/dist/date-time-picker.css-mistica.js +2 -2
  32. package/dist/dialog.css-mistica.js +15 -15
  33. package/dist/divider.css-mistica.js +5 -5
  34. package/dist/double-field.css-mistica.js +4 -4
  35. package/dist/drawer.css-mistica.js +15 -15
  36. package/dist/empty-state-card.css-mistica.js +4 -4
  37. package/dist/empty-state.css-mistica.js +14 -14
  38. package/dist/fade-in.css-mistica.js +1 -1
  39. package/dist/feedback.css-mistica.js +14 -14
  40. package/dist/file-upload.css-mistica.js +14 -14
  41. package/dist/fixed-footer-layout.css-mistica.js +12 -12
  42. package/dist/form.css-mistica.js +2 -2
  43. package/dist/generated/mistica-icons/icon-artificial-intelligence-filled.js +7 -7
  44. package/dist/generated/mistica-icons/icon-artificial-intelligence-light.js +9 -9
  45. package/dist/generated/mistica-icons/icon-artificial-intelligence-regular.js +12 -12
  46. package/dist/grid-layout.css-mistica.js +9 -9
  47. package/dist/grid.css-mistica.js +147 -147
  48. package/dist/header.css-mistica.js +5 -5
  49. package/dist/hero.css-mistica.js +11 -11
  50. package/dist/horizontal-scroll.css-mistica.js +3 -3
  51. package/dist/icon-button.css-mistica.js +66 -66
  52. package/dist/icons/icon-chevron.css-mistica.js +6 -6
  53. package/dist/icons/icon-error.css-mistica.js +3 -3
  54. package/dist/image.css-mistica.js +11 -11
  55. package/dist/index.d.ts +1 -0
  56. package/dist/index.js +4 -0
  57. package/dist/inline.css-mistica.js +16 -16
  58. package/dist/list.css-mistica.js +15 -15
  59. package/dist/loading-bar.css-mistica.js +5 -5
  60. package/dist/loading-screen.css-mistica.js +15 -15
  61. package/dist/logo.css-mistica.js +9 -9
  62. package/dist/menu.css-mistica.js +27 -24
  63. package/dist/menu.css.d.ts +1 -0
  64. package/dist/menu.d.ts +26 -7
  65. package/dist/menu.js +170 -143
  66. package/dist/mosaic.css-mistica.js +3 -3
  67. package/dist/navigation-bar.css-mistica.js +45 -45
  68. package/dist/navigation-breadcrumbs.css-mistica.js +5 -5
  69. package/dist/package-version.js +2 -2
  70. package/dist/pin-field.css-mistica.js +10 -10
  71. package/dist/popover.css-mistica.js +2 -2
  72. package/dist/progress-bar.css-mistica.js +12 -12
  73. package/dist/radio-button.css-mistica.js +33 -33
  74. package/dist/rating.css-mistica.js +12 -12
  75. package/dist/responsive-layout.css-mistica.js +20 -20
  76. package/dist/screen-reader-only.css-mistica.js +2 -2
  77. package/dist/select.css-mistica.js +29 -29
  78. package/dist/sheet-action-row.css-mistica.js +2 -2
  79. package/dist/sheet-common.css-mistica.js +16 -16
  80. package/dist/sheet-info.css-mistica.js +4 -4
  81. package/dist/skeletons.css-mistica.js +12 -12
  82. package/dist/skins/skin-contract.css-mistica.js +686 -686
  83. package/dist/skip-link.css-mistica.js +3 -3
  84. package/dist/slider.css-mistica.js +28 -28
  85. package/dist/snackbar.css-mistica.js +16 -16
  86. package/dist/spinner.css-mistica.js +5 -5
  87. package/dist/square.css-mistica.js +2 -2
  88. package/dist/stack.css-mistica.js +9 -9
  89. package/dist/stacking-group.css-mistica.js +2 -2
  90. package/dist/stepper.css-mistica.js +16 -16
  91. package/dist/switch-component.css-mistica.js +53 -53
  92. package/dist/table.css-mistica.js +24 -24
  93. package/dist/tabs.css-mistica.js +30 -30
  94. package/dist/tag.css-mistica.js +5 -5
  95. package/dist/text-field-base.css-mistica.js +30 -30
  96. package/dist/text-field-components.css-mistica.js +19 -19
  97. package/dist/text-link.css-mistica.js +10 -10
  98. package/dist/text.css-mistica.js +13 -13
  99. package/dist/theme-context.css-mistica.js +2 -2
  100. package/dist/timeline.css-mistica.js +18 -18
  101. package/dist/timer.css-mistica.js +13 -13
  102. package/dist/tooltip.css-mistica.js +12 -12
  103. package/dist/touchable.css-mistica.js +5 -5
  104. package/dist/utils/aspect-ratio-support.css-mistica.js +7 -7
  105. package/dist/video.css-mistica.js +2 -2
  106. package/dist/vivinho-loading-animation/vivinho-loading-animation.css-mistica.js +3 -3
  107. package/dist-es/accordion.css-mistica.js +7 -7
  108. package/dist-es/align.css-mistica.js +2 -2
  109. package/dist-es/autocomplete.css-mistica.js +2 -2
  110. package/dist-es/avatar.css-mistica.js +2 -2
  111. package/dist-es/badge.css-mistica.js +2 -2
  112. package/dist-es/box.css-mistica.js +15 -15
  113. package/dist-es/boxed.css-mistica.js +25 -25
  114. package/dist-es/button-group.css-mistica.js +2 -2
  115. package/dist-es/button-layout.css-mistica.js +16 -16
  116. package/dist-es/button.css-mistica.js +38 -38
  117. package/dist-es/callout.css-mistica.js +12 -12
  118. package/dist-es/card-internal.css-mistica.js +20 -20
  119. package/dist-es/carousel.css-mistica.js +10 -10
  120. package/dist-es/checkbox.css-mistica.js +14 -14
  121. package/dist-es/chip.css-mistica.js +17 -17
  122. package/dist-es/circle.css-mistica.js +2 -2
  123. package/dist-es/community/advanced-data-card.css-mistica.js +7 -7
  124. package/dist-es/community/ai-card.css-mistica.js +4 -0
  125. package/dist-es/community/ai-card.js +257 -0
  126. package/dist-es/community/blocks.css-mistica.js +2 -2
  127. package/dist-es/community/example-component.css-mistica.js +2 -2
  128. package/dist-es/community/index.js +5 -4
  129. package/dist-es/counter.css-mistica.js +2 -2
  130. package/dist-es/cover-hero.css-mistica.js +4 -4
  131. package/dist-es/credit-card-number-field.css-mistica.js +4 -4
  132. package/dist-es/date-field.css-mistica.js +1 -1
  133. package/dist-es/date-time-picker.css-mistica.js +2 -2
  134. package/dist-es/dialog.css-mistica.js +5 -5
  135. package/dist-es/divider.css-mistica.js +5 -5
  136. package/dist-es/double-field.css-mistica.js +4 -4
  137. package/dist-es/drawer.css-mistica.js +2 -2
  138. package/dist-es/empty-state-card.css-mistica.js +2 -2
  139. package/dist-es/empty-state.css-mistica.js +7 -7
  140. package/dist-es/fade-in.css-mistica.js +1 -1
  141. package/dist-es/feedback.css-mistica.js +2 -2
  142. package/dist-es/file-upload.css-mistica.js +8 -8
  143. package/dist-es/fixed-footer-layout.css-mistica.js +4 -4
  144. package/dist-es/form.css-mistica.js +2 -2
  145. package/dist-es/generated/mistica-icons/icon-artificial-intelligence-filled.js +12 -12
  146. package/dist-es/generated/mistica-icons/icon-artificial-intelligence-light.js +15 -15
  147. package/dist-es/generated/mistica-icons/icon-artificial-intelligence-regular.js +17 -17
  148. package/dist-es/grid-layout.css-mistica.js +4 -4
  149. package/dist-es/grid.css-mistica.js +127 -127
  150. package/dist-es/header.css-mistica.js +2 -2
  151. package/dist-es/hero.css-mistica.js +4 -4
  152. package/dist-es/horizontal-scroll.css-mistica.js +2 -2
  153. package/dist-es/icon-button.css-mistica.js +56 -56
  154. package/dist-es/icons/icon-chevron.css-mistica.js +4 -4
  155. package/dist-es/icons/icon-error.css-mistica.js +2 -2
  156. package/dist-es/image.css-mistica.js +4 -4
  157. package/dist-es/index.js +2183 -2182
  158. package/dist-es/inline.css-mistica.js +10 -10
  159. package/dist-es/list.css-mistica.js +2 -2
  160. package/dist-es/loading-bar.css-mistica.js +2 -2
  161. package/dist-es/loading-screen.css-mistica.js +5 -5
  162. package/dist-es/logo.css-mistica.js +7 -7
  163. package/dist-es/menu.css-mistica.js +15 -15
  164. package/dist-es/menu.js +213 -186
  165. package/dist-es/mosaic.css-mistica.js +2 -2
  166. package/dist-es/navigation-bar.css-mistica.js +20 -20
  167. package/dist-es/navigation-breadcrumbs.css-mistica.js +2 -2
  168. package/dist-es/package-version.js +2 -2
  169. package/dist-es/pin-field.css-mistica.js +2 -2
  170. package/dist-es/popover.css-mistica.js +2 -2
  171. package/dist-es/progress-bar.css-mistica.js +8 -8
  172. package/dist-es/radio-button.css-mistica.js +25 -25
  173. package/dist-es/rating.css-mistica.js +4 -4
  174. package/dist-es/responsive-layout.css-mistica.js +7 -7
  175. package/dist-es/screen-reader-only.css-mistica.js +2 -2
  176. package/dist-es/select.css-mistica.js +20 -20
  177. package/dist-es/sheet-action-row.css-mistica.js +2 -2
  178. package/dist-es/sheet-common.css-mistica.js +2 -2
  179. package/dist-es/sheet-info.css-mistica.js +2 -2
  180. package/dist-es/skeletons.css-mistica.js +8 -8
  181. package/dist-es/skins/skin-contract.css-mistica.js +686 -686
  182. package/dist-es/skip-link.css-mistica.js +2 -2
  183. package/dist-es/slider.css-mistica.js +20 -20
  184. package/dist-es/snackbar.css-mistica.js +5 -5
  185. package/dist-es/spinner.css-mistica.js +2 -2
  186. package/dist-es/square.css-mistica.js +2 -2
  187. package/dist-es/stack.css-mistica.js +7 -7
  188. package/dist-es/stacking-group.css-mistica.js +2 -2
  189. package/dist-es/stepper.css-mistica.js +4 -4
  190. package/dist-es/style.css +1 -1
  191. package/dist-es/switch-component.css-mistica.js +41 -41
  192. package/dist-es/table.css-mistica.js +11 -11
  193. package/dist-es/tabs.css-mistica.js +21 -21
  194. package/dist-es/tag.css-mistica.js +2 -2
  195. package/dist-es/text-field-base.css-mistica.js +17 -17
  196. package/dist-es/text-field-components.css-mistica.js +4 -4
  197. package/dist-es/text-link.css-mistica.js +9 -9
  198. package/dist-es/text.css-mistica.js +8 -8
  199. package/dist-es/theme-context.css-mistica.js +2 -2
  200. package/dist-es/timeline.css-mistica.js +11 -11
  201. package/dist-es/timer.css-mistica.js +7 -7
  202. package/dist-es/tooltip.css-mistica.js +3 -3
  203. package/dist-es/touchable.css-mistica.js +2 -2
  204. package/dist-es/utils/aspect-ratio-support.css-mistica.js +4 -4
  205. package/dist-es/video.css-mistica.js +2 -2
  206. package/dist-es/vivinho-loading-animation/vivinho-loading-animation.css-mistica.js +2 -2
  207. package/doc/components.md +4 -4
  208. package/doc/layout.md +1 -0
  209. package/package.json +1 -1
  210. package/src/community/__stories__/ai-card-story.tsx +101 -0
  211. package/src/community/ai-card.css.ts +135 -0
  212. package/src/community/ai-card.tsx +231 -0
  213. package/src/community/index.tsx +1 -0
  214. package/src/index.tsx +1 -0
  215. package/src/menu.css.ts +6 -0
  216. package/src/menu.tsx +99 -30
  217. package/src/package-version.tsx +1 -1
@@ -0,0 +1,135 @@
1
+ import {createVar, globalStyle, style, keyframes} from '@vanilla-extract/css';
2
+ import {sprinkles} from '../sprinkles.css';
3
+ import {vars as skinVars} from '../skins/skin-contract.css';
4
+ import * as mq from '../media-queries.css';
5
+
6
+ const borderColorVar = createVar();
7
+
8
+ export const vars = {borderColorVar};
9
+
10
+ const fill = (color: string) => `linear-gradient(${color}, ${color}) padding-box`;
11
+ const borderLayer = `${borderColorVar} border-box`;
12
+
13
+ const containerBackground = (overlayColor: string) =>
14
+ `${fill(overlayColor)}, ${fill(skinVars.colors.backgroundContainer)}, ${borderLayer}`;
15
+
16
+ export const container = style([
17
+ sprinkles({
18
+ display: 'flex',
19
+ alignItems: 'center',
20
+ padding: 16,
21
+ width: '100%',
22
+ }),
23
+ {
24
+ position: 'relative',
25
+ maxWidth: '100%',
26
+ minWidth: 288,
27
+ minHeight: 64,
28
+ gap: 8,
29
+ borderRadius: skinVars.borderRadii.container,
30
+ border: '1px solid transparent',
31
+ boxSizing: 'border-box',
32
+ textAlign: 'left',
33
+ vars: {
34
+ [borderColorVar]: `linear-gradient(${skinVars.colors.border}, ${skinVars.colors.border})`,
35
+ },
36
+ background: containerBackground('transparent'),
37
+ },
38
+ ]);
39
+
40
+ export const containerInteractive = style({
41
+ transition: 'background 120ms ease',
42
+ ':focus-visible': {
43
+ outline: `2px solid ${skinVars.colors.borderSelected}`,
44
+ outlineOffset: 2,
45
+ },
46
+ ':active': {
47
+ background: containerBackground(skinVars.colors.backgroundContainerPressed),
48
+ },
49
+ '@media': {
50
+ [mq.supportsHover]: {
51
+ ':hover': {
52
+ background: containerBackground(skinVars.colors.backgroundContainerHover),
53
+ },
54
+ ':active': {
55
+ background: containerBackground(skinVars.colors.backgroundContainerPressed),
56
+ },
57
+ },
58
+ [mq.touchableOnly]: {
59
+ transition: 'none',
60
+ },
61
+ },
62
+ });
63
+
64
+ export const textLine = style([
65
+ sprinkles({
66
+ display: 'flex',
67
+ alignItems: 'center',
68
+ }),
69
+ {
70
+ flex: '1 1 auto',
71
+ minWidth: 0,
72
+ maxWidth: '100%',
73
+ },
74
+ ]);
75
+
76
+ export const slot = style([
77
+ sprinkles({
78
+ display: 'flex',
79
+ alignItems: 'center',
80
+ justifyContent: 'center',
81
+ }),
82
+ {
83
+ flex: '0 0 auto',
84
+ lineHeight: 0,
85
+ },
86
+ ]);
87
+
88
+ globalStyle(`${slot} > svg`, {
89
+ display: 'block',
90
+ });
91
+
92
+ export const textWrapper = style({
93
+ display: 'grid',
94
+ width: '100%',
95
+ alignItems: 'center',
96
+ alignContent: 'center',
97
+ });
98
+
99
+ const textContent = style({
100
+ gridArea: '1 / 1',
101
+ whiteSpace: 'normal',
102
+ overflowWrap: 'anywhere',
103
+ wordBreak: 'break-word',
104
+ });
105
+
106
+ export const ghost = style([
107
+ textContent,
108
+ {
109
+ visibility: 'hidden',
110
+ pointerEvents: 'none',
111
+ userSelect: 'none',
112
+ },
113
+ ]);
114
+
115
+ export const visibleContent = textContent;
116
+
117
+ const caretBlink = keyframes({
118
+ '0%': {opacity: 1},
119
+ '50%': {opacity: 0},
120
+ });
121
+
122
+ export const caret = style({
123
+ color: skinVars.colors.textBrand,
124
+ });
125
+
126
+ export const caretHidden = style({
127
+ visibility: 'hidden',
128
+ });
129
+
130
+ export const caretBlinking = style({
131
+ animationName: caretBlink,
132
+ animationDuration: '1060ms',
133
+ animationTimingFunction: 'step-end',
134
+ animationIterationCount: 'infinite',
135
+ });
@@ -0,0 +1,231 @@
1
+ 'use client';
2
+ import * as React from 'react';
3
+ import classnames from 'classnames';
4
+ import {BaseTouchable} from '../touchable';
5
+ import {Text3} from '../text';
6
+ import {vars} from '../skins/skin-contract.css';
7
+ import {useIsInViewport} from '../hooks';
8
+ import {isClientSide} from '../utils/environment';
9
+ import {applyCssVars} from '../utils/css';
10
+ import * as styles from './ai-card.css';
11
+
12
+ import type {TouchableComponentProps} from '../touchable';
13
+
14
+ type CommonProps = {
15
+ /** Static text shown before the animated words. */
16
+ text: string;
17
+ /** Words to animate in sequence. typed, held, then erased one by one. Maximum of 4 words. */
18
+ words?: ReadonlyArray<string>;
19
+ /** Number of characters to keep at the start of each word during deletion. Omit to erase fully. */
20
+ deleteChars?: number;
21
+ /** Wraps the text line after this many characters. */
22
+ lineBreakAtChars?: number;
23
+ /** Border color. Accepts any CSS color or gradient string. Defaults to the skin border token. */
24
+ borderColor?: string;
25
+ /** Icon or element rendered on the left side of the card. */
26
+ asset?: React.ReactElement;
27
+ };
28
+
29
+ export type AiCardProps = TouchableComponentProps<CommonProps>;
30
+
31
+ type AnimationStage = 'typing' | 'holding' | 'deleting' | 'done';
32
+
33
+ const TYPING_SPEEDS = [50, 60, 70, 80];
34
+ const DELETE_SPEED = 30;
35
+ const HOLD_DURATION = 2000;
36
+ const MAX_WORDS = 4;
37
+
38
+ const getRandomTypingSpeed = (previousSpeed?: number) => {
39
+ const candidates = TYPING_SPEEDS.filter((speed) => speed !== previousSpeed);
40
+ return candidates[Math.floor(Math.random() * candidates.length)];
41
+ };
42
+
43
+ const getDeleteFloor = (currentWord: string, nextWord: string, deleteChars?: number) => {
44
+ const sharedStartFloor = currentWord[0]?.toLowerCase() === nextWord[0]?.toLowerCase() ? 1 : 0;
45
+ const configuredDeleteFloor = typeof deleteChars === 'number' ? Math.max(0, Math.floor(deleteChars)) : 0;
46
+ return Math.max(sharedStartFloor, configuredDeleteFloor);
47
+ };
48
+
49
+ const getPrefersReducedMotion = () =>
50
+ isClientSide() && !!window.matchMedia?.('(prefers-reduced-motion: reduce)').matches;
51
+
52
+ type AnimationState = {index: number; text: string; stage: AnimationStage};
53
+
54
+ const INITIAL_STATE: AnimationState = {index: 0, text: '', stage: 'typing'};
55
+
56
+ const tick = (state: AnimationState, words: ReadonlyArray<string>, deleteChars?: number): AnimationState => {
57
+ const {index, text, stage} = state;
58
+ const currentWord = words[index] || '';
59
+ const nextWord = words[index + 1] || '';
60
+ const deleteFloor = getDeleteFloor(currentWord, nextWord, deleteChars);
61
+
62
+ if (stage === 'typing') {
63
+ if (text.length < currentWord.length) return {...state, text: currentWord.slice(0, text.length + 1)};
64
+ return {...state, stage: 'holding'};
65
+ }
66
+ if (stage === 'holding') {
67
+ if (index === words.length - 1) return {...state, stage: 'done'};
68
+ return text.length > deleteFloor
69
+ ? {...state, stage: 'deleting'}
70
+ : {index: index + 1, text, stage: 'typing'};
71
+ }
72
+ if (stage === 'deleting') {
73
+ if (text.length <= deleteFloor) {
74
+ let commonLen = 0;
75
+ while (
76
+ commonLen < text.length &&
77
+ commonLen < nextWord.length &&
78
+ text[commonLen] === nextWord[commonLen]
79
+ ) {
80
+ commonLen++;
81
+ }
82
+ return {index: index + 1, text: text.slice(0, commonLen), stage: 'typing'};
83
+ }
84
+ return {...state, text: text.slice(0, -1)};
85
+ }
86
+ return state;
87
+ };
88
+
89
+ const useAiCardAnimation = ({
90
+ words,
91
+ deleteChars,
92
+ prefersReducedMotion,
93
+ isInViewport,
94
+ }: {
95
+ words: ReadonlyArray<string>;
96
+ deleteChars?: number;
97
+ prefersReducedMotion: boolean;
98
+ isInViewport: boolean;
99
+ }) => {
100
+ const wordsKey = words.join('\0');
101
+ const wordCount = words.length;
102
+ const [state, setState] = React.useState<AnimationState>(INITIAL_STATE);
103
+ const prevSpeedRef = React.useRef<number | undefined>(undefined);
104
+ const wordsRef = React.useRef(words);
105
+ wordsRef.current = words;
106
+
107
+ React.useEffect(() => {
108
+ setState(
109
+ prefersReducedMotion && wordCount
110
+ ? {index: wordCount - 1, text: wordsRef.current.at(-1) || '', stage: 'done'}
111
+ : INITIAL_STATE
112
+ );
113
+ }, [wordsKey, prefersReducedMotion, wordCount]);
114
+
115
+ React.useEffect(() => {
116
+ if (state.stage === 'done' || !isInViewport || prefersReducedMotion || !wordCount) return;
117
+ let speed: number;
118
+ if (state.stage === 'typing') {
119
+ speed = getRandomTypingSpeed(prevSpeedRef.current);
120
+ prevSpeedRef.current = speed;
121
+ } else if (state.stage === 'holding') {
122
+ speed = HOLD_DURATION;
123
+ } else {
124
+ speed = DELETE_SPEED;
125
+ }
126
+ const id = window.setTimeout(() => setState((s) => tick(s, wordsRef.current, deleteChars)), speed);
127
+ return () => window.clearTimeout(id);
128
+ }, [state, isInViewport, prefersReducedMotion, wordCount, deleteChars]);
129
+
130
+ return {
131
+ dynamicText: state.text,
132
+ isDone: state.stage === 'done',
133
+ shouldBlinkCaret: !prefersReducedMotion && !!wordCount && state.stage === 'holding' && isInViewport,
134
+ };
135
+ };
136
+
137
+ const AiCard = ({
138
+ text,
139
+ words = [],
140
+ deleteChars,
141
+ lineBreakAtChars,
142
+ borderColor,
143
+ asset,
144
+ dataAttributes,
145
+ 'aria-label': ariaLabel,
146
+ ...touchableProps
147
+ }: AiCardProps): JSX.Element => {
148
+ const textLineRef = React.useRef<HTMLDivElement>(null);
149
+ const prefersReducedMotion = getPrefersReducedMotion();
150
+ const isInViewport = useIsInViewport(textLineRef, false);
151
+ const isInteractive = 'onPress' in touchableProps || 'href' in touchableProps || 'to' in touchableProps;
152
+
153
+ const safeWords = React.useMemo(
154
+ () =>
155
+ words
156
+ .map((word) => word.trim())
157
+ .filter((word) => word.length > 0)
158
+ .slice(0, MAX_WORDS),
159
+ [words]
160
+ );
161
+
162
+ const longestWord = React.useMemo(
163
+ () => safeWords.reduce((acc, word) => (word.length > acc.length ? word : acc), safeWords[0] || ''),
164
+ [safeWords]
165
+ );
166
+
167
+ const {dynamicText, isDone, shouldBlinkCaret} = useAiCardAnimation({
168
+ words: safeWords,
169
+ deleteChars,
170
+ prefersReducedMotion,
171
+ isInViewport,
172
+ });
173
+
174
+ const lastWord = safeWords.at(-1) ?? '';
175
+ const autoAriaLabel = [text, lastWord].filter(Boolean).join('') || undefined;
176
+
177
+ const textLineStyle: React.CSSProperties =
178
+ typeof lineBreakAtChars === 'number'
179
+ ? {maxWidth: `min(100%, ${Math.max(1, Math.floor(lineBreakAtChars))}ch)`}
180
+ : {};
181
+
182
+ return (
183
+ <BaseTouchable
184
+ className={classnames(styles.container, {[styles.containerInteractive]: isInteractive})}
185
+ style={
186
+ borderColor
187
+ ? applyCssVars({
188
+ [styles.vars.borderColorVar]: /gradient/.test(borderColor)
189
+ ? borderColor
190
+ : `linear-gradient(${borderColor}, ${borderColor})`,
191
+ })
192
+ : undefined
193
+ }
194
+ dataAttributes={{'component-name': 'AiCard', testid: 'AiCard', ...dataAttributes}}
195
+ {...(touchableProps as any)}
196
+ aria-label={ariaLabel ?? autoAriaLabel}
197
+ >
198
+ {asset && (
199
+ <span className={styles.slot} aria-hidden="true">
200
+ {asset}
201
+ </span>
202
+ )}
203
+ <div ref={textLineRef} className={styles.textLine} aria-hidden="true" style={textLineStyle}>
204
+ <Text3 regular color={vars.colors.textPrimary} as="span">
205
+ <span className={styles.textWrapper}>
206
+ <span className={styles.ghost}>
207
+ {text}
208
+ {longestWord}
209
+ </span>
210
+ <span className={styles.visibleContent}>
211
+ {text}
212
+ {dynamicText}
213
+ {!prefersReducedMotion && !!safeWords.length && (
214
+ <span
215
+ className={classnames(styles.caret, {
216
+ [styles.caretBlinking]: shouldBlinkCaret,
217
+ [styles.caretHidden]: isDone,
218
+ })}
219
+ >
220
+ |
221
+ </span>
222
+ )}
223
+ </span>
224
+ </span>
225
+ </Text3>
226
+ </div>
227
+ </BaseTouchable>
228
+ );
229
+ };
230
+
231
+ export default AiCard;
@@ -1,3 +1,4 @@
1
+ export {default as AiCard} from './ai-card';
1
2
  export {default as ExampleComponent} from './example-component';
2
3
  export {default as AdvancedDataCard} from './advanced-data-card';
3
4
  export {
package/src/index.tsx CHANGED
@@ -274,6 +274,7 @@ export type {TextToken, Dictionary} from './text-tokens';
274
274
  * the community.js export has issues because it exports an ES module and next12 interterprets it as a CommonJS module
275
275
  * importing from /dist/ is not an option because those modules don't get the context from the theme provider
276
276
  */
277
+ export {default as CommunityAiCard} from './community/ai-card';
277
278
  export {default as CommunityExampleComponent} from './community/example-component';
278
279
  export {default as CommunityAdvancedDataCard} from './community/advanced-data-card';
279
280
  export {
package/src/menu.css.ts CHANGED
@@ -126,6 +126,12 @@ export const itemContent = style({
126
126
  alignItems: 'center',
127
127
  });
128
128
 
129
+ export const itemTextContent = style({
130
+ display: 'flex',
131
+ flexDirection: 'column',
132
+ gap: 2,
133
+ });
134
+
129
135
  export const iconContainer = style({
130
136
  display: 'flex',
131
137
  paddingRight: 8,
package/src/menu.tsx CHANGED
@@ -10,7 +10,7 @@ import {Portal} from './portal';
10
10
  import Box from './box';
11
11
  import Inline from './inline';
12
12
  import Touchable from './touchable';
13
- import {Text3} from './text';
13
+ import {Text2, Text3} from './text';
14
14
  import {vars} from './skins/skin-contract.css';
15
15
  import Divider from './divider';
16
16
  import Checkbox from './checkbox';
@@ -53,24 +53,44 @@ const getItemIndexInMenu = (menu: HTMLElement | null, item: HTMLElement | null):
53
53
 
54
54
  interface MenuItemBaseProps {
55
55
  label: string;
56
+ description?: string;
56
57
  Icon?: (props: IconProps) => JSX.Element;
57
58
  destructive?: boolean;
58
59
  disabled?: boolean;
59
- onPress: (item: number) => void;
60
60
  dataAttributes?: DataAttributes;
61
61
  }
62
62
 
63
- interface MenuItemWithoutControlProps extends MenuItemBaseProps {
63
+ interface MenuItemOnPressProps extends MenuItemBaseProps {
64
+ onPress: (item: number) => void;
65
+ controlType?: 'checkbox';
66
+ checked?: boolean;
67
+ href?: undefined;
68
+ to?: undefined;
69
+ }
70
+
71
+ interface MenuItemHrefProps extends MenuItemBaseProps {
72
+ href: string;
73
+ newTab?: boolean;
74
+ loadOnTop?: boolean;
75
+ onNavigate?: () => void | Promise<void>;
76
+ onPress?: undefined;
77
+ to?: undefined;
64
78
  controlType?: undefined;
65
79
  checked?: undefined;
66
80
  }
67
81
 
68
- interface MenuItemWithControlProps extends MenuItemBaseProps {
69
- controlType?: 'checkbox';
70
- checked?: boolean;
82
+ interface MenuItemToProps extends MenuItemBaseProps {
83
+ to: string;
84
+ newTab?: boolean;
85
+ fullPageOnWebView?: boolean;
86
+ onNavigate?: () => void | Promise<void>;
87
+ onPress?: undefined;
88
+ href?: undefined;
89
+ controlType?: undefined;
90
+ checked?: undefined;
71
91
  }
72
92
 
73
- type MenuItemProps = ExclusifyUnion<MenuItemWithControlProps | MenuItemWithoutControlProps>;
93
+ type MenuItemProps = ExclusifyUnion<MenuItemOnPressProps | MenuItemHrefProps | MenuItemToProps>;
74
94
 
75
95
  export const MenuItem = ({
76
96
  label,
@@ -78,8 +98,15 @@ export const MenuItem = ({
78
98
  destructive,
79
99
  disabled,
80
100
  onPress,
101
+ href,
102
+ to,
103
+ newTab,
104
+ loadOnTop,
105
+ fullPageOnWebView,
106
+ onNavigate,
81
107
  controlType,
82
108
  checked,
109
+ description,
83
110
  dataAttributes,
84
111
  }: MenuItemProps): JSX.Element => {
85
112
  const {focusedItem, setFocusedItem, closeMenu, isMenuOpen} = useMenuContext();
@@ -93,6 +120,30 @@ export const MenuItem = ({
93
120
 
94
121
  const menuItemDataAttributes = {testid: 'MenuItem', ...dataAttributes};
95
122
 
123
+ const renderTextContent = (id?: string) => (
124
+ <div id={id} className={styles.itemTextContent}>
125
+ <Text3 regular color={contentColor}>
126
+ {label}
127
+ </Text3>
128
+ {description && (
129
+ <Text2 regular color={vars.colors.textSecondary}>
130
+ {description}
131
+ </Text2>
132
+ )}
133
+ </div>
134
+ );
135
+
136
+ const renderItemContent = (labelId?: string) => (
137
+ <div className={styles.itemContent}>
138
+ {Icon && (
139
+ <div className={styles.iconContainer}>
140
+ <Icon size={24} color={contentColor} />
141
+ </div>
142
+ )}
143
+ {renderTextContent(labelId)}
144
+ </div>
145
+ );
146
+
96
147
  const renderContent = () =>
97
148
  controlType === 'checkbox' ? (
98
149
  <Checkbox
@@ -101,37 +152,64 @@ export const MenuItem = ({
101
152
  checked={checked}
102
153
  onChange={() => {
103
154
  if (isMenuOpen && itemIndex !== null) {
104
- onPress(itemIndex);
155
+ onPress?.(itemIndex);
105
156
  closeMenu();
106
157
  }
107
158
  }}
108
159
  disabled={disabled}
109
160
  role="menuitemcheckbox"
110
161
  dataAttributes={menuItemDataAttributes}
111
- render={({controlElement}) => (
162
+ render={({controlElement, labelId}) => (
112
163
  <Box paddingX={8} paddingY={12}>
113
164
  <Inline space="between" alignItems="center">
114
- <div className={styles.itemContent}>
115
- {Icon && (
116
- <div className={styles.iconContainer}>
117
- <Icon size={24} color={contentColor} />
118
- </div>
119
- )}
120
- <Text3 regular color={contentColor}>
121
- {label}
122
- </Text3>
123
- </div>
165
+ {renderItemContent(labelId)}
124
166
  <Box paddingLeft={16}>{controlElement}</Box>
125
167
  </Inline>
126
168
  </Box>
127
169
  )}
128
170
  />
171
+ ) : href ? (
172
+ <Touchable
173
+ ref={itemRef}
174
+ href={href}
175
+ newTab={newTab}
176
+ loadOnTop={loadOnTop}
177
+ onNavigate={() => {
178
+ closeMenu();
179
+ onNavigate?.();
180
+ }}
181
+ disabled={disabled}
182
+ role="menuitem"
183
+ dataAttributes={menuItemDataAttributes}
184
+ >
185
+ <Box paddingX={8} paddingY={12}>
186
+ {renderItemContent()}
187
+ </Box>
188
+ </Touchable>
189
+ ) : to ? (
190
+ <Touchable
191
+ ref={itemRef}
192
+ to={to}
193
+ newTab={newTab}
194
+ fullPageOnWebView={fullPageOnWebView}
195
+ onNavigate={() => {
196
+ closeMenu();
197
+ onNavigate?.();
198
+ }}
199
+ disabled={disabled}
200
+ role="menuitem"
201
+ dataAttributes={menuItemDataAttributes}
202
+ >
203
+ <Box paddingX={8} paddingY={12}>
204
+ {renderItemContent()}
205
+ </Box>
206
+ </Touchable>
129
207
  ) : (
130
208
  <Touchable
131
209
  ref={itemRef}
132
210
  onPress={() => {
133
211
  if (isMenuOpen && itemIndex !== null) {
134
- onPress(itemIndex);
212
+ onPress?.(itemIndex);
135
213
  closeMenu();
136
214
  }
137
215
  }}
@@ -140,16 +218,7 @@ export const MenuItem = ({
140
218
  dataAttributes={menuItemDataAttributes}
141
219
  >
142
220
  <Box paddingX={8} paddingY={12}>
143
- <div className={styles.itemContent}>
144
- {Icon && (
145
- <div className={styles.iconContainer}>
146
- <Icon size={24} color={contentColor} />
147
- </div>
148
- )}
149
- <Text3 regular color={contentColor}>
150
- {label}
151
- </Text3>
152
- </div>
221
+ {renderItemContent()}
153
222
  </Box>
154
223
  </Touchable>
155
224
  );
@@ -1,2 +1,2 @@
1
1
  // DO NOT EDIT THIS FILE. It's autogenerated by set-version.js
2
- export const PACKAGE_VERSION = '16.62.0-beta.1' as string;
2
+ export const PACKAGE_VERSION = '16.63.0' as string;