@bundle-stats/ui 4.15.1 → 4.16.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 (165) hide show
  1. package/lib/assets/icons.svg +84 -70
  2. package/lib/assets/icons.svg.js +10 -0
  3. package/lib/assets/icons.svg.js.map +1 -1
  4. package/lib/components/bundle-assets/bundle-assets.js +9 -2
  5. package/lib/components/bundle-assets/bundle-assets.js.map +1 -1
  6. package/lib/components/bundle-assets-totals/bundle-assets-totals.js +21 -9
  7. package/lib/components/bundle-assets-totals/bundle-assets-totals.js.map +1 -1
  8. package/lib/components/bundle-modules/bundle-modules.js +9 -2
  9. package/lib/components/bundle-modules/bundle-modules.js.map +1 -1
  10. package/lib/components/bundle-packages/bundle-packages.js +9 -2
  11. package/lib/components/bundle-packages/bundle-packages.js.map +1 -1
  12. package/lib/components/entry-info/entry-info.js +8 -4
  13. package/lib/components/entry-info/entry-info.js.map +1 -1
  14. package/lib/components/entry-info/entry-info.module.css +1 -1
  15. package/lib/components/metrics-table-export/index.js +18 -0
  16. package/lib/components/metrics-table-export/index.js.map +1 -0
  17. package/lib/components/metrics-table-export/metrics-table-export.js +86 -0
  18. package/lib/components/metrics-table-export/metrics-table-export.js.map +1 -0
  19. package/lib/components/metrics-table-export/metrics-table-export.module.css +8 -0
  20. package/lib/components/metrics-table-options/index.js.map +1 -1
  21. package/lib/components/metrics-table-options/metrics-table-options.js +7 -15
  22. package/lib/components/metrics-table-options/metrics-table-options.js.map +1 -1
  23. package/lib/components/preview-source/index.js +18 -0
  24. package/lib/components/preview-source/index.js.map +1 -0
  25. package/lib/components/preview-source/preview-source.js +25 -0
  26. package/lib/components/preview-source/preview-source.js.map +1 -0
  27. package/lib/components/preview-source/preview-source.module.css +28 -0
  28. package/lib/css/variables.css +1 -0
  29. package/lib/i18n.js +3 -0
  30. package/lib/i18n.js.map +1 -1
  31. package/lib/ui/button/button.js +2 -1
  32. package/lib/ui/button/button.js.map +1 -1
  33. package/lib/ui/copy-to-clipboard/copy-to-clipboard.js +56 -0
  34. package/lib/ui/copy-to-clipboard/copy-to-clipboard.js.map +1 -0
  35. package/lib/ui/copy-to-clipboard/copy-to-clipboard.module.css +15 -0
  36. package/lib/ui/copy-to-clipboard/index.js +18 -0
  37. package/lib/ui/copy-to-clipboard/index.js.map +1 -0
  38. package/lib/ui/dialog/dialog.js +27 -0
  39. package/lib/ui/dialog/dialog.js.map +1 -0
  40. package/lib/ui/dialog/dialog.module.css +89 -0
  41. package/lib/ui/dialog/index.js +18 -0
  42. package/lib/ui/dialog/index.js.map +1 -0
  43. package/lib/ui/icon/icon.js +3 -0
  44. package/lib/ui/icon/icon.js.map +1 -1
  45. package/lib/ui/index.js +7 -1
  46. package/lib/ui/index.js.map +1 -1
  47. package/lib/ui/tabs/tabs.js +2 -2
  48. package/lib/ui/tabs/tabs.js.map +1 -1
  49. package/lib/ui/textarea/index.js +18 -0
  50. package/lib/ui/textarea/index.js.map +1 -0
  51. package/lib/ui/textarea/textarea.js +16 -0
  52. package/lib/ui/textarea/textarea.js.map +1 -0
  53. package/lib/ui/textarea/textarea.module.css +64 -0
  54. package/lib-esm/assets/icons.svg +84 -70
  55. package/lib-esm/assets/icons.svg.js +10 -0
  56. package/lib-esm/assets/icons.svg.js.map +1 -1
  57. package/lib-esm/components/bundle-assets/bundle-assets.js +9 -2
  58. package/lib-esm/components/bundle-assets/bundle-assets.js.map +1 -1
  59. package/lib-esm/components/bundle-assets-totals/bundle-assets-totals.js +21 -9
  60. package/lib-esm/components/bundle-assets-totals/bundle-assets-totals.js.map +1 -1
  61. package/lib-esm/components/bundle-modules/bundle-modules.js +9 -2
  62. package/lib-esm/components/bundle-modules/bundle-modules.js.map +1 -1
  63. package/lib-esm/components/bundle-packages/bundle-packages.js +9 -2
  64. package/lib-esm/components/bundle-packages/bundle-packages.js.map +1 -1
  65. package/lib-esm/components/entry-info/entry-info.js +9 -5
  66. package/lib-esm/components/entry-info/entry-info.js.map +1 -1
  67. package/lib-esm/components/entry-info/entry-info.module.css +1 -1
  68. package/lib-esm/components/metrics-table-export/index.js +2 -0
  69. package/lib-esm/components/metrics-table-export/index.js.map +1 -0
  70. package/lib-esm/components/metrics-table-export/metrics-table-export.js +61 -0
  71. package/lib-esm/components/metrics-table-export/metrics-table-export.js.map +1 -0
  72. package/lib-esm/components/metrics-table-export/metrics-table-export.module.css +8 -0
  73. package/lib-esm/components/metrics-table-options/index.js.map +1 -1
  74. package/lib-esm/components/metrics-table-options/metrics-table-options.js +7 -15
  75. package/lib-esm/components/metrics-table-options/metrics-table-options.js.map +1 -1
  76. package/lib-esm/components/preview-source/index.js +2 -0
  77. package/lib-esm/components/preview-source/index.js.map +1 -0
  78. package/lib-esm/components/preview-source/preview-source.js +18 -0
  79. package/lib-esm/components/preview-source/preview-source.js.map +1 -0
  80. package/lib-esm/components/preview-source/preview-source.module.css +28 -0
  81. package/lib-esm/css/variables.css +1 -0
  82. package/lib-esm/i18n.js +3 -0
  83. package/lib-esm/i18n.js.map +1 -1
  84. package/lib-esm/ui/button/button.js +1 -0
  85. package/lib-esm/ui/button/button.js.map +1 -1
  86. package/lib-esm/ui/copy-to-clipboard/copy-to-clipboard.js +26 -0
  87. package/lib-esm/ui/copy-to-clipboard/copy-to-clipboard.js.map +1 -0
  88. package/lib-esm/ui/copy-to-clipboard/copy-to-clipboard.module.css +15 -0
  89. package/lib-esm/ui/copy-to-clipboard/index.js +2 -0
  90. package/lib-esm/ui/copy-to-clipboard/index.js.map +1 -0
  91. package/lib-esm/ui/dialog/dialog.js +19 -0
  92. package/lib-esm/ui/dialog/dialog.js.map +1 -0
  93. package/lib-esm/ui/dialog/dialog.module.css +89 -0
  94. package/lib-esm/ui/dialog/index.js +2 -0
  95. package/lib-esm/ui/dialog/index.js.map +1 -0
  96. package/lib-esm/ui/icon/icon.js +3 -0
  97. package/lib-esm/ui/icon/icon.js.map +1 -1
  98. package/lib-esm/ui/index.js +3 -0
  99. package/lib-esm/ui/index.js.map +1 -1
  100. package/lib-esm/ui/tabs/tabs.js +2 -2
  101. package/lib-esm/ui/tabs/tabs.js.map +1 -1
  102. package/lib-esm/ui/textarea/index.js +2 -0
  103. package/lib-esm/ui/textarea/index.js.map +1 -0
  104. package/lib-esm/ui/textarea/textarea.js +9 -0
  105. package/lib-esm/ui/textarea/textarea.js.map +1 -0
  106. package/lib-esm/ui/textarea/textarea.module.css +64 -0
  107. package/package.json +11 -11
  108. package/src/assets/icons/clipboard-check.svg +5 -0
  109. package/src/assets/icons/clipboard.svg +5 -0
  110. package/src/assets/icons/download.svg +4 -0
  111. package/src/assets/icons.svg +84 -70
  112. package/src/assets/icons.svg.jsx +36 -0
  113. package/src/components/bundle-assets/bundle-assets.jsx +15 -2
  114. package/src/components/bundle-assets-totals/bundle-assets-totals.jsx +54 -35
  115. package/src/components/bundle-modules/bundle-modules.tsx +15 -2
  116. package/src/components/bundle-packages/bundle-packages.jsx +15 -2
  117. package/src/components/entry-info/entry-info.module.css +1 -1
  118. package/src/components/entry-info/entry-info.tsx +23 -9
  119. package/src/components/metrics-table-export/index.ts +1 -0
  120. package/src/components/metrics-table-export/metrics-table-export.module.css +8 -0
  121. package/src/components/metrics-table-export/metrics-table-export.stories.tsx +30 -0
  122. package/src/components/metrics-table-export/metrics-table-export.tsx +103 -0
  123. package/src/components/metrics-table-options/metrics-table-options.stories.tsx +22 -0
  124. package/src/components/metrics-table-options/metrics-table-options.tsx +25 -0
  125. package/src/components/preview-source/index.ts +1 -0
  126. package/src/components/preview-source/preview-source.module.css +28 -0
  127. package/src/components/preview-source/preview-source.stories.tsx +35 -0
  128. package/src/components/preview-source/preview-source.tsx +47 -0
  129. package/src/css/variables.css +1 -0
  130. package/src/i18n.js +4 -0
  131. package/src/ui/button/button.tsx +2 -1
  132. package/src/ui/copy-to-clipboard/copy-to-clipboard.module.css +15 -0
  133. package/src/ui/copy-to-clipboard/copy-to-clipboard.stories.tsx +33 -0
  134. package/src/ui/copy-to-clipboard/copy-to-clipboard.tsx +53 -0
  135. package/src/ui/copy-to-clipboard/index.ts +1 -0
  136. package/src/ui/dialog/dialog.module.css +89 -0
  137. package/src/ui/dialog/dialog.stories.tsx +67 -0
  138. package/src/ui/dialog/dialog.tsx +62 -0
  139. package/src/ui/dialog/index.ts +1 -0
  140. package/src/ui/icon/icon.tsx +3 -0
  141. package/src/ui/index.js +3 -0
  142. package/src/ui/tabs/tabs.jsx +2 -2
  143. package/src/ui/textarea/index.ts +1 -0
  144. package/src/ui/textarea/textarea.module.css +64 -0
  145. package/src/ui/textarea/textarea.stories.tsx +37 -0
  146. package/src/ui/textarea/textarea.tsx +23 -0
  147. package/types/components/metrics-table-export/index.d.ts +1 -0
  148. package/types/components/metrics-table-export/metrics-table-export.d.ts +10 -0
  149. package/types/components/metrics-table-options/index.d.ts +1 -1
  150. package/types/components/metrics-table-options/metrics-table-options.d.ts +9 -17
  151. package/types/components/preview-source/index.d.ts +1 -0
  152. package/types/components/preview-source/preview-source.d.ts +8 -0
  153. package/types/i18n.d.ts +3 -0
  154. package/types/ui/button/button.d.ts +26 -0
  155. package/types/ui/copy-to-clipboard/copy-to-clipboard.d.ts +7 -0
  156. package/types/ui/copy-to-clipboard/index.d.ts +1 -0
  157. package/types/ui/dialog/dialog.d.ts +13 -0
  158. package/types/ui/dialog/index.d.ts +1 -0
  159. package/types/ui/icon/icon.d.ts +6 -0
  160. package/types/ui/index.d.ts +3 -0
  161. package/types/ui/tabs/tabs.d.ts +3 -3
  162. package/types/ui/textarea/index.d.ts +1 -0
  163. package/types/ui/textarea/textarea.d.ts +7 -0
  164. package/src/components/metrics-table-options/metrics-table-options.jsx +0 -30
  165. /package/src/components/metrics-table-options/{index.js → index.ts} +0 -0
@@ -0,0 +1,103 @@
1
+ import type { ComponentProps } from 'react';
2
+ import React, { useState } from 'react';
3
+ import type { ReportMetricRow } from '@bundle-stats/utils';
4
+
5
+ import { Stack } from '../../layout/stack';
6
+ import { Tabs } from '../../ui/tabs';
7
+ import { PreviewSource } from '../preview-source';
8
+ import css from './metrics-table-export.module.css';
9
+
10
+ const generateSourceJSON = (items: Array<ReportMetricRow>): string => {
11
+ const output = items.map((item) => ({
12
+ name: item.label,
13
+ runs: item.runs?.map((itemRun) => ({
14
+ name: itemRun?.name || '',
15
+ size: itemRun?.value || 0,
16
+ })),
17
+ }));
18
+
19
+ return JSON.stringify(output, null, 2);
20
+ };
21
+
22
+ const CSV_SEPARATOR = ', ';
23
+
24
+ const generateSourceCSV = (items: Array<ReportMetricRow>): string => {
25
+ const output = [];
26
+
27
+ output.push(
28
+ [
29
+ 'Name',
30
+ ...(items?.[0]?.runs?.map((_, index) => {
31
+ if (index === 0) {
32
+ return `Run ${index + 1}(current)`;
33
+ }
34
+
35
+ return `Run ${index + 1}(baseline)`;
36
+ }) || []),
37
+ ].join(CSV_SEPARATOR),
38
+ );
39
+
40
+ items?.forEach((item) => {
41
+ const row: Array<string | number> = [item.label];
42
+
43
+ item.runs?.forEach((itemRun) => {
44
+ row.push(itemRun?.value || 0);
45
+ });
46
+
47
+ output.push(row.join(CSV_SEPARATOR));
48
+ });
49
+
50
+ return output.join('\r');
51
+ };
52
+
53
+ const SOURCES = [
54
+ {
55
+ label: 'CSV',
56
+ transformFn: generateSourceCSV,
57
+ extension: '.csv',
58
+ },
59
+ {
60
+ label: 'JSON',
61
+ transformFn: generateSourceJSON,
62
+ extension: '.json',
63
+ },
64
+ ];
65
+
66
+ type MetricsTableExportProps = {
67
+ items: Array<ReportMetricRow>;
68
+ download: string;
69
+ } & ComponentProps<typeof Stack>;
70
+
71
+ export const MetricsTableExport = (props: MetricsTableExportProps) => {
72
+ const { items, download, ...restProps } = props;
73
+ const [selectedId, setSelectedId] = useState(SOURCES[0].label);
74
+
75
+ return (
76
+ <Stack space="small" {...restProps}>
77
+ <Tabs>
78
+ {SOURCES.map((source) => (
79
+ <Tabs.Item
80
+ onClick={() => setSelectedId(source.label)}
81
+ isTabActive={selectedId === source.label}
82
+ key={source.label}
83
+ >
84
+ {source.label}
85
+ </Tabs.Item>
86
+ ))}
87
+ </Tabs>
88
+ <div className={css.panels}>
89
+ {SOURCES.map(
90
+ (source) =>
91
+ selectedId === source.label && (
92
+ <PreviewSource
93
+ source={source.transformFn(items)}
94
+ download={`${download}.${source.extension}`}
95
+ className={css.panel}
96
+ key={source.label}
97
+ />
98
+ ),
99
+ )}
100
+ </div>
101
+ </Stack>
102
+ );
103
+ };
@@ -0,0 +1,22 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ // eslint-disable-next-line import/no-extraneous-dependencies
3
+ import { action } from '@storybook/addon-actions';
4
+
5
+ import { getWrapperDecorator } from '../../stories';
6
+ import { MetricsTableOptions } from '.';
7
+
8
+ const meta: Meta<typeof MetricsTableOptions> = {
9
+ title: 'Components/MetricsTableOptions',
10
+ component: MetricsTableOptions,
11
+ decorators: [getWrapperDecorator()],
12
+ args: {
13
+ onResetClick: action('RESET'),
14
+ onViewAllClick: action('VIEW_ALL'),
15
+ },
16
+ };
17
+
18
+ export default meta;
19
+
20
+ type Story = StoryObj<typeof meta>;
21
+
22
+ export const Default: Story = {};
@@ -0,0 +1,25 @@
1
+ import type { ComponentProps } from 'react';
2
+ import React from 'react';
3
+
4
+ import I18N from '../../i18n';
5
+ import { Dropdown, DropdownItem } from '../../ui/dropdown';
6
+ import { Separator } from '../../layout/separator';
7
+
8
+ type MetricsTableOptionsProps = {
9
+ onViewAllClick?: () => void;
10
+ onResetClick?: () => void;
11
+ onExportClick?: () => void;
12
+ } & ComponentProps<typeof Dropdown>;
13
+
14
+ export const MetricsTableOptions = (props: MetricsTableOptionsProps) => {
15
+ const { onViewAllClick, onResetClick, onExportClick, ...restProps } = props;
16
+
17
+ return (
18
+ <Dropdown glyph="more-vertical" {...restProps}>
19
+ {onResetClick && <DropdownItem onClick={onResetClick}>{I18N.RESET_FILTERS}</DropdownItem>}
20
+ {onViewAllClick && <DropdownItem onClick={onViewAllClick}>{I18N.VIEW_ALL}</DropdownItem>}
21
+ {((onResetClick || onViewAllClick) && onExportClick) && <Separator />}
22
+ {onExportClick && <DropdownItem onClick={onExportClick}>{I18N.EXPORT}</DropdownItem>}
23
+ </Dropdown>
24
+ );
25
+ };
@@ -0,0 +1 @@
1
+ export * from './preview-source';
@@ -0,0 +1,28 @@
1
+ .root {
2
+ position: relative;
3
+ }
4
+
5
+ .toolbar {
6
+ position: absolute;
7
+ z-index: 2;
8
+ top: var(--space-xsmall);
9
+ right: var(--space-xsmall);
10
+ }
11
+
12
+ /** fix control group with nested components */
13
+ .downloadTooltip > a {
14
+ border-top-left-radius: inherit;
15
+ border-bottom-left-radius: inherit;
16
+ }
17
+
18
+ .downloadTooltip > a:hover,
19
+ .downloadTooltip > a:active,
20
+ .downloadTooltip > a:focus {
21
+ box-shadow: none;
22
+ }
23
+
24
+ .content {
25
+ position: relative;
26
+ z-index: 1;
27
+ height: 100%;
28
+ }
@@ -0,0 +1,35 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+
3
+ import { getWrapperDecorator } from '../../stories';
4
+ import { PreviewSource } from '.';
5
+
6
+ const meta: Meta<typeof PreviewSource> = {
7
+ title: 'Components/PreviewSource',
8
+ component: PreviewSource,
9
+ decorators: [getWrapperDecorator()],
10
+ };
11
+
12
+ export default meta;
13
+
14
+ type Story = StoryObj<typeof meta>;
15
+
16
+ const SOURCE = JSON.stringify(
17
+ [
18
+ {
19
+ label: 'Hey',
20
+ runs: [
21
+ { value: 1, name: 'a' },
22
+ { value: 2, name: 'b' },
23
+ ],
24
+ },
25
+ ],
26
+ null,
27
+ 2,
28
+ );
29
+
30
+ export const Default: Story = {
31
+ args: {
32
+ source: SOURCE,
33
+ download: 'bundle-stats--assets.json',
34
+ },
35
+ };
@@ -0,0 +1,47 @@
1
+ import type { ComponentProps } from 'react';
2
+ import React from 'react';
3
+ import cx from 'classnames';
4
+
5
+ import { Button, BUTTON_GLYPHS } from '../../ui/button';
6
+ import { CopyToClipboard } from '../../ui/copy-to-clipboard';
7
+ import { ControlGroup } from '../../ui/control-group';
8
+ import { Tooltip } from '../../ui/tooltip';
9
+ import { Textarea } from '../../ui/textarea';
10
+ import css from './preview-source.module.css';
11
+
12
+ type PreviewSourceProps = {
13
+ source: string;
14
+ /**
15
+ * Link download attribute
16
+ * @example
17
+ * bundle-stats--assets.json
18
+ */
19
+ download?: string;
20
+ } & ComponentProps<'div'>;
21
+
22
+ export const PreviewSource = (props: PreviewSourceProps) => {
23
+ const { className = '', source, download = false, ...restProps } = props;
24
+
25
+ return (
26
+ <div className={cx(css.root, className)} {...restProps}>
27
+ <ControlGroup className={css.toolbar}>
28
+ <CopyToClipboard outline size="small" text={source} />
29
+ {download && (
30
+ <Tooltip title={`Download ${download}`} className={css.downloadTooltip}>
31
+ <Button
32
+ outline
33
+ rightGlyph={BUTTON_GLYPHS.DOWNLOAD}
34
+ size="small"
35
+ as="a"
36
+ href={`data:application/json,${encodeURIComponent(source)}`}
37
+ download={download}
38
+ />
39
+ </Tooltip>
40
+ )}
41
+ </ControlGroup>
42
+ <Textarea previewSource readOnly rows={12} className={css.content}>
43
+ {source}
44
+ </Textarea>
45
+ </div>
46
+ );
47
+ };
@@ -176,6 +176,7 @@
176
176
  --color-outline-warning: var(--color-yellow-200);
177
177
  --color-outline-danger: var(--color-red-100);
178
178
 
179
+ --color-backdrop: rgba(0, 0, 0, 0.05);
179
180
  --color-shadow: rgba(0, 0, 0, 0.2);
180
181
 
181
182
  --transition-duration-in: 0.2s;
package/src/i18n.js CHANGED
@@ -21,6 +21,10 @@ export default {
21
21
 
22
22
  RESET_FILTERS: 'Reset filters',
23
23
  VIEW_ALL: 'View all entries',
24
+ EXPORT: 'Export',
25
+
26
+ COPY_TO_CLIPBOARD: 'Copy to clipboard',
27
+ COPY_TO_CLIPBOARD_DONE: 'Copied to clipboard',
24
28
 
25
29
  GITHUB_LINK_TITLE: 'View BundleStats project on GitHub',
26
30
  REPORT_ISSUE: 'Report issue',
@@ -1,5 +1,4 @@
1
1
  import React from 'react';
2
-
3
2
  import cx from 'classnames';
4
3
  import { Button as ButtonBaseComponent } from 'ariakit/button';
5
4
 
@@ -21,6 +20,8 @@ export const BUTTON_KIND = {
21
20
  SUCCES: 'success',
22
21
  } as const;
23
22
 
23
+ export const BUTTON_GLYPHS = BaseIcon.ICONS;
24
+
24
25
  type Size = (typeof BUTTON_SIZE)[keyof typeof BUTTON_SIZE];
25
26
  type Kind = (typeof BUTTON_KIND)[keyof typeof BUTTON_KIND];
26
27
 
@@ -0,0 +1,15 @@
1
+ .root {
2
+ white-space: nowrap;
3
+ /** override tooltip styles */
4
+ color: var(--color-text-ultra-light) !important;
5
+ }
6
+
7
+ /** done state */
8
+ .root svg {
9
+ transition: var(--ui-transition-out);
10
+ }
11
+
12
+ .done svg {
13
+ color: var(--color-success);
14
+ transition: var(--ui-transition-in);
15
+ }
@@ -0,0 +1,33 @@
1
+ import { Meta, StoryObj } from '@storybook/react';
2
+ import { CopyToClipboard } from '.';
3
+
4
+ const meta: Meta<typeof CopyToClipboard> = {
5
+ title: 'UI/CopyToClipboard',
6
+ component: CopyToClipboard,
7
+ };
8
+
9
+ export default meta;
10
+
11
+ type Story = StoryObj<Meta>;
12
+
13
+ export const Default: Story = {
14
+ args: {
15
+ text: 'Lorem ipsum dolor sit amd',
16
+ },
17
+ };
18
+
19
+ export const Inline: Story = {
20
+ args: {
21
+ ...Default.args,
22
+ children: Default.args?.text,
23
+ outline: true,
24
+ size: 'small',
25
+ },
26
+ };
27
+
28
+ export const Outline: Story = {
29
+ args: {
30
+ ...Default.args,
31
+ outline: true,
32
+ },
33
+ };
@@ -0,0 +1,53 @@
1
+ import type { ComponentProps } from 'react';
2
+ import React, { useCallback, useState } from 'react';
3
+ import cx from 'classnames';
4
+ import { useCopyToClipboard } from 'react-use';
5
+
6
+ import I18N from '../../i18n';
7
+ import { Button } from '../button';
8
+ import { Icon } from '../icon';
9
+ import { Tooltip } from '../tooltip';
10
+ import css from './copy-to-clipboard.module.css';
11
+
12
+ export type CopyToClipboardProps = {
13
+ /**
14
+ * Text to copy to clipboard
15
+ */
16
+ text: string;
17
+ } & ComponentProps<typeof Button>;
18
+
19
+ export const CopyToClipboard = (props: CopyToClipboardProps) => {
20
+ const { className = '', children, text, ...restProps } = props;
21
+
22
+ const [, copyToClipboard] = useCopyToClipboard();
23
+ const [doneTimeout, setDoneTimeout] = useState<ReturnType<typeof setTimeout> | null>();
24
+
25
+ const handleOnClick = useCallback(() => {
26
+ if (doneTimeout) {
27
+ clearTimeout(doneTimeout);
28
+ setDoneTimeout(null);
29
+ }
30
+
31
+ setDoneTimeout(null);
32
+ copyToClipboard(text);
33
+
34
+ const done = setTimeout(() => setDoneTimeout(null), 2000);
35
+ setDoneTimeout(done);
36
+ }, [text, doneTimeout, setDoneTimeout]);
37
+
38
+ const rootClassName = cx(css.root, className, doneTimeout && css.done);
39
+
40
+ return (
41
+ <Tooltip
42
+ title={doneTimeout ? I18N.COPY_TO_CLIPBOARD_DONE : I18N.COPY_TO_CLIPBOARD}
43
+ as={Button}
44
+ type="button"
45
+ rightGlyph={doneTimeout ? Icon.ICONS.CLIPBOARD_CHECK : Icon.ICONS.CLIPBOARD}
46
+ onClick={handleOnClick}
47
+ className={rootClassName}
48
+ {...restProps}
49
+ >
50
+ {children}
51
+ </Tooltip>
52
+ );
53
+ };
@@ -0,0 +1 @@
1
+ export * from './copy-to-clipboard';
@@ -0,0 +1,89 @@
1
+ .backdrop {
2
+ display: flex;
3
+ align-items: center;
4
+ justify-content: center;
5
+ z-index: 99999 !important;
6
+ }
7
+
8
+ .backdrop::before {
9
+ content: ' ';
10
+ position: absolute;
11
+ left: 0;
12
+ top: 0;
13
+ width: 100%;
14
+ height: 100%;
15
+ background: var(--color-backdrop);
16
+ filter:
17
+ }
18
+
19
+ .dialog {
20
+ --dialog-header-height: var(--space-medium);
21
+ --dialog-vspace: var(--space-xsmall);
22
+ --dialog-padding: var(--space-xsmall);
23
+
24
+ width: calc(100vw - 2 * var(--dialog-padding));
25
+ position: relative;
26
+ border-radius: var(--radius-medium);
27
+ padding: var(--dialog-padding);
28
+ background: var(--color-background);
29
+ box-shadow: 0 0 var(--space-medium) var(--color-shadow);
30
+ }
31
+
32
+ .header {
33
+ min-height: var(--dialog-header-height);
34
+ }
35
+
36
+ .header:empty {
37
+ display: none;
38
+ }
39
+
40
+ .headerClose {
41
+ position: absolute;
42
+ right: var(--space-xxsmall);
43
+ top: var(--space-xxsmall);
44
+ padding: var(--space-xxxsmall);
45
+ flex: 0 0 auto;
46
+ color: var(--color-outline-dark);
47
+ }
48
+
49
+ .headerClose:hover,
50
+ .headerClose:focus,
51
+ .headerClose:active {
52
+ color: var(--color-text-ultra-light);
53
+ }
54
+
55
+ .content {
56
+ width: 100%;
57
+ max-height: calc(100vh - 2 * var(--space-medium) - 2 * var(--dialog-padding) - var(--dialog-header-height) - var(--dialog-vspace));
58
+ padding-bottom: 0.3em; /* extra space to compensate for the title line-height and allow to show focus state correctly on buttons */
59
+ overflow: auto;
60
+ }
61
+
62
+ /** Width variation */
63
+ .width--narrow {
64
+ max-width: 480px;
65
+ }
66
+
67
+ .width--medium {
68
+ max-width: 748px;
69
+ }
70
+
71
+ .width--wide {
72
+ max-width: 1024px;
73
+ }
74
+
75
+ .width--extra-wide {
76
+ max-width: var(--max-width);
77
+ }
78
+
79
+ /** Center variation */
80
+ .center {
81
+ text-align: center;
82
+ }
83
+
84
+ @media (min-width: 768px) {
85
+ .dialog {
86
+ --dialog-vspace: var(--space-medium);
87
+ --dialog-padding: var(--space-large);
88
+ }
89
+ }
@@ -0,0 +1,67 @@
1
+ import React, { Fragment } from 'react';
2
+ import type { Meta, StoryObj } from '@storybook/react';
3
+
4
+ import { Dropdown, DropdownItem } from '../dropdown';
5
+ import { Button } from '../button';
6
+ import { Dialog, useDialogState } from '.';
7
+
8
+ const meta: Meta<typeof Dialog> = {
9
+ title: 'UI/Dialog',
10
+ component: Dialog,
11
+ };
12
+
13
+ export default meta;
14
+
15
+ type Story = StoryObj<typeof meta>;
16
+
17
+ export const Default: Story = {
18
+ render: function DefaultDialog() {
19
+ const dialog = useDialogState();
20
+
21
+ return (
22
+ <>
23
+ <Button type="button" onClick={dialog.toggle}>
24
+ Open
25
+ </Button>
26
+ <Dialog title="Title" state={dialog}>
27
+ <div style={{ maxWidth: 480 }}>
28
+ <Dropdown label="Select option">
29
+ <DropdownItem>Option 1</DropdownItem>
30
+ <DropdownItem>Option 2</DropdownItem>
31
+ <DropdownItem>Option 3</DropdownItem>
32
+ </Dropdown>
33
+ <br />
34
+ <br />
35
+ {[1, 2, 3].map((item) => (
36
+ <Fragment key={item}>
37
+ <p>
38
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec vulputate, lectus
39
+ vitae tristique facilisis, quam ex vulputate risus, vitae mattis eros diam quis
40
+ velit. Pellentesque habitant morbi tristique senectus et netus et malesuada fames
41
+ ac turpis egestas. Sed et velit tortor. Suspendisse risus ipsum, convallis
42
+ ultricies justo vel, auctor semper est. Sed viverra pulvinar odio eget pharetra.
43
+ In eleifend felis nec odio convallis, eleifend rhoncus diam maximus. Cras diam
44
+ libero, egestas blandit vestibulum nec, volutpat a mi. Etiam blandit turpis ac
45
+ nulla molestie vehicula. Duis bibendum a nisl sollicitudin lobortis. Vestibulum
46
+ lobortis eget justo ac elementum. Vestibulum vel lorem arcu. Etiam sagittis luctus
47
+ justo eu interdum. Ut lobortis tempus turpis, vitae auctor elit congue id.
48
+ </p>
49
+ <br />
50
+ <p>
51
+ Nulla eu ullamcorper quam. Mauris rhoncus tristique dui in rutrum. Ut eget justo a
52
+ lorem efficitur consequat. Nunc sed erat at ipsum convallis iaculis at ac lacus.
53
+ Etiam pellentesque felis quis maximus ultrices. Vivamus non pretium quam. Aenean
54
+ vel nunc fringilla, lacinia felis a, vehicula purus. Vestibulum in varius purus.
55
+ Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos
56
+ himenaeos. Quisque laoreet ultrices ligula eget iaculis. Vivamus lacinia ante
57
+ mattis lorem ultricies consequat. Integer dui odio, semper ac neque vitae,
58
+ elementum hendrerit ante. Lorem ipsum dolor sit amet, consectetur adipiscing elit.
59
+ </p>
60
+ </Fragment>
61
+ ))}
62
+ </div>
63
+ </Dialog>
64
+ </>
65
+ );
66
+ },
67
+ };
@@ -0,0 +1,62 @@
1
+ import type { ComponentProps, ReactNode } from 'react';
2
+ import React from 'react';
3
+ import cx from 'classnames';
4
+ import { Dialog as AriakitDialog, DialogDismiss as AriakitDialogDismiss } from 'ariakit/dialog';
5
+
6
+ import { Stack } from '../../layout/stack';
7
+ import { BUTTON_GLYPHS, Button } from '../button';
8
+ import css from './dialog.module.css';
9
+
10
+ export { useDialogState } from 'ariakit/dialog';
11
+
12
+ type AriakitDialogProps = ComponentProps<typeof AriakitDialog>;
13
+
14
+ type DialogProps = {
15
+ title?: ReactNode;
16
+ width?: 'narrow' | 'medium' | 'wide' | 'extra-wide';
17
+ center?: boolean;
18
+ modal?: AriakitDialogProps['modal'];
19
+ state: AriakitDialogProps['state'];
20
+ } & Omit<ComponentProps<'div'>, 'title'>;
21
+
22
+ export const Dialog = (props: DialogProps) => {
23
+ const {
24
+ state,
25
+ className = '',
26
+ title = '',
27
+ width = 'medium',
28
+ center = false,
29
+ modal = false,
30
+ children,
31
+ } = props;
32
+
33
+ const rootClassName = cx(css.dialog, width && css[`width--${width}`], center && css.center);
34
+
35
+ return (
36
+ <AriakitDialog
37
+ state={state}
38
+ modal={modal}
39
+ backdrop
40
+ backdropProps={{
41
+ className: css.backdrop,
42
+ }}
43
+ portal
44
+ className={rootClassName}
45
+ >
46
+ {!modal && (
47
+ <AriakitDialogDismiss as={Button} glyph={BUTTON_GLYPHS.CLOSE} className={css.headerClose}>
48
+ {/* null content to prevent DialogDismiss to render the default "x" */}
49
+ {null}
50
+ </AriakitDialogDismiss>
51
+ )}
52
+ <Stack space="medium">
53
+ {title && (
54
+ <header className={css.header}>
55
+ {typeof title === 'string' ? <h2>{title}</h2> : title}
56
+ </header>
57
+ )}
58
+ <div className={cx(css.content, className)}>{children}</div>
59
+ </Stack>
60
+ </AriakitDialog>
61
+ );
62
+ };
@@ -0,0 +1 @@
1
+ export * from './dialog';
@@ -13,6 +13,9 @@ const ICONS = {
13
13
  CLOSE: 'close',
14
14
  CLOCK: 'clock',
15
15
  COMMIT: 'commit',
16
+ CLIPBOARD: 'clipboard',
17
+ CLIPBOARD_CHECK: 'clipboard-check',
18
+ DOWNLOAD: 'download',
16
19
  ERROR: 'error',
17
20
  EXTERNAL_LINK: 'external-link',
18
21
  FILTER: 'filter',
package/src/ui/index.js CHANGED
@@ -1,7 +1,9 @@
1
1
  export { Alert } from './alert';
2
2
  export { Button } from './button';
3
+ export { CopyToClipboard } from './copy-to-clipboard';
3
4
  export { Container } from './container';
4
5
  export { ControlGroup } from './control-group';
6
+ export { Dialog } from './dialog';
5
7
  export { Dropdown } from './dropdown';
6
8
  export { EmptySet } from './empty-set';
7
9
  export { FileName } from './file-name';
@@ -15,4 +17,5 @@ export { Skeleton } from './skeleton';
15
17
  export { Table } from './table';
16
18
  export { Tabs } from './tabs';
17
19
  export { Tag } from './tag';
20
+ export { Textarea } from './textarea';
18
21
  export { Tooltip } from './tooltip';
@@ -4,7 +4,7 @@ import cx from 'classnames';
4
4
 
5
5
  import css from './tabs.module.css';
6
6
 
7
- const Item = ({ isTabActive, as: Component, className,...restProps }) => (
7
+ const Item = ({ isTabActive, as: Component = 'span', className = '', ...restProps }) => (
8
8
  <Component className={cx(css.item, className, isTabActive && css.itemActive)} {...restProps} />
9
9
  );
10
10
 
@@ -20,7 +20,7 @@ Item.propTypes = {
20
20
  as: PropTypes.elementType,
21
21
  };
22
22
 
23
- export const Tabs = ({ className, ...restProps }) => (
23
+ export const Tabs = ({ className = '', ...restProps }) => (
24
24
  <nav className={cx(css.root, className)} {...restProps} />
25
25
  );
26
26
 
@@ -0,0 +1 @@
1
+ export * from './textarea';