@pgcorp/ui-kit 0.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/LICENSE +21 -0
- package/README.md +157 -0
- package/docs/accessibility.md +27 -0
- package/docs/getting-started.md +80 -0
- package/docs/licensing.md +30 -0
- package/docs/public-api.md +39 -0
- package/docs/security.md +21 -0
- package/docs/theming.md +35 -0
- package/docs/versioning.md +23 -0
- package/index.ts +3 -0
- package/package.json +592 -0
- package/src/components/icons/_internal/generatedSputnigUiIconMarkup.ts +971 -0
- package/src/components/icons/sputnigUiIcons.ts +530 -0
- package/src/components/layout/SAppShell.vue +232 -0
- package/src/components/layout/SDockRegion.vue +200 -0
- package/src/components/layout/SResizeHandle.vue +251 -0
- package/src/components/layout/SSideMenuButton.vue +104 -0
- package/src/components/layout/SSideMenuRail.vue +47 -0
- package/src/components/layout/SStack.css +25 -0
- package/src/components/layout/SStack.vue +57 -0
- package/src/components/layout/SStatusBarShell.css +15 -0
- package/src/components/layout/SStatusBarShell.vue +26 -0
- package/src/components/layout/SWorkbenchLayout.vue +53 -0
- package/src/components/layout/SWorkbenchPanelShell.css +161 -0
- package/src/components/layout/SWorkbenchPanelShell.vue +286 -0
- package/src/components/layout/_internal/SAppBrand.css +45 -0
- package/src/components/layout/_internal/SAppBrand.vue +37 -0
- package/src/components/layout/_internal/SAppBrandContent.vue +30 -0
- package/src/components/layout/types.ts +28 -0
- package/src/components/shared/_internal/SDialogSurface.css +127 -0
- package/src/components/shared/_internal/SDialogSurface.vue +117 -0
- package/src/components/shared/_internal/SDisclosureSummary.css +92 -0
- package/src/components/shared/_internal/SDisclosureSummary.vue +121 -0
- package/src/components/shared/_internal/SListboxGroup.css +18 -0
- package/src/components/shared/_internal/SListboxGroup.vue +24 -0
- package/src/components/shared/_internal/SMenuItem.css +112 -0
- package/src/components/shared/_internal/SMenuItem.vue +213 -0
- package/src/components/shared/_internal/SMenuLayer.css +21 -0
- package/src/components/shared/_internal/SMenuLayer.vue +48 -0
- package/src/components/shared/_internal/SMenuSeparator.css +5 -0
- package/src/components/shared/_internal/SMenuSeparator.vue +13 -0
- package/src/components/shared/_internal/SToastItem.css +65 -0
- package/src/components/shared/_internal/SToastItem.vue +58 -0
- package/src/components/shared/_internal/STreeNode.vue +761 -0
- package/src/components/shared/_internal/dialogSurface.ts +105 -0
- package/src/components/shared/_internal/focusTrap.ts +40 -0
- package/src/components/shared/_internal/listboxContext.ts +16 -0
- package/src/components/shared/_internal/markerContract.ts +10 -0
- package/src/components/shared/_internal/menuContext.ts +23 -0
- package/src/components/shared/_internal/selectionContract.ts +239 -0
- package/src/components/shared/_internal/sidebarGroupContext.ts +31 -0
- package/src/components/shared/_internal/tabsContext.ts +57 -0
- package/src/components/shared/_internal/treeRuntime.ts +203 -0
- package/src/components/shared/_internal/useSelectionRoving.ts +162 -0
- package/src/components/shared/codeLanguages.ts +168 -0
- package/src/components/shared/complex/SKanbanCard.css +8 -0
- package/src/components/shared/complex/SKanbanCard.vue +76 -0
- package/src/components/shared/complex/SKanbanLane.vue +82 -0
- package/src/components/shared/complex/STree.css +24 -0
- package/src/components/shared/complex/STree.vue +288 -0
- package/src/components/shared/complex/treeAdapter.ts +210 -0
- package/src/components/shared/complex/types.ts +102 -0
- package/src/components/shared/containers/SCloudChrome.css +103 -0
- package/src/components/shared/containers/SCloudChrome.vue +87 -0
- package/src/components/shared/containers/SCloudChromeEmpty.css +10 -0
- package/src/components/shared/containers/SCloudChromeEmpty.vue +18 -0
- package/src/components/shared/containers/SCloudChromeRow.css +78 -0
- package/src/components/shared/containers/SCloudChromeRow.vue +108 -0
- package/src/components/shared/containers/SCollapsiblePanel.css +57 -0
- package/src/components/shared/containers/SCollapsiblePanel.vue +360 -0
- package/src/components/shared/containers/SDrawer.css +60 -0
- package/src/components/shared/containers/SDrawer.vue +86 -0
- package/src/components/shared/containers/SFormGrid.css +26 -0
- package/src/components/shared/containers/SFormGrid.vue +46 -0
- package/src/components/shared/containers/SFormGridItem.css +18 -0
- package/src/components/shared/containers/SFormGridItem.vue +29 -0
- package/src/components/shared/containers/SLeftSidebar.vue +74 -0
- package/src/components/shared/containers/SModal.css +27 -0
- package/src/components/shared/containers/SModal.vue +105 -0
- package/src/components/shared/containers/SPageHeader.css +66 -0
- package/src/components/shared/containers/SPageHeader.vue +45 -0
- package/src/components/shared/containers/SPanel.css +134 -0
- package/src/components/shared/containers/SPanel.vue +169 -0
- package/src/components/shared/containers/SPopover.css +35 -0
- package/src/components/shared/containers/SPopover.vue +551 -0
- package/src/components/shared/containers/SSidebarGroup.css +12 -0
- package/src/components/shared/containers/SSidebarGroup.vue +443 -0
- package/src/components/shared/containers/SSidebarSection.css +41 -0
- package/src/components/shared/containers/SSidebarSection.vue +268 -0
- package/src/components/shared/containers/disclosureSummary.ts +48 -0
- package/src/components/shared/containers/popover.ts +72 -0
- package/src/components/shared/containers/sidebar.ts +206 -0
- package/src/components/shared/controls/SButton.css +274 -0
- package/src/components/shared/controls/SButton.vue +211 -0
- package/src/components/shared/controls/SCheckbox.css +65 -0
- package/src/components/shared/controls/SCheckbox.vue +130 -0
- package/src/components/shared/controls/SCheckboxGroup.vue +134 -0
- package/src/components/shared/controls/SChoiceCards.css +29 -0
- package/src/components/shared/controls/SChoiceCards.vue +145 -0
- package/src/components/shared/controls/SColorSelect.vue +146 -0
- package/src/components/shared/controls/SComboboxTrigger.css +35 -0
- package/src/components/shared/controls/SComboboxTrigger.vue +122 -0
- package/src/components/shared/controls/SContextMenu.css +11 -0
- package/src/components/shared/controls/SContextMenu.vue +186 -0
- package/src/components/shared/controls/SContextToggleButton.vue +64 -0
- package/src/components/shared/controls/SDragHandle.vue +100 -0
- package/src/components/shared/controls/SDropdownMenu.css +5 -0
- package/src/components/shared/controls/SDropdownMenu.vue +188 -0
- package/src/components/shared/controls/SField.css +79 -0
- package/src/components/shared/controls/SField.vue +89 -0
- package/src/components/shared/controls/SFieldGroup.css +58 -0
- package/src/components/shared/controls/SFieldGroup.vue +117 -0
- package/src/components/shared/controls/SFieldLabel.css +12 -0
- package/src/components/shared/controls/SFieldLabel.vue +51 -0
- package/src/components/shared/controls/SInlineTokenEditor.css +61 -0
- package/src/components/shared/controls/SInlineTokenEditor.vue +1081 -0
- package/src/components/shared/controls/SInputText.css +66 -0
- package/src/components/shared/controls/SInputText.vue +321 -0
- package/src/components/shared/controls/SInteractiveSurface.css +72 -0
- package/src/components/shared/controls/SInteractiveSurface.vue +326 -0
- package/src/components/shared/controls/SLink.css +114 -0
- package/src/components/shared/controls/SLink.ts +85 -0
- package/src/components/shared/controls/SLink.vue +114 -0
- package/src/components/shared/controls/SListbox.css +31 -0
- package/src/components/shared/controls/SListbox.vue +388 -0
- package/src/components/shared/controls/SListboxOption.css +63 -0
- package/src/components/shared/controls/SListboxOption.vue +85 -0
- package/src/components/shared/controls/SMarker.css +19 -0
- package/src/components/shared/controls/SMarker.vue +33 -0
- package/src/components/shared/controls/SMenuSurface.css +20 -0
- package/src/components/shared/controls/SMenuSurface.vue +333 -0
- package/src/components/shared/controls/SSegmentedControl.css +38 -0
- package/src/components/shared/controls/SSegmentedControl.vue +134 -0
- package/src/components/shared/controls/SSelect.css +12 -0
- package/src/components/shared/controls/SSelect.vue +338 -0
- package/src/components/shared/controls/STextarea.css +31 -0
- package/src/components/shared/controls/STextarea.vue +316 -0
- package/src/components/shared/controls/_internal/SInlineTokenSurface.vue +224 -0
- package/src/components/shared/controls/menu.ts +178 -0
- package/src/components/shared/data-display/SActionCard.css +6 -0
- package/src/components/shared/data-display/SActionCard.vue +49 -0
- package/src/components/shared/data-display/SActionList.css +15 -0
- package/src/components/shared/data-display/SActionList.vue +34 -0
- package/src/components/shared/data-display/SActionListItem.css +114 -0
- package/src/components/shared/data-display/SActionListItem.vue +230 -0
- package/src/components/shared/data-display/SBadge.css +73 -0
- package/src/components/shared/data-display/SBadge.vue +52 -0
- package/src/components/shared/data-display/SCodeBlock.css +56 -0
- package/src/components/shared/data-display/SCodeBlock.vue +188 -0
- package/src/components/shared/data-display/SCodeEditor.css +178 -0
- package/src/components/shared/data-display/SCodeEditor.vue +274 -0
- package/src/components/shared/data-display/SCodeSearchPanel.vue +327 -0
- package/src/components/shared/data-display/SCopyField.vue +77 -0
- package/src/components/shared/data-display/SDocBlock.css +136 -0
- package/src/components/shared/data-display/SDocBlock.vue +358 -0
- package/src/components/shared/data-display/SEmptyState.css +42 -0
- package/src/components/shared/data-display/SEmptyState.vue +79 -0
- package/src/components/shared/data-display/SExpandableText.vue +314 -0
- package/src/components/shared/data-display/SJsonTree.css +6 -0
- package/src/components/shared/data-display/SJsonTree.vue +111 -0
- package/src/components/shared/data-display/SKeyValueGrid.css +37 -0
- package/src/components/shared/data-display/SKeyValueGrid.vue +277 -0
- package/src/components/shared/data-display/SLinkedSystemsList.vue +270 -0
- package/src/components/shared/data-display/SMessage.css +88 -0
- package/src/components/shared/data-display/SMessage.vue +152 -0
- package/src/components/shared/data-display/SMetricCard.css +32 -0
- package/src/components/shared/data-display/SMetricCard.vue +117 -0
- package/src/components/shared/data-display/SProgressBar.css +84 -0
- package/src/components/shared/data-display/SProgressBar.vue +114 -0
- package/src/components/shared/data-display/SProgressIndicator.css +59 -0
- package/src/components/shared/data-display/SProgressIndicator.vue +104 -0
- package/src/components/shared/data-display/SSectionHeader.css +84 -0
- package/src/components/shared/data-display/SSectionHeader.vue +58 -0
- package/src/components/shared/data-display/SStatus.css +90 -0
- package/src/components/shared/data-display/SStatus.vue +79 -0
- package/src/components/shared/data-display/STable.css +83 -0
- package/src/components/shared/data-display/STable.vue +339 -0
- package/src/components/shared/data-display/SToastContainer.css +53 -0
- package/src/components/shared/data-display/SToastContainer.vue +31 -0
- package/src/components/shared/data-display/STooltip.css +114 -0
- package/src/components/shared/data-display/STooltip.vue +550 -0
- package/src/components/shared/data-display/STooltipAnchor.css +21 -0
- package/src/components/shared/data-display/STooltipAnchor.vue +119 -0
- package/src/components/shared/data-display/SVirtualList.css +24 -0
- package/src/components/shared/data-display/SVirtualList.ts +19 -0
- package/src/components/shared/data-display/SVirtualList.vue +361 -0
- package/src/components/shared/data-display/json.ts +136 -0
- package/src/components/shared/data-display/table.ts +76 -0
- package/src/components/shared/database/SDataGrid.css +3 -0
- package/src/components/shared/database/SDataGrid.vue +464 -0
- package/src/components/shared/database/SSqlEditor.vue +363 -0
- package/src/components/shared/database/dataGrid.ts +61 -0
- package/src/components/shared/feedback/SAsyncState.css +26 -0
- package/src/components/shared/feedback/SAsyncState.vue +190 -0
- package/src/components/shared/graph/SGraphViewport.ts +1 -0
- package/src/components/shared/graph/SGraphViewport.vue +858 -0
- package/src/components/shared/graph/_internal/graphViewportContract.ts +362 -0
- package/src/components/shared/markdown.ts +169 -0
- package/src/components/shared/navigation/SBottomNav.vue +692 -0
- package/src/components/shared/navigation/SBreadcrumbs.css +26 -0
- package/src/components/shared/navigation/SBreadcrumbs.vue +155 -0
- package/src/components/shared/navigation/SCatalogNavigator.css +100 -0
- package/src/components/shared/navigation/SCatalogNavigator.vue +308 -0
- package/src/components/shared/navigation/STab.vue +363 -0
- package/src/components/shared/navigation/STabList.vue +442 -0
- package/src/components/shared/navigation/STabPanel.vue +165 -0
- package/src/components/shared/navigation/STabPanels.vue +33 -0
- package/src/components/shared/navigation/STabs.vue +617 -0
- package/src/components/shared/navigation/SWizardSteps.css +69 -0
- package/src/components/shared/navigation/SWizardSteps.vue +171 -0
- package/src/components/shared/navigation/_internal/SCatalogNavigatorGroup.vue +63 -0
- package/src/components/shared/persona/SPersonaProjectPicker.css +36 -0
- package/src/components/shared/persona/SPersonaProjectPicker.vue +361 -0
- package/src/components/shared/persona/SPersonaRunWorkbench.css +109 -0
- package/src/components/shared/persona/SPersonaRunWorkbench.vue +549 -0
- package/src/components/shared/persona/_internal/SPersonaRunConsole.css +40 -0
- package/src/components/shared/persona/_internal/SPersonaRunConsole.vue +298 -0
- package/src/components/shared/persona/_internal/inventory.ts +110 -0
- package/src/components/shared/persona/_internal/labels.ts +79 -0
- package/src/components/shared/persona/presentation.ts +371 -0
- package/src/components/shared/persona/types.ts +256 -0
- package/src/composables/uiPreferencesReset.ts +52 -0
- package/src/composables/useClipboard.ts +37 -0
- package/src/composables/useDialogOverlay.ts +85 -0
- package/src/composables/useDisclosure.ts +40 -0
- package/src/composables/useFloatingPosition.ts +245 -0
- package/src/composables/useNotifier.ts +26 -0
- package/src/composables/useSidebarPanelState.ts +610 -0
- package/src/internal/codeEditorContract.ts +110 -0
- package/src/internal/codeEditorExtensions.ts +27 -0
- package/src/internal/feedbackPresentation.ts +136 -0
- package/src/internal/fieldSurface.ts +59 -0
- package/src/internal/focusNavigation.ts +69 -0
- package/src/internal/inlineTokenEditorContract.ts +136 -0
- package/src/internal/interactiveElement.ts +33 -0
- package/src/internal/layerStack.ts +202 -0
- package/src/internal/linkTarget.ts +256 -0
- package/src/internal/ownedAttrs.ts +464 -0
- package/src/internal/passiveContentContract.ts +129 -0
- package/src/internal/pointerInteractionLease.ts +81 -0
- package/src/internal/runtimeContract.ts +194 -0
- package/src/internal/runtimeTheme.ts +159 -0
- package/src/internal/semanticSizing.ts +60 -0
- package/src/internal/useMenuLayer.ts +129 -0
- package/src/stores/useNotifierStore.ts +69 -0
- package/src/styles/reference.css +4 -0
- package/src/styles/style.css +151 -0
- package/src/styles/tokens.css +836 -0
- package/src/theme.ts +449 -0
- package/src/types/highlight-js-lib.d.ts +76 -0
- package/src/variants.ts +15 -0
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div
|
|
3
|
+
v-bind="ownedAttrs.bindings()"
|
|
4
|
+
ref="sectionEl"
|
|
5
|
+
class="s-sidebar-section-wrapper"
|
|
6
|
+
:class="{ 's-sidebar-section-wrapper--resizable-before': isResizableBefore }"
|
|
7
|
+
:style="style"
|
|
8
|
+
>
|
|
9
|
+
<!-- Разделитель должен быть ПЕРЕД секцией, а не внутри нее. -->
|
|
10
|
+
<SResizeHandle
|
|
11
|
+
v-if="isResizableBefore"
|
|
12
|
+
orientation="horizontal"
|
|
13
|
+
label="Изменить высоту секций"
|
|
14
|
+
:value="resizeContract.value"
|
|
15
|
+
:min="resizeContract.min"
|
|
16
|
+
:max="resizeContract.max"
|
|
17
|
+
:step="10"
|
|
18
|
+
:data-test-id="resizeContract.testId"
|
|
19
|
+
@resize-start="handleResizeStart"
|
|
20
|
+
@resize="handleResize"
|
|
21
|
+
@resize-end="handleResizeEnd"
|
|
22
|
+
/>
|
|
23
|
+
<div class="s-sidebar-section">
|
|
24
|
+
<div class="s-sidebar-section-header" :class="{ 's-sidebar-section-header--mobile': layout === 'mobile' }">
|
|
25
|
+
<div class="s-sidebar-section-trigger">
|
|
26
|
+
<SButton
|
|
27
|
+
:id="triggerId"
|
|
28
|
+
:size="layout === 'mobile' ? 'xs' : 'sm'"
|
|
29
|
+
severity="secondary"
|
|
30
|
+
appearance="ghost"
|
|
31
|
+
shape="row"
|
|
32
|
+
width="full"
|
|
33
|
+
content-align="start"
|
|
34
|
+
data-sidebar-section-toggle
|
|
35
|
+
:disabled="disabled"
|
|
36
|
+
:disclosure="{ expanded: isOpen, controls: panelId, iconPlacement: 'leading' }"
|
|
37
|
+
@click="disclosure.toggle"
|
|
38
|
+
>
|
|
39
|
+
<SDisclosureSummary
|
|
40
|
+
:title="summary"
|
|
41
|
+
:typography="summaryTypography"
|
|
42
|
+
:casing="summaryCase"
|
|
43
|
+
:state="summaryState"
|
|
44
|
+
:facts="summaryFacts"
|
|
45
|
+
:badges="summaryBadges"
|
|
46
|
+
:status="summaryStatus"
|
|
47
|
+
/>
|
|
48
|
+
</SButton>
|
|
49
|
+
</div>
|
|
50
|
+
<div v-if="hasActions" class="s-sidebar-section-actions">
|
|
51
|
+
<slot name="actions" />
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
<div
|
|
55
|
+
v-if="isOpen"
|
|
56
|
+
:id="panelId"
|
|
57
|
+
:aria-labelledby="triggerId"
|
|
58
|
+
role="region"
|
|
59
|
+
tabindex="0"
|
|
60
|
+
class="s-sidebar-section-content"
|
|
61
|
+
>
|
|
62
|
+
<slot />
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
</template>
|
|
67
|
+
|
|
68
|
+
<script setup lang="ts">
|
|
69
|
+
import { computed, inject, onBeforeUnmount, onMounted, ref, useId, watch } from 'vue';
|
|
70
|
+
|
|
71
|
+
import { useDisclosure } from '../../../composables/useDisclosure';
|
|
72
|
+
import { useOwnedAttrs } from '../../../internal/ownedAttrs';
|
|
73
|
+
import SResizeHandle from '../../layout/SResizeHandle.vue';
|
|
74
|
+
import {
|
|
75
|
+
sidebarGroupContextKey,
|
|
76
|
+
type RegisteredSidebarSection,
|
|
77
|
+
} from '../_internal/sidebarGroupContext';
|
|
78
|
+
import SDisclosureSummary from '../_internal/SDisclosureSummary.vue';
|
|
79
|
+
import SButton from '../controls/SButton.vue';
|
|
80
|
+
import type {
|
|
81
|
+
SDisclosureSummaryBadge,
|
|
82
|
+
SDisclosureSummaryCase,
|
|
83
|
+
SDisclosureSummaryFact,
|
|
84
|
+
SDisclosureSummaryState,
|
|
85
|
+
SDisclosureSummaryStatus,
|
|
86
|
+
SDisclosureSummaryTypography,
|
|
87
|
+
} from './disclosureSummary';
|
|
88
|
+
|
|
89
|
+
defineOptions({
|
|
90
|
+
name: 'SSidebarSection',
|
|
91
|
+
inheritAttrs: false,
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
const props = withDefaults(defineProps<{
|
|
95
|
+
summary: string;
|
|
96
|
+
summaryTypography?: SDisclosureSummaryTypography;
|
|
97
|
+
summaryCase?: SDisclosureSummaryCase;
|
|
98
|
+
summaryState?: SDisclosureSummaryState;
|
|
99
|
+
summaryFacts?: readonly SDisclosureSummaryFact[];
|
|
100
|
+
summaryBadges?: readonly SDisclosureSummaryBadge[];
|
|
101
|
+
summaryStatus?: SDisclosureSummaryStatus;
|
|
102
|
+
stateKey: string;
|
|
103
|
+
layout?: 'desktop' | 'mobile';
|
|
104
|
+
disabled?: boolean;
|
|
105
|
+
}>(), {
|
|
106
|
+
summaryTypography: 'strong',
|
|
107
|
+
summaryCase: 'uppercase',
|
|
108
|
+
summaryState: undefined,
|
|
109
|
+
summaryFacts: () => [],
|
|
110
|
+
summaryBadges: () => [],
|
|
111
|
+
summaryStatus: undefined,
|
|
112
|
+
layout: 'desktop',
|
|
113
|
+
disabled: false,
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
const slots = defineSlots<{
|
|
117
|
+
default(): unknown;
|
|
118
|
+
actions?(): unknown;
|
|
119
|
+
}>();
|
|
120
|
+
|
|
121
|
+
const sectionEl = ref<HTMLDivElement | null>(null);
|
|
122
|
+
const ownedAttrs = useOwnedAttrs({ component: 'SSidebarSection', owner: 'sidebar section wrapper' });
|
|
123
|
+
const hasActions = computed(() => Boolean(slots.actions));
|
|
124
|
+
const injectedGroupContext = inject(sidebarGroupContextKey);
|
|
125
|
+
if (!injectedGroupContext) {
|
|
126
|
+
throw new Error('SSidebarSection: component must be rendered inside SSidebarGroup');
|
|
127
|
+
}
|
|
128
|
+
const groupContext = injectedGroupContext;
|
|
129
|
+
if (props.stateKey.trim().length === 0) {
|
|
130
|
+
throw new TypeError('SSidebarSection: stateKey must be a non-empty string');
|
|
131
|
+
}
|
|
132
|
+
const registeredStateKey = props.stateKey;
|
|
133
|
+
const disclosureBaseId = `s-sidebar-section-${useId()}-${props.stateKey.replace(/[^a-zA-Z0-9_-]/g, '-')}`;
|
|
134
|
+
let unregisterSection: (() => void) | null = null;
|
|
135
|
+
const isRegistered = ref(false);
|
|
136
|
+
|
|
137
|
+
onMounted(() => {
|
|
138
|
+
const element = sectionEl.value;
|
|
139
|
+
if (!element) {
|
|
140
|
+
throw new Error('SSidebarSection: section wrapper is unavailable during mount');
|
|
141
|
+
}
|
|
142
|
+
unregisterSection = groupContext.registerSection({ key: registeredStateKey, element });
|
|
143
|
+
isRegistered.value = true;
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
onBeforeUnmount(() => {
|
|
147
|
+
isRegistered.value = false;
|
|
148
|
+
unregisterSection?.();
|
|
149
|
+
unregisterSection = null;
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
watch(() => props.stateKey, (stateKey) => {
|
|
153
|
+
if (stateKey !== registeredStateKey) {
|
|
154
|
+
throw new Error(
|
|
155
|
+
`SSidebarSection: stateKey is immutable after setup; received '${registeredStateKey}' -> '${stateKey}'`,
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
const sectionIndex = computed(() => {
|
|
161
|
+
const index = groupContext.orderedSections.value.findIndex(
|
|
162
|
+
(section) => section.key === registeredStateKey,
|
|
163
|
+
);
|
|
164
|
+
if (isRegistered.value && index < 0) {
|
|
165
|
+
throw new Error(
|
|
166
|
+
`SSidebarSection: registered section '${registeredStateKey}' is absent from group order`,
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
return index;
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
const isOpen = computed(() => {
|
|
173
|
+
const state = groupContext.sectionsState.value[registeredStateKey];
|
|
174
|
+
if (!state) {
|
|
175
|
+
throw new Error(
|
|
176
|
+
`SSidebarSection: sectionsState has no entry for stateKey '${registeredStateKey}'`,
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
return state.isOpen;
|
|
180
|
+
});
|
|
181
|
+
const disclosure = useDisclosure({
|
|
182
|
+
id: disclosureBaseId,
|
|
183
|
+
open: isOpen,
|
|
184
|
+
disabled: () => props.disabled ?? false,
|
|
185
|
+
onUpdate: () => groupContext.toggleSection(registeredStateKey),
|
|
186
|
+
});
|
|
187
|
+
const { triggerId, panelId } = disclosure;
|
|
188
|
+
|
|
189
|
+
const style = computed(() => {
|
|
190
|
+
return groupContext.getSectionStyle(registeredStateKey);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
const isResizableBefore = computed(() => {
|
|
194
|
+
if (sectionIndex.value <= 0 || !isOpen.value) {
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
const sections = groupContext.orderedSections.value;
|
|
198
|
+
for (let i = sectionIndex.value - 1; i >= 0; i--) {
|
|
199
|
+
if (sections[i]?.state.isOpen) return true;
|
|
200
|
+
}
|
|
201
|
+
return false;
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
const topSectionIndex = computed(() => {
|
|
205
|
+
for (let index = sectionIndex.value - 1; index >= 0; index--) {
|
|
206
|
+
if (groupContext.orderedSections.value[index]?.state.isOpen) return index;
|
|
207
|
+
}
|
|
208
|
+
return -1;
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
function measuredHeight(section: RegisteredSidebarSection): number {
|
|
212
|
+
const height = section.element.offsetHeight;
|
|
213
|
+
if (!Number.isFinite(height) || height < 0) {
|
|
214
|
+
throw new RangeError(
|
|
215
|
+
`SSidebarSection: sections.${section.key}.offsetHeight must be a non-negative finite number`,
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
return height;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function resizePair(): readonly [RegisteredSidebarSection, RegisteredSidebarSection] {
|
|
222
|
+
if (topSectionIndex.value < 0 || sectionIndex.value < 0) {
|
|
223
|
+
throw new Error(`SSidebarSection: resize pair is unavailable for section '${registeredStateKey}'`);
|
|
224
|
+
}
|
|
225
|
+
const sections = groupContext.orderedSections.value;
|
|
226
|
+
const top = sections[topSectionIndex.value];
|
|
227
|
+
const bottom = sections[sectionIndex.value];
|
|
228
|
+
if (!top || !bottom) {
|
|
229
|
+
throw new Error(
|
|
230
|
+
`SSidebarSection: resize pair indices are outside mounted group order for section '${registeredStateKey}'`,
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
return [top, bottom];
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const resizeContract = computed(() => {
|
|
237
|
+
const [top, bottom] = resizePair();
|
|
238
|
+
const value = Math.max(top.state.minSize, top.state.size ?? measuredHeight(top));
|
|
239
|
+
const bottomSize = Math.max(
|
|
240
|
+
bottom.state.minSize,
|
|
241
|
+
bottom.state.size ?? measuredHeight(bottom),
|
|
242
|
+
);
|
|
243
|
+
const pairSize = value + bottomSize;
|
|
244
|
+
return {
|
|
245
|
+
value,
|
|
246
|
+
min: top.state.minSize,
|
|
247
|
+
max: Math.max(top.state.minSize, pairSize - bottom.state.minSize),
|
|
248
|
+
testId: `sidebar-resize-${top.key}-${bottom.key}`,
|
|
249
|
+
};
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
function handleResizeStart(): void {
|
|
253
|
+
const [top, bottom] = resizePair();
|
|
254
|
+
groupContext.beginResize(top.key, bottom.key);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function handleResize(value: number): void {
|
|
258
|
+
const [top, bottom] = resizePair();
|
|
259
|
+
groupContext.resizeSections(top.key, bottom.key, value);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function handleResizeEnd(): void {
|
|
263
|
+
const [top, bottom] = resizePair();
|
|
264
|
+
groupContext.finishResize(top.key, bottom.key);
|
|
265
|
+
}
|
|
266
|
+
</script>
|
|
267
|
+
|
|
268
|
+
<style lang="postcss" src="./SSidebarSection.css" scoped></style>
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { BadgeSeverity } from '../data-display/SBadge.vue'
|
|
2
|
+
import type { StatusTone } from '../data-display/SStatus.vue'
|
|
3
|
+
|
|
4
|
+
/** Типографика заголовка disclosure. / Disclosure summary typography. */
|
|
5
|
+
export type SDisclosureSummaryTypography = 'default' | 'strong' | 'mono'
|
|
6
|
+
|
|
7
|
+
/** Регистр заголовка disclosure. / Disclosure summary letter case. */
|
|
8
|
+
export type SDisclosureSummaryCase = 'sentence' | 'uppercase'
|
|
9
|
+
|
|
10
|
+
/** Семантическое состояние с системной иконкой. / Semantic state with a system-owned icon. */
|
|
11
|
+
export type SDisclosureSummaryState = 'neutral' | 'progress' | 'success' | 'warn' | 'danger'
|
|
12
|
+
|
|
13
|
+
/** Семантический тон факта. / Semantic fact tone. */
|
|
14
|
+
export type SDisclosureSummaryTone = 'neutral' | 'info' | 'success' | 'warn' | 'danger'
|
|
15
|
+
|
|
16
|
+
/** Короткая пара label/value в заголовке. / Compact label/value pair in a summary. */
|
|
17
|
+
export interface SDisclosureSummaryFact {
|
|
18
|
+
/** Стабильная identity факта, не зависящая от отображаемого значения. / Stable fact identity independent of its displayed value. */
|
|
19
|
+
id: string
|
|
20
|
+
/** Необязательная подпись значения. / Optional value label. */
|
|
21
|
+
label?: string
|
|
22
|
+
/** Отображаемое непустое значение. / Non-empty displayed value. */
|
|
23
|
+
value: string | number
|
|
24
|
+
/** Семантический тон факта. / Semantic tone of the fact. */
|
|
25
|
+
tone?: SDisclosureSummaryTone
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Неинтерактивный badge в заголовке. / Non-interactive summary badge. */
|
|
29
|
+
export interface SDisclosureSummaryBadge {
|
|
30
|
+
/** Стабильная identity badge, не зависящая от отображаемой подписи. / Stable badge identity independent of its displayed label. */
|
|
31
|
+
id: string
|
|
32
|
+
/** Непустая подпись badge. / Non-empty badge label. */
|
|
33
|
+
label: string | number
|
|
34
|
+
/** Семантическая важность badge. / Semantic badge severity. */
|
|
35
|
+
severity?: BadgeSeverity
|
|
36
|
+
/** Дополнительная нативная подсказка. / Additional native tooltip. */
|
|
37
|
+
title?: string
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Текстовый статус в конце заголовка. / Trailing text status in a summary. */
|
|
41
|
+
export interface SDisclosureSummaryStatus {
|
|
42
|
+
/** Непустая подпись статуса. / Non-empty status label. */
|
|
43
|
+
label: string
|
|
44
|
+
/** Семантический тон статуса. / Semantic status tone. */
|
|
45
|
+
tone?: StatusTone
|
|
46
|
+
/** Дополнительная нативная подсказка. / Additional native tooltip. */
|
|
47
|
+
title?: string
|
|
48
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { InteractiveElementRefTarget } from '../../../internal/interactiveElement'
|
|
2
|
+
import type { SButtonDisclosure } from '../controls/SButton.vue'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Режим реакции custom trigger на единый click-контракт.
|
|
6
|
+
* Custom-trigger activation mode for the unified click contract.
|
|
7
|
+
*/
|
|
8
|
+
export type SPopoverTriggerActivation = 'toggle' | 'manual'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Допустимая цель callback-ref custom trigger.
|
|
12
|
+
* Supported callback-ref target for a custom trigger.
|
|
13
|
+
*/
|
|
14
|
+
export type SPopoverTriggerRefTarget = InteractiveElementRefTarget
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Полный контракт для `v-bind` непосредственно на button-root custom trigger.
|
|
18
|
+
* Complete contract for `v-bind` directly on the custom trigger button root.
|
|
19
|
+
*/
|
|
20
|
+
export interface SPopoverTriggerBinding {
|
|
21
|
+
readonly ref: (target: SPopoverTriggerRefTarget) => void
|
|
22
|
+
readonly disclosure: SButtonDisclosure
|
|
23
|
+
readonly 'aria-expanded': boolean
|
|
24
|
+
readonly 'aria-haspopup': 'dialog'
|
|
25
|
+
readonly 'aria-controls': string
|
|
26
|
+
readonly onClick: (event: MouseEvent) => void
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Binder создаёт один контракт для каждого уникального trigger id.
|
|
31
|
+
* The binder creates one contract for every unique trigger id.
|
|
32
|
+
*/
|
|
33
|
+
export type SPopoverTriggerBinder = (
|
|
34
|
+
triggerId: string,
|
|
35
|
+
activation?: SPopoverTriggerActivation,
|
|
36
|
+
) => SPopoverTriggerBinding
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Публичный scoped-контракт custom trigger.
|
|
40
|
+
* Public scoped contract for custom triggers.
|
|
41
|
+
*/
|
|
42
|
+
export interface SPopoverTriggerSlotProps {
|
|
43
|
+
readonly bindTrigger: SPopoverTriggerBinder
|
|
44
|
+
readonly open: (triggerId?: string) => void
|
|
45
|
+
readonly close: () => void
|
|
46
|
+
readonly isOpen: boolean
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Содержимое built-in trigger получает только наблюдаемое состояние.
|
|
51
|
+
* Built-in trigger content receives observable state only.
|
|
52
|
+
*/
|
|
53
|
+
export interface SPopoverDefaultSlotProps {
|
|
54
|
+
readonly isOpen: boolean
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Панель может запросить явное закрытие с возвратом focus к trigger.
|
|
59
|
+
* The panel may explicitly close and restore focus to its trigger.
|
|
60
|
+
*/
|
|
61
|
+
export interface SPopoverPanelSlotProps {
|
|
62
|
+
readonly close: () => void
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Публичный programmatic API использует зарегистрированный anchor.
|
|
67
|
+
* The public programmatic API uses a registered anchor.
|
|
68
|
+
*/
|
|
69
|
+
export interface SPopoverPublicApi {
|
|
70
|
+
open: (triggerId?: string) => void
|
|
71
|
+
close: () => void
|
|
72
|
+
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Состояние размера и раскрытия одной sidebar-секции.
|
|
3
|
+
* Size and disclosure state of one sidebar section.
|
|
4
|
+
*/
|
|
5
|
+
export interface SectionState {
|
|
6
|
+
readonly isOpen: boolean;
|
|
7
|
+
readonly size: number | null;
|
|
8
|
+
readonly minSize: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Точная controlled-state карта под управлением consumer.
|
|
13
|
+
* Exact controlled state map owned by the consumer.
|
|
14
|
+
*/
|
|
15
|
+
export type SidebarSectionsState = Readonly<Record<string, Readonly<SectionState>>>;
|
|
16
|
+
|
|
17
|
+
/** Exact payload map общего владельца sidebar-секций. / Exact payload map of the shared sidebar-section owner. */
|
|
18
|
+
export interface SidebarGroupEmits {
|
|
19
|
+
'update-sections': [state: SidebarSectionsState];
|
|
20
|
+
'toggle-section': [key: string];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Одна точная immutable-замена секции в controlled transition.
|
|
25
|
+
* One exact immutable section replacement in a controlled transition.
|
|
26
|
+
*/
|
|
27
|
+
export interface SidebarSectionReplacement {
|
|
28
|
+
readonly key: string;
|
|
29
|
+
readonly state: Readonly<SectionState>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const SECTION_STATE_FIELDS = Object.freeze(['isOpen', 'minSize', 'size'] as const);
|
|
33
|
+
|
|
34
|
+
function assertPlainDataRecord(
|
|
35
|
+
owner: string,
|
|
36
|
+
coordinate: string,
|
|
37
|
+
value: unknown,
|
|
38
|
+
): asserts value is Record<string, unknown> {
|
|
39
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
40
|
+
throw new TypeError(`${owner}: ${coordinate} must be a plain data record`);
|
|
41
|
+
}
|
|
42
|
+
const prototype = Object.getPrototypeOf(value);
|
|
43
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
44
|
+
throw new TypeError(`${owner}: ${coordinate} must be a plain data record`);
|
|
45
|
+
}
|
|
46
|
+
if (Object.getOwnPropertySymbols(value).length > 0) {
|
|
47
|
+
throw new TypeError(`${owner}: ${coordinate} must not contain symbol properties`);
|
|
48
|
+
}
|
|
49
|
+
for (const [key, descriptor] of Object.entries(Object.getOwnPropertyDescriptors(value))) {
|
|
50
|
+
if (!descriptor.enumerable || !('value' in descriptor)) {
|
|
51
|
+
throw new TypeError(`${owner}: ${coordinate}.${key} must be an enumerable data property`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function assertExactSectionState(
|
|
57
|
+
owner: string,
|
|
58
|
+
coordinate: string,
|
|
59
|
+
value: unknown,
|
|
60
|
+
): asserts value is SectionState {
|
|
61
|
+
assertPlainDataRecord(owner, coordinate, value);
|
|
62
|
+
const keys = Object.keys(value).sort();
|
|
63
|
+
if (
|
|
64
|
+
keys.length !== SECTION_STATE_FIELDS.length
|
|
65
|
+
|| keys.some((key, index) => key !== SECTION_STATE_FIELDS[index])
|
|
66
|
+
) {
|
|
67
|
+
throw new TypeError(
|
|
68
|
+
`${owner}: ${coordinate} must contain exactly [${SECTION_STATE_FIELDS.join(', ')}]`,
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
if (typeof value.isOpen !== 'boolean') {
|
|
72
|
+
throw new TypeError(`${owner}: ${coordinate}.isOpen must be a boolean`);
|
|
73
|
+
}
|
|
74
|
+
if (typeof value.minSize !== 'number' || !Number.isFinite(value.minSize) || value.minSize < 0) {
|
|
75
|
+
throw new TypeError(`${owner}: ${coordinate}.minSize must be a non-negative finite number`);
|
|
76
|
+
}
|
|
77
|
+
if (
|
|
78
|
+
value.size !== null
|
|
79
|
+
&& (
|
|
80
|
+
typeof value.size !== 'number'
|
|
81
|
+
|| !Number.isFinite(value.size)
|
|
82
|
+
|| value.size < value.minSize
|
|
83
|
+
)
|
|
84
|
+
) {
|
|
85
|
+
throw new TypeError(
|
|
86
|
+
`${owner}: ${coordinate}.size must be null or a finite number not below minSize`,
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Проверяет полный точный controlled-контракт геометрии sidebar.
|
|
93
|
+
* Validates the complete exact controlled sidebar geometry contract.
|
|
94
|
+
*/
|
|
95
|
+
export function assertSidebarSectionsState(
|
|
96
|
+
state: unknown,
|
|
97
|
+
owner = 'SSidebarGroup',
|
|
98
|
+
): asserts state is SidebarSectionsState {
|
|
99
|
+
assertPlainDataRecord(owner, 'sectionsState', state);
|
|
100
|
+
const openSections: SectionState[] = [];
|
|
101
|
+
for (const [key, section] of Object.entries(state)) {
|
|
102
|
+
if (key.trim().length === 0) {
|
|
103
|
+
throw new TypeError(`${owner}: section keys must be non-empty strings`);
|
|
104
|
+
}
|
|
105
|
+
assertExactSectionState(owner, `sectionsState.${key}`, section);
|
|
106
|
+
if (section.isOpen) openSections.push(section);
|
|
107
|
+
}
|
|
108
|
+
const flexibleOpenCount = openSections.filter((section) => section.size === null).length;
|
|
109
|
+
if (openSections.length > 0 && flexibleOpenCount !== 1) {
|
|
110
|
+
throw new TypeError(
|
|
111
|
+
`${owner}: exactly one open section must have size=null; received ${flexibleOpenCount}`,
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function immutableSectionState(state: Readonly<SectionState>): Readonly<SectionState> {
|
|
117
|
+
return Object.freeze({
|
|
118
|
+
isOpen: state.isOpen,
|
|
119
|
+
size: state.size,
|
|
120
|
+
minSize: state.minSize,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Проверяет, копирует и глубоко замораживает полное controlled-state значение.
|
|
126
|
+
* Validates, copies, and deeply freezes one complete controlled state value.
|
|
127
|
+
*/
|
|
128
|
+
export function createSidebarSectionsState(
|
|
129
|
+
state: unknown,
|
|
130
|
+
owner = 'SSidebarGroup',
|
|
131
|
+
): SidebarSectionsState {
|
|
132
|
+
assertSidebarSectionsState(state, owner);
|
|
133
|
+
return Object.freeze(Object.fromEntries(
|
|
134
|
+
Object.entries(state).map(([key, section]) => [key, immutableSectionState(section)]),
|
|
135
|
+
));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Создаёт deeply immutable controlled transition без мутации или сериализации входа.
|
|
140
|
+
* Creates one deeply immutable controlled transition without mutating or serializing its input.
|
|
141
|
+
*/
|
|
142
|
+
export function createSidebarSectionsTransition(
|
|
143
|
+
current: SidebarSectionsState,
|
|
144
|
+
replacements: readonly SidebarSectionReplacement[],
|
|
145
|
+
): SidebarSectionsState {
|
|
146
|
+
assertSidebarSectionsState(current);
|
|
147
|
+
if (!Array.isArray(replacements) || replacements.length === 0) {
|
|
148
|
+
throw new TypeError('SSidebarGroup: a transition requires at least one section replacement');
|
|
149
|
+
}
|
|
150
|
+
const replacementsByKey = new Map<string, Readonly<SectionState>>();
|
|
151
|
+
for (const [index, replacement] of replacements.entries()) {
|
|
152
|
+
assertPlainDataRecord('SSidebarGroup', `replacements[${index}]`, replacement);
|
|
153
|
+
const replacementKeys = Object.keys(replacement).sort();
|
|
154
|
+
if (
|
|
155
|
+
replacementKeys.length !== 2
|
|
156
|
+
|| replacementKeys[0] !== 'key'
|
|
157
|
+
|| replacementKeys[1] !== 'state'
|
|
158
|
+
) {
|
|
159
|
+
throw new TypeError(
|
|
160
|
+
`SSidebarGroup: replacements[${index}] must contain exactly [key, state]`,
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
if (typeof replacement.key !== 'string' || replacement.key.trim().length === 0) {
|
|
164
|
+
throw new TypeError(`SSidebarGroup: replacements[${index}].key must be a non-empty string`);
|
|
165
|
+
}
|
|
166
|
+
if (!Object.hasOwn(current, replacement.key)) {
|
|
167
|
+
throw new Error(`SSidebarGroup: cannot replace unknown section '${replacement.key}'`);
|
|
168
|
+
}
|
|
169
|
+
if (replacementsByKey.has(replacement.key)) {
|
|
170
|
+
throw new Error(`SSidebarGroup: duplicate replacement for section '${replacement.key}'`);
|
|
171
|
+
}
|
|
172
|
+
assertExactSectionState(
|
|
173
|
+
'SSidebarGroup',
|
|
174
|
+
`replacements[${index}].state`,
|
|
175
|
+
replacement.state,
|
|
176
|
+
);
|
|
177
|
+
replacementsByKey.set(replacement.key, replacement.state);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const next = Object.fromEntries(Object.entries(current).map(([key, state]) => [
|
|
181
|
+
key,
|
|
182
|
+
immutableSectionState(replacementsByKey.get(key) ?? state),
|
|
183
|
+
]));
|
|
184
|
+
return createSidebarSectionsState(next);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Сравнивает точные public state fields без coercion.
|
|
189
|
+
* Compares the exact public state fields without coercion.
|
|
190
|
+
*/
|
|
191
|
+
export function sidebarSectionsStateEqual(
|
|
192
|
+
left: SidebarSectionsState,
|
|
193
|
+
right: SidebarSectionsState,
|
|
194
|
+
): boolean {
|
|
195
|
+
const leftKeys = Object.keys(left);
|
|
196
|
+
const rightKeys = Object.keys(right);
|
|
197
|
+
return leftKeys.length === rightKeys.length && leftKeys.every((key) => {
|
|
198
|
+
const leftSection = left[key];
|
|
199
|
+
const rightSection = right[key];
|
|
200
|
+
return leftSection !== undefined
|
|
201
|
+
&& rightSection !== undefined
|
|
202
|
+
&& leftSection.isOpen === rightSection.isOpen
|
|
203
|
+
&& leftSection.size === rightSection.size
|
|
204
|
+
&& leftSection.minSize === rightSection.minSize;
|
|
205
|
+
});
|
|
206
|
+
}
|