diff --git a/frontend/package.json b/frontend/package.json
index b1b5d100226e..cbfe07d5a968 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -44,6 +44,7 @@
"@sentry/react": "8.41.0",
"@sentry/webpack-plugin": "2.22.6",
"@signozhq/badge": "0.0.2",
+ "@signozhq/button": "0.0.2",
"@signozhq/calendar": "0.0.0",
"@signozhq/callout": "0.0.2",
"@signozhq/design-tokens": "1.1.4",
diff --git a/frontend/src/container/SpanList/SearchFilters.tsx b/frontend/src/container/SpanList/SearchFilters.tsx
new file mode 100644
index 000000000000..23428e328027
--- /dev/null
+++ b/frontend/src/container/SpanList/SearchFilters.tsx
@@ -0,0 +1,19 @@
+import { SearchOutlined } from '@ant-design/icons';
+import { Input } from 'antd';
+
+function SearchFilters(): JSX.Element {
+ return (
+
+
+ }
+ size="middle"
+ className="search-filters__search-input"
+ />
+
+
+ );
+}
+
+export default SearchFilters;
diff --git a/frontend/src/container/SpanList/SpanList.styles.scss b/frontend/src/container/SpanList/SpanList.styles.scss
new file mode 100644
index 000000000000..7791d4c18500
--- /dev/null
+++ b/frontend/src/container/SpanList/SpanList.styles.scss
@@ -0,0 +1,81 @@
+.span-list {
+ height: 100%;
+ display: flex;
+ flex-direction: column;
+
+ &__header {
+ padding: 16px;
+ border-bottom: 1px solid var(--bg-slate-400);
+ // background: var(--bg-vanilla-100);
+ }
+
+ &__content {
+ flex: 1;
+ overflow: hidden;
+ }
+}
+
+.search-filters {
+ display: flex;
+ align-items: center;
+ gap: 16px;
+
+ &__input {
+ flex: 1;
+ max-width: 400px;
+ }
+
+ &__search-input {
+ .ant-input-prefix {
+ color: var(--text-slate-400);
+ }
+ }
+}
+
+.span-table {
+ height: 100%;
+
+ .span-name-with-expand {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+
+ .expand-button {
+ min-width: auto;
+ padding: 0;
+ height: auto;
+ border: none;
+ box-shadow: none;
+ display: flex;
+
+ &:hover {
+ background: var(--bg-slate-100);
+ }
+ }
+ }
+
+ .span-name {
+ padding-left: 24px;
+ }
+
+ [data-slot='table-cell'] {
+ font-size: 13px;
+ }
+ thead > [data-slot='table-row'] {
+ background: var(--bg-slate-400);
+ [data-slot='table-head'] {
+ color: var(--bg-vanilla-400);
+ font-size: 11px;
+ font-style: normal;
+ font-weight: 600;
+ line-height: 18px; /* 163.636% */
+ letter-spacing: 0.44px;
+ text-transform: uppercase;
+ }
+ }
+ [data-slot='table-row'] {
+ &:hover {
+ background: var(--bg-slate-500);
+ }
+ }
+}
diff --git a/frontend/src/container/SpanList/SpanList.tsx b/frontend/src/container/SpanList/SpanList.tsx
new file mode 100644
index 000000000000..f6a771e75a72
--- /dev/null
+++ b/frontend/src/container/SpanList/SpanList.tsx
@@ -0,0 +1,43 @@
+import './SpanList.styles.scss';
+
+import { useMemo } from 'react';
+import { Span } from 'types/api/trace/getTraceV2';
+
+import { mockEntrySpanData } from './mockData';
+import SearchFilters from './SearchFilters';
+import SpanTable from './SpanTable';
+import { transformEntrySpansToHierarchy } from './utils';
+
+interface SpanListProps {
+ traceId?: string;
+ setSelectedSpan?: (span: Span) => void;
+}
+
+function SpanList({ traceId, setSelectedSpan }: SpanListProps): JSX.Element {
+ const hierarchicalData = useMemo(
+ () => transformEntrySpansToHierarchy(mockEntrySpanData),
+ [],
+ );
+
+ return (
+
+ );
+}
+
+SpanList.defaultProps = {
+ traceId: undefined,
+ setSelectedSpan: (): void => {},
+};
+
+export default SpanList;
diff --git a/frontend/src/container/SpanList/SpanTable.tsx b/frontend/src/container/SpanList/SpanTable.tsx
new file mode 100644
index 000000000000..98f0dde3a43d
--- /dev/null
+++ b/frontend/src/container/SpanList/SpanTable.tsx
@@ -0,0 +1,367 @@
+import { Button } from '@signozhq/button';
+import { ColumnDef, DataTable, Row } from '@signozhq/table';
+import { ChevronDownIcon, ChevronRightIcon } from 'lucide-react';
+import { useCallback, useMemo, useState } from 'react';
+import { Span } from 'types/api/trace/getTraceV2';
+
+import { HierarchicalSpanData, ServiceEntrySpan, SpanDataRow } from './types';
+import { fetchServiceSpans, formatDuration } from './utils';
+
+// Constants
+const SPAN_TYPE_ENTRY = 'entry-span';
+const SPAN_TYPE_SERVICE = 'service-span';
+
+interface SpanTableProps {
+ data: HierarchicalSpanData;
+ traceId?: string;
+ setSelectedSpan?: (span: Span) => void;
+}
+
+interface TableRowData {
+ id: string;
+ type: typeof SPAN_TYPE_ENTRY | typeof SPAN_TYPE_SERVICE;
+ spanName?: string;
+ serviceName?: string;
+ spanCount?: number;
+ duration?: string;
+ timestamp?: string;
+ statusCode?: string;
+ httpMethod?: string;
+ spanId?: string;
+ originalData?: ServiceEntrySpan | SpanDataRow;
+ isLoading?: boolean;
+}
+
+function SpanTable({
+ data,
+ traceId,
+ setSelectedSpan,
+}: SpanTableProps): JSX.Element {
+ const [expandedEntrySpans, setExpandedEntrySpans] = useState<
+ Record
+ >({});
+ const [loadingSpans, setLoadingSpans] = useState>({});
+
+ const handleEntrySpanClick = useCallback(
+ async (entrySpan: ServiceEntrySpan) => {
+ const spanId = entrySpan.spanData.data.span_id;
+
+ if (expandedEntrySpans[spanId]) {
+ // Collapse - remove from expanded spans
+ const { [spanId]: removed, ...rest } = expandedEntrySpans;
+ setExpandedEntrySpans(rest);
+ return;
+ }
+
+ // Expand - fetch service spans
+ if (!entrySpan.serviceSpans && traceId) {
+ setLoadingSpans((prev) => ({ ...prev, [spanId]: true }));
+
+ try {
+ const serviceSpans = await fetchServiceSpans(
+ traceId,
+ entrySpan.serviceName,
+ );
+ const updatedEntrySpan = {
+ ...entrySpan,
+ serviceSpans,
+ isExpanded: true,
+ };
+
+ setExpandedEntrySpans((prev) => ({
+ ...prev,
+ [spanId]: updatedEntrySpan,
+ }));
+ } catch (error) {
+ console.error('Failed to fetch service spans:', error);
+ } finally {
+ setLoadingSpans((prev) => ({ ...prev, [spanId]: false }));
+ }
+ } else {
+ // Already have service spans, just toggle expansion
+ setExpandedEntrySpans((prev) => ({
+ ...prev,
+ [spanId]: { ...entrySpan, isExpanded: true },
+ }));
+ }
+ },
+ [expandedEntrySpans, traceId],
+ );
+
+ const handleSpanClick = useCallback(
+ (span: SpanDataRow) => {
+ if (setSelectedSpan) {
+ // Convert span data to the format expected by SpanDetailsDrawer
+ const convertedSpan = ({
+ id: span.data.span_id,
+ traceID: span.data.trace_id,
+ spanID: span.data.span_id,
+ parentSpanID: '',
+ operationName: span.data.name,
+ startTime: new Date(span.timestamp).getTime() * 1000000, // Convert to nanoseconds
+ duration: span.data.duration_nano,
+ tags: [],
+ logs: [],
+ process: {
+ serviceName: span.data['service.name'],
+ tags: [],
+ },
+ } as unknown) as Span;
+ setSelectedSpan(convertedSpan);
+ }
+ },
+ [setSelectedSpan],
+ );
+
+ const renderNameCell = useCallback(
+ ({ row }: { row: Row }): JSX.Element => {
+ const { original } = row;
+ if (original.type === SPAN_TYPE_ENTRY) {
+ const entrySpan = original.originalData as ServiceEntrySpan;
+ const spanId = entrySpan.spanData.data.span_id;
+ const isExpanded = !!expandedEntrySpans[spanId];
+ const isLoading = loadingSpans[spanId];
+
+ return (
+
+
+ ) : (
+
+ )
+ }
+ onClick={(): Promise => handleEntrySpanClick(entrySpan)}
+ className="expand-button"
+ />
+ {original.spanName}
+
+ );
+ }
+ // Service span (nested)
+ return {original.spanName}
;
+ },
+ [expandedEntrySpans, loadingSpans, handleEntrySpanClick],
+ );
+
+ const renderServiceCell = useCallback(
+ ({ row }: { row: Row }): JSX.Element => {
+ const { original } = row;
+ return {original.serviceName};
+ },
+ [],
+ );
+
+ const renderSpanCountCell = useCallback(
+ ({ row }: { row: Row }): JSX.Element => {
+ const { original } = row;
+ if (original.type === SPAN_TYPE_ENTRY && original.spanCount !== undefined) {
+ return {original.spanCount};
+ }
+ return -;
+ },
+ [],
+ );
+
+ const renderDurationCell = useCallback(
+ ({ row }: { row: Row }): JSX.Element => {
+ const { original } = row;
+ return {original.duration};
+ },
+ [],
+ );
+
+ const renderTimestampCell = useCallback(
+ ({ row }: { row: Row }): JSX.Element => {
+ const { original } = row;
+ return (
+
+ {new Date(original.timestamp || '').toLocaleString()}
+
+ );
+ },
+ [],
+ );
+
+ const renderStatusCodeCell = useCallback(
+ ({ row }: { row: Row }): JSX.Element => {
+ const { original } = row;
+ return {original.statusCode};
+ },
+ [],
+ );
+
+ const renderHttpMethodCell = useCallback(
+ // eslint-disable-next-line react/no-unused-prop-types
+ ({ row }: { row: Row }): JSX.Element => {
+ const { original } = row;
+ return {original.httpMethod || '-'};
+ },
+ [],
+ );
+
+ const columns: ColumnDef[] = [
+ {
+ id: 'name',
+ header: 'Span Name',
+ accessorKey: 'spanName',
+ cell: renderNameCell,
+ },
+ {
+ id: 'timestamp',
+ header: 'Timestamp',
+ accessorKey: 'timestamp',
+ size: 180,
+ cell: renderTimestampCell,
+ },
+ {
+ id: 'httpMethod',
+ header: 'Method',
+ accessorKey: 'httpMethod',
+ size: 80,
+ cell: renderHttpMethodCell,
+ },
+ {
+ id: 'statusCode',
+ header: 'Status',
+ accessorKey: 'statusCode',
+ size: 80,
+ cell: renderStatusCodeCell,
+ },
+ {
+ id: 'service',
+ header: 'Service',
+ accessorKey: 'serviceName',
+ size: 120,
+ cell: renderServiceCell,
+ },
+ {
+ id: 'spans',
+ header: 'Spans',
+ accessorKey: 'spanCount',
+ size: 80,
+ cell: renderSpanCountCell,
+ },
+ {
+ id: 'duration',
+ header: 'Duration',
+ accessorKey: 'duration',
+ size: 120,
+ cell: renderDurationCell,
+ },
+ ];
+
+ const flattenedData = useMemo(() => {
+ const result: TableRowData[] = [];
+
+ data.entrySpans.forEach((entrySpan) => {
+ const spanId = entrySpan.spanData.data.span_id;
+
+ // Calculate span count for this service
+ const expandedSpan = expandedEntrySpans[spanId];
+ const spanCount = expandedSpan?.serviceSpans?.length || 0;
+
+ // Add entry span row
+ result.push({
+ id: spanId,
+ type: SPAN_TYPE_ENTRY,
+ spanName: entrySpan.spanData.data.name,
+ serviceName: entrySpan.serviceName,
+ spanCount: spanCount > 0 ? spanCount : undefined,
+ duration: formatDuration(entrySpan.spanData.data.duration_nano),
+ timestamp: entrySpan.spanData.timestamp,
+ statusCode: entrySpan.spanData.data.response_status_code,
+ httpMethod: entrySpan.spanData.data.http_method,
+ spanId,
+ originalData: entrySpan,
+ });
+
+ // Add service spans if expanded
+ if (expandedSpan?.serviceSpans) {
+ expandedSpan.serviceSpans.forEach((serviceSpan) => {
+ result.push({
+ id: serviceSpan.data.span_id,
+ type: SPAN_TYPE_SERVICE,
+ spanName: serviceSpan.data.name,
+ serviceName: serviceSpan.data['service.name'],
+ duration: formatDuration(serviceSpan.data.duration_nano),
+ timestamp: serviceSpan.timestamp,
+ statusCode: serviceSpan.data.response_status_code,
+ httpMethod: serviceSpan.data.http_method,
+ spanId: serviceSpan.data.span_id,
+ originalData: serviceSpan,
+ });
+ });
+ }
+ });
+
+ return result;
+ }, [data.entrySpans, expandedEntrySpans]);
+
+ const handleRowClick = useCallback(
+ (row: Row) => {
+ const { original } = row;
+ if (original.type === SPAN_TYPE_ENTRY) {
+ handleEntrySpanClick(original.originalData as ServiceEntrySpan);
+ } else if (original.type === SPAN_TYPE_SERVICE) {
+ handleSpanClick(original.originalData as SpanDataRow);
+ }
+ },
+ [handleEntrySpanClick, handleSpanClick],
+ );
+
+ const args = {
+ columns,
+ tableId: 'span-list-table',
+ enableSorting: false,
+ enableFiltering: false,
+ enableGlobalFilter: false,
+ enableColumnReordering: false,
+ enableColumnResizing: false,
+ enableColumnPinning: false,
+ enableRowSelection: false,
+ enablePagination: false,
+ showHeaders: true,
+ defaultColumnWidth: 150,
+ minColumnWidth: 80,
+ maxColumnWidth: 300,
+ enableVirtualization: false,
+ fixedHeight: 600,
+ };
+
+ return (
+
+
+
+ );
+}
+
+SpanTable.defaultProps = {
+ traceId: undefined,
+ setSelectedSpan: undefined,
+};
+
+export default SpanTable;
diff --git a/frontend/src/container/SpanList/mockData.ts b/frontend/src/container/SpanList/mockData.ts
new file mode 100644
index 000000000000..4fb4dbf96ef0
--- /dev/null
+++ b/frontend/src/container/SpanList/mockData.ts
@@ -0,0 +1,96 @@
+import { SpanDataRow } from './types';
+
+export const mockEntrySpanData: SpanDataRow[] = [
+ {
+ data: {
+ duration_nano: 3878813,
+ http_method: '',
+ // eslint-disable-next-line sonarjs/no-duplicate-string
+ name: 'CurrencyService/Convert',
+ response_status_code: '0',
+ 'service.name': 'currencyservice',
+ span_id: '8439d7461ab457a2',
+ timestamp: '2025-09-03T11:33:46.060209146Z',
+ trace_id: '5ad9c01671f0e38582efe03bbf81360a',
+ },
+ timestamp: '2025-09-03T11:33:46.060209146Z',
+ },
+ {
+ data: {
+ duration_nano: 12485759,
+ http_method: '',
+ name: 'CurrencyService/Convert',
+ response_status_code: '0',
+ 'service.name': 'currencyservice',
+ span_id: '7dbf7811106b1049',
+ timestamp: '2025-09-03T11:33:46.058715482Z',
+ trace_id: '5ad9c01671f0e38582efe03bbf81360a',
+ },
+ timestamp: '2025-09-03T11:33:46.058715482Z',
+ },
+ {
+ data: {
+ duration_nano: 6950595,
+ http_method: '',
+ name: 'CurrencyService/Convert',
+ response_status_code: '0',
+ 'service.name': 'currencyservice',
+ span_id: '9adcdf1baa56d23a',
+ timestamp: '2025-09-03T11:33:46.058319328Z',
+ trace_id: '5ad9c01671f0e38582efe03bbf81360a',
+ },
+ timestamp: '2025-09-03T11:33:46.058319328Z',
+ },
+ {
+ data: {
+ duration_nano: 5323696,
+ http_method: '',
+ name: 'CurrencyService/Convert',
+ response_status_code: '0',
+ 'service.name': 'currencyservice',
+ span_id: '1a1b01a750dfe4d6',
+ timestamp: '2025-09-03T11:33:46.057323233Z',
+ trace_id: '5ad9c01671f0e38582efe03bbf81360a',
+ },
+ timestamp: '2025-09-03T11:33:46.057323233Z',
+ },
+ {
+ data: {
+ duration_nano: 133659,
+ http_method: '',
+ name: 'oteldemo.ProductCatalogService/GetProduct',
+ response_status_code: '0',
+ 'service.name': 'productcatalogservice',
+ span_id: '485a987e9dbf8ddd',
+ timestamp: '2025-09-03T11:33:45.963838319Z',
+ trace_id: '5ad9c01671f0e38582efe03bbf81360a',
+ },
+ timestamp: '2025-09-03T11:33:45.963838319Z',
+ },
+ {
+ data: {
+ duration_nano: 245000000,
+ http_method: 'GET',
+ name: '/api/checkout',
+ response_status_code: '200',
+ 'service.name': 'checkoutservice',
+ span_id: 'abc123def456',
+ timestamp: '2025-09-03T11:33:45.800000000Z',
+ trace_id: '5ad9c01671f0e38582efe03bbf81360a',
+ },
+ timestamp: '2025-09-03T11:33:45.800000000Z',
+ },
+ {
+ data: {
+ duration_nano: 150000000,
+ http_method: 'POST',
+ name: '/api/payment',
+ response_status_code: '200',
+ 'service.name': 'paymentservice',
+ span_id: 'def456ghi789',
+ timestamp: '2025-09-03T11:33:45.750000000Z',
+ trace_id: '5ad9c01671f0e38582efe03bbf81360a',
+ },
+ timestamp: '2025-09-03T11:33:45.750000000Z',
+ },
+];
diff --git a/frontend/src/container/SpanList/types.ts b/frontend/src/container/SpanList/types.ts
new file mode 100644
index 000000000000..7890f425028e
--- /dev/null
+++ b/frontend/src/container/SpanList/types.ts
@@ -0,0 +1,47 @@
+export interface SpanData {
+ duration_nano: number;
+ http_method: string;
+ name: string;
+ response_status_code: string;
+ 'service.name': string;
+ span_id: string;
+ timestamp: string;
+ trace_id: string;
+}
+
+export interface SpanDataRow {
+ data: SpanData;
+ timestamp: string;
+}
+
+export interface ApiResponse {
+ status: string;
+ data: {
+ type: string;
+ meta: {
+ rowsScanned: number;
+ bytesScanned: number;
+ durationMs: number;
+ };
+ data: {
+ results: Array<{
+ queryName: string;
+ nextCursor: string;
+ rows: SpanDataRow[];
+ }>;
+ };
+ };
+}
+
+export interface ServiceEntrySpan {
+ spanData: SpanDataRow;
+ serviceName: string;
+ isExpanded?: boolean;
+ serviceSpans?: SpanDataRow[]; // All spans for this service when expanded
+ isLoadingServiceSpans?: boolean;
+}
+
+export interface HierarchicalSpanData {
+ entrySpans: ServiceEntrySpan[];
+ totalTraceTime: number;
+}
diff --git a/frontend/src/container/SpanList/utils.ts b/frontend/src/container/SpanList/utils.ts
new file mode 100644
index 000000000000..402c6f2975d8
--- /dev/null
+++ b/frontend/src/container/SpanList/utils.ts
@@ -0,0 +1,89 @@
+import { HierarchicalSpanData, ServiceEntrySpan, SpanDataRow } from './types';
+
+export function formatDuration(durationNano: number): string {
+ if (durationNano < 1000) {
+ return `${durationNano}ns`;
+ }
+ if (durationNano < 1000000) {
+ return `${(durationNano / 1000).toFixed(2)}μs`;
+ }
+ if (durationNano < 1000000000) {
+ return `${(durationNano / 1000000).toFixed(2)}ms`;
+ }
+ return `${(durationNano / 1000000000).toFixed(2)}s`;
+}
+
+export function transformEntrySpansToHierarchy(
+ entrySpans: SpanDataRow[],
+): HierarchicalSpanData {
+ let totalTraceTime = 0;
+
+ // Calculate total trace time from all entry spans
+ entrySpans.forEach((span) => {
+ totalTraceTime += span.data.duration_nano;
+ });
+
+ // Transform entry spans to ServiceEntrySpan structure
+ const entrySpansList: ServiceEntrySpan[] = entrySpans.map((span) => ({
+ spanData: span,
+ serviceName: span.data['service.name'],
+ isExpanded: false,
+ serviceSpans: undefined,
+ isLoadingServiceSpans: false,
+ }));
+
+ // Sort by timestamp (most recent first)
+ entrySpansList.sort(
+ (a, b) =>
+ new Date(b.spanData.timestamp).getTime() -
+ new Date(a.spanData.timestamp).getTime(),
+ );
+
+ return {
+ entrySpans: entrySpansList,
+ totalTraceTime,
+ };
+}
+
+// Mock function to simulate fetching service spans
+export function fetchServiceSpans(
+ traceId: string,
+ serviceName: string,
+): Promise {
+ // This would normally make an API call to get spans for the specific service
+ // For now, return mock data filtered by service name
+ return new Promise((resolve) => {
+ setTimeout(() => {
+ // Mock response - in real implementation, this would call the API
+ const mockServiceSpans: SpanDataRow[] = [
+ {
+ data: {
+ duration_nano: 1500000,
+ http_method: 'GET',
+ name: `${serviceName}/internal-call-1`,
+ response_status_code: '200',
+ 'service.name': serviceName,
+ span_id: `${serviceName}-span-1`,
+ timestamp: '2025-09-03T11:33:46.100000000Z',
+ trace_id: traceId,
+ },
+ timestamp: '2025-09-03T11:33:46.100000000Z',
+ },
+ {
+ data: {
+ duration_nano: 2500000,
+ http_method: 'POST',
+ name: `${serviceName}/internal-call-2`,
+ response_status_code: '200',
+ 'service.name': serviceName,
+ span_id: `${serviceName}-span-2`,
+ timestamp: '2025-09-03T11:33:46.200000000Z',
+ trace_id: traceId,
+ },
+ timestamp: '2025-09-03T11:33:46.200000000Z',
+ },
+ ];
+ resolve(mockServiceSpans);
+ }, 500); // Simulate network delay
+ });
+}
diff --git a/frontend/src/pages/TraceDetailV2/TraceDetailV2.tsx b/frontend/src/pages/TraceDetailV2/TraceDetailV2.tsx
index c672e2264997..080130ebe41e 100644
--- a/frontend/src/pages/TraceDetailV2/TraceDetailV2.tsx
+++ b/frontend/src/pages/TraceDetailV2/TraceDetailV2.tsx
@@ -1,9 +1,11 @@
import './TraceDetailV2.styles.scss';
+import { UnorderedListOutlined } from '@ant-design/icons';
import { Button, Tabs } from 'antd';
import FlamegraphImg from 'assets/TraceDetail/Flamegraph';
import TraceFlamegraph from 'container/PaginatedTraceFlamegraph/PaginatedTraceFlamegraph';
import SpanDetailsDrawer from 'container/SpanDetailsDrawer/SpanDetailsDrawer';
+import SpanList from 'container/SpanList/SpanList';
import TraceMetadata from 'container/TraceMetadata/TraceMetadata';
import TraceWaterfall, {
IInterestedSpan,
@@ -118,6 +120,19 @@ function TraceDetailsV2(): JSX.Element {
>
),
},
+ {
+ label: (
+ }
+ className="flamegraph-waterfall-toggle"
+ >
+ Span list
+
+ ),
+ key: 'span-list',
+ children: ,
+ },
];
return (
diff --git a/frontend/yarn.lock b/frontend/yarn.lock
index 99360b670401..6c8ab0ee8ede 100644
--- a/frontend/yarn.lock
+++ b/frontend/yarn.lock
@@ -4247,7 +4247,7 @@
tailwind-merge "^2.5.2"
tailwindcss-animate "^1.0.7"
-"@signozhq/button@^0.0.2":
+"@signozhq/button@0.0.2", "@signozhq/button@^0.0.2":
version "0.0.2"
resolved "https://registry.yarnpkg.com/@signozhq/button/-/button-0.0.2.tgz#c13edef1e735134b784a41f874b60a14bc16993f"
integrity sha512-434/gbTykC00LrnzFPp7c33QPWZkf9n+8+SToLZFTB0rzcaS/xoB4b7QKhvk+8xLCj4zpw6BxfeRAL+gSoOUJw==