git-truck 0.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/.eslintrc.json +20 -0
- package/.github/workflows/test-and-build.yml +39 -0
- package/.husky/pre-commit +4 -0
- package/.truckignore +1 -0
- package/.vscode/extensions.json +7 -0
- package/.vscode/launch.json +24 -0
- package/.vscode/settings.json +6 -0
- package/LICENSE +21 -0
- package/README.md +60 -0
- package/app/README.md +46 -0
- package/app/entry.client.tsx +4 -0
- package/app/entry.server.tsx +27 -0
- package/app/parser/.eslintignore +3 -0
- package/app/parser/.eslintrc.json +18 -0
- package/app/parser/src/TruckIgnore.server.ts +20 -0
- package/app/parser/src/constants.ts +1 -0
- package/app/parser/src/hydrate.server.ts +199 -0
- package/app/parser/src/index.ts +5 -0
- package/app/parser/src/log.server.ts +97 -0
- package/app/parser/src/model.ts +77 -0
- package/app/parser/src/parse.server.ts +276 -0
- package/app/parser/src/parse.test.ts +32 -0
- package/app/parser/src/queue.ts +86 -0
- package/app/parser/src/util.test.ts +8 -0
- package/app/parser/src/util.ts +216 -0
- package/app/root.tsx +35 -0
- package/app/routes/index.tsx +43 -0
- package/app/src/authorUnionUtil.test.ts +82 -0
- package/app/src/authorUnionUtil.ts +52 -0
- package/app/src/components/AuthorDistFragment.tsx +27 -0
- package/app/src/components/AuthorDistOther.tsx +24 -0
- package/app/src/components/Chart.tsx +362 -0
- package/app/src/components/Details.tsx +177 -0
- package/app/src/components/EnumSelect.tsx +31 -0
- package/app/src/components/GlobalInfo.tsx +17 -0
- package/app/src/components/Legend.tsx +65 -0
- package/app/src/components/LegendFragment.tsx +29 -0
- package/app/src/components/LegendOther.tsx +43 -0
- package/app/src/components/Main.tsx +19 -0
- package/app/src/components/Options.tsx +24 -0
- package/app/src/components/Providers.tsx +121 -0
- package/app/src/components/SearchBar.tsx +36 -0
- package/app/src/components/SidePanel.tsx +25 -0
- package/app/src/components/Spacer.tsx +62 -0
- package/app/src/components/Toggle.tsx +21 -0
- package/app/src/components/Tooltip.tsx +131 -0
- package/app/src/components/util.tsx +150 -0
- package/app/src/const.ts +5 -0
- package/app/src/contexts/DataContext.ts +12 -0
- package/app/src/contexts/MetricContext.ts +14 -0
- package/app/src/contexts/OptionsContext.ts +46 -0
- package/app/src/contexts/SearchContext.ts +16 -0
- package/app/src/extension-color.ts +34 -0
- package/app/src/hooks.ts +17 -0
- package/app/src/lang-map.d.ts +3 -0
- package/app/src/metrics.ts +319 -0
- package/app/src/react-app-env.d.ts +1 -0
- package/app/src/reportWebVitals.ts +15 -0
- package/app/src/setupTests.ts +5 -0
- package/app/src/util.ts +33 -0
- package/app/styles/App.css +3 -0
- package/app/styles/Chart.css +26 -0
- package/app/styles/index.css +35 -0
- package/app/styles/vars.css +17 -0
- package/cli.js +2 -0
- package/package.json +99 -0
- package/parse.sh +26 -0
- package/project-statement.md +43 -0
- package/public/favicon.ico +0 -0
- package/remix.config.js +21 -0
- package/remix.env.d.ts +2 -0
- package/server.js +41 -0
- package/truckconfig.json +8 -0
- package/tsconfig.json +20 -0
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { Box, BoxSubTitle, LegendDot } from "./util"
|
|
2
|
+
import { useMouse } from "react-use"
|
|
3
|
+
import { useMemo, useRef } from "react"
|
|
4
|
+
import styled from "styled-components"
|
|
5
|
+
import { HydratedGitBlobObject } from "../../parser/src/model"
|
|
6
|
+
import { useOptions } from "../contexts/OptionsContext"
|
|
7
|
+
import { Spacer } from "./Spacer"
|
|
8
|
+
import { useMetricCaches } from "../contexts/MetricContext"
|
|
9
|
+
import { MetricType } from "../metrics"
|
|
10
|
+
import { useCSSVar } from "../hooks"
|
|
11
|
+
import { dateFormatRelative } from "../util"
|
|
12
|
+
|
|
13
|
+
const TooltipBox = styled(Box)<{
|
|
14
|
+
visible: boolean
|
|
15
|
+
right: boolean
|
|
16
|
+
}>`
|
|
17
|
+
padding: calc(0.5 * var(--unit)) var(--unit);
|
|
18
|
+
min-width: 0;
|
|
19
|
+
width: max-content;
|
|
20
|
+
position: absolute;
|
|
21
|
+
top: 0px;
|
|
22
|
+
left: 0px;
|
|
23
|
+
will-change: transform visibility;
|
|
24
|
+
display: flex;
|
|
25
|
+
border-radius: calc(2 * var(--unit));
|
|
26
|
+
align-items: center;
|
|
27
|
+
|
|
28
|
+
pointer-events: none;
|
|
29
|
+
visibility: ${({ visible }) => (visible ? "visible" : "hidden")};
|
|
30
|
+
`
|
|
31
|
+
|
|
32
|
+
const TooltipContainer = styled.div`
|
|
33
|
+
position: absolute;
|
|
34
|
+
inset: 0;
|
|
35
|
+
pointer-events: none;
|
|
36
|
+
overflow: hidden;
|
|
37
|
+
`
|
|
38
|
+
|
|
39
|
+
interface TooltipProps {
|
|
40
|
+
hoveredBlob: HydratedGitBlobObject | null
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function Tooltip({ hoveredBlob }: TooltipProps) {
|
|
44
|
+
const tooltipContainerRef = useRef<HTMLDivElement>(null)
|
|
45
|
+
const { metricType } = useOptions()
|
|
46
|
+
const documentElementRef = useRef(document.documentElement)
|
|
47
|
+
const mouse = useMouse(documentElementRef)
|
|
48
|
+
const unitRaw = useCSSVar("--unit")
|
|
49
|
+
const unit = unitRaw ? Number(unitRaw.replace("px", "")) : 0
|
|
50
|
+
const metricCaches = useMetricCaches()
|
|
51
|
+
const color = useMemo(() => {
|
|
52
|
+
if (!hoveredBlob) {
|
|
53
|
+
return null
|
|
54
|
+
}
|
|
55
|
+
const { colormap } = metricCaches.get(metricType)!
|
|
56
|
+
const color = colormap.get(hoveredBlob.path)
|
|
57
|
+
return color
|
|
58
|
+
}, [hoveredBlob, metricCaches, metricType])
|
|
59
|
+
const toolTipWidth = tooltipContainerRef.current
|
|
60
|
+
? tooltipContainerRef.current.getBoundingClientRect().width
|
|
61
|
+
: 0
|
|
62
|
+
|
|
63
|
+
const right = mouse.docX + toolTipWidth < window.innerWidth - 3 * unit
|
|
64
|
+
|
|
65
|
+
const visible = hoveredBlob !== null
|
|
66
|
+
const transformStyles = { transform: "none" }
|
|
67
|
+
if (visible) {
|
|
68
|
+
if (right)
|
|
69
|
+
transformStyles.transform = `translate(calc(var(--unit) + ${mouse.docX}px), calc(var(--unit) + ${mouse.docY}px))`
|
|
70
|
+
else
|
|
71
|
+
transformStyles.transform = `translate(calc(var(--unit) * -1 + ${mouse.docX}px - 100%), calc(var(--unit) + ${mouse.docY}px))`
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return (
|
|
75
|
+
<TooltipContainer>
|
|
76
|
+
<TooltipBox
|
|
77
|
+
ref={tooltipContainerRef}
|
|
78
|
+
right={right}
|
|
79
|
+
visible={visible}
|
|
80
|
+
style={transformStyles}
|
|
81
|
+
>
|
|
82
|
+
{color ? <LegendDot dotColor={color} /> : null}
|
|
83
|
+
<Spacer horizontal />
|
|
84
|
+
<BoxSubTitle>{hoveredBlob?.name}</BoxSubTitle>
|
|
85
|
+
<Spacer horizontal />
|
|
86
|
+
<ColorMetricDependentInfo
|
|
87
|
+
metric={metricType}
|
|
88
|
+
hoveredBlob={hoveredBlob}
|
|
89
|
+
/>
|
|
90
|
+
</TooltipBox>
|
|
91
|
+
</TooltipContainer>
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function ColorMetricDependentInfo(props: {
|
|
96
|
+
metric: MetricType
|
|
97
|
+
hoveredBlob: HydratedGitBlobObject | null
|
|
98
|
+
}) {
|
|
99
|
+
switch (props.metric) {
|
|
100
|
+
case "MOST_COMMITS":
|
|
101
|
+
const noCommits = props.hoveredBlob?.noCommits
|
|
102
|
+
if (!noCommits) return null
|
|
103
|
+
return (
|
|
104
|
+
<>
|
|
105
|
+
{noCommits} commit{noCommits > 1 ? <>s</> : null}
|
|
106
|
+
</>
|
|
107
|
+
)
|
|
108
|
+
case "LAST_CHANGED":
|
|
109
|
+
const epoch = props.hoveredBlob?.lastChangeEpoch
|
|
110
|
+
if (!epoch) return null
|
|
111
|
+
return <>{dateFormatRelative(epoch)}</>
|
|
112
|
+
case "SINGLE_AUTHOR":
|
|
113
|
+
const authors = props.hoveredBlob
|
|
114
|
+
? Object.entries(props.hoveredBlob?.authors)
|
|
115
|
+
: []
|
|
116
|
+
switch (authors.length) {
|
|
117
|
+
case 0:
|
|
118
|
+
return null
|
|
119
|
+
case 1:
|
|
120
|
+
return <>{authors[0][0]} dominates</>
|
|
121
|
+
default:
|
|
122
|
+
return <>{authors.length} authors</>
|
|
123
|
+
}
|
|
124
|
+
case "TOP_CONTRIBUTOR":
|
|
125
|
+
const dominant = props.hoveredBlob?.dominantAuthor
|
|
126
|
+
if (!dominant) return null
|
|
127
|
+
return <>{dominant[0]}</>
|
|
128
|
+
default:
|
|
129
|
+
return null
|
|
130
|
+
}
|
|
131
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import styled from "styled-components"
|
|
2
|
+
|
|
3
|
+
export const BoxTitle = styled.h2`
|
|
4
|
+
font-size: 1.5em;
|
|
5
|
+
font-weight: bold;
|
|
6
|
+
margin-bottom: 0;
|
|
7
|
+
margin-top: 0;
|
|
8
|
+
color: var(--title-color);
|
|
9
|
+
overflow: hidden;
|
|
10
|
+
text-overflow: ellipsis;
|
|
11
|
+
white-space: nowrap;
|
|
12
|
+
`
|
|
13
|
+
|
|
14
|
+
export const BoxSubTitle = styled.h2`
|
|
15
|
+
font-size: 1em;
|
|
16
|
+
font-weight: bold;
|
|
17
|
+
margin-bottom: 0;
|
|
18
|
+
margin-top: 0;
|
|
19
|
+
color: var(--title-color);
|
|
20
|
+
`
|
|
21
|
+
|
|
22
|
+
export const CloseButton = styled.button`
|
|
23
|
+
background: none;
|
|
24
|
+
border: none;
|
|
25
|
+
font-size: larger;
|
|
26
|
+
position: absolute;
|
|
27
|
+
top: calc(var(--unit));
|
|
28
|
+
right: calc(var(--unit));
|
|
29
|
+
cursor: pointer;
|
|
30
|
+
`
|
|
31
|
+
|
|
32
|
+
export const Container = styled.div`
|
|
33
|
+
height: 100%;
|
|
34
|
+
display: grid;
|
|
35
|
+
grid-template-columns: var(--side-panel-width) 1fr;
|
|
36
|
+
grid-template-rows: 1fr;
|
|
37
|
+
`
|
|
38
|
+
|
|
39
|
+
export const Box = styled.div`
|
|
40
|
+
/* border: 1px var(--border-color-alpha) solid; */
|
|
41
|
+
margin: var(--unit);
|
|
42
|
+
color: var(--text-color);
|
|
43
|
+
width: calc(var(--side-panel-width) - 6 * var(--unit));
|
|
44
|
+
background-color: #fff;
|
|
45
|
+
border-radius: var(--unit);
|
|
46
|
+
padding: calc(2 * var(--unit));
|
|
47
|
+
position: relative;
|
|
48
|
+
/* Generated with: https://shadows.brumm.af/ */
|
|
49
|
+
box-shadow: var(--shadow);
|
|
50
|
+
`
|
|
51
|
+
|
|
52
|
+
export const Stack = styled.div`
|
|
53
|
+
display: flex;
|
|
54
|
+
flex-direction: column;
|
|
55
|
+
`
|
|
56
|
+
|
|
57
|
+
export const Label = styled.label`
|
|
58
|
+
padding-left: calc(var(--unit) + var(--border-width));
|
|
59
|
+
font-weight: bold;
|
|
60
|
+
font-size: 0.8em;
|
|
61
|
+
`
|
|
62
|
+
|
|
63
|
+
export const Select = styled.select`
|
|
64
|
+
width: 100%;
|
|
65
|
+
display: block;
|
|
66
|
+
padding: var(--unit);
|
|
67
|
+
border: 1px var(--border-color) solid;
|
|
68
|
+
border-radius: calc(0.5 * var(--unit));
|
|
69
|
+
`
|
|
70
|
+
|
|
71
|
+
export const SearchField = styled.input`
|
|
72
|
+
border: 1px var(--border-color) solid;
|
|
73
|
+
flex-grow: 1;
|
|
74
|
+
border-radius: calc(0.5 * var(--unit));
|
|
75
|
+
padding: var(--unit);
|
|
76
|
+
`
|
|
77
|
+
|
|
78
|
+
export const LegendEntry = styled.div`
|
|
79
|
+
font-size: small;
|
|
80
|
+
position: relative;
|
|
81
|
+
display: flex;
|
|
82
|
+
flex-direction: row;
|
|
83
|
+
place-items: center;
|
|
84
|
+
line-height: 100%;
|
|
85
|
+
margin: 0px;
|
|
86
|
+
`
|
|
87
|
+
|
|
88
|
+
export const LegendDot = styled.div<{ dotColor: string }>`
|
|
89
|
+
height: 100%;
|
|
90
|
+
aspect-ratio: 1;
|
|
91
|
+
width: 1em;
|
|
92
|
+
border-radius: 50%;
|
|
93
|
+
background-color: ${({ dotColor }) => dotColor};
|
|
94
|
+
box-shadow: var(--small-shadow);
|
|
95
|
+
`
|
|
96
|
+
|
|
97
|
+
export const LegendLable = styled.p`
|
|
98
|
+
padding: 0px;
|
|
99
|
+
margin: 0px;
|
|
100
|
+
font-weight: bold;
|
|
101
|
+
`
|
|
102
|
+
|
|
103
|
+
export const ToggleButton = styled.button<{
|
|
104
|
+
collapse: boolean
|
|
105
|
+
relative: boolean
|
|
106
|
+
}>`
|
|
107
|
+
position: ${(props) => (props.relative ? "relative" : "absolute")};
|
|
108
|
+
top: ${(props) => (props.relative ? "unset" : "var(--unit)")};
|
|
109
|
+
right: ${(props) => (props.relative ? "unset" : "var(--unit)")};
|
|
110
|
+
border: none;
|
|
111
|
+
background-color: rgba(0, 0, 0, 0);
|
|
112
|
+
transition-duration: 0.4s;
|
|
113
|
+
color: grey;
|
|
114
|
+
transform-origin: 50% 55%;
|
|
115
|
+
transform: ${(props) => (props.collapse ? "rotate(180deg)" : "none")};
|
|
116
|
+
font-size: large;
|
|
117
|
+
&:hover {
|
|
118
|
+
color: #606060;
|
|
119
|
+
cursor: pointer;
|
|
120
|
+
}
|
|
121
|
+
`
|
|
122
|
+
|
|
123
|
+
export const LegendGradient = styled.div<{ min: string; max: string }>`
|
|
124
|
+
background-image: linear-gradient(
|
|
125
|
+
to right,
|
|
126
|
+
${(props) => `${props.min},${props.max}`}
|
|
127
|
+
);
|
|
128
|
+
width: 100%;
|
|
129
|
+
height: 20px;
|
|
130
|
+
border-radius: calc(var(--unit) * 0.5);
|
|
131
|
+
`
|
|
132
|
+
|
|
133
|
+
export const GradientLegendDiv = styled.div`
|
|
134
|
+
display: flex;
|
|
135
|
+
flex-direction: row;
|
|
136
|
+
justify-content: space-between;
|
|
137
|
+
`
|
|
138
|
+
|
|
139
|
+
export const DetailsKey = styled.span<{ grow?: boolean }>`
|
|
140
|
+
white-space: pre;
|
|
141
|
+
font-size: 0.9em;
|
|
142
|
+
font-weight: 500;
|
|
143
|
+
opacity: 0.7;
|
|
144
|
+
`
|
|
145
|
+
|
|
146
|
+
export const DetailsValue = styled.p`
|
|
147
|
+
overflow-wrap: anywhere;
|
|
148
|
+
font-size: 0.9em;
|
|
149
|
+
text-align: right;
|
|
150
|
+
`
|
package/app/src/const.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { createContext, useContext } from "react"
|
|
2
|
+
import { ParserData } from "../../parser/src/model"
|
|
3
|
+
|
|
4
|
+
export const DataContext = createContext<ParserData | undefined>(undefined)
|
|
5
|
+
|
|
6
|
+
export function useData() {
|
|
7
|
+
const context = useContext(DataContext)
|
|
8
|
+
if (!context) {
|
|
9
|
+
throw new Error("useData must be used within a DataContext")
|
|
10
|
+
}
|
|
11
|
+
return context
|
|
12
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { createContext, useContext } from "react"
|
|
2
|
+
import { MetricCache, MetricType } from "../metrics"
|
|
3
|
+
|
|
4
|
+
export const MetricContext = createContext<
|
|
5
|
+
Map<MetricType, MetricCache> | undefined
|
|
6
|
+
>(undefined)
|
|
7
|
+
|
|
8
|
+
export function useMetricCaches() {
|
|
9
|
+
const context = useContext(MetricContext)
|
|
10
|
+
if (!context) {
|
|
11
|
+
throw new Error("useMetricCache must be used within a MetricContext")
|
|
12
|
+
}
|
|
13
|
+
return context
|
|
14
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { createContext, useContext } from "react"
|
|
2
|
+
import { HydratedGitBlobObject } from "../../parser/src/model"
|
|
3
|
+
import { Metric, MetricType } from "../metrics"
|
|
4
|
+
|
|
5
|
+
export const Chart = {
|
|
6
|
+
BUBBLE_CHART: "Bubble chart",
|
|
7
|
+
TREE_MAP: "Tree map",
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export type ChartType = keyof typeof Chart
|
|
11
|
+
|
|
12
|
+
export interface Options {
|
|
13
|
+
metricType: MetricType
|
|
14
|
+
chartType: ChartType
|
|
15
|
+
clickedBlob: HydratedGitBlobObject | null
|
|
16
|
+
setClickedBlob: (blob: HydratedGitBlobObject | null) => void
|
|
17
|
+
setMetricType: (metricType: MetricType) => void
|
|
18
|
+
setChartType: (chartType: ChartType) => void
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const OptionsContext = createContext<Options | undefined>(undefined)
|
|
22
|
+
|
|
23
|
+
export function useOptions() {
|
|
24
|
+
const context = useContext(OptionsContext)
|
|
25
|
+
if (!context) {
|
|
26
|
+
throw new Error("useSearch must be used within a SearchProvider")
|
|
27
|
+
}
|
|
28
|
+
return context
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function getDefaultOptions() {
|
|
32
|
+
return {
|
|
33
|
+
metricType: Object.keys(Metric)[0] as MetricType,
|
|
34
|
+
chartType: Object.keys(Chart)[0] as ChartType,
|
|
35
|
+
clickedBlob: null,
|
|
36
|
+
setClickedBlob: () => {
|
|
37
|
+
throw new Error("No setClickedBlob function provided")
|
|
38
|
+
},
|
|
39
|
+
setChartType: () => {
|
|
40
|
+
throw new Error("No chartTypeSetter provided")
|
|
41
|
+
},
|
|
42
|
+
setMetricType: () => {
|
|
43
|
+
throw new Error("No metricTypeSetter provided")
|
|
44
|
+
},
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { createContext, Dispatch, SetStateAction, useContext } from "react"
|
|
2
|
+
|
|
3
|
+
type Search = {
|
|
4
|
+
searchText: string
|
|
5
|
+
setSearchText: Dispatch<SetStateAction<string>>
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export const SearchContext = createContext<Search | undefined>(undefined)
|
|
9
|
+
|
|
10
|
+
export function useSearch(): Search {
|
|
11
|
+
const context = useContext(SearchContext)
|
|
12
|
+
if (!context) {
|
|
13
|
+
throw new Error("useSearch must be used within a SearchProvider")
|
|
14
|
+
}
|
|
15
|
+
return context
|
|
16
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import gitcolors from "github-colors/colors.json"
|
|
2
|
+
import langMap from "lang-map"
|
|
3
|
+
|
|
4
|
+
interface ColorResult {
|
|
5
|
+
lang: string
|
|
6
|
+
color: string | null
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const lowercasedColors = new Map<string, ColorResult>()
|
|
10
|
+
for (const [key, value] of Object.entries(gitcolors)) {
|
|
11
|
+
lowercasedColors.set(key.toLowerCase(), {
|
|
12
|
+
...value,
|
|
13
|
+
lang: key,
|
|
14
|
+
})
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function getColorFromExtension(extension: string) {
|
|
18
|
+
const langMatches = langMap.languages(extension.toLowerCase())
|
|
19
|
+
const langs = []
|
|
20
|
+
if (!langMatches) return null
|
|
21
|
+
let colorResult = null
|
|
22
|
+
// Loop through lang resuts
|
|
23
|
+
for (const langResult of langMatches) {
|
|
24
|
+
// If we have a color for the language, return it
|
|
25
|
+
let match = lowercasedColors.get(langResult)
|
|
26
|
+
if (match) {
|
|
27
|
+
colorResult = match
|
|
28
|
+
langs.push(colorResult.lang)
|
|
29
|
+
break
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
if (!colorResult) return null
|
|
33
|
+
return colorResult.color
|
|
34
|
+
}
|
package/app/src/hooks.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { MutableRefObject, useMemo } from "react"
|
|
2
|
+
import { useComponentSize as useCompSize } from "react-use-size"
|
|
3
|
+
|
|
4
|
+
type RefAndSize = [MutableRefObject<any>, { width: number; height: number }]
|
|
5
|
+
|
|
6
|
+
export function useComponentSize() {
|
|
7
|
+
const { ref, width, height } = useCompSize()
|
|
8
|
+
const size: RefAndSize = useMemo(
|
|
9
|
+
() => [ref, { width, height }],
|
|
10
|
+
[ref, width, height]
|
|
11
|
+
)
|
|
12
|
+
return size
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function useCSSVar(varName: string) {
|
|
16
|
+
return getComputedStyle(document.documentElement).getPropertyValue(varName)
|
|
17
|
+
}
|