@connectorvol/tree 2.1.2 → 4.1.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/dist/(classes)/chessTree.svelte.d.ts +83 -0
- package/dist/(classes)/chessTree.svelte.js +198 -0
- package/dist/(components)/DrillBreadcrumbs.svelte +135 -0
- package/dist/(components)/DrillBreadcrumbs.svelte.d.ts +20 -0
- package/dist/(components)/DrillForkSanLabel.svelte +26 -0
- package/dist/(components)/DrillForkSanLabel.svelte.d.ts +11 -0
- package/dist/(components)/DrillVariationList.svelte +65 -0
- package/dist/(components)/DrillVariationList.svelte.d.ts +15 -0
- package/dist/(components)/Move.svelte +242 -173
- package/dist/(components)/Move.svelte.d.ts +11 -5
- package/dist/(components)/MoveComment.svelte +32 -0
- package/dist/(components)/MoveComment.svelte.d.ts +11 -0
- package/dist/(components)/MoveContextMenu.svelte +73 -0
- package/dist/(components)/MoveContextMenu.svelte.d.ts +17 -0
- package/dist/(components)/MoveSanWithMenu.svelte +158 -0
- package/dist/(components)/MoveSanWithMenu.svelte.d.ts +42 -0
- package/dist/(components)/NagBadges.svelte +37 -0
- package/dist/(components)/NagBadges.svelte.d.ts +9 -0
- package/dist/(components)/TreeViewer.svelte +861 -42
- package/dist/(components)/TreeViewer.svelte.d.ts +29 -3
- package/dist/(components)/TreeViewerPanelManager.svelte +18 -155
- package/dist/(components)/VariantDropdownNavigator.svelte +173 -0
- package/dist/(components)/VariantDropdownNavigator.svelte.d.ts +16 -0
- package/dist/(components)/VariationGroup.svelte +100 -0
- package/dist/(components)/VariationGroup.svelte.d.ts +21 -0
- package/dist/(constants)/png.d.ts +1 -1
- package/dist/(constants)/png.js +2 -4
- package/dist/(models)/nagCatalog.d.ts +36 -0
- package/dist/(models)/nagCatalog.js +97 -0
- package/dist/(models)/pgnNodeCustomData.d.ts +3 -0
- package/dist/(utils)/context.d.ts +10 -3
- package/dist/(utils)/context.js +3 -14
- package/dist/(utils)/createPreviewHover.d.ts +19 -0
- package/dist/(utils)/createPreviewHover.js +37 -0
- package/dist/(utils)/nagBadgeStyle.d.ts +1 -0
- package/dist/(utils)/nagBadgeStyle.js +1 -0
- package/dist/(utils)/scrollToActiveMove.js +6 -5
- package/dist/(utils)/transformNag.d.ts +1 -1
- package/dist/(utils)/transformNag.js +1 -34
- package/dist/(utils)/transformPgnToChessNode.js +13 -0
- package/dist/(utils)/treeViewerPanelNavigation.svelte.d.ts +39 -0
- package/dist/(utils)/treeViewerPanelNavigation.svelte.js +257 -0
- package/dist/components/ui/button/button-variants.d.ts +65 -0
- package/dist/components/ui/button/button-variants.js +28 -0
- package/dist/components/ui/button/button.svelte +3 -43
- package/dist/components/ui/button/button.svelte.d.ts +1 -61
- package/dist/components/ui/button/index.d.ts +3 -2
- package/dist/components/ui/button/index.js +3 -4
- package/dist/components/ui/button-group/button-group-separator.svelte.d.ts +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/package.json +4 -4
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { ChessTreeNode } from "../(models)/chessTreeNode.js";
|
|
3
|
+
|
|
4
|
+
import ArrowUpIcon from "@lucide/svelte/icons/arrow-up";
|
|
5
|
+
import CheckCheckIcon from "@lucide/svelte/icons/check-check";
|
|
6
|
+
import FlagIcon from "@lucide/svelte/icons/flag";
|
|
7
|
+
import Trash2Icon from "@lucide/svelte/icons/trash-2";
|
|
8
|
+
import * as ContextMenu from "../components/ui/context-menu/index.js";
|
|
9
|
+
|
|
10
|
+
import type { ChessTree } from "../(classes)/chessTree.svelte.js";
|
|
11
|
+
|
|
12
|
+
type TMoveContextMenuProps = {
|
|
13
|
+
/** Возвращает узел хода, к которому применяется меню. */
|
|
14
|
+
chessNode: ChessTreeNode;
|
|
15
|
+
/** Возвращает дерево партии. */
|
|
16
|
+
chessTree: ChessTree;
|
|
17
|
+
/** Возвращает true, если ход отображается в ячейке верхней сетки. */
|
|
18
|
+
isHighestLevel: boolean;
|
|
19
|
+
/** Возвращает true, если показывать пункт «Поднять приоритет варианта». */
|
|
20
|
+
showUpPriorityItem: boolean;
|
|
21
|
+
/** Возвращает колбэк после удаления варианта. */
|
|
22
|
+
onDeleteVariant: () => void;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const {
|
|
26
|
+
chessNode,
|
|
27
|
+
chessTree,
|
|
28
|
+
isHighestLevel,
|
|
29
|
+
showUpPriorityItem,
|
|
30
|
+
onDeleteVariant,
|
|
31
|
+
}: TMoveContextMenuProps = $props();
|
|
32
|
+
</script>
|
|
33
|
+
|
|
34
|
+
<ContextMenu.Content class="outline-none">
|
|
35
|
+
{#if !isHighestLevel}
|
|
36
|
+
<ContextMenu.Item
|
|
37
|
+
class="gap-4"
|
|
38
|
+
onclick={() => chessTree.makeMainVariant(chessNode)}
|
|
39
|
+
>Сделать вариант главной линией <ContextMenu.Shortcut
|
|
40
|
+
><CheckCheckIcon /></ContextMenu.Shortcut
|
|
41
|
+
></ContextMenu.Item
|
|
42
|
+
>
|
|
43
|
+
{#if showUpPriorityItem}
|
|
44
|
+
<ContextMenu.Item
|
|
45
|
+
class="gap-4"
|
|
46
|
+
onclick={() => chessTree.upPriority(chessNode)}
|
|
47
|
+
>Поднять приоритет варианта <ContextMenu.Shortcut
|
|
48
|
+
><ArrowUpIcon /></ContextMenu.Shortcut
|
|
49
|
+
></ContextMenu.Item
|
|
50
|
+
>
|
|
51
|
+
{/if}
|
|
52
|
+
{:else}
|
|
53
|
+
<ContextMenu.Item
|
|
54
|
+
class="gap-4"
|
|
55
|
+
onclick={() =>
|
|
56
|
+
(chessTree.forcedNodeId = chessNode.parentId ?? null)}
|
|
57
|
+
>Завершить главную линию <ContextMenu.Shortcut
|
|
58
|
+
><FlagIcon /></ContextMenu.Shortcut
|
|
59
|
+
></ContextMenu.Item
|
|
60
|
+
>
|
|
61
|
+
{/if}
|
|
62
|
+
<ContextMenu.Separator />
|
|
63
|
+
<ContextMenu.Item
|
|
64
|
+
class="gap-4 "
|
|
65
|
+
onclick={() => {
|
|
66
|
+
chessTree.deleteVariant(chessNode);
|
|
67
|
+
onDeleteVariant();
|
|
68
|
+
}}
|
|
69
|
+
>Удалить вариант <ContextMenu.Shortcut
|
|
70
|
+
><Trash2Icon /></ContextMenu.Shortcut
|
|
71
|
+
></ContextMenu.Item
|
|
72
|
+
>
|
|
73
|
+
</ContextMenu.Content>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ChessTreeNode } from "../(models)/chessTreeNode.js";
|
|
2
|
+
import type { ChessTree } from "../(classes)/chessTree.svelte.js";
|
|
3
|
+
type TMoveContextMenuProps = {
|
|
4
|
+
/** Возвращает узел хода, к которому применяется меню. */
|
|
5
|
+
chessNode: ChessTreeNode;
|
|
6
|
+
/** Возвращает дерево партии. */
|
|
7
|
+
chessTree: ChessTree;
|
|
8
|
+
/** Возвращает true, если ход отображается в ячейке верхней сетки. */
|
|
9
|
+
isHighestLevel: boolean;
|
|
10
|
+
/** Возвращает true, если показывать пункт «Поднять приоритет варианта». */
|
|
11
|
+
showUpPriorityItem: boolean;
|
|
12
|
+
/** Возвращает колбэк после удаления варианта. */
|
|
13
|
+
onDeleteVariant: () => void;
|
|
14
|
+
};
|
|
15
|
+
declare const MoveContextMenu: import("svelte").Component<TMoveContextMenuProps, {}, "">;
|
|
16
|
+
type MoveContextMenu = ReturnType<typeof MoveContextMenu>;
|
|
17
|
+
export default MoveContextMenu;
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { ChessTreeNode } from "../(models)/chessTreeNode.js";
|
|
3
|
+
|
|
4
|
+
import * as ContextMenu from "../components/ui/context-menu/index.js";
|
|
5
|
+
import NagBadges from "./NagBadges.svelte";
|
|
6
|
+
import MoveWithIcon from "./MoveWithIcon.svelte";
|
|
7
|
+
import MoveContextMenu from "./MoveContextMenu.svelte";
|
|
8
|
+
import type { PieceSet } from "@connectorvol/shared";
|
|
9
|
+
import { ACTIVE_MOVE_CLASS } from "../(utils)/scrollToActiveMove.js";
|
|
10
|
+
import type { ChessTree } from "../(classes)/chessTree.svelte.js";
|
|
11
|
+
|
|
12
|
+
type TPreviewHoverHandlers = {
|
|
13
|
+
/** Возвращает обработчик `mouseenter` для превью FEN. */
|
|
14
|
+
onmouseenter: (e: MouseEvent) => void;
|
|
15
|
+
/** Возвращает обработчик `mouseleave` для превью FEN. */
|
|
16
|
+
onmouseleave: (e: MouseEvent) => void;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
type TMoveSanWithMenuProps = {
|
|
20
|
+
/** Возвращает узел хода. */
|
|
21
|
+
chessNode: ChessTreeNode;
|
|
22
|
+
/** Возвращает родительский узел (для подписей полухода) или null. */
|
|
23
|
+
parentNode: ChessTreeNode | null;
|
|
24
|
+
/** Возвращает набор фигур для иконок SAN. */
|
|
25
|
+
pieceSet: PieceSet;
|
|
26
|
+
/** Возвращает true, если клик по ходу активен. */
|
|
27
|
+
selectable: boolean;
|
|
28
|
+
/** Возвращает true, если ход отображается в верхней строке сетки. */
|
|
29
|
+
isHighestLevel: boolean;
|
|
30
|
+
/** Возвращает true, если этот ход совпадает с текущим выбранным узлом. */
|
|
31
|
+
isCurrentMove: boolean;
|
|
32
|
+
/** Возвращает обработчики превью при наведении. */
|
|
33
|
+
previewHover: TPreviewHoverHandlers;
|
|
34
|
+
/** Представляет обработчик выбора узла по событию `mousedown`. */
|
|
35
|
+
onMoveMouseDown: () => void;
|
|
36
|
+
/** Возвращает класс отступов для inline-триггера не на верхнем уровне. */
|
|
37
|
+
spaceInlineBlockClass: string;
|
|
38
|
+
/** Возвращает список NAG после SAN. */
|
|
39
|
+
nags: number[] | undefined;
|
|
40
|
+
/** Возвращает дерево партии. */
|
|
41
|
+
chessTree: ChessTree;
|
|
42
|
+
/** Возвращает true, если показывать пункт поднятия приоритета в меню. */
|
|
43
|
+
showUpPriorityItem: boolean;
|
|
44
|
+
/** Возвращает колбэк после удаления варианта. */
|
|
45
|
+
onDeleteVariant: () => void;
|
|
46
|
+
/** Возвращает true, если у ячейки SAN в верхней сетке нужна нижняя граница при неполном ряду (нет чёрной ноды). */
|
|
47
|
+
mainRowBottomEdge?: boolean | undefined;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const {
|
|
51
|
+
chessNode,
|
|
52
|
+
parentNode,
|
|
53
|
+
pieceSet,
|
|
54
|
+
selectable,
|
|
55
|
+
isHighestLevel,
|
|
56
|
+
isCurrentMove,
|
|
57
|
+
previewHover,
|
|
58
|
+
onMoveMouseDown,
|
|
59
|
+
spaceInlineBlockClass,
|
|
60
|
+
nags,
|
|
61
|
+
chessTree,
|
|
62
|
+
showUpPriorityItem,
|
|
63
|
+
onDeleteVariant,
|
|
64
|
+
mainRowBottomEdge = false,
|
|
65
|
+
}: TMoveSanWithMenuProps = $props();
|
|
66
|
+
|
|
67
|
+
const node = $derived(chessNode);
|
|
68
|
+
const selectedOnHighestLevel = $derived(isCurrentMove && isHighestLevel);
|
|
69
|
+
|
|
70
|
+
/** Представляет true, если событие пришло из области NAG (ПКМ там не переключает узел). */
|
|
71
|
+
function isFromNagBadges(target: EventTarget | null): boolean {
|
|
72
|
+
return (
|
|
73
|
+
target instanceof Element && target.closest("[data-nag-badges]") !== null
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Представляет выбор узла по ЛКМ везде, по ПКМ — только по SAN (не по NAG); прочие кнопки игнорируются.
|
|
79
|
+
*/
|
|
80
|
+
function handleRowMouseDown(e: MouseEvent) {
|
|
81
|
+
if (e.button === 0) {
|
|
82
|
+
onMoveMouseDown();
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
if (e.button === 2 && !isFromNagBadges(e.target)) {
|
|
86
|
+
onMoveMouseDown();
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
</script>
|
|
90
|
+
|
|
91
|
+
<ContextMenu.Root>
|
|
92
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
93
|
+
<span
|
|
94
|
+
class={{
|
|
95
|
+
"items-center gap-0": true,
|
|
96
|
+
"flex w-full min-w-0 justify-center": isHighestLevel,
|
|
97
|
+
"inline-flex": !isHighestLevel,
|
|
98
|
+
[`${spaceInlineBlockClass} text-center`]: isHighestLevel,
|
|
99
|
+
[`z-10 ${ACTIVE_MOVE_CLASS}`]: selectedOnHighestLevel,
|
|
100
|
+
[ACTIVE_MOVE_CLASS]: isCurrentMove && !isHighestLevel,
|
|
101
|
+
"border-b border-gray-300": isHighestLevel && mainRowBottomEdge,
|
|
102
|
+
"tree-viewer-overflow-tail-border": isHighestLevel && mainRowBottomEdge,
|
|
103
|
+
}}
|
|
104
|
+
onmousedown={handleRowMouseDown}
|
|
105
|
+
>
|
|
106
|
+
<ContextMenu.Trigger
|
|
107
|
+
class={{
|
|
108
|
+
"outline-none focus:outline-none focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-offset-0": true,
|
|
109
|
+
"cursor-pointer ": selectable,
|
|
110
|
+
"cursor-default": !selectable,
|
|
111
|
+
inline: !isHighestLevel,
|
|
112
|
+
}}
|
|
113
|
+
>
|
|
114
|
+
<span
|
|
115
|
+
role="group"
|
|
116
|
+
aria-label="Нотация хода, превью доски при наведении"
|
|
117
|
+
class="inline-flex items-baseline"
|
|
118
|
+
onmouseenter={previewHover.onmouseenter}
|
|
119
|
+
onmouseleave={previewHover.onmouseleave}
|
|
120
|
+
>
|
|
121
|
+
{#if !isHighestLevel && node.data.fullMoves && node.data.ply % 2 === 1}
|
|
122
|
+
{node.data.fullMoves}.
|
|
123
|
+
{/if}
|
|
124
|
+
{#if !isHighestLevel && node.data.fullMoves && node.data.ply % 2 === 0 && (parentNode?.children?.[0]?.id !== chessNode.id || (parentNode?.children?.length ?? 0) > 1)}
|
|
125
|
+
{node.data.fullMoves - 1}...
|
|
126
|
+
{/if}
|
|
127
|
+
<span
|
|
128
|
+
class={{
|
|
129
|
+
"inline-grid items-baseline": true,
|
|
130
|
+
}}
|
|
131
|
+
>
|
|
132
|
+
<span
|
|
133
|
+
class="invisible col-start-1 row-start-1 font-bold"
|
|
134
|
+
aria-hidden="true"
|
|
135
|
+
>
|
|
136
|
+
<MoveWithIcon san={node.data.san} ply={node.data.ply} {pieceSet} />
|
|
137
|
+
</span>
|
|
138
|
+
<span
|
|
139
|
+
class={{
|
|
140
|
+
"col-start-1 row-start-1": true,
|
|
141
|
+
"font-bold": isCurrentMove,
|
|
142
|
+
}}
|
|
143
|
+
>
|
|
144
|
+
<MoveWithIcon san={node.data.san} ply={node.data.ply} {pieceSet} />
|
|
145
|
+
</span>
|
|
146
|
+
</span>
|
|
147
|
+
</span>
|
|
148
|
+
</ContextMenu.Trigger>
|
|
149
|
+
<NagBadges {nags} class="!ml-0.5 shrink-0" />
|
|
150
|
+
</span>
|
|
151
|
+
<MoveContextMenu
|
|
152
|
+
{chessNode}
|
|
153
|
+
{chessTree}
|
|
154
|
+
{isHighestLevel}
|
|
155
|
+
{showUpPriorityItem}
|
|
156
|
+
{onDeleteVariant}
|
|
157
|
+
/>
|
|
158
|
+
</ContextMenu.Root>
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { ChessTreeNode } from "../(models)/chessTreeNode.js";
|
|
2
|
+
import type { PieceSet } from "@connectorvol/shared";
|
|
3
|
+
import type { ChessTree } from "../(classes)/chessTree.svelte.js";
|
|
4
|
+
type TPreviewHoverHandlers = {
|
|
5
|
+
/** Возвращает обработчик `mouseenter` для превью FEN. */
|
|
6
|
+
onmouseenter: (e: MouseEvent) => void;
|
|
7
|
+
/** Возвращает обработчик `mouseleave` для превью FEN. */
|
|
8
|
+
onmouseleave: (e: MouseEvent) => void;
|
|
9
|
+
};
|
|
10
|
+
type TMoveSanWithMenuProps = {
|
|
11
|
+
/** Возвращает узел хода. */
|
|
12
|
+
chessNode: ChessTreeNode;
|
|
13
|
+
/** Возвращает родительский узел (для подписей полухода) или null. */
|
|
14
|
+
parentNode: ChessTreeNode | null;
|
|
15
|
+
/** Возвращает набор фигур для иконок SAN. */
|
|
16
|
+
pieceSet: PieceSet;
|
|
17
|
+
/** Возвращает true, если клик по ходу активен. */
|
|
18
|
+
selectable: boolean;
|
|
19
|
+
/** Возвращает true, если ход отображается в верхней строке сетки. */
|
|
20
|
+
isHighestLevel: boolean;
|
|
21
|
+
/** Возвращает true, если этот ход совпадает с текущим выбранным узлом. */
|
|
22
|
+
isCurrentMove: boolean;
|
|
23
|
+
/** Возвращает обработчики превью при наведении. */
|
|
24
|
+
previewHover: TPreviewHoverHandlers;
|
|
25
|
+
/** Представляет обработчик выбора узла по событию `mousedown`. */
|
|
26
|
+
onMoveMouseDown: () => void;
|
|
27
|
+
/** Возвращает класс отступов для inline-триггера не на верхнем уровне. */
|
|
28
|
+
spaceInlineBlockClass: string;
|
|
29
|
+
/** Возвращает список NAG после SAN. */
|
|
30
|
+
nags: number[] | undefined;
|
|
31
|
+
/** Возвращает дерево партии. */
|
|
32
|
+
chessTree: ChessTree;
|
|
33
|
+
/** Возвращает true, если показывать пункт поднятия приоритета в меню. */
|
|
34
|
+
showUpPriorityItem: boolean;
|
|
35
|
+
/** Возвращает колбэк после удаления варианта. */
|
|
36
|
+
onDeleteVariant: () => void;
|
|
37
|
+
/** Возвращает true, если у ячейки SAN в верхней сетке нужна нижняя граница при неполном ряду (нет чёрной ноды). */
|
|
38
|
+
mainRowBottomEdge?: boolean | undefined;
|
|
39
|
+
};
|
|
40
|
+
declare const MoveSanWithMenu: import("svelte").Component<TMoveSanWithMenuProps, {}, "">;
|
|
41
|
+
type MoveSanWithMenu = ReturnType<typeof MoveSanWithMenu>;
|
|
42
|
+
export default MoveSanWithMenu;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { nagBadgeBackground, transformNag } from "@connectorvol/shared";
|
|
3
|
+
import { cn } from "../utils.js";
|
|
4
|
+
import { nagDescription } from "../(models)/nagCatalog.js";
|
|
5
|
+
|
|
6
|
+
type TNagBadgesProps = {
|
|
7
|
+
/** Возвращает список NAG для отображения. */
|
|
8
|
+
nags: number[] | undefined;
|
|
9
|
+
/** Возвращает дополнительные классы корневого контейнера. */
|
|
10
|
+
class?: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const { nags, class: className = "" }: TNagBadgesProps = $props();
|
|
14
|
+
</script>
|
|
15
|
+
|
|
16
|
+
{#if nags?.length}
|
|
17
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
18
|
+
<span
|
|
19
|
+
data-nag-badges
|
|
20
|
+
class={cn(
|
|
21
|
+
"ml-0.5 inline-flex max-w-full flex-nowrap items-center gap-0.5 align-middle whitespace-nowrap overflow-hidden",
|
|
22
|
+
className,
|
|
23
|
+
)}
|
|
24
|
+
oncontextmenu={(e) => e.preventDefault()}
|
|
25
|
+
>
|
|
26
|
+
{#each nags ?? [] as nag, i (i)}
|
|
27
|
+
{@const label = transformNag(nag)}
|
|
28
|
+
{#if label}
|
|
29
|
+
<span
|
|
30
|
+
class="inline-flex h-[1.125rem] w-[1.125rem] shrink-0 cursor-help items-center justify-center rounded-full border border-white/25 text-center font-serif text-[0.625rem] font-semibold leading-none text-white shadow-[0_1px_2px_rgba(0,0,0,0.25)]"
|
|
31
|
+
style:background={nagBadgeBackground(nag)}
|
|
32
|
+
title={nagDescription(nag)}>{label}</span
|
|
33
|
+
>
|
|
34
|
+
{/if}
|
|
35
|
+
{/each}
|
|
36
|
+
</span>
|
|
37
|
+
{/if}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
type TNagBadgesProps = {
|
|
2
|
+
/** Возвращает список NAG для отображения. */
|
|
3
|
+
nags: number[] | undefined;
|
|
4
|
+
/** Возвращает дополнительные классы корневого контейнера. */
|
|
5
|
+
class?: string;
|
|
6
|
+
};
|
|
7
|
+
declare const NagBadges: import("svelte").Component<TNagBadgesProps, {}, "">;
|
|
8
|
+
type NagBadges = ReturnType<typeof NagBadges>;
|
|
9
|
+
export default NagBadges;
|