@cloud-ru/ds-uikit-product-quota 1.0.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 (183) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/LICENSE +201 -0
  3. package/README.md +458 -0
  4. package/dist/cjs/components/QuotaWidget/QuotaIncreaseButton.d.ts +4 -0
  5. package/dist/cjs/components/QuotaWidget/QuotaIncreaseButton.js +15 -0
  6. package/dist/cjs/components/QuotaWidget/QuotaMobileHeader.d.ts +4 -0
  7. package/dist/cjs/components/QuotaWidget/QuotaMobileHeader.js +15 -0
  8. package/dist/cjs/components/QuotaWidget/QuotaWidget.d.ts +12 -0
  9. package/dist/cjs/components/QuotaWidget/QuotaWidget.js +44 -0
  10. package/dist/cjs/components/QuotaWidget/QuotaWidgetContent.d.ts +27 -0
  11. package/dist/cjs/components/QuotaWidget/QuotaWidgetContent.js +33 -0
  12. package/dist/cjs/components/QuotaWidget/components/ProjectHeader/ProjectHeader.d.ts +7 -0
  13. package/dist/cjs/components/QuotaWidget/components/ProjectHeader/ProjectHeader.js +19 -0
  14. package/dist/cjs/components/QuotaWidget/components/ProjectHeader/index.d.ts +1 -0
  15. package/dist/cjs/components/QuotaWidget/components/ProjectHeader/index.js +17 -0
  16. package/dist/cjs/components/QuotaWidget/components/ProjectHeader/styles.module.css +30 -0
  17. package/dist/cjs/components/QuotaWidget/index.d.ts +2 -0
  18. package/dist/cjs/components/QuotaWidget/index.js +18 -0
  19. package/dist/cjs/components/QuotaWidget/styles.module.css +49 -0
  20. package/dist/cjs/components/QuotaWidgetCard/QuotaWidgetCard.d.ts +13 -0
  21. package/dist/cjs/components/QuotaWidgetCard/QuotaWidgetCard.js +53 -0
  22. package/dist/cjs/components/QuotaWidgetCard/components/QuotaTooltip/QuotaTooltip.d.ts +6 -0
  23. package/dist/cjs/components/QuotaWidgetCard/components/QuotaTooltip/QuotaTooltip.js +23 -0
  24. package/dist/cjs/components/QuotaWidgetCard/components/QuotaTooltip/index.d.ts +1 -0
  25. package/dist/cjs/components/QuotaWidgetCard/components/QuotaTooltip/index.js +17 -0
  26. package/dist/cjs/components/QuotaWidgetCard/components/QuotaTooltip/styles.module.css +34 -0
  27. package/dist/cjs/components/QuotaWidgetCard/index.d.ts +1 -0
  28. package/dist/cjs/components/QuotaWidgetCard/index.js +17 -0
  29. package/dist/cjs/components/QuotaWidgetCard/styles.module.css +97 -0
  30. package/dist/cjs/components/QuotaWidgetMini/QuotaWidgetMini.d.ts +7 -0
  31. package/dist/cjs/components/QuotaWidgetMini/QuotaWidgetMini.js +27 -0
  32. package/dist/cjs/components/QuotaWidgetMini/index.d.ts +1 -0
  33. package/dist/cjs/components/QuotaWidgetMini/index.js +17 -0
  34. package/dist/cjs/components/QuotaWidgetMini/styles.module.css +15 -0
  35. package/dist/cjs/components/index.d.ts +3 -0
  36. package/dist/cjs/components/index.js +19 -0
  37. package/dist/cjs/constants.d.ts +17 -0
  38. package/dist/cjs/constants.js +20 -0
  39. package/dist/cjs/helperComponents/QuotaCardsGrid/QuotaCardsGrid.d.ts +6 -0
  40. package/dist/cjs/helperComponents/QuotaCardsGrid/QuotaCardsGrid.js +32 -0
  41. package/dist/cjs/helperComponents/QuotaCardsGrid/components/Grid/Grid.d.ts +6 -0
  42. package/dist/cjs/helperComponents/QuotaCardsGrid/components/Grid/Grid.js +30 -0
  43. package/dist/cjs/helperComponents/QuotaCardsGrid/components/Grid/components/QuotaWidgetCardsSkeleton/QuotaCardsSkeleton.d.ts +6 -0
  44. package/dist/cjs/helperComponents/QuotaCardsGrid/components/Grid/components/QuotaWidgetCardsSkeleton/QuotaCardsSkeleton.js +16 -0
  45. package/dist/cjs/helperComponents/QuotaCardsGrid/components/Grid/components/QuotaWidgetCardsSkeleton/index.d.ts +1 -0
  46. package/dist/cjs/helperComponents/QuotaCardsGrid/components/Grid/components/QuotaWidgetCardsSkeleton/index.js +17 -0
  47. package/dist/cjs/helperComponents/QuotaCardsGrid/components/Grid/components/QuotaWidgetCardsSkeleton/styles.module.css +10 -0
  48. package/dist/cjs/helperComponents/QuotaCardsGrid/components/Grid/index.d.ts +1 -0
  49. package/dist/cjs/helperComponents/QuotaCardsGrid/components/Grid/index.js +17 -0
  50. package/dist/cjs/helperComponents/QuotaCardsGrid/components/Grid/styles.module.css +9 -0
  51. package/dist/cjs/helperComponents/QuotaCardsGrid/components/QuotaError/QuotaError.d.ts +5 -0
  52. package/dist/cjs/helperComponents/QuotaCardsGrid/components/QuotaError/QuotaError.js +12 -0
  53. package/dist/cjs/helperComponents/QuotaCardsGrid/components/QuotaError/index.d.ts +1 -0
  54. package/dist/cjs/helperComponents/QuotaCardsGrid/components/QuotaError/index.js +17 -0
  55. package/dist/cjs/helperComponents/QuotaCardsGrid/index.d.ts +1 -0
  56. package/dist/cjs/helperComponents/QuotaCardsGrid/index.js +17 -0
  57. package/dist/cjs/helperComponents/QuotaCardsGrid/styles.module.css +16 -0
  58. package/dist/cjs/index.d.ts +5 -0
  59. package/dist/cjs/index.js +21 -0
  60. package/dist/cjs/locale/index.d.ts +91 -0
  61. package/dist/cjs/locale/index.js +50 -0
  62. package/dist/cjs/package.json +3 -0
  63. package/dist/cjs/style.css +260 -0
  64. package/dist/cjs/types.d.ts +34 -0
  65. package/dist/cjs/types.js +2 -0
  66. package/dist/cjs/utils/checkIsExceeded.d.ts +2 -0
  67. package/dist/cjs/utils/checkIsExceeded.js +5 -0
  68. package/dist/cjs/utils/formatNumber.d.ts +1 -0
  69. package/dist/cjs/utils/formatNumber.js +6 -0
  70. package/dist/cjs/utils/getPercent.d.ts +2 -0
  71. package/dist/cjs/utils/getPercent.js +9 -0
  72. package/dist/cjs/utils/index.d.ts +3 -0
  73. package/dist/cjs/utils/index.js +19 -0
  74. package/dist/esm/components/QuotaWidget/QuotaIncreaseButton.d.ts +4 -0
  75. package/dist/esm/components/QuotaWidget/QuotaIncreaseButton.js +9 -0
  76. package/dist/esm/components/QuotaWidget/QuotaMobileHeader.d.ts +4 -0
  77. package/dist/esm/components/QuotaWidget/QuotaMobileHeader.js +9 -0
  78. package/dist/esm/components/QuotaWidget/QuotaWidget.d.ts +12 -0
  79. package/dist/esm/components/QuotaWidget/QuotaWidget.js +41 -0
  80. package/dist/esm/components/QuotaWidget/QuotaWidgetContent.d.ts +27 -0
  81. package/dist/esm/components/QuotaWidget/QuotaWidgetContent.js +27 -0
  82. package/dist/esm/components/QuotaWidget/components/ProjectHeader/ProjectHeader.d.ts +7 -0
  83. package/dist/esm/components/QuotaWidget/components/ProjectHeader/ProjectHeader.js +13 -0
  84. package/dist/esm/components/QuotaWidget/components/ProjectHeader/index.d.ts +1 -0
  85. package/dist/esm/components/QuotaWidget/components/ProjectHeader/index.js +1 -0
  86. package/dist/esm/components/QuotaWidget/components/ProjectHeader/styles.module.css +30 -0
  87. package/dist/esm/components/QuotaWidget/index.d.ts +2 -0
  88. package/dist/esm/components/QuotaWidget/index.js +2 -0
  89. package/dist/esm/components/QuotaWidget/styles.module.css +49 -0
  90. package/dist/esm/components/QuotaWidgetCard/QuotaWidgetCard.d.ts +13 -0
  91. package/dist/esm/components/QuotaWidgetCard/QuotaWidgetCard.js +47 -0
  92. package/dist/esm/components/QuotaWidgetCard/components/QuotaTooltip/QuotaTooltip.d.ts +6 -0
  93. package/dist/esm/components/QuotaWidgetCard/components/QuotaTooltip/QuotaTooltip.js +17 -0
  94. package/dist/esm/components/QuotaWidgetCard/components/QuotaTooltip/index.d.ts +1 -0
  95. package/dist/esm/components/QuotaWidgetCard/components/QuotaTooltip/index.js +1 -0
  96. package/dist/esm/components/QuotaWidgetCard/components/QuotaTooltip/styles.module.css +34 -0
  97. package/dist/esm/components/QuotaWidgetCard/index.d.ts +1 -0
  98. package/dist/esm/components/QuotaWidgetCard/index.js +1 -0
  99. package/dist/esm/components/QuotaWidgetCard/styles.module.css +97 -0
  100. package/dist/esm/components/QuotaWidgetMini/QuotaWidgetMini.d.ts +7 -0
  101. package/dist/esm/components/QuotaWidgetMini/QuotaWidgetMini.js +21 -0
  102. package/dist/esm/components/QuotaWidgetMini/index.d.ts +1 -0
  103. package/dist/esm/components/QuotaWidgetMini/index.js +1 -0
  104. package/dist/esm/components/QuotaWidgetMini/styles.module.css +15 -0
  105. package/dist/esm/components/index.d.ts +3 -0
  106. package/dist/esm/components/index.js +3 -0
  107. package/dist/esm/constants.d.ts +17 -0
  108. package/dist/esm/constants.js +17 -0
  109. package/dist/esm/helperComponents/QuotaCardsGrid/QuotaCardsGrid.d.ts +6 -0
  110. package/dist/esm/helperComponents/QuotaCardsGrid/QuotaCardsGrid.js +26 -0
  111. package/dist/esm/helperComponents/QuotaCardsGrid/components/Grid/Grid.d.ts +6 -0
  112. package/dist/esm/helperComponents/QuotaCardsGrid/components/Grid/Grid.js +24 -0
  113. package/dist/esm/helperComponents/QuotaCardsGrid/components/Grid/components/QuotaWidgetCardsSkeleton/QuotaCardsSkeleton.d.ts +6 -0
  114. package/dist/esm/helperComponents/QuotaCardsGrid/components/Grid/components/QuotaWidgetCardsSkeleton/QuotaCardsSkeleton.js +10 -0
  115. package/dist/esm/helperComponents/QuotaCardsGrid/components/Grid/components/QuotaWidgetCardsSkeleton/index.d.ts +1 -0
  116. package/dist/esm/helperComponents/QuotaCardsGrid/components/Grid/components/QuotaWidgetCardsSkeleton/index.js +1 -0
  117. package/dist/esm/helperComponents/QuotaCardsGrid/components/Grid/components/QuotaWidgetCardsSkeleton/styles.module.css +10 -0
  118. package/dist/esm/helperComponents/QuotaCardsGrid/components/Grid/index.d.ts +1 -0
  119. package/dist/esm/helperComponents/QuotaCardsGrid/components/Grid/index.js +1 -0
  120. package/dist/esm/helperComponents/QuotaCardsGrid/components/Grid/styles.module.css +9 -0
  121. package/dist/esm/helperComponents/QuotaCardsGrid/components/QuotaError/QuotaError.d.ts +5 -0
  122. package/dist/esm/helperComponents/QuotaCardsGrid/components/QuotaError/QuotaError.js +9 -0
  123. package/dist/esm/helperComponents/QuotaCardsGrid/components/QuotaError/index.d.ts +1 -0
  124. package/dist/esm/helperComponents/QuotaCardsGrid/components/QuotaError/index.js +1 -0
  125. package/dist/esm/helperComponents/QuotaCardsGrid/index.d.ts +1 -0
  126. package/dist/esm/helperComponents/QuotaCardsGrid/index.js +1 -0
  127. package/dist/esm/helperComponents/QuotaCardsGrid/styles.module.css +16 -0
  128. package/dist/esm/index.d.ts +5 -0
  129. package/dist/esm/index.js +5 -0
  130. package/dist/esm/locale/index.d.ts +91 -0
  131. package/dist/esm/locale/index.js +47 -0
  132. package/dist/esm/style.css +260 -0
  133. package/dist/esm/types.d.ts +34 -0
  134. package/dist/esm/types.js +1 -0
  135. package/dist/esm/utils/checkIsExceeded.d.ts +2 -0
  136. package/dist/esm/utils/checkIsExceeded.js +1 -0
  137. package/dist/esm/utils/formatNumber.d.ts +1 -0
  138. package/dist/esm/utils/formatNumber.js +3 -0
  139. package/dist/esm/utils/getPercent.d.ts +2 -0
  140. package/dist/esm/utils/getPercent.js +6 -0
  141. package/dist/esm/utils/index.d.ts +3 -0
  142. package/dist/esm/utils/index.js +3 -0
  143. package/dist/tsconfig.cjs.tsbuildinfo +1 -0
  144. package/dist/tsconfig.esm.tsbuildinfo +1 -0
  145. package/package.json +63 -0
  146. package/src/components/QuotaWidget/QuotaIncreaseButton.tsx +21 -0
  147. package/src/components/QuotaWidget/QuotaMobileHeader.tsx +18 -0
  148. package/src/components/QuotaWidget/QuotaWidget.tsx +109 -0
  149. package/src/components/QuotaWidget/QuotaWidgetContent.tsx +91 -0
  150. package/src/components/QuotaWidget/components/ProjectHeader/ProjectHeader.tsx +59 -0
  151. package/src/components/QuotaWidget/components/ProjectHeader/index.ts +1 -0
  152. package/src/components/QuotaWidget/components/ProjectHeader/styles.module.scss +29 -0
  153. package/src/components/QuotaWidget/index.ts +2 -0
  154. package/src/components/QuotaWidget/styles.module.scss +52 -0
  155. package/src/components/QuotaWidgetCard/QuotaWidgetCard.tsx +127 -0
  156. package/src/components/QuotaWidgetCard/components/QuotaTooltip/QuotaTooltip.tsx +48 -0
  157. package/src/components/QuotaWidgetCard/components/QuotaTooltip/index.ts +1 -0
  158. package/src/components/QuotaWidgetCard/components/QuotaTooltip/styles.module.scss +30 -0
  159. package/src/components/QuotaWidgetCard/index.ts +1 -0
  160. package/src/components/QuotaWidgetCard/styles.module.scss +97 -0
  161. package/src/components/QuotaWidgetMini/QuotaWidgetMini.tsx +93 -0
  162. package/src/components/QuotaWidgetMini/index.ts +1 -0
  163. package/src/components/QuotaWidgetMini/styles.module.scss +18 -0
  164. package/src/components/index.ts +3 -0
  165. package/src/constants.ts +17 -0
  166. package/src/helperComponents/QuotaCardsGrid/QuotaCardsGrid.tsx +54 -0
  167. package/src/helperComponents/QuotaCardsGrid/components/Grid/Grid.tsx +47 -0
  168. package/src/helperComponents/QuotaCardsGrid/components/Grid/components/QuotaWidgetCardsSkeleton/QuotaCardsSkeleton.tsx +24 -0
  169. package/src/helperComponents/QuotaCardsGrid/components/Grid/components/QuotaWidgetCardsSkeleton/index.ts +1 -0
  170. package/src/helperComponents/QuotaCardsGrid/components/Grid/components/QuotaWidgetCardsSkeleton/styles.module.scss +15 -0
  171. package/src/helperComponents/QuotaCardsGrid/components/Grid/index.ts +1 -0
  172. package/src/helperComponents/QuotaCardsGrid/components/Grid/styles.module.scss +12 -0
  173. package/src/helperComponents/QuotaCardsGrid/components/QuotaError/QuotaError.tsx +31 -0
  174. package/src/helperComponents/QuotaCardsGrid/components/QuotaError/index.ts +1 -0
  175. package/src/helperComponents/QuotaCardsGrid/index.ts +1 -0
  176. package/src/helperComponents/QuotaCardsGrid/styles.module.scss +16 -0
  177. package/src/index.ts +5 -0
  178. package/src/locale/index.ts +51 -0
  179. package/src/types.ts +35 -0
  180. package/src/utils/checkIsExceeded.ts +3 -0
  181. package/src/utils/formatNumber.ts +3 -0
  182. package/src/utils/getPercent.ts +9 -0
  183. package/src/utils/index.ts +3 -0
@@ -0,0 +1,109 @@
1
+ import { isMobileLayout, useAdaptiveLayout } from '@cloud-ru/ds-adaptive';
2
+ import { Button, ButtonProps } from '@cloud-ru/ds-button';
3
+ import { Dropdown } from '@cloud-ru/ds-dropdown';
4
+ import { ChevronDownSVG, ChevronUpSVG } from '@cloud-ru/ds-icons/interface/system';
5
+ import { WithSupportProps } from '@cloud-ru/ds-utils';
6
+ import { useState } from 'react';
7
+
8
+ import { TEST_IDS } from '../../constants';
9
+ import { quotaLocale } from '../../locale';
10
+ import { QuotaWidgetPropsBase } from '../../types';
11
+ import { checkIsExceeded } from '../../utils';
12
+ import { QuotaIncreaseButton } from './QuotaIncreaseButton';
13
+ import { QuotaMobileHeader } from './QuotaMobileHeader';
14
+ import { QuotaWidgetContent } from './QuotaWidgetContent';
15
+
16
+ export type QuotaWidgetProps = WithSupportProps<
17
+ QuotaWidgetPropsBase & {
18
+ /** Ссылка на страницу квот по проекту */
19
+ quotasUrl: string;
20
+ /** Колбек клика по ссылке на страницу квот по проекту */
21
+ onQuotasUrlClick?: () => void;
22
+ /** Свойства кнопки открытия виджета */
23
+ buttonProps?: Pick<ButtonProps, 'size' | 'className' | 'fullWidth' | 'label' | 'appearance' | 'disabled' | 'view'>;
24
+ }
25
+ >;
26
+
27
+ export function QuotaWidget({
28
+ quotas,
29
+ disableSorting,
30
+ loading,
31
+ error,
32
+ onRefresh,
33
+ projectName,
34
+ quotasUrl,
35
+ canEditQuota,
36
+ hideIncreaseQuotaButton,
37
+ onIncreaseQuotaClick,
38
+ onWidgetOpen,
39
+ onQuotasUrlClick,
40
+ buttonProps,
41
+ ...props
42
+ }: QuotaWidgetProps) {
43
+ const { t } = quotaLocale.useTranslations();
44
+ const [isOpen, setIsOpen] = useState(false);
45
+ const { layoutType } = useAdaptiveLayout();
46
+ const isMobile = isMobileLayout(layoutType);
47
+ const rootTestId = props['data-test-id'] as string | undefined;
48
+ const useMatrixTestIds = Boolean(rootTestId && rootTestId !== TEST_IDS.quotaWidget.root);
49
+ const contentTestId = useMatrixTestIds && rootTestId ? `content-${rootTestId}` : TEST_IDS.quotaWidget.content;
50
+ const triggerTestId = useMatrixTestIds && rootTestId ? `trigger-${rootTestId}` : TEST_IDS.quotaWidget.trigger;
51
+
52
+ const handleOpenChange = (open: boolean) => {
53
+ setIsOpen(open);
54
+ if (open) {
55
+ onWidgetOpen?.();
56
+ }
57
+ };
58
+
59
+ const exhaustedCount = quotas.filter(checkIsExceeded).length;
60
+
61
+ // На мобилке лист full-height (snap `1`): шапку и кнопку выносим в pinned-слоты BottomSheet
62
+ // (`title` сверху, `footer` снизу), а карточки скроллятся в теле листа — как в макете.
63
+ const showMobileIncreaseButton = isMobile && canEditQuota && !error && !hideIncreaseQuotaButton;
64
+
65
+ return (
66
+ <Dropdown
67
+ {...props}
68
+ placement='bottom-end'
69
+ // Mobile: лист на всю высоту экрана (число `1` = full-height snap; safe-area даёт сам BottomSheet).
70
+ snapPoints={[1]}
71
+ title={isMobile ? <QuotaMobileHeader projectName={projectName} /> : undefined}
72
+ footer={showMobileIncreaseButton ? <QuotaIncreaseButton onClick={onIncreaseQuotaClick} /> : undefined}
73
+ open={isOpen}
74
+ onOpenChange={handleOpenChange}
75
+ content={
76
+ <QuotaWidgetContent
77
+ quotas={quotas}
78
+ disableSorting={disableSorting}
79
+ loading={loading}
80
+ error={error}
81
+ onRefresh={onRefresh}
82
+ projectName={projectName}
83
+ quotasUrl={quotasUrl}
84
+ canEditQuota={canEditQuota}
85
+ hideIncreaseQuotaButton={hideIncreaseQuotaButton}
86
+ onIncreaseQuotaClick={onIncreaseQuotaClick}
87
+ onQuotasUrlClick={onQuotasUrlClick}
88
+ hideMobileHeader={isMobile}
89
+ hideMobileIncreaseButton={isMobile}
90
+ data-test-id={contentTestId}
91
+ />
92
+ }
93
+ >
94
+ <Button
95
+ // На мобилке триггер бордюрный (`outline` — как в макете кнопки квот),
96
+ // на desktop — плоский `function`. Переопределяется через `buttonProps.view`.
97
+ view={isMobile ? 'outline' : 'function'}
98
+ label={t('quotas')}
99
+ appearance='neutral'
100
+ iconPosition='after'
101
+ icon={isOpen ? <ChevronUpSVG /> : <ChevronDownSVG />}
102
+ counter={exhaustedCount > 0 ? { value: exhaustedCount, appearance: 'red' } : undefined}
103
+ size='m'
104
+ data-test-id={triggerTestId}
105
+ {...buttonProps}
106
+ />
107
+ </Dropdown>
108
+ );
109
+ }
@@ -0,0 +1,91 @@
1
+ import { isMobileLayout, useAdaptiveLayout } from '@cloud-ru/ds-adaptive';
2
+ import { WithSupportProps } from '@cloud-ru/ds-utils';
3
+
4
+ import { TEST_IDS } from '../../constants';
5
+ import { QuotaCardsGrid } from '../../helperComponents/QuotaCardsGrid';
6
+ import { QuotaWidgetPropsBase } from '../../types';
7
+ import { ProjectHeader } from './components/ProjectHeader';
8
+ import { QuotaIncreaseButton } from './QuotaIncreaseButton';
9
+ import { QuotaMobileHeader } from './QuotaMobileHeader';
10
+ import styles from './styles.module.scss';
11
+
12
+ export type QuotaWidgetContentProps = WithSupportProps<
13
+ QuotaWidgetPropsBase & {
14
+ /** Ссылка на страницу квот по проекту */
15
+ quotasUrl: string;
16
+ /** Колбек клика по ссылке на страницу квот по проекту */
17
+ onQuotasUrlClick?: () => void;
18
+ /**
19
+ * Не рендерить мобильную шапку (её выносят в pinned-`title` контейнера, напр. BottomSheet).
20
+ * На desktop не влияет.
21
+ */
22
+ hideMobileHeader?: boolean;
23
+ /**
24
+ * Не рендерить мобильную кнопку расширения (её выносят в pinned-`footer` контейнера).
25
+ * На desktop не влияет.
26
+ */
27
+ hideMobileIncreaseButton?: boolean;
28
+ }
29
+ >;
30
+
31
+ /**
32
+ * Голый контент квота-виджета: шапка проекта + карточки + кнопка расширения — без триггера,
33
+ * дропдауна и аккордеона. Используется внутри `QuotaWidget` (в дропдауне/BottomSheet) и напрямую,
34
+ * когда контейнер-раскрытие даёт кто-то снаружи (напр. `sideBlock` формы уже открывает BottomSheet).
35
+ *
36
+ * На full-height BottomSheet шапка/кнопка уезжают в pinned-слоты `title`/`footer` (см. `QuotaWidget`),
37
+ * а сюда остаются только карточки — тогда `hideMobileHeader`/`hideMobileIncreaseButton` = true.
38
+ */
39
+ export function QuotaWidgetContent({
40
+ quotas,
41
+ disableSorting,
42
+ loading,
43
+ error,
44
+ onRefresh,
45
+ projectName,
46
+ quotasUrl,
47
+ canEditQuota,
48
+ hideIncreaseQuotaButton,
49
+ onIncreaseQuotaClick,
50
+ onQuotasUrlClick,
51
+ hideMobileHeader = false,
52
+ hideMobileIncreaseButton = false,
53
+ ...props
54
+ }: QuotaWidgetContentProps) {
55
+ const { layoutType } = useAdaptiveLayout();
56
+ const isMobile = isMobileLayout(layoutType);
57
+ const contentTestId = props['data-test-id'] ?? TEST_IDS.quotaWidget.content;
58
+
59
+ const showMobileIncreaseButton =
60
+ isMobile && !hideMobileIncreaseButton && canEditQuota && !error && !hideIncreaseQuotaButton;
61
+
62
+ return (
63
+ <div className={styles.content} data-mobile={isMobile} data-test-id={contentTestId}>
64
+ {isMobile ? (
65
+ // Мобильная шапка (макет): «Квоты» + имя проекта, без пиктограммы/ссылки/подзаголовка «Остаток…».
66
+ !hideMobileHeader && <QuotaMobileHeader projectName={projectName} />
67
+ ) : (
68
+ // Desktop-popover: полная шапка проекта (с пиктограммой, ссылкой и кнопкой расширения).
69
+ <ProjectHeader
70
+ projectName={projectName}
71
+ quotasUrl={quotasUrl}
72
+ canEditQuota={canEditQuota}
73
+ error={error}
74
+ onIncreaseQuotaClick={onIncreaseQuotaClick}
75
+ hideIncreaseQuotaButton={hideIncreaseQuotaButton}
76
+ onQuotasUrlClick={onQuotasUrlClick}
77
+ />
78
+ )}
79
+
80
+ <QuotaCardsGrid
81
+ quotas={quotas}
82
+ disableSorting={disableSorting}
83
+ loading={loading}
84
+ error={error}
85
+ onRefresh={onRefresh}
86
+ />
87
+
88
+ {showMobileIncreaseButton && <QuotaIncreaseButton onClick={onIncreaseQuotaClick} />}
89
+ </div>
90
+ );
91
+ }
@@ -0,0 +1,59 @@
1
+ import { Button } from '@cloud-ru/ds-button';
2
+ import { QuotaSVG } from '@cloud-ru/ds-icons/interface/product';
3
+ import { TitleClickable } from '@cloud-ru/ds-uikit-product-title-clickable';
4
+
5
+ import { TEST_IDS } from '../../../../constants';
6
+ import { quotaLocale } from '../../../../locale';
7
+ import { QuotaWidgetPropsBase } from '../../../../types';
8
+ import styles from './styles.module.scss';
9
+
10
+ type ProjectHeaderProps = Pick<
11
+ QuotaWidgetPropsBase,
12
+ 'projectName' | 'canEditQuota' | 'error' | 'hideIncreaseQuotaButton' | 'onIncreaseQuotaClick'
13
+ > & {
14
+ quotasUrl: string;
15
+ onQuotasUrlClick?: () => void;
16
+ };
17
+
18
+ const RUSSIAN_CHARS_REGEXP = /[А-Яа-яЁё]/;
19
+
20
+ export function ProjectHeader({
21
+ projectName,
22
+ quotasUrl,
23
+ canEditQuota,
24
+ error,
25
+ onIncreaseQuotaClick,
26
+ hideIncreaseQuotaButton,
27
+ onQuotasUrlClick,
28
+ }: ProjectHeaderProps) {
29
+ const { t } = quotaLocale.useTranslations();
30
+
31
+ const widgetTitle = RUSSIAN_CHARS_REGEXP.test(projectName) ? 'widgetTitle.quotes' : 'widgetTitle.noQuotes';
32
+
33
+ return (
34
+ <div className={styles.header} data-test-id={TEST_IDS.quotaWidget.projectHeader}>
35
+ <div className={styles.projectWrapper}>
36
+ <TitleClickable
37
+ className={styles.titleLink}
38
+ title={t(widgetTitle, { project: projectName })}
39
+ href={quotasUrl}
40
+ target='_blank'
41
+ icon={<QuotaSVG />}
42
+ onClick={onQuotasUrlClick}
43
+ />
44
+
45
+ <p className={styles.subtitle}>{t('widgetSubtitle')}</p>
46
+ </div>
47
+
48
+ {canEditQuota && !error && !hideIncreaseQuotaButton && (
49
+ <Button
50
+ view='outline'
51
+ appearance='neutral'
52
+ label={t('increaseQuota')}
53
+ size='m'
54
+ onClick={onIncreaseQuotaClick}
55
+ />
56
+ )}
57
+ </div>
58
+ );
59
+ }
@@ -0,0 +1 @@
1
+ export * from './ProjectHeader';
@@ -0,0 +1,29 @@
1
+ @use '@cloud-ru/ds-figma-variables/build/scss/styles/styles.module' as base;
2
+
3
+ .header {
4
+ display: flex;
5
+ gap: base.$sn-primitive-dimension-8;
6
+ align-items: center;
7
+ width: 100%;
8
+ }
9
+
10
+ .projectWrapper {
11
+ display: flex;
12
+ flex: 1 0 0;
13
+ flex-direction: column;
14
+ align-items: flex-start;
15
+
16
+ min-width: 0;
17
+ }
18
+
19
+ .titleLink {
20
+ gap: base.$sn-primitive-dimension-8;
21
+ }
22
+
23
+ .subtitle {
24
+ @include base.composite-var(base.$base-styles, 'sn', 'regular', 'body', 's');
25
+
26
+ width: 100%;
27
+ margin: 0;
28
+ color: base.$sn-theme-color-available-version-textTertiary;
29
+ }
@@ -0,0 +1,2 @@
1
+ export * from './QuotaWidget';
2
+ export * from './QuotaWidgetContent';
@@ -0,0 +1,52 @@
1
+ @use '@cloud-ru/ds-figma-variables/build/scss/styles/styles.module' as base;
2
+
3
+ .content {
4
+ display: flex;
5
+ flex-direction: column;
6
+ gap: base.$sn-primitive-dimension-16;
7
+ align-items: flex-start;
8
+
9
+ width: 620px;
10
+ min-width: 620px;
11
+ padding: base.$sn-primitive-dimension-12 base.$sn-primitive-dimension-16;
12
+
13
+ /* Mobile: dropdown раскрывается в BottomSheet на всю ширину экрана — фиксированные 620px дают
14
+ горизонтальный overflow, поэтому тянемся по контейнеру. Padding убираем: body-инсет уже даёт
15
+ контейнер (BottomSheet / ModalCustom), собственный padding был бы двойным. */
16
+ &[data-mobile='true'] {
17
+ width: 100%;
18
+ min-width: 0;
19
+ height: 100%;
20
+ padding: 0;
21
+ }
22
+ }
23
+
24
+ /* Кнопка «Увеличить квоту» внизу BottomSheet на мобилке — на всю ширину. */
25
+ .increaseButton {
26
+ width: 100%;
27
+ }
28
+
29
+ /* Мобильная шапка (макет): заголовок + имя проекта, слева, без пиктограмм/ссылок. */
30
+ .mobileHeader {
31
+ display: flex;
32
+ flex-direction: column;
33
+ gap: base.$sn-primitive-dimension-2;
34
+ align-items: flex-start;
35
+
36
+ width: 100%;
37
+ }
38
+
39
+ .mobileTitle {
40
+ @include base.composite-var(base.$base-styles, 'sn', 'regular', 'title', 'l');
41
+
42
+ margin: 0;
43
+ color: base.$sn-theme-color-available-version-textMain;
44
+ }
45
+
46
+ .mobileSubtitle {
47
+ @include base.composite-var(base.$base-styles, 'sn', 'regular', 'body', 'm');
48
+
49
+ width: 100%;
50
+ margin: 0;
51
+ color: base.$sn-theme-color-available-version-textSecondary;
52
+ }
@@ -0,0 +1,127 @@
1
+ import { Button } from '@cloud-ru/ds-button';
2
+ import { UpdateSVG } from '@cloud-ru/ds-icons/interface/system';
3
+ import { InfoBlock } from '@cloud-ru/ds-info-block';
4
+ import { ProgressBar } from '@cloud-ru/ds-progress-bar';
5
+ import { SkeletonText } from '@cloud-ru/ds-skeleton';
6
+ import { Tooltip } from '@cloud-ru/ds-tooltip';
7
+ import { TruncateString } from '@cloud-ru/ds-truncate-string';
8
+ import { Typography } from '@cloud-ru/ds-typography';
9
+ import { WithSupportProps } from '@cloud-ru/ds-utils';
10
+
11
+ import { quotaLocale } from '../../locale';
12
+ import { QuotaItem } from '../../types';
13
+ import { checkIsExceeded, formatNumber, getPercent } from '../../utils';
14
+ import { QuotaTooltip } from './components/QuotaTooltip';
15
+ import styles from './styles.module.scss';
16
+
17
+ export type QuotaWidgetCardProps = WithSupportProps<{
18
+ /** Отображаемая квота */
19
+ quota: QuotaItem;
20
+ /** Состояние «не удалось загрузить данные» (Figma: noData=true) */
21
+ noData?: boolean;
22
+ /** Состояние загрузки (Figma: loading=true) */
23
+ loading?: boolean;
24
+ /** Колбек кнопки «Обновить» в состоянии noData */
25
+ onRefresh?: () => void;
26
+ }>;
27
+
28
+ const QUOTA_USAGE_PROGRESS = {
29
+ HIGH: 90,
30
+ MEDIUM: 70,
31
+ } as const;
32
+
33
+ function getProgressAppearance(percent: number) {
34
+ if (percent >= QUOTA_USAGE_PROGRESS.HIGH) {
35
+ return 'red';
36
+ }
37
+ if (percent >= QUOTA_USAGE_PROGRESS.MEDIUM) {
38
+ return 'yellow';
39
+ }
40
+ return 'green';
41
+ }
42
+
43
+ export function QuotaWidgetCard({ quota, noData = false, loading = false, onRefresh, ...props }: QuotaWidgetCardProps) {
44
+ const { t } = quotaLocale.useTranslations();
45
+
46
+ if (noData) {
47
+ return (
48
+ <div className={styles.card} data-no-data {...props}>
49
+ <InfoBlock
50
+ size='s'
51
+ align='vertical'
52
+ title={quota.name}
53
+ content={t('errorText')}
54
+ footer={
55
+ onRefresh ? (
56
+ <Button
57
+ view='simple'
58
+ appearance='neutral'
59
+ size='s'
60
+ label={t('errorButton')}
61
+ icon={<UpdateSVG size={16} />}
62
+ iconPosition='after'
63
+ onClick={onRefresh}
64
+ />
65
+ ) : undefined
66
+ }
67
+ />
68
+ </div>
69
+ );
70
+ }
71
+
72
+ if (loading) {
73
+ return (
74
+ <div className={styles.card} data-loading {...props}>
75
+ <SkeletonText loading lines={1} variant='body' size='m' />
76
+ <SkeletonText loading lines={1} variant='body' size='m' />
77
+ <SkeletonText loading lines={1} variant='body' size='l' />
78
+ </div>
79
+ );
80
+ }
81
+
82
+ const percent = getPercent(quota);
83
+
84
+ const rows = [
85
+ {
86
+ label: t('cardAvailable'),
87
+ value: quota.limit,
88
+ },
89
+ {
90
+ label: t('cardRemaining'),
91
+ value: quota.remains,
92
+ },
93
+ ];
94
+
95
+ return (
96
+ <Tooltip triggerClassName={styles.tooltip} hoverDelayOpen={500} tip={<QuotaTooltip quota={quota} />}>
97
+ <div className={styles.card} data-exhausted={checkIsExceeded(quota)} {...props}>
98
+ <div className={styles.header}>
99
+ <Typography variant='body' size='m' className={styles.title}>
100
+ <TruncateString maxLines={2} text={quota.name} />
101
+ </Typography>
102
+
103
+ <span className={styles.percent}>{Math.round(percent)}%</span>
104
+ </div>
105
+
106
+ <ProgressBar
107
+ className={styles.progressBar}
108
+ progress={percent}
109
+ size='xs'
110
+ appearance={getProgressAppearance(percent)}
111
+ />
112
+
113
+ <div className={styles.info}>
114
+ {rows.map(row => (
115
+ <div className={styles.row} key={row.label}>
116
+ <span className={styles.label}>{row.label}</span>
117
+
118
+ <span className={styles.value}>
119
+ {formatNumber(row.value)} <span className={styles.unit}>{quota.unitDisplayName}</span>
120
+ </span>
121
+ </div>
122
+ ))}
123
+ </div>
124
+ </div>
125
+ </Tooltip>
126
+ );
127
+ }
@@ -0,0 +1,48 @@
1
+ import { APPEARANCE, Divider } from '@cloud-ru/ds-divider';
2
+ import { Typography } from '@cloud-ru/ds-typography';
3
+
4
+ import { TEST_IDS } from '../../../../constants';
5
+ import { quotaLocale } from '../../../../locale';
6
+ import { QuotaItem } from '../../../../types';
7
+ import { checkIsExceeded, formatNumber } from '../../../../utils';
8
+ import styles from './styles.module.scss';
9
+
10
+ type QuotaTooltipProps = {
11
+ quota: QuotaItem;
12
+ };
13
+
14
+ export function QuotaTooltip({ quota }: QuotaTooltipProps) {
15
+ const { t } = quotaLocale.useTranslations();
16
+
17
+ const rows = [
18
+ { label: t('tooltipAvailable'), value: quota.limit },
19
+ { label: t('tooltipUsed'), value: quota.usage },
20
+ { label: t('tooltipRemaining'), value: quota.remains },
21
+ ];
22
+
23
+ const exhaustedHint = `${t('tooltipExhaustedHint.first')} ${t('tooltipExhaustedHint.second')}`;
24
+
25
+ return (
26
+ <div className={styles.content} data-test-id={TEST_IDS.quotaWidgetCard.tooltip}>
27
+ <div className={styles.lists}>
28
+ {rows.map(row => (
29
+ <div className={styles.row} key={row.label}>
30
+ <span className={styles.rowLabel}>{row.label}</span>
31
+ <span className={styles.rowValue}>
32
+ {formatNumber(row.value)} {quota.unitDisplayName}
33
+ </span>
34
+ </div>
35
+ ))}
36
+ </div>
37
+
38
+ {checkIsExceeded(quota) && (
39
+ <>
40
+ <Divider appearance={APPEARANCE.InvertNeutral} />
41
+ <Typography variant='body' size='m'>
42
+ {exhaustedHint}
43
+ </Typography>
44
+ </>
45
+ )}
46
+ </div>
47
+ );
48
+ }
@@ -0,0 +1 @@
1
+ export * from './QuotaTooltip';
@@ -0,0 +1,30 @@
1
+ @use '@cloud-ru/ds-figma-variables/build/scss/styles/styles.module' as base;
2
+
3
+ .content {
4
+ display: flex;
5
+ flex-direction: column;
6
+ row-gap: base.$sn-primitive-dimension-4;
7
+ align-items: stretch;
8
+ }
9
+
10
+ .lists {
11
+ display: flex;
12
+ flex-direction: column;
13
+ row-gap: base.$sn-primitive-dimension-4;
14
+ }
15
+
16
+ .row {
17
+ display: flex;
18
+ column-gap: base.$sn-primitive-dimension-8;
19
+ align-items: center;
20
+ justify-content: space-between;
21
+ }
22
+
23
+ .rowLabel,
24
+ .rowValue {
25
+ @include base.composite-var(base.$base-styles, 'sn', 'regular', 'body', 'm');
26
+ }
27
+
28
+ .rowValue {
29
+ text-align: right;
30
+ }
@@ -0,0 +1 @@
1
+ export * from './QuotaWidgetCard';
@@ -0,0 +1,97 @@
1
+ @use '@cloud-ru/ds-figma-variables/build/scss/styles/styles.module' as base;
2
+ @use '@cloud-ru/ds-figma-variables/build/scss/components/card.module' as card;
3
+
4
+ /* Обёртка триггера тултипа — грид-элемент вместо самой карточки, поэтому она обязана
5
+ отдавать карточке всю ширину трека, а не сжиматься по контенту. */
6
+ .tooltip {
7
+ display: flex;
8
+ width: 100%;
9
+ max-width: 100%;
10
+ }
11
+
12
+ /* figma-selected-block: quotaCard root (node 2921:16397) */
13
+ .card {
14
+ display: flex;
15
+ flex: 1 1 auto;
16
+ flex-direction: column;
17
+ row-gap: base.$sn-primitive-dimension-4;
18
+
19
+ box-sizing: border-box;
20
+
21
+ /* Ширину карточки задаёт трек грида: desktop 2 колонки, mobile 1 колонка — на всю ширину.
22
+ Своей ширины у карточки нет, только нижняя граница (в Figma-мастере карточка 288px).
23
+ `min()` снимает эту границу, когда контейнер уже неё, — узкий лист не переполняется. */
24
+ min-width: min(256px, 100%);
25
+ max-width: 100%;
26
+ padding: base.$sn-primitive-dimension-8;
27
+ border-radius: card.$sn-card-anatomy-radius-s-container-borderRadius;
28
+
29
+ background-color: base.$sn-theme-color-neutral-decorTransparent;
30
+ backdrop-filter: blur(calc(base.$sn-acrylic-blurBackground1Level / 2));
31
+
32
+ &[data-exhausted='true'] {
33
+ background-color: base.simple-var(base.$sn-theme, 'color', 'red', 'background');
34
+ }
35
+ }
36
+
37
+ /* figma-selected-block: title row */
38
+ .header {
39
+ display: flex;
40
+ gap: base.$sn-primitive-dimension-8;
41
+ align-items: flex-end;
42
+ justify-content: space-between;
43
+
44
+ width: 100%;
45
+ }
46
+
47
+ .title {
48
+ flex: 1 1 0;
49
+ min-width: 0;
50
+ color: base.$sn-theme-color-available-version-textMain;
51
+ }
52
+
53
+ .percent {
54
+ @include base.composite-var(base.$base-styles, 'sn', 'thin', 'label', 'm');
55
+
56
+ flex-shrink: 0;
57
+ color: base.$sn-theme-color-available-version-textTertiary;
58
+ }
59
+
60
+ .progressBar {
61
+ width: 100%;
62
+ }
63
+
64
+ .info {
65
+ display: flex;
66
+ flex-direction: column;
67
+ row-gap: base.$sn-primitive-dimension-2;
68
+ width: 100%;
69
+ }
70
+
71
+ .row {
72
+ display: flex;
73
+ column-gap: base.$sn-primitive-dimension-8;
74
+ align-items: center;
75
+ justify-content: space-between;
76
+
77
+ width: 100%;
78
+ }
79
+
80
+ .label {
81
+ @include base.composite-var(base.$base-styles, 'sn', 'thin', 'label', 's');
82
+
83
+ color: base.$sn-theme-color-available-version-textTertiary;
84
+ }
85
+
86
+ .value {
87
+ @include base.composite-var(base.$base-styles, 'sn', 'thin', 'label', 'l');
88
+
89
+ color: base.$sn-theme-color-available-version-textMain;
90
+ white-space: nowrap;
91
+ }
92
+
93
+ .unit {
94
+ @include base.composite-var(base.$base-styles, 'sn', 'thin', 'label', 'l');
95
+
96
+ color: base.$sn-theme-color-available-version-textTertiary;
97
+ }