diff --git a/src/components/echarts/chart.tsx b/src/components/echarts/chart.tsx index 8341565c..0275fa19 100644 --- a/src/components/echarts/chart.tsx +++ b/src/components/echarts/chart.tsx @@ -18,6 +18,7 @@ const Chart: React.FC<{ const chart = useRef(); const resizeable = useRef(false); const resizeObserver = useRef(); + const finished = useRef(false); useImperativeHandle(ref, () => { return { @@ -49,7 +50,7 @@ const Chart: React.FC<{ useEffect(() => { const handleOnFinished = () => { - if (!chart.current) return; + if (!chart.current || finished.current) return; const currentChart = chart.current; const optionsYAxis = currentChart.getOption()?.yAxis; @@ -75,22 +76,32 @@ const Chart: React.FC<{ const ticksList = axes.map((axis) => axis.scale.getTicks()); const counts = ticksList.map((t) => t.length); - if (counts[0] === counts[1]) return; - const unifiedCount = Math.max(counts[0], counts[1]); const newMax0 = intervals[0] * (unifiedCount - 1); const newMax1 = intervals[1] * (unifiedCount - 1); + // get yaxis max value + const maxValue0 = Math.max(); + const maxValue1 = yAxisModels[1].get('max'); + + // if newMax0 equal to maxValue0, and newMax1 equal to maxValue1, do not update yAxis + if (counts[0] === counts[1]) return; + const yAxis: any[] = [{}, {}]; if (counts[0] < unifiedCount) { yAxis[0].max = newMax0; + yAxis[0].interval = intervals[0]; + yAxis[0].splitNumber = unifiedCount; } if (counts[1] < unifiedCount) { yAxis[1].max = newMax1; + yAxis[1].interval = intervals[1]; + yAxis[1].splitNumber = unifiedCount; } + finished.current = true; setTimeout(() => { currentChart.setOption({ @@ -112,6 +123,7 @@ const Chart: React.FC<{ useEffect(() => { resizeable.current = false; + finished.current = false; resize(); setOption(options); resizeable.current = true; diff --git a/src/components/scroller-modal/gs-drawer.tsx b/src/components/scroller-modal/gs-drawer.tsx new file mode 100644 index 00000000..64464ff8 --- /dev/null +++ b/src/components/scroller-modal/gs-drawer.tsx @@ -0,0 +1,19 @@ +import { useEscHint } from '@/hooks/use-esc-hint'; +import { Drawer, type DrawerProps } from 'antd'; + +const ScrollerModal = (props: DrawerProps) => { + const { EscHint } = useEscHint({ + enabled: !props.keyboard && props.open + }); + + return ( + <> + + {props.children} + {EscHint} + + + ); +}; + +export default ScrollerModal; diff --git a/src/config/hotkeys.ts b/src/config/hotkeys.ts index 0beb1af9..d1f928de 100644 --- a/src/config/hotkeys.ts +++ b/src/config/hotkeys.ts @@ -27,7 +27,8 @@ const KeybindingsMap = { NEW3: ['Ctrl+3', 'Meta+3'], NEW4: ['Ctrl+4', 'Meta+4'], FOCUS: ['/', '/'], - ADD: ['Alt+Ctrl+Enter', 'Alt+Meta+Enter'] + ADD: ['Alt+Ctrl+Enter', 'Alt+Meta+Enter'], + ESC: ['Esc', 'Esc'] }; type KeyBindingType = keyof typeof KeybindingsMap; diff --git a/src/config/theme/dark.ts b/src/config/theme/dark.ts index 9ccefad4..0ad4d88d 100644 --- a/src/config/theme/dark.ts +++ b/src/config/theme/dark.ts @@ -24,7 +24,9 @@ export default { }, Tabs: { titleFontSizeLG: 14 - // cardBg: '#1D1E20' + }, + DatePicker: { + fontSizeLG: 14 }, Menu: { iconSize: 16, diff --git a/src/global.less b/src/global.less index 68f13e3d..4e77151b 100644 --- a/src/global.less +++ b/src/global.less @@ -84,6 +84,7 @@ html { --ant-color-border: #d9d9d9; --ant-line-type: solid; --ant-line-width: 1px; + --color-esc-hint-bg: rgba(0, 0, 0, 75%); } html[data-theme='realDark'] { @@ -99,6 +100,7 @@ html[data-theme='realDark'] { --color-modal-content-bg: #1f1f1f; --color-modal-box-shadow: none; --color-spotlight-bg: #3e3e3e; + --color-esc-hint-bg: rgba(150, 150, 150, 75%); background: #141414; diff --git a/src/hooks/use-esc-hint.tsx b/src/hooks/use-esc-hint.tsx new file mode 100644 index 00000000..b3606214 --- /dev/null +++ b/src/hooks/use-esc-hint.tsx @@ -0,0 +1,93 @@ +import HotKeys from '@/config/hotkeys'; +import { useIntl } from '@umijs/max'; +import { createStyles } from 'antd-style'; +import { throttle } from 'lodash'; +import { useEffect, useMemo, useRef, useState } from 'react'; +import { useHotkeys } from 'react-hotkeys-hook'; + +const useStyles = createStyles(({ css, token }) => ({ + hintOverlay: css` + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background: var(--color-esc-hint-bg); + color: ${token.colorTextLightSolid}; + padding: 16px 24px; + border-radius: 4px; + z-index: 2000; + font-size: 14px; + pointer-events: none; + animation: fadeInOut 2s ease-in-out; + @keyframes fadeInOut { + 0% { + opacity: 0; + } + 10% { + opacity: 1; + } + 90% { + opacity: 1; + } + 100% { + opacity: 0; + } + } + ` +})); + +export function useEscHint(options?: { + enabled?: boolean; + message?: string; + throttleDelay?: number; +}) { + const { enabled = true, message, throttleDelay = 3000 } = options || {}; + const intl = useIntl(); + const { styles } = useStyles(); + const [visible, setVisible] = useState(false); + const timeoutRef = useRef(null); + + const showHintThrottled = useMemo( + () => + throttle( + () => { + if (timeoutRef.current) clearTimeout(timeoutRef.current); + setVisible(true); + timeoutRef.current = setTimeout(() => setVisible(false), 2000); + }, + throttleDelay, + { + leading: true, + trailing: false + } + ), + [throttleDelay] + ); + + useHotkeys( + HotKeys.ESC, + () => { + showHintThrottled(); + }, + { + enabled: enabled + } + ); + + useEffect(() => { + return () => { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + } + showHintThrottled.cancel(); + }; + }, [showHintThrottled]); + + const EscHint = visible ? ( +
+ {message || intl.formatMessage({ id: 'common.tips.escape.disable' })} +
+ ) : null; + + return { EscHint }; +} diff --git a/src/locales/en-US/common.ts b/src/locales/en-US/common.ts index 22136980..4141834d 100644 --- a/src/locales/en-US/common.ts +++ b/src/locales/en-US/common.ts @@ -247,5 +247,7 @@ export default { 'common.appearance.theme': 'Theme', 'common.page.wentwrong': 'Something went wrong.', 'common.page.refresh.tips': - 'Oops! Something went wrong. Try refreshing the page.' + 'Oops! Something went wrong. Try refreshing the page.', + 'common.tips.escape.disable': + 'Click Cancel or the X at the top right to close.' }; diff --git a/src/locales/ja-JP/common.ts b/src/locales/ja-JP/common.ts index e8410274..777b8d75 100644 --- a/src/locales/ja-JP/common.ts +++ b/src/locales/ja-JP/common.ts @@ -247,7 +247,9 @@ export default { 'common.appearance.theme': 'Theme', 'common.page.wentwrong': 'Something went wrong.', 'common.page.refresh.tips': - 'Oops! Something went wrong. Try refreshing the page.' + 'Oops! Something went wrong. Try refreshing the page.', + 'common.tips.escape.disable': + 'Click Cancel or the X at the top right to close.' }; // ========== To-Do: Translate Keys (Remove After Translation) ========== @@ -263,4 +265,5 @@ export default { // 10. 'common.appearance.theme': 'Theme', // 11. 'common.page.wentwrong': 'Something went wrong.', // 12. 'common.page.refresh.tips': 'Oops! Something went wrong. Try refreshing the page.' +// 13. 'common.tips.escape.disable': 'Click Cancel or the X at the top right to close.' // ========== End of To-Do List ========== diff --git a/src/locales/ru-RU/common.ts b/src/locales/ru-RU/common.ts index 8256d45d..de0c43a0 100644 --- a/src/locales/ru-RU/common.ts +++ b/src/locales/ru-RU/common.ts @@ -245,9 +245,12 @@ export default { 'common.button.forgotpassword': 'Забыли пароль?', 'common.appearance.theme': 'Тема', 'common.page.wentwrong': 'Что-то пошло не так.', - 'common.page.refresh.tips': 'Упс! Что-то пошло не так. Попробуйте обновить страницу.' + 'common.page.refresh.tips': + 'Упс! Что-то пошло не так. Попробуйте обновить страницу.', + 'common.tips.escape.disable': + 'Click Cancel or the X at the top right to close.' }; // ========== To-Do: Translate Keys (Remove After Translation) ========== - +// 1. 'common.tips.escape.disable': 'Click Cancel or the X at the top right to close.' // ========== End of To-Do List ========== diff --git a/src/locales/zh-CN/common.ts b/src/locales/zh-CN/common.ts index bbe485e8..5908779a 100644 --- a/src/locales/zh-CN/common.ts +++ b/src/locales/zh-CN/common.ts @@ -241,5 +241,6 @@ export default { 'common.button.forgotpassword': '忘记密码?', 'common.appearance.theme': '主题', 'common.page.wentwrong': '哎呀,出了点问题', - 'common.page.refresh.tips': '出了点问题,试试刷新页面吧!' + 'common.page.refresh.tips': '出了点问题,试试刷新页面吧!', + 'common.tips.escape.disable': '请点击「取消」按钮或右上角 X 关闭窗口' }; diff --git a/src/pages/dashboard/components/active-table.tsx b/src/pages/dashboard/components/active-table.tsx index 3464394c..61efb795 100644 --- a/src/pages/dashboard/components/active-table.tsx +++ b/src/pages/dashboard/components/active-table.tsx @@ -75,7 +75,6 @@ const ActiveTable = () => { left={ diff --git a/src/pages/dashboard/components/usage-inner/index.tsx b/src/pages/dashboard/components/usage-inner/index.tsx index e8356bb6..9d6f9bc6 100644 --- a/src/pages/dashboard/components/usage-inner/index.tsx +++ b/src/pages/dashboard/components/usage-inner/index.tsx @@ -14,7 +14,7 @@ const FilterWrapper = styled.div` display: flex; justify-content: space-between; align-items: center; - margin-bottom: 16px; + margin-bottom: 0px; .selection { display: flex; align-items: center; @@ -61,44 +61,48 @@ const UsageInner: FC<{ paddingRight: string }> = ({ paddingRight }) => { xl={16} style={{ paddingRight: paddingRight }} > - - {intl.formatMessage({ id: 'dashboard.usage' })} - - -
- - - -
- -
- {/* */} +
+ + {intl.formatMessage({ id: 'dashboard.usage' })} + + + +
+ + + +
+
+
= (props) => { return ( - + ); }; diff --git a/src/pages/llmodels/components/deploy-builtin-modal.tsx b/src/pages/llmodels/components/deploy-builtin-modal.tsx index bcf1600e..3c1834e0 100644 --- a/src/pages/llmodels/components/deploy-builtin-modal.tsx +++ b/src/pages/llmodels/components/deploy-builtin-modal.tsx @@ -1,9 +1,10 @@ import ModalFooter from '@/components/modal-footer'; +import GSDrawer from '@/components/scroller-modal/gs-drawer'; import { PageActionType } from '@/config/types'; import { createAxiosToken } from '@/hooks/use-chunk-request'; import { CloseOutlined } from '@ant-design/icons'; import { useIntl } from '@umijs/max'; -import { Button, Drawer } from 'antd'; +import { Button } from 'antd'; import _ from 'lodash'; import React, { useEffect, useMemo, useRef, useState } from 'react'; import styled from 'styled-components'; @@ -500,7 +501,7 @@ const AddModal: React.FC = (props) => { }, []); return ( - = (props) => { - + ); }; diff --git a/src/pages/llmodels/components/deploy-modal.tsx b/src/pages/llmodels/components/deploy-modal.tsx index 99e78f0d..6a2d2f8d 100644 --- a/src/pages/llmodels/components/deploy-modal.tsx +++ b/src/pages/llmodels/components/deploy-modal.tsx @@ -1,8 +1,9 @@ import ModalFooter from '@/components/modal-footer'; +import GSDrawer from '@/components/scroller-modal/gs-drawer'; import { PageActionType } from '@/config/types'; import { CloseOutlined } from '@ant-design/icons'; import { useIntl } from '@umijs/max'; -import { Button, Drawer } from 'antd'; +import { Button } from 'antd'; import _ from 'lodash'; import { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import styled from 'styled-components'; @@ -14,6 +15,8 @@ import { import { FormContext } from '../config/form-context'; import { FormData, SourceType } from '../config/types'; import { + MessageStatus, + WarningStausOptions, checkOnlyAscendNPU, useCheckCompatibility, useSelectModel @@ -106,6 +109,7 @@ const AddModal: FC = (props) => { setWarningStatus, handleBackendChangeBefore, cancelEvaluate, + unlockWarningStatus, handleOnValuesChange: handleOnValuesChangeBefore, handleEvaluateOnChange, warningStatus, @@ -141,7 +145,8 @@ const AddModal: FC = (props) => { /** * - * @param state target to distinguish the evaluate state + * @param state target to distinguish the evaluate state, current evaluate state + * can be 'model', 'file' or 'form'. */ const setEvaluteState = (state: EvaluateProccessType) => { evaluateStateRef.current.state = state; @@ -203,6 +208,7 @@ const AddModal: FC = (props) => { }); if (item.fakeName) { + unlockWarningStatus(); const currentModelId = requestModelIdRef.current; setEvaluteState(EvaluateProccess.file); const evaluateRes = await handleEvaluateOnChange?.({ @@ -245,7 +251,10 @@ const AddModal: FC = (props) => { * evaluate: true means select a model file from the evaluate result */ updateRequestModelId(); + + // If the evaluate is false, it means that the user selects a new model or the first time to open the modal. if (!evaluate) { + unlockWarningStatus(); setEvaluteState(EvaluateProccess.model); setSelectedModel(item); form.current?.form?.resetFields(resetFieldsByModel); @@ -260,8 +269,8 @@ const AddModal: FC = (props) => { setIsGGUF(false); const modelInfo = onSelectModel(item, props.source); if ( - !isHolderRef.current.model && - evaluateStateRef.current.state === EvaluateProccess.model + evaluateStateRef.current.state === EvaluateProccess.model && + item.evaluateResult ) { handleShowCompatibleAlert(item.evaluateResult); form.current?.setFieldsValue?.({ @@ -365,17 +374,20 @@ const AddModal: FC = (props) => { return warningStatus.show && warningStatus.type !== 'success'; }, [warningStatus.show, warningStatus.type]); - const displayEvaluateStatus = (data: { - show?: boolean; - flag: Record; - }) => { - setIsHolderRef(data.flag); - setWarningStatus({ - show: isHolderRef.current.model || isHolderRef.current.file, - title: '', - type: 'transition', - message: intl.formatMessage({ id: 'models.form.evaluating' }) - }); + // This is only a placeholder for querying the model or file during the transition period. + const displayEvaluateStatus = ( + params: MessageStatus, + options?: WarningStausOptions + ) => { + setWarningStatus( + { + show: params.show, + title: '', + type: 'transition', + message: intl.formatMessage({ id: 'models.form.evaluating' }) + }, + options + ); }; useEffect(() => { @@ -395,7 +407,7 @@ const AddModal: FC = (props) => { }, [open, props.gpuOptions.length]); return ( - {title} @@ -435,6 +447,7 @@ const AddModal: FC = (props) => { modelSource={props.source} onSelectModel={handleOnSelectModel} displayEvaluateStatus={displayEvaluateStatus} + unlockWarningStatus={unlockWarningStatus} gpuOptions={props.gpuOptions} > @@ -535,7 +548,7 @@ const AddModal: FC = (props) => { - + ); }; diff --git a/src/pages/llmodels/components/model-card.tsx b/src/pages/llmodels/components/model-card.tsx index 79adb554..420d8f6e 100644 --- a/src/pages/llmodels/components/model-card.tsx +++ b/src/pages/llmodels/components/model-card.tsx @@ -20,9 +20,9 @@ import React, { useState } from 'react'; import 'simplebar-react/dist/simplebar.min.css'; +import styled from 'styled-components'; import { downloadModelFile, - downloadModelScopeModelfile, queryHuggingfaceModelDetail, queryModelScopeModelDetail } from '../apis'; @@ -30,6 +30,41 @@ import { modelSourceMap } from '../config'; import '../style/model-card.less'; import TitleWrapper from './title-wrapper'; +const MkdTitle = styled.span` + cursor: pointer; + background-color: var(--ant-color-fill-tertiary); + display: flex; + justify-content: space-between; + align-items: center; + padding: 10px; + height: 36px; +`; + +const MarkDownTitle: React.FC<{ + collapsed: boolean; + loading: boolean; + onCollapse: () => void; +}> = ({ collapsed, loading, onCollapse }) => { + const intl = useIntl(); + return ( + + + {' '} + {intl.formatMessage({ id: 'models.readme' })} + + + {collapsed ? ( + + ) : loading ? ( + + ) : ( + + )} + + + ); +}; + const ModelCard: React.FC<{ onCollapse: (flag: boolean) => void; setIsGGUF: (flag: boolean) => void; @@ -69,6 +104,7 @@ const ModelCard: React.FC<{ return modelData?.ModelType?.[0]; } }, [modelData, modelSource]); + const loadFile = useCallback(async (repo: string, sha: string) => { try { axiosTokenRef.current?.abort?.(); @@ -95,27 +131,6 @@ const ModelCard: React.FC<{ } }; - const loadConfig = useCallback(async (repo: string, sha: string) => { - try { - loadConfigTokenRef.current?.abort?.(); - loadConfigTokenRef.current = new AbortController(); - const res = await downloadModelFile( - { - repo, - revision: sha, - path: 'config.json' - }, - { - signal: loadConfigTokenRef.current.signal - } - ); - return res || null; - } catch (error) { - console.log('error======', error); - return null; - } - }, []); - const removeMetadata = useCallback((str: string) => { let indexes = []; let index = str.indexOf('---'); @@ -165,23 +180,6 @@ const ModelCard: React.FC<{ } }; - const loadModelscopeModelConfig = useCallback(async (name: string) => { - try { - loadConfigJsonTokenRef.current?.abort?.(); - loadConfigJsonTokenRef.current = new AbortController(); - return await downloadModelScopeModelfile( - { - name: name - }, - { - signal: loadConfigJsonTokenRef.current.token - } - ); - } catch (error) { - return null; - } - }, []); - const getModelScopeModelDetail = async () => { try { const data = await queryModelScopeModelDetail( @@ -352,27 +350,25 @@ const ModelCard: React.FC<{ overflow: 'hidden' }} > - - - {' '} - README.md - - - {collapsed ? : } - - - - - + + + + + + )} diff --git a/src/pages/llmodels/components/search-model.tsx b/src/pages/llmodels/components/search-model.tsx index c1995ea3..6acd54d1 100644 --- a/src/pages/llmodels/components/search-model.tsx +++ b/src/pages/llmodels/components/search-model.tsx @@ -18,7 +18,11 @@ import { modelSourceMap } from '../config'; import { handleRecognizeAudioModel } from '../config/audio-catalog'; -import { checkCurrentbackend } from '../hooks'; +import { + MessageStatus, + WarningStausOptions, + checkCurrentbackend +} from '../hooks'; import SearchStyle from '../style/search-result.less'; import SearchInput from './search-input'; import SearchResult from './search-result'; @@ -37,10 +41,11 @@ interface SearchInputProps { setLoadingModel?: (flag: boolean) => void; onSourceChange?: (source: string) => void; onSelectModel: (model: any, evaluate?: boolean) => void; - displayEvaluateStatus?: (data: { - show?: boolean; - flag: Record; - }) => void; + unlockWarningStatus?: () => void; + displayEvaluateStatus?: ( + data: MessageStatus, + options?: WarningStausOptions + ) => void; } const SearchModel: React.FC = (props) => { @@ -52,7 +57,8 @@ const SearchModel: React.FC = (props) => { gpuOptions, setLoadingModel, onSelectModel, - displayEvaluateStatus + displayEvaluateStatus, + unlockWarningStatus } = props; const [dataSource, setDataSource] = useState<{ repoOptions: any[]; @@ -296,19 +302,6 @@ const SearchModel: React.FC = (props) => { (item) => item.id === currentRef.current ); - /** - * if item is GGUF, the evaluating would be do after selecting the model file, Or the evaluate status of model would be overrided the - * file evaluate status. - */ - if (currentItem && !currentItem.isGGUF) { - displayEvaluateStatus?.({ - show: false, - flag: { - model: false - } - }); - } - if (currentItem) { handleOnSelectModel(currentItem, true); } @@ -372,12 +365,18 @@ const SearchModel: React.FC = (props) => { networkError: false, sortType: sort }); - displayEvaluateStatus?.({ - show: true, - flag: { - model: list.length > 0 + + // It's a new request, so we need to reset the state + unlockWarningStatus?.(); + displayEvaluateStatus?.( + { + show: list.length > 0, + message: '' + }, + { + override: true } - }); + ); handleOnSelectModel(list[0]); setLoadingModel?.(false); @@ -393,9 +392,7 @@ const SearchModel: React.FC = (props) => { setLoadingModel?.(false); displayEvaluateStatus?.({ show: false, - flag: { - model: false - } + message: '' }); handleOnSelectModel({}); cacheRepoOptions.current = []; @@ -460,12 +457,17 @@ const SearchModel: React.FC = (props) => { repoOptions: currentList }; }); - displayEvaluateStatus?.({ - show: true, - flag: { - model: true + unlockWarningStatus?.(); + // reset evaluate status + displayEvaluateStatus?.( + { + show: true, + message: '' + }, + { + override: true } - }); + ); console.log('isEvaluating:', isEvaluating); handleOnSelectModel(currentList[0]); handleEvaluate(currentList); diff --git a/src/pages/llmodels/components/search-result.tsx b/src/pages/llmodels/components/search-result.tsx index e7712e06..0e400e69 100644 --- a/src/pages/llmodels/components/search-result.tsx +++ b/src/pages/llmodels/components/search-result.tsx @@ -128,4 +128,4 @@ const SearchResult: React.FC = (props) => { ); }; -export default React.memo(SearchResult); +export default SearchResult; diff --git a/src/pages/llmodels/download/index.tsx b/src/pages/llmodels/download/index.tsx index d3ed9a78..b31266a0 100644 --- a/src/pages/llmodels/download/index.tsx +++ b/src/pages/llmodels/download/index.tsx @@ -1,7 +1,8 @@ import ModalFooter from '@/components/modal-footer'; +import GSDrawer from '@/components/scroller-modal/gs-drawer'; import { CloseOutlined } from '@ant-design/icons'; import { useIntl } from '@umijs/max'; -import { Button, Drawer } from 'antd'; +import { Button } from 'antd'; import { debounce } from 'lodash'; import React, { useCallback, useEffect, useRef, useState } from 'react'; import ColumnWrapper from '../components/column-wrapper'; @@ -125,7 +126,7 @@ const DownloadModel: React.FC = (props) => { }, [open, source]); return ( - = (props) => { - + ); }; diff --git a/src/pages/llmodels/hooks/index.ts b/src/pages/llmodels/hooks/index.ts index c2a28104..db2df3a0 100644 --- a/src/pages/llmodels/hooks/index.ts +++ b/src/pages/llmodels/hooks/index.ts @@ -26,7 +26,7 @@ import { ListItem } from '../config/types'; -type MessageStatus = { +export type MessageStatus = { show: boolean; title?: string; type?: Global.MessageType; @@ -36,7 +36,7 @@ type MessageStatus = { evaluateResult?: EvaluateResult; }; -type WarningStausOptions = { +export type WarningStausOptions = { lockAfterUpdate?: boolean; override?: boolean; }; diff --git a/src/pages/llmodels/style/model-card.less b/src/pages/llmodels/style/model-card.less index 0575900f..b94e813b 100644 --- a/src/pages/llmodels/style/model-card.less +++ b/src/pages/llmodels/style/model-card.less @@ -24,16 +24,6 @@ display: flex; justify-content: flex-end; } - - .mkd-title { - cursor: pointer; - background-color: var(--ant-color-fill-tertiary); - display: flex; - justify-content: space-between; - align-items: center; - padding: 10px; - height: 36px; - } } .card-wrapper {