tinywidgets 1.3.12 → 1.5.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.
- package/package.json +15 -11
- package/src/common/functions.tsx +18 -7
- package/src/components/App/index.css.ts +0 -9
- package/src/components/App/index.tsx +7 -6
- package/src/components/Axis/index.css.ts +13 -0
- package/src/components/Axis/index.tsx +82 -0
- package/src/components/ImageLabel/index.css.ts +5 -0
- package/src/components/ImageLabel/index.tsx +72 -0
- package/src/components/Loading/index.css.ts +84 -0
- package/src/components/Loading/index.tsx +137 -0
- package/src/components/Metric/index.css.ts +0 -2
- package/src/components/Metric/index.tsx +3 -2
- package/src/components/Tag/index.css.ts +0 -2
- package/src/components/Tag/index.tsx +3 -2
- package/src/index.ts +3 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tinywidgets",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"author": "jamesgpearce",
|
|
5
5
|
"repository": "github:tinyplex/tinywidgets",
|
|
6
6
|
"license": "MIT",
|
|
@@ -22,26 +22,30 @@
|
|
|
22
22
|
"@eslint/js": "^9.30.1",
|
|
23
23
|
"@types/react": "^19.2.14",
|
|
24
24
|
"@types/react-dom": "^19.2.3",
|
|
25
|
-
"cspell": "^
|
|
25
|
+
"cspell": "^10.0.0",
|
|
26
26
|
"eslint": "^9.30.1",
|
|
27
27
|
"eslint-plugin-import": "^2.32.0",
|
|
28
28
|
"eslint-plugin-react": "^7.37.5",
|
|
29
29
|
"eslint-plugin-react-hooks": "^5.2.0",
|
|
30
|
-
"globals": "^17.
|
|
31
|
-
"prettier": "^3.8.
|
|
30
|
+
"globals": "^17.5.0",
|
|
31
|
+
"prettier": "^3.8.3",
|
|
32
32
|
"prettier-plugin-organize-imports": "^4.3.0",
|
|
33
|
-
"
|
|
34
|
-
"
|
|
33
|
+
"react": "^19.2.5",
|
|
34
|
+
"react-dom": "^19.2.5",
|
|
35
|
+
"typescript": "^6.0.3",
|
|
36
|
+
"typescript-eslint": "^8.58.2"
|
|
35
37
|
},
|
|
36
38
|
"exports": {
|
|
37
39
|
".": "./src/index.ts",
|
|
38
40
|
"./css": "./src/index.css.ts"
|
|
39
41
|
},
|
|
40
42
|
"dependencies": {
|
|
41
|
-
"@vanilla-extract/css": "^1.
|
|
42
|
-
"lucide-react": "^
|
|
43
|
+
"@vanilla-extract/css": "^1.20.1",
|
|
44
|
+
"lucide-react": "^1.8.0",
|
|
45
|
+
"tinybase": "^8.2.0"
|
|
46
|
+
},
|
|
47
|
+
"peerDependencies": {
|
|
43
48
|
"react": "^19.2.4",
|
|
44
|
-
"react-dom": "^19.2.4"
|
|
45
|
-
"tinybase": "^8.0.2"
|
|
49
|
+
"react-dom": "^19.2.4"
|
|
46
50
|
}
|
|
47
|
-
}
|
|
51
|
+
}
|
package/src/common/functions.tsx
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import type {StyleRule} from '@vanilla-extract/css';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
createElement,
|
|
4
|
+
isValidElement,
|
|
5
|
+
type ComponentType,
|
|
6
|
+
type ReactNode,
|
|
7
|
+
} from 'react';
|
|
3
8
|
import {screens} from '../css/screens';
|
|
4
9
|
|
|
5
10
|
/**
|
|
@@ -30,12 +35,18 @@ export const classNames = (
|
|
|
30
35
|
export const renderComponentOrNode = (
|
|
31
36
|
ComponentOrNode: ComponentType | ReactNode,
|
|
32
37
|
fallback: ReactNode = null,
|
|
33
|
-
) =>
|
|
34
|
-
ComponentOrNode
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
38
|
+
): ReactNode => {
|
|
39
|
+
if (ComponentOrNode == null) {
|
|
40
|
+
return fallback;
|
|
41
|
+
}
|
|
42
|
+
if (isValidElement(ComponentOrNode)) {
|
|
43
|
+
return ComponentOrNode;
|
|
44
|
+
}
|
|
45
|
+
if (['string', 'number', 'boolean'].includes(typeof ComponentOrNode)) {
|
|
46
|
+
return ComponentOrNode as string | number | boolean;
|
|
47
|
+
}
|
|
48
|
+
return createElement(ComponentOrNode as ComponentType);
|
|
49
|
+
};
|
|
39
50
|
|
|
40
51
|
export const large = (style: StyleRule) => ({
|
|
41
52
|
'@media': {[`screen and (min-width: ${screens.large}px)`]: style},
|
|
@@ -12,9 +12,7 @@ export const appLayout = style({
|
|
|
12
12
|
});
|
|
13
13
|
|
|
14
14
|
export const header = style({
|
|
15
|
-
display: 'flex',
|
|
16
15
|
justifyContent: 'space-between',
|
|
17
|
-
alignItems: 'center',
|
|
18
16
|
gap: dimensions.padding,
|
|
19
17
|
padding: dimensions.padding,
|
|
20
18
|
position: 'fixed',
|
|
@@ -29,9 +27,7 @@ export const header = style({
|
|
|
29
27
|
});
|
|
30
28
|
|
|
31
29
|
export const topNav = style({
|
|
32
|
-
display: 'flex',
|
|
33
30
|
justifyContent: 'space-between',
|
|
34
|
-
alignItems: 'center',
|
|
35
31
|
gap: dimensions.padding,
|
|
36
32
|
flex: 1,
|
|
37
33
|
});
|
|
@@ -39,9 +35,6 @@ export const topNav = style({
|
|
|
39
35
|
export const sideNavButton = style(large({display: 'none!important'}));
|
|
40
36
|
|
|
41
37
|
export const title = style({
|
|
42
|
-
display: 'flex',
|
|
43
|
-
alignItems: 'center',
|
|
44
|
-
gap: dimensions.padding,
|
|
45
38
|
...large({
|
|
46
39
|
width: `calc(${dimensions.sideNavWidth} - 2 * ${dimensions.padding})`,
|
|
47
40
|
}),
|
|
@@ -84,9 +77,7 @@ export const mainHasFooter = style({
|
|
|
84
77
|
});
|
|
85
78
|
|
|
86
79
|
export const footer = style({
|
|
87
|
-
display: 'flex',
|
|
88
80
|
justifyContent: 'right',
|
|
89
|
-
alignItems: 'center',
|
|
90
81
|
gap: dimensions.padding,
|
|
91
82
|
paddingLeft: dimensions.padding,
|
|
92
83
|
paddingRight: dimensions.padding,
|
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
useSideNavIsOpen,
|
|
26
26
|
useToggleSideNavIsOpenCallback,
|
|
27
27
|
} from '../../stores/SessionStore.tsx';
|
|
28
|
+
import {Axis} from '../Axis/index.tsx';
|
|
28
29
|
import {Button} from '../Button/index.tsx';
|
|
29
30
|
import {
|
|
30
31
|
app,
|
|
@@ -165,7 +166,7 @@ const Layout = ({
|
|
|
165
166
|
<LayoutContext.Provider value={{portal: ref.current}}>
|
|
166
167
|
{sessionStoreIsReady && routeStoreIsReady && localStoreIsReady ? (
|
|
167
168
|
<>
|
|
168
|
-
<header className={header}>
|
|
169
|
+
<Axis as="header" className={header}>
|
|
169
170
|
{hasSideNav ? (
|
|
170
171
|
<Button
|
|
171
172
|
variant="icon"
|
|
@@ -177,10 +178,10 @@ const Layout = ({
|
|
|
177
178
|
<nav className={title}>
|
|
178
179
|
{renderComponentOrNode(titleComponentOrNode)}
|
|
179
180
|
</nav>
|
|
180
|
-
<nav className={topNav}>
|
|
181
|
+
<Axis as="nav" className={topNav}>
|
|
181
182
|
{renderComponentOrNode(topNavLeftComponentOrNode, <div />)}
|
|
182
183
|
{renderComponentOrNode(topNavRightComponentOrNode, <div />)}
|
|
183
|
-
</
|
|
184
|
+
</Axis>
|
|
184
185
|
<Button
|
|
185
186
|
variant="icon"
|
|
186
187
|
onClick={toggleDarkChoice}
|
|
@@ -197,7 +198,7 @@ const Layout = ({
|
|
|
197
198
|
{renderComponentOrNode(sideNavComponentOrNode)}
|
|
198
199
|
</nav>
|
|
199
200
|
) : null}
|
|
200
|
-
</
|
|
201
|
+
</Axis>
|
|
201
202
|
<main
|
|
202
203
|
className={classNames(
|
|
203
204
|
main,
|
|
@@ -208,9 +209,9 @@ const Layout = ({
|
|
|
208
209
|
{renderComponentOrNode(mainComponentOrNode)}
|
|
209
210
|
</main>
|
|
210
211
|
{hasFooter ? (
|
|
211
|
-
<footer className={footer}>
|
|
212
|
+
<Axis as="footer" className={footer}>
|
|
212
213
|
{renderComponentOrNode(footerComponentOrNode)}
|
|
213
|
-
</
|
|
214
|
+
</Axis>
|
|
214
215
|
) : null}
|
|
215
216
|
</>
|
|
216
217
|
) : null}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import {style, styleVariants} from '@vanilla-extract/css';
|
|
2
|
+
|
|
3
|
+
export const axis = style({
|
|
4
|
+
display: 'flex',
|
|
5
|
+
alignItems: 'center',
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
export const axisVariants = styleVariants({
|
|
9
|
+
horizontal: {},
|
|
10
|
+
vertical: {
|
|
11
|
+
flexDirection: 'column',
|
|
12
|
+
},
|
|
13
|
+
});
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import type {MouseEventHandler, ReactNode} from 'react';
|
|
2
|
+
import {classNames} from '../../common/functions';
|
|
3
|
+
import {axis, axisVariants} from './index.css';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The `Axis` component displays its children along a flex axis, aligning them
|
|
7
|
+
* in the center of the cross-axis.
|
|
8
|
+
*
|
|
9
|
+
* This is useful for compact layouts where icons, avatars, images, and text
|
|
10
|
+
* should share a common visual center line, or for stacked layouts that should
|
|
11
|
+
* remain horizontally centered.
|
|
12
|
+
*
|
|
13
|
+
* @param props The props for the component.
|
|
14
|
+
* @returns The Axis component.
|
|
15
|
+
* @example
|
|
16
|
+
* ```tsx
|
|
17
|
+
* <Axis>
|
|
18
|
+
* <Image src="/favicon.svg" variant="logo" />
|
|
19
|
+
* TinyWidgets
|
|
20
|
+
* </Axis>
|
|
21
|
+
* ```
|
|
22
|
+
* This example shows a logo image and text aligned along the same
|
|
23
|
+
* horizontal axis.
|
|
24
|
+
* @example
|
|
25
|
+
* ```tsx
|
|
26
|
+
* <Axis variant="vertical">
|
|
27
|
+
* <Image src="/favicon.svg" variant="logo" />
|
|
28
|
+
* TinyWidgets
|
|
29
|
+
* </Axis>
|
|
30
|
+
* ```
|
|
31
|
+
* This example shows the same content arranged on a vertical axis.
|
|
32
|
+
* @icon Lucide.AlignCenterHorizontal
|
|
33
|
+
*/
|
|
34
|
+
export const Axis = ({
|
|
35
|
+
as: Component = 'div',
|
|
36
|
+
variant = 'horizontal',
|
|
37
|
+
className,
|
|
38
|
+
title,
|
|
39
|
+
onClick,
|
|
40
|
+
children,
|
|
41
|
+
}: {
|
|
42
|
+
/**
|
|
43
|
+
* The HTML element used to wrap the axis, one of:
|
|
44
|
+
* - `div`
|
|
45
|
+
* - `nav`
|
|
46
|
+
* - `span`
|
|
47
|
+
* - `h1`
|
|
48
|
+
* - `header`
|
|
49
|
+
* - `footer`
|
|
50
|
+
*/
|
|
51
|
+
readonly as?: 'div' | 'nav' | 'span' | 'h1' | 'header' | 'footer';
|
|
52
|
+
/**
|
|
53
|
+
* A variant of the axis, one of:
|
|
54
|
+
* - `horizontal`
|
|
55
|
+
* - `vertical`
|
|
56
|
+
*/
|
|
57
|
+
readonly variant?: keyof typeof axisVariants;
|
|
58
|
+
/**
|
|
59
|
+
* An extra CSS class name for the component.
|
|
60
|
+
*/
|
|
61
|
+
readonly className?: string;
|
|
62
|
+
/**
|
|
63
|
+
* Alternative text shown when the user hovers over the component.
|
|
64
|
+
*/
|
|
65
|
+
readonly title?: string;
|
|
66
|
+
/**
|
|
67
|
+
* A handler called when the user clicks on the component.
|
|
68
|
+
*/
|
|
69
|
+
readonly onClick?: MouseEventHandler<HTMLElement>;
|
|
70
|
+
/**
|
|
71
|
+
* The children of the component, arranged along the selected axis.
|
|
72
|
+
*/
|
|
73
|
+
readonly children: ReactNode;
|
|
74
|
+
}) => (
|
|
75
|
+
<Component
|
|
76
|
+
className={classNames(axis, axisVariants[variant], className)}
|
|
77
|
+
title={title}
|
|
78
|
+
onClick={onClick}
|
|
79
|
+
>
|
|
80
|
+
{children}
|
|
81
|
+
</Component>
|
|
82
|
+
);
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type {ComponentType, MouseEventHandler, ReactNode} from 'react';
|
|
2
|
+
import {classNames, renderComponentOrNode} from '../../common/functions';
|
|
3
|
+
import {Axis} from '../Axis';
|
|
4
|
+
import {imageLabel} from './index.css';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The `ImageLabel` component displays an image and a text label along a shared
|
|
8
|
+
* horizontal axis.
|
|
9
|
+
*
|
|
10
|
+
* It is useful for common UI patterns such as brand marks, avatars with names,
|
|
11
|
+
* or any compact media-and-label combination.
|
|
12
|
+
*
|
|
13
|
+
* @param props The props for the component.
|
|
14
|
+
* @returns The ImageLabel component.
|
|
15
|
+
* @example
|
|
16
|
+
* ```tsx
|
|
17
|
+
* <ImageLabel
|
|
18
|
+
* image={<Image src="/favicon.svg" variant="avatar" />}
|
|
19
|
+
* text="TinyWidgets"
|
|
20
|
+
* />
|
|
21
|
+
* ```
|
|
22
|
+
* This example shows a logo image with a text label.
|
|
23
|
+
* @example
|
|
24
|
+
* ```tsx
|
|
25
|
+
* <ImageLabel
|
|
26
|
+
* as="h1"
|
|
27
|
+
* image={<Image src="/favicon.svg" variant="logo"/>}
|
|
28
|
+
* text={<b>TinyWidgets</b>}
|
|
29
|
+
* />
|
|
30
|
+
* ```
|
|
31
|
+
* This example shows the component used as a heading with rich text content.
|
|
32
|
+
* @icon Lucide.Captions
|
|
33
|
+
*/
|
|
34
|
+
export const ImageLabel = ({
|
|
35
|
+
as = 'div',
|
|
36
|
+
image,
|
|
37
|
+
text,
|
|
38
|
+
className,
|
|
39
|
+
onClick,
|
|
40
|
+
}: {
|
|
41
|
+
/**
|
|
42
|
+
* The HTML element used to wrap the image label, one of:
|
|
43
|
+
* - `div`
|
|
44
|
+
* - `nav`
|
|
45
|
+
* - `span`
|
|
46
|
+
* - `h1`
|
|
47
|
+
* - `header`
|
|
48
|
+
* - `footer`
|
|
49
|
+
*/
|
|
50
|
+
readonly as?: 'div' | 'nav' | 'span' | 'h1' | 'header' | 'footer';
|
|
51
|
+
/**
|
|
52
|
+
* A component or element which renders the image for the label.
|
|
53
|
+
*/
|
|
54
|
+
readonly image: ComponentType | ReactNode;
|
|
55
|
+
/**
|
|
56
|
+
* A component, element, or string which renders the label text.
|
|
57
|
+
*/
|
|
58
|
+
readonly text: ComponentType | ReactNode;
|
|
59
|
+
/**
|
|
60
|
+
* An extra CSS class name for the component.
|
|
61
|
+
*/
|
|
62
|
+
readonly className?: string;
|
|
63
|
+
/**
|
|
64
|
+
* A handler called when the user clicks on the component.
|
|
65
|
+
*/
|
|
66
|
+
readonly onClick?: MouseEventHandler<HTMLElement>;
|
|
67
|
+
}) => (
|
|
68
|
+
<Axis as={as} className={classNames(imageLabel, className)} onClick={onClick}>
|
|
69
|
+
{renderComponentOrNode(image)}
|
|
70
|
+
{renderComponentOrNode(text)}
|
|
71
|
+
</Axis>
|
|
72
|
+
);
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import {keyframes, style} from '@vanilla-extract/css';
|
|
2
|
+
import {colors} from '../../css/colors.css';
|
|
3
|
+
import {dimensions} from '../../css/dimensions.css';
|
|
4
|
+
|
|
5
|
+
const spin = keyframes({
|
|
6
|
+
from: {transform: 'rotate(0deg)'},
|
|
7
|
+
to: {transform: 'rotate(360deg)'},
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
const shimmer = keyframes({
|
|
11
|
+
from: {transform: 'translateX(-100%)'},
|
|
12
|
+
to: {transform: 'translateX(100%)'},
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
export const loading = style({
|
|
16
|
+
display: 'grid',
|
|
17
|
+
gridTemplateRows: 'auto minmax(0, 1fr)',
|
|
18
|
+
gap: `calc(${dimensions.padding} / 1.5)`,
|
|
19
|
+
alignContent: 'start',
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
export const header = style({
|
|
23
|
+
display: 'flex',
|
|
24
|
+
alignItems: 'center',
|
|
25
|
+
gap: `calc(${dimensions.padding} / 2)`,
|
|
26
|
+
color: colors.foregroundDim,
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
export const spinner = style({
|
|
30
|
+
width: dimensions.icon,
|
|
31
|
+
height: dimensions.icon,
|
|
32
|
+
borderRadius: '999px',
|
|
33
|
+
border: `2px solid ${colors.backgroundHover}`,
|
|
34
|
+
borderTopColor: colors.accent,
|
|
35
|
+
animation: `${spin} .9s linear infinite`,
|
|
36
|
+
'@media': {
|
|
37
|
+
'(prefers-reduced-motion: reduce)': {
|
|
38
|
+
animation: 'none',
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
export const label = style({
|
|
44
|
+
fontSize: '.95rem',
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
export const rows = style({
|
|
48
|
+
position: 'relative',
|
|
49
|
+
display: 'grid',
|
|
50
|
+
gap: `calc(${dimensions.padding} / 2)`,
|
|
51
|
+
minHeight: 0,
|
|
52
|
+
overflow: 'hidden',
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
export const row = style({
|
|
56
|
+
position: 'relative',
|
|
57
|
+
overflow: 'hidden',
|
|
58
|
+
height: '.875rem',
|
|
59
|
+
borderRadius: dimensions.radius,
|
|
60
|
+
background: colors.background2,
|
|
61
|
+
selectors: {
|
|
62
|
+
'&::after': {
|
|
63
|
+
content: '""',
|
|
64
|
+
position: 'absolute',
|
|
65
|
+
inset: 0,
|
|
66
|
+
background: `linear-gradient(
|
|
67
|
+
90deg,
|
|
68
|
+
transparent,
|
|
69
|
+
${colors.backgroundHover},
|
|
70
|
+
transparent
|
|
71
|
+
)`,
|
|
72
|
+
animation: `${shimmer} 1.6s ease-in-out infinite`,
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
'@media': {
|
|
76
|
+
'(prefers-reduced-motion: reduce)': {
|
|
77
|
+
selectors: {
|
|
78
|
+
'&::after': {
|
|
79
|
+
animation: 'none',
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
});
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import {useEffect, useRef, useState} from 'react';
|
|
2
|
+
import {classNames} from '../../common/functions';
|
|
3
|
+
import {
|
|
4
|
+
header,
|
|
5
|
+
label,
|
|
6
|
+
loading,
|
|
7
|
+
row,
|
|
8
|
+
rows as rowsClass,
|
|
9
|
+
spinner,
|
|
10
|
+
} from './index.css';
|
|
11
|
+
|
|
12
|
+
const WIDTHS = ['100%', '92%', '84%', '96%', '72%', '88%', '67%', '81%'];
|
|
13
|
+
const DEFAULT_ROW_COUNT = 5;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The `Loading` component displays a compact spinner with optional shimmer rows
|
|
17
|
+
* to reserve space while content is loading.
|
|
18
|
+
*
|
|
19
|
+
* @param props The props for the component.
|
|
20
|
+
* @returns The Loading component.
|
|
21
|
+
* @example
|
|
22
|
+
* ```tsx
|
|
23
|
+
* <Loading labelText="Loading dependencies" />
|
|
24
|
+
* ```
|
|
25
|
+
* This example shows the default loading panel. Add a CSS height to the
|
|
26
|
+
* component to fit more placeholder rows automatically.
|
|
27
|
+
* @example
|
|
28
|
+
* ```tsx
|
|
29
|
+
* <>
|
|
30
|
+
* <style>{`
|
|
31
|
+
* .tallLoading {
|
|
32
|
+
* height: 12rem;
|
|
33
|
+
* }
|
|
34
|
+
* `}</style>
|
|
35
|
+
* <Loading className="tallLoading" labelText="Loading dependencies" />
|
|
36
|
+
* </>
|
|
37
|
+
* ```
|
|
38
|
+
* This example uses CSS to make the loading panel taller, allowing more
|
|
39
|
+
* placeholder rows to fit automatically.
|
|
40
|
+
* @icon Lucide.LoaderCircle
|
|
41
|
+
*/
|
|
42
|
+
export const Loading = ({
|
|
43
|
+
labelText = 'Loading',
|
|
44
|
+
className,
|
|
45
|
+
}: {
|
|
46
|
+
/**
|
|
47
|
+
* The label shown beside the spinner.
|
|
48
|
+
*/
|
|
49
|
+
readonly labelText?: string;
|
|
50
|
+
/**
|
|
51
|
+
* An extra CSS class name for the component.
|
|
52
|
+
*/
|
|
53
|
+
readonly className?: string;
|
|
54
|
+
}) => {
|
|
55
|
+
const rootRef = useRef<HTMLDivElement>(null);
|
|
56
|
+
const headerRef = useRef<HTMLDivElement>(null);
|
|
57
|
+
const rowsRef = useRef<HTMLDivElement>(null);
|
|
58
|
+
const measureRowRef = useRef<HTMLDivElement>(null);
|
|
59
|
+
|
|
60
|
+
const [rowCount, setRowCount] = useState(DEFAULT_ROW_COUNT);
|
|
61
|
+
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
const updateRowCount = () => {
|
|
64
|
+
const root = rootRef.current;
|
|
65
|
+
const header = headerRef.current;
|
|
66
|
+
const rows = rowsRef.current;
|
|
67
|
+
const measureRow = measureRowRef.current;
|
|
68
|
+
if (
|
|
69
|
+
root == null ||
|
|
70
|
+
header == null ||
|
|
71
|
+
rows == null ||
|
|
72
|
+
measureRow == null
|
|
73
|
+
) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const rootStyles = getComputedStyle(root);
|
|
78
|
+
const rowsStyles = getComputedStyle(rows);
|
|
79
|
+
const rootGap =
|
|
80
|
+
parseFloat(rootStyles.rowGap || rootStyles.gap || '0') || 0;
|
|
81
|
+
const rowsGap =
|
|
82
|
+
parseFloat(rowsStyles.rowGap || rowsStyles.gap || '0') || 0;
|
|
83
|
+
const availableHeight =
|
|
84
|
+
root.getBoundingClientRect().height -
|
|
85
|
+
header.getBoundingClientRect().height -
|
|
86
|
+
rootGap;
|
|
87
|
+
const rowHeight = measureRow.getBoundingClientRect().height;
|
|
88
|
+
|
|
89
|
+
const nextRowCount = Math.max(
|
|
90
|
+
0,
|
|
91
|
+
Math.floor((availableHeight + rowsGap) / (rowHeight + rowsGap)),
|
|
92
|
+
);
|
|
93
|
+
setRowCount((currentRowCount) =>
|
|
94
|
+
currentRowCount === nextRowCount ? currentRowCount : nextRowCount,
|
|
95
|
+
);
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
updateRowCount();
|
|
99
|
+
if (typeof ResizeObserver === 'undefined') {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const resizeObserver = new ResizeObserver(updateRowCount);
|
|
104
|
+
if (rootRef.current != null) {
|
|
105
|
+
resizeObserver.observe(rootRef.current);
|
|
106
|
+
}
|
|
107
|
+
if (headerRef.current != null) {
|
|
108
|
+
resizeObserver.observe(headerRef.current);
|
|
109
|
+
}
|
|
110
|
+
return () => resizeObserver.disconnect();
|
|
111
|
+
}, []);
|
|
112
|
+
|
|
113
|
+
return (
|
|
114
|
+
<div ref={rootRef} className={classNames(loading, className)}>
|
|
115
|
+
<div ref={headerRef} className={header}>
|
|
116
|
+
<span className={spinner} />
|
|
117
|
+
<span className={label}>{labelText}</span>
|
|
118
|
+
</div>
|
|
119
|
+
<div ref={rowsRef} className={rowsClass}>
|
|
120
|
+
<div
|
|
121
|
+
ref={measureRowRef}
|
|
122
|
+
className={row}
|
|
123
|
+
style={{position: 'absolute', width: 0, visibility: 'hidden'}}
|
|
124
|
+
/>
|
|
125
|
+
{rowCount > 0
|
|
126
|
+
? Array.from({length: rowCount}, (_, index) => (
|
|
127
|
+
<div
|
|
128
|
+
key={index}
|
|
129
|
+
className={row}
|
|
130
|
+
style={{width: WIDTHS[index % WIDTHS.length]}}
|
|
131
|
+
/>
|
|
132
|
+
))
|
|
133
|
+
: null}
|
|
134
|
+
</div>
|
|
135
|
+
</div>
|
|
136
|
+
);
|
|
137
|
+
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type {ComponentType, ReactNode} from 'react';
|
|
2
2
|
import {classNames, renderComponentOrNode} from '../../common/functions';
|
|
3
3
|
import {iconSize} from '../../css/dimensions.css';
|
|
4
|
+
import {Axis} from '../Axis';
|
|
4
5
|
import {metric, metricLabel, metricNumber} from './index.css';
|
|
5
6
|
|
|
6
7
|
/**
|
|
@@ -47,10 +48,10 @@ export const Metric = ({
|
|
|
47
48
|
readonly className?: string;
|
|
48
49
|
}) => (
|
|
49
50
|
<div className={classNames(metric, className)}>
|
|
50
|
-
<
|
|
51
|
+
<Axis className={metricLabel}>
|
|
51
52
|
{Icon ? <Icon className={iconSize} /> : null}
|
|
52
53
|
{renderComponentOrNode(titleComponentOrNode)}
|
|
53
|
-
</
|
|
54
|
+
</Axis>
|
|
54
55
|
<div className={metricNumber}>
|
|
55
56
|
{renderComponentOrNode(numberComponentOrNode)}
|
|
56
57
|
</div>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type {ComponentType, ReactNode} from 'react';
|
|
2
2
|
import {classNames, renderComponentOrNode} from '../../common/functions';
|
|
3
|
+
import {Axis} from '../Axis';
|
|
3
4
|
import {tag, tagIcon, tagVariants} from './index.css';
|
|
4
5
|
|
|
5
6
|
/**
|
|
@@ -61,12 +62,12 @@ export const Tag = ({
|
|
|
61
62
|
}) => {
|
|
62
63
|
const icon = Icon ? <Icon className={tagIcon} /> : null;
|
|
63
64
|
return (
|
|
64
|
-
<
|
|
65
|
+
<Axis
|
|
65
66
|
className={classNames(tag, tagVariants[variant], className)}
|
|
66
67
|
title={alt}
|
|
67
68
|
>
|
|
68
69
|
{icon}
|
|
69
70
|
{renderComponentOrNode(title)}
|
|
70
|
-
</
|
|
71
|
+
</Axis>
|
|
71
72
|
);
|
|
72
73
|
};
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export {App} from './components/App/index.tsx';
|
|
2
|
+
export {Axis} from './components/Axis/index.tsx';
|
|
2
3
|
export {Button} from './components/Button/index.tsx';
|
|
3
4
|
export {Card} from './components/Card/index.tsx';
|
|
4
5
|
export {Checkbox} from './components/Checkbox/index.tsx';
|
|
@@ -8,6 +9,8 @@ export {Detail} from './components/Detail/index.tsx';
|
|
|
8
9
|
export {Flyout} from './components/Flyout/index.tsx';
|
|
9
10
|
export {Hr} from './components/Hr/index.tsx';
|
|
10
11
|
export {Image} from './components/Image/index.tsx';
|
|
12
|
+
export {ImageLabel} from './components/ImageLabel/index.tsx';
|
|
13
|
+
export {Loading} from './components/Loading/index.tsx';
|
|
11
14
|
export {Metric} from './components/Metric/index.tsx';
|
|
12
15
|
export {Row} from './components/Row/index.tsx';
|
|
13
16
|
export {Select} from './components/Select/index.tsx';
|