diff --git a/core/instrument/src/babel/extract-component.ts b/core/instrument/src/babel/extract-component.ts
index 49148c090..6b15a50ae 100644
--- a/core/instrument/src/babel/extract-component.ts
+++ b/core/instrument/src/babel/extract-component.ts
@@ -152,7 +152,10 @@ export const extractStoreComponent = async (
if (store.doc) {
const doc: Document = store.doc;
if (doc.componentsLookup) {
- const componentNames = Object.keys(doc.componentsLookup);
+ const componentNames = Object.keys({
+ ...doc.componentsLookup,
+ [doc.component as string]: doc.component,
+ });
if (componentNames) {
for (const componentName of componentNames) {
const { component, componentPackage } = await extractComponent(
diff --git a/examples/gatsby/.config/buildtime.js b/examples/gatsby/.config/buildtime.js
index 453184547..9d5126076 100644
--- a/examples/gatsby/.config/buildtime.js
+++ b/examples/gatsby/.config/buildtime.js
@@ -18,7 +18,9 @@ module.exports = {
'../../stories/src/stories_native/*.stories.@(js|jsx|tsx|mdx)',
'../../stories/src/mdx-stories/*.mdx',
'../../../ui/app/src/**/*.stories.@(js|jsx|tsx|mdx)',
+ '../../../ui/components/src/**/*.mdx',
'../../../ui/components/src/**/*.stories.@(js|jsx|tsx|mdx)',
+ '../../../ui/blocks/src/**/*.mdx',
'../../../ui/blocks/src/**/*.@(stories.@(js|jsx|tsx)|mdx)',
'../../../ui/design-tokens/src/**/*.stories.@(js|jsx|tsx|mdx)',
'../../../core/core/src/stories/**/*.stories.@(js|jsx|tsx|mdx)',
diff --git a/examples/nextjs/.config/buildtime.js b/examples/nextjs/.config/buildtime.js
index 1e8337f33..f0f7811d4 100644
--- a/examples/nextjs/.config/buildtime.js
+++ b/examples/nextjs/.config/buildtime.js
@@ -18,7 +18,9 @@ module.exports = {
'../../stories/src/stories_native/*.stories.@(js|jsx|tsx|mdx)',
'../../stories/src/mdx-stories/*.mdx',
'../../../ui/app/src/**/*.stories.@(js|jsx|tsx|mdx)',
+ '../../../ui/components/src/**/*.mdx',
'../../../ui/components/src/**/*.stories.@(js|jsx|tsx|mdx)',
+ '../../../ui/blocks/src/**/*.mdx',
'../../../ui/blocks/src/**/*.@(stories.@(js|jsx|tsx)|mdx)',
'../../../ui/design-tokens/src/**/*.stories.@(js|jsx|tsx|mdx)',
'../../../core/core/src/stories/**/*.stories.@(js|jsx|tsx|mdx)',
diff --git a/examples/stories/src/blogs/component-stats.mdx b/examples/stories/src/blogs/component-stats.mdx
new file mode 100644
index 000000000..c2b6e7783
--- /dev/null
+++ b/examples/stories/src/blogs/component-stats.mdx
@@ -0,0 +1,63 @@
+---
+title: Introducing JSX stats
+type: blog
+date: 2020-12-08
+author: atanasster
+route: /blogs/components-stats
+description: Introducing the addon-stats - cross-reference components and attributes usage statistics for jsx
+tags:
+ - jsx
+ - stats
+ - components usage
+image: /static/components-usage-blog.jpg
+component: BlockContainer
+---
+import { BlockContainer } from '../../../../ui/components/src/BlockContainer/BlockContainer.tsx';
+import { ComponentJSX, Playground } from '@component-controls/blocks';
+import { ComponentUsage, AttributeUsage, ComponentUsageList, AttributesUsageList } from '@component-controls/addon-stats';
+
+
+# Addon stats
+
+We just published the new jsx instrumenting feature and related cross/usage jsx components statistics for component-controls.
+
+This unique feature allows you to view the jsx trees of your componemts and answer to questions such as - which of my components is the most used as a building block, and which attributes of each component are most used.
+
+
+# JSX Tree display
+
+This is displayed on the documentation page of each component, where you can see a tree of jsx nodes and the attributes used in each node.
+
+Here is a live example of [BlockContainer](/api/components-blockcontainer--overview) component:
+
+
+
+
+
+# Addon-stats
+
+The addon stats contains several api functions, react hooks and ui elements to make it easy to display cross-usage statistics on the components in your documentation site.
+
+## Components usage summary
+
+Components usage - how many times a component is being used from another component and which of it's properties are used
+
+
+
+## Attributes usage summary
+
+Attributes usage - how many times an attribute is being set on a component, and the component it is being set on
+
+
+
+## Components usage details
+
+How many times a component is being used from another component, with a list of the components using it
+
+
+
+## Attributes usage details
+
+How many times an attribute is being used on a component, with a list of those components
+
+
diff --git a/examples/stories/src/blogs/media/components-usage-blog.jpg b/examples/stories/src/blogs/media/components-usage-blog.jpg
new file mode 100644
index 000000000..ff484b662
Binary files /dev/null and b/examples/stories/src/blogs/media/components-usage-blog.jpg differ
diff --git a/examples/stories/src/components/Button.tsx b/examples/stories/src/components/Button.tsx
index da616f21e..f12b6b187 100644
--- a/examples/stories/src/components/Button.tsx
+++ b/examples/stories/src/components/Button.tsx
@@ -67,7 +67,7 @@ export const Button: FC = ({
padding,
}}
>
- {children}
+
{children}
);
diff --git a/plugins/addon-stats/.config/buildtime.js b/plugins/addon-stats/.config/buildtime.js
new file mode 100644
index 000000000..9e2516647
--- /dev/null
+++ b/plugins/addon-stats/.config/buildtime.js
@@ -0,0 +1,5 @@
+module.exports = {
+ stories: [
+ '../src/**/*.stories.tsx',
+ ],
+};
\ No newline at end of file
diff --git a/plugins/addon-stats/.eslintignore b/plugins/addon-stats/.eslintignore
new file mode 100644
index 000000000..53c37a166
--- /dev/null
+++ b/plugins/addon-stats/.eslintignore
@@ -0,0 +1 @@
+dist
\ No newline at end of file
diff --git a/plugins/addon-stats/LICENSE.md b/plugins/addon-stats/LICENSE.md
new file mode 100644
index 000000000..a64cf9401
--- /dev/null
+++ b/plugins/addon-stats/LICENSE.md
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2020 Atanas Stoyanov
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
\ No newline at end of file
diff --git a/plugins/addon-stats/README.md b/plugins/addon-stats/README.md
new file mode 100644
index 000000000..4498f3232
--- /dev/null
+++ b/plugins/addon-stats/README.md
@@ -0,0 +1,195 @@
+# Table of contents
+
+- [In action](#in-action)
+- [Overview](#overview)
+- [Getting Started](#getting-started)
+ - [Install](#install)
+ - [Usage](#usage)
+- [API](#api)
+ - [useComponentUsageAggregate](#insusecomponentusageaggregateins)
+ - [useAttributesUsageAggregate](#insuseattributesusageaggregateins)
+ - [AttributeUsage](#insattributeusageins)
+ - [AttributesUsageDetails](#insattributesusagedetailsins)
+ - [AttributesUsageList](#insattributesusagelistins)
+ - [ComponentUsage](#inscomponentusageins)
+ - [ComponentUsageDetails](#inscomponentusagedetailsins)
+ - [ComponentUsageList](#inscomponentusagelistins)
+
+# In action
+
+[Example site](https://component-controls.com/api/components-actioncontainer--overview/viewport)
+
+# Overview
+
+Addon to collect and display statistics for component-controls
+
+# Getting Started
+
+## Install
+
+```sh
+yarn add @component-controls/addon-stats --dev
+```
+
+## Usage
+
+
+```
+import { ComponentUsage, AttributeUsage, ComponentUsageList, AttributesUsageList } from '@component-controls/addon-stats';
+
+
+## Attributes usage summary
+
+Attributes usage - how many times an attribute is being set on a component, and on which component it is being set
+
+
+
+## Components usage details
+
+How many times a component is being used from another component, with a list of the components using it
+
+
+
+## Attributes usage details
+
+How many times an attribute is being used on a component, with a list of those components
+
+
+
+```
+
+# API
+
+
+
+
+
+## useComponentUsageAggregate
+
+_useComponentUsageAggregate [source code](https://github.com/ccontrols/component-controls/tree/master/plugins/viewport-plugin/src/hooks/components.ts)_
+
+### properties
+
+| Name | Type | Description |
+| --------- | ------------- | ----------- |
+| `filter*` | _StatsFilter_ | |
+
+## useAttributesUsageAggregate
+
+_useAttributesUsageAggregate [source code](https://github.com/ccontrols/component-controls/tree/master/plugins/viewport-plugin/src/hooks/components.ts)_
+
+### properties
+
+| Name | Type | Description |
+| --------- | ------------- | ----------- |
+| `filter*` | _StatsFilter_ | |
+
+## AttributeUsage
+
+_AttributeUsage [source code](https://github.com/ccontrols/component-controls/tree/master/plugins/viewport-plugin/src/ui/AttributeUsage/AttributeUsage.tsx)_
+
+### properties
+
+| Name | Type | Description |
+| ------------- | ---------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
+| `filter*` | _StatsFilter_ | |
+| `title` | _string_ | optional section title for the block. |
+| `description` | _string_ | optional markdown description. |
+| `id` | _string_ | optional id to be used for the block if no id is provided, one will be calculated automatically from the title. |
+| `collapsible` | _boolean_ | if false, will nothave a collapsible frame. |
+| `data-testid` | _string_ | testing id |
+| `plain` | _boolean_ | inner container variant. default to 'inner' to display a border and shadow |
+| `sx` | _ThemeUIStyleObject_ | |
+| `ref` | _((instance: HTMLDivElement) => void) \| RefObject<HTMLDivElement>_ | |
+
+## AttributesUsageDetails
+
+_AttributesUsageDetails [source code](https://github.com/ccontrols/component-controls/tree/master/plugins/viewport-plugin/src/ui/AttributesUsageDetails/AttributesUsageDetails.tsx)_
+
+### properties
+
+| Name | Type | Description |
+| ------------- | ---------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
+| `stats*` | _AttributeAggregateRow_ | |
+| `title` | _string_ | optional section title for the block. |
+| `description` | _string_ | optional markdown description. |
+| `id` | _string_ | optional id to be used for the block if no id is provided, one will be calculated automatically from the title. |
+| `collapsible` | _boolean_ | if false, will nothave a collapsible frame. |
+| `data-testid` | _string_ | testing id |
+| `plain` | _boolean_ | inner container variant. default to 'inner' to display a border and shadow |
+| `sx` | _ThemeUIStyleObject_ | |
+| `ref` | _((instance: HTMLDivElement) => void) \| RefObject<HTMLDivElement>_ | |
+
+## AttributesUsageList
+
+_AttributesUsageList [source code](https://github.com/ccontrols/component-controls/tree/master/plugins/viewport-plugin/src/ui/AttributesUsageList/AttributesUsageList.tsx)_
+
+### properties
+
+| Name | Type | Description |
+| ------------- | ---------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
+| `filter*` | _StatsFilter_ | |
+| `title` | _string_ | optional section title for the block. |
+| `description` | _string_ | optional markdown description. |
+| `id` | _string_ | optional id to be used for the block if no id is provided, one will be calculated automatically from the title. |
+| `collapsible` | _boolean_ | if false, will nothave a collapsible frame. |
+| `data-testid` | _string_ | testing id |
+| `plain` | _boolean_ | inner container variant. default to 'inner' to display a border and shadow |
+| `sx` | _ThemeUIStyleObject_ | |
+| `ref` | _((instance: HTMLDivElement) => void) \| RefObject<HTMLDivElement>_ | |
+
+## ComponentUsage
+
+_ComponentUsage [source code](https://github.com/ccontrols/component-controls/tree/master/plugins/viewport-plugin/src/ui/ComponentUsage/ComponentUsage.tsx)_
+
+### properties
+
+| Name | Type | Description |
+| ------------- | ---------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
+| `filter*` | _StatsFilter_ | |
+| `title` | _string_ | optional section title for the block. |
+| `description` | _string_ | optional markdown description. |
+| `id` | _string_ | optional id to be used for the block if no id is provided, one will be calculated automatically from the title. |
+| `collapsible` | _boolean_ | if false, will nothave a collapsible frame. |
+| `data-testid` | _string_ | testing id |
+| `plain` | _boolean_ | inner container variant. default to 'inner' to display a border and shadow |
+| `sx` | _ThemeUIStyleObject_ | |
+| `ref` | _((instance: HTMLDivElement) => void) \| RefObject<HTMLDivElement>_ | |
+
+## ComponentUsageDetails
+
+_ComponentUsageDetails [source code](https://github.com/ccontrols/component-controls/tree/master/plugins/viewport-plugin/src/ui/ComponentUsageDetails/ComponentUsageDetails.tsx)_
+
+### properties
+
+| Name | Type | Description |
+| ------------- | ---------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
+| `stats*` | _ComponentStats_ | |
+| `title` | _string_ | optional section title for the block. |
+| `description` | _string_ | optional markdown description. |
+| `id` | _string_ | optional id to be used for the block if no id is provided, one will be calculated automatically from the title. |
+| `collapsible` | _boolean_ | if false, will nothave a collapsible frame. |
+| `data-testid` | _string_ | testing id |
+| `plain` | _boolean_ | inner container variant. default to 'inner' to display a border and shadow |
+| `sx` | _ThemeUIStyleObject_ | |
+| `ref` | _((instance: HTMLDivElement) => void) \| RefObject<HTMLDivElement>_ | |
+
+## ComponentUsageList
+
+_ComponentUsageList [source code](https://github.com/ccontrols/component-controls/tree/master/plugins/viewport-plugin/src/ui/ComponentUsageList/ComponentUsageList.tsx)_
+
+### properties
+
+| Name | Type | Description |
+| ------------- | ---------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
+| `filter*` | _StatsFilter_ | |
+| `title` | _string_ | optional section title for the block. |
+| `description` | _string_ | optional markdown description. |
+| `id` | _string_ | optional id to be used for the block if no id is provided, one will be calculated automatically from the title. |
+| `collapsible` | _boolean_ | if false, will nothave a collapsible frame. |
+| `data-testid` | _string_ | testing id |
+| `plain` | _boolean_ | inner container variant. default to 'inner' to display a border and shadow |
+| `sx` | _ThemeUIStyleObject_ | |
+| `ref` | _((instance: HTMLDivElement) => void) \| RefObject<HTMLDivElement>_ | |
+
+
diff --git a/plugins/addon-stats/package.json b/plugins/addon-stats/package.json
new file mode 100644
index 000000000..914f867e1
--- /dev/null
+++ b/plugins/addon-stats/package.json
@@ -0,0 +1,59 @@
+{
+ "name": "@component-controls/addon-stats",
+ "version": "2.1.0",
+ "description": "Component controls stats addon",
+ "keywords": [
+ "addon",
+ "stats"
+ ],
+ "main": "dist/index.js",
+ "module": "dist/index.esm.js",
+ "typings": "dist/index.d.ts",
+ "files": [
+ "dist/",
+ "package.json",
+ "README.md"
+ ],
+ "scripts": {
+ "build": "yarn cross-env NODE_ENV=production rollup -c",
+ "dev": "yarn cross-env NODE_ENV=development rollup -cw",
+ "docs": "ts-md",
+ "fix": "yarn lint --fix",
+ "lint": "yarn eslint . --ext mdx,ts,tsx",
+ "prepare": "yarn build",
+ "test": "cc-jest -c ./.config"
+ },
+ "homepage": "https://github.com/ccontrols/component-controls",
+ "bugs": {
+ "url": "https://github.com/ccontrols/component-controls/issues"
+ },
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/ccontrols/component-controls.git",
+ "directory": "plugins/viewport-plugin"
+ },
+ "license": "MIT",
+ "dependencies": {
+ "@component-controls/blocks": "^2.1.0",
+ "@component-controls/components": "^2.1.0",
+ "@component-controls/core": "^2.1.0"
+ },
+ "devDependencies": {
+ "@types/react": "^16.9.34",
+ "react": "^17.0.1",
+ "react-table": "^7.0.0",
+ "theme-ui": "^0.6.0-alpha.1",
+ "typescript": "^4.0.5"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17",
+ "react-table": ">= 7.0.0",
+ "theme-ui": ">= 0.4.0-rc.1"
+ },
+ "publishConfig": {
+ "access": "public"
+ },
+ "authors": [
+ "Atanas Stoyanov"
+ ]
+}
diff --git a/plugins/addon-stats/rollup.config.js b/plugins/addon-stats/rollup.config.js
new file mode 100644
index 000000000..ba1c61c86
--- /dev/null
+++ b/plugins/addon-stats/rollup.config.js
@@ -0,0 +1,5 @@
+import { config } from '../../rollup-config';
+
+export default config({
+ input: ['./src/index.ts'],
+});
diff --git a/plugins/addon-stats/src/api/components.ts b/plugins/addon-stats/src/api/components.ts
new file mode 100644
index 000000000..b671413ac
--- /dev/null
+++ b/plugins/addon-stats/src/api/components.ts
@@ -0,0 +1,232 @@
+import {
+ Store,
+ getComponentName,
+ JSXTree,
+ defaultExport,
+} from '@component-controls/core';
+import {
+ ComponentStatsList,
+ StatsFilter,
+ ComponentAggregateStats,
+ AttributeAggregateRow,
+ AttributeAggregateStats,
+} from '../types';
+
+export const getComponentUsageStats = (
+ store: Store,
+ filter?: StatsFilter,
+): ComponentStatsList => {
+ const stats: ComponentStatsList = Object.keys(store.stories).reduce(
+ (acc: ComponentStatsList, storyId: string): ComponentStatsList => {
+ const story = store.stories[storyId];
+ const { doc: docId = '' } = story;
+ const doc = store.docs[docId];
+ if (doc && story.component) {
+ const componentName = getComponentName(story.component) || '';
+ const componentHash = doc.componentsLookup?.[componentName] || '';
+ const component = store.components[componentHash];
+ if (
+ component &&
+ (typeof filter !== 'function' || filter({ component, doc }))
+ ) {
+ if (acc[componentHash] !== undefined) {
+ return {
+ ...acc,
+ [componentHash]: {
+ ...acc[componentHash],
+ stories: [...acc[componentHash].stories, story],
+ },
+ };
+ } else {
+ return {
+ ...acc,
+ [componentHash]: {
+ name: store.components[componentHash].name,
+ stories: [story],
+ usedBy: {},
+ attributes: {},
+ },
+ };
+ }
+ }
+ }
+ return acc;
+ },
+ {},
+ );
+ const jsxTreeStats = (
+ componentHash: string,
+ tree: JSXTree,
+ stats: ComponentStatsList,
+ ) => {
+ if (tree.length) {
+ tree.forEach(item => {
+ let stat = stats[item.componentKey || ''];
+ if (!stat && item.importedName === defaultExport) {
+ const docHash = Object.keys(stats).find(
+ key => stats[key].name === item.name,
+ );
+ if (docHash) {
+ stat = stats[docHash];
+ }
+ }
+ if (stat) {
+ if (stat.usedBy[componentHash] !== undefined) {
+ stat.usedBy[componentHash].count =
+ stat.usedBy[componentHash].count + 1;
+ } else {
+ stat.usedBy[componentHash] = {
+ count: 1,
+ node: item,
+ };
+ }
+ if (item.attributes) {
+ item.attributes.forEach(attr => {
+ if (stat.attributes[attr] !== undefined) {
+ stat.attributes[attr] = stat.attributes[attr] + 1;
+ } else {
+ stat.attributes[attr] = 1;
+ }
+ });
+ }
+ }
+ if (item.children) {
+ jsxTreeStats(componentHash, item.children, stats);
+ }
+ });
+ }
+ };
+ Object.keys(stats).forEach(hash => {
+ const component = store.components[hash];
+ if (component) {
+ const { jsx } = component;
+ if (jsx) {
+ jsxTreeStats(hash, jsx, stats);
+ }
+ }
+ });
+ return stats;
+};
+
+export const getComponentUsageAggregate = (
+ store: Store,
+ stats: ComponentStatsList,
+): ComponentAggregateStats => {
+ return {
+ data: Object.keys(stats).map(key => {
+ const component = stats[key];
+ const storiesCount = component.stories.length;
+ const usedByCount = Object.keys(component.usedBy).reduce(
+ (acc, item) => acc + component.usedBy[item].count,
+ 0,
+ );
+ const attributesCount = Object.keys(component.attributes).reduce(
+ (acc, item) => acc + component.attributes[item],
+ 0,
+ );
+ return {
+ component: store.components[key],
+ attributesCount,
+ storiesCount,
+ usedByCount,
+ stats: stats[key],
+ componentHash: key,
+ };
+ }),
+ maxStories: Object.keys(stats).reduce(
+ (acc, key) => Math.max(acc, stats[key].stories.length),
+ 0,
+ ),
+ maxUsed: Object.keys(stats).reduce(
+ (acc, key) =>
+ Math.max(
+ acc,
+ Object.keys(stats[key].usedBy).reduce(
+ (acc, item) => acc + stats[key].usedBy[item].count,
+ 0,
+ ),
+ ),
+ 0,
+ ),
+ maxAttributes: Object.keys(stats).reduce(
+ (acc, key) =>
+ Math.max(
+ acc,
+ Object.keys(stats[key].attributes).reduce(
+ (acc, item) => acc + stats[key].attributes[item],
+ 0,
+ ),
+ ),
+ 0,
+ ),
+ };
+};
+
+export const getAttributeUsageAggregate = (
+ store: Store,
+ stats: ComponentStatsList,
+): AttributeAggregateStats => {
+ const data: Record = Object.keys(stats).reduce(
+ (acc, key) => {
+ const component = stats[key];
+ const attributes = Object.keys(component.attributes).reduce(
+ (acc: Record, attribute) => {
+ const newItem: AttributeAggregateRow = acc[attribute]
+ ? {
+ attribute,
+ components: {
+ ...acc[attribute].components,
+ [key]: {
+ name: store.components[key].name,
+ count:
+ (acc[attribute].components[key]?.count || 0) +
+ component.attributes[attribute],
+ },
+ },
+ componentsCount:
+ acc[attribute].componentsCount +
+ (acc[attribute].components[key] ? 0 : 1),
+ usedByCount:
+ acc[attribute].usedByCount + component.attributes[attribute],
+ }
+ : {
+ attribute,
+ components: {
+ [key]: {
+ count: component.attributes[attribute],
+ name: store.components[key].name,
+ },
+ },
+ componentsCount: 1,
+ usedByCount: component.attributes[attribute],
+ };
+ return { ...acc, [attribute]: newItem };
+ },
+ acc,
+ );
+ return { ...acc, ...attributes };
+ },
+ {},
+ );
+ return {
+ data: Object.keys(data).reduce(
+ (acc: AttributeAggregateRow[], item) => [...acc, data[item]],
+ [],
+ ),
+ maxUsed: Object.keys(data).reduce(
+ (acc, key) => Math.max(acc, data[key].usedByCount),
+ 0,
+ ),
+ maxComponentsCount: Object.keys(data).reduce(
+ (acc, key) =>
+ Math.max(
+ acc,
+ Object.keys(data[key].components).reduce(
+ (acc, item) => acc + data[key].components[item].count,
+ 0,
+ ),
+ ),
+ 0,
+ ),
+ };
+};
diff --git a/plugins/addon-stats/src/api/index.ts b/plugins/addon-stats/src/api/index.ts
new file mode 100644
index 000000000..099b463e3
--- /dev/null
+++ b/plugins/addon-stats/src/api/index.ts
@@ -0,0 +1 @@
+export * from './components';
\ No newline at end of file
diff --git a/plugins/addon-stats/src/hooks/components.ts b/plugins/addon-stats/src/hooks/components.ts
new file mode 100644
index 000000000..31a5a17a8
--- /dev/null
+++ b/plugins/addon-stats/src/hooks/components.ts
@@ -0,0 +1,38 @@
+import { useMemo } from 'react';
+import { useStore } from '@component-controls/store';
+import {
+ getComponentUsageAggregate,
+ getComponentUsageStats,
+ getAttributeUsageAggregate,
+} from '../api/components';
+import {
+ StatsFilter,
+ ComponentAggregateStats,
+ AttributeAggregateStats,
+} from '../types';
+
+export const useComponentUsageAggregate = ({
+ filter,
+}: {
+ filter?: StatsFilter;
+}): ComponentAggregateStats => {
+ const store = useStore();
+ const stats = useMemo(() => {
+ const stats = getComponentUsageStats(store, filter);
+ return getComponentUsageAggregate(store, stats);
+ }, [filter, store]);
+ return stats;
+};
+
+export const useAttributesUsageAggregate = ({
+ filter,
+}: {
+ filter?: StatsFilter;
+}): AttributeAggregateStats => {
+ const store = useStore();
+ const stats = useMemo(() => {
+ const stats = getComponentUsageStats(store, filter);
+ return getAttributeUsageAggregate(store, stats);
+ }, [filter, store]);
+ return stats;
+};
diff --git a/plugins/addon-stats/src/hooks/index.ts b/plugins/addon-stats/src/hooks/index.ts
new file mode 100644
index 000000000..099b463e3
--- /dev/null
+++ b/plugins/addon-stats/src/hooks/index.ts
@@ -0,0 +1 @@
+export * from './components';
\ No newline at end of file
diff --git a/plugins/addon-stats/src/index.ts b/plugins/addon-stats/src/index.ts
new file mode 100644
index 000000000..0ebe78b1f
--- /dev/null
+++ b/plugins/addon-stats/src/index.ts
@@ -0,0 +1,4 @@
+export * from './types';
+export * from './api';
+export * from './hooks';
+export * from './ui';
diff --git a/plugins/addon-stats/src/types.ts b/plugins/addon-stats/src/types.ts
new file mode 100644
index 000000000..f1315dba4
--- /dev/null
+++ b/plugins/addon-stats/src/types.ts
@@ -0,0 +1,74 @@
+import { Story, Document, Component, JSXNode } from '@component-controls/core';
+
+export interface ComponentStats {
+ /**
+ * list of stories for this component
+ */
+ stories: Story[];
+ /**
+ * list of components that are using this component, and how many times
+ */
+ usedBy: Record<
+ string,
+ {
+ count: number;
+ node: JSXNode;
+ }
+ >;
+ /**
+ * list of the used attributes for this component, and how many times other components are using this attribute
+ */
+ attributes: Record;
+ /**
+ * name of the component
+ */
+ name: string;
+}
+
+export type ComponentStatsList = Record;
+
+/**
+ * stats filter callback function
+ */
+export type StatsFilter = ({
+ doc,
+ component,
+}: {
+ doc: Document;
+ component: Component;
+}) => boolean;
+
+export interface ComponentAggregateRow {
+ component: Component;
+ componentHash: string;
+ stats: ComponentStats;
+ storiesCount: number;
+ usedByCount: number;
+ attributesCount: number;
+}
+
+export interface ComponentAggregateStats {
+ data: ComponentAggregateRow[];
+ maxStories: number;
+ maxUsed: number;
+ maxAttributes: number;
+}
+
+export interface AttributeAggregateRow {
+ attribute: string;
+ components: Record<
+ string,
+ {
+ name: string;
+ count: number;
+ }
+ >;
+ componentsCount: number;
+ usedByCount: number;
+}
+
+export interface AttributeAggregateStats {
+ data: AttributeAggregateRow[];
+ maxUsed: number;
+ maxComponentsCount: number;
+}
diff --git a/plugins/addon-stats/src/ui/AttributeUsage/AttributeUsage.tsx b/plugins/addon-stats/src/ui/AttributeUsage/AttributeUsage.tsx
new file mode 100644
index 000000000..9ed638193
--- /dev/null
+++ b/plugins/addon-stats/src/ui/AttributeUsage/AttributeUsage.tsx
@@ -0,0 +1,125 @@
+/* eslint-disable react/display-name */
+/** @jsx jsx */
+import { FC, useMemo } from 'react';
+import { jsx, Box } from 'theme-ui';
+import {
+ Table,
+ Column,
+ BlockContainer,
+ BlockContainerProps,
+ ProgressIndicator,
+ Tag,
+ Link,
+} from '@component-controls/components';
+import { LocalImport } from '@component-controls/blocks';
+import { StatsFilter, AttributeAggregateRow } from '../../types';
+import { useAttributesUsageAggregate } from '../../hooks/components';
+
+export type AttributeUsageProps = {
+ filter?: StatsFilter;
+ linkAttributes?: boolean;
+} & BlockContainerProps;
+
+export const AttributeUsage: FC = ({
+ filter,
+ linkAttributes = true,
+ ...rest
+}) => {
+ const { data, maxUsed, maxComponentsCount } = useAttributesUsageAggregate({
+ filter,
+ });
+ const columns = useMemo(
+ () =>
+ [
+ {
+ Header: 'attribute',
+ accessor: 'attribute',
+ Cell: ({
+ row: {
+ original: { attribute },
+ },
+ }) => (
+
+ {linkAttributes ? (
+ {attribute}
+ ) : (
+ attribute
+ )}
+
+ ),
+ },
+ {
+ Header: 'components',
+ width: '40%',
+ accessor: 'components',
+ Cell: ({
+ row: {
+ original: { components },
+ },
+ }) => (
+
+ {Object.keys(components).map(key => (
+
+ ))}
+
+ ),
+ },
+ {
+ Header: '#used',
+ accessor: 'componentsCount',
+ isSortedDesc: true,
+ isSorted: true,
+ Cell: ({
+ row: {
+ original: { componentsCount },
+ },
+ }) => (
+
+ ),
+ },
+ {
+ Header: '#total used',
+ accessor: 'usedByCount',
+ isSortedDesc: true,
+ isSorted: true,
+ Cell: ({
+ row: {
+ original: { usedByCount },
+ },
+ }) => ,
+ },
+ ] as Column[],
+ [maxUsed, maxComponentsCount, linkAttributes],
+ );
+ return (
+
+