@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,443 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div v-bind="ownedAttrs.bindings()" ref="groupRef" class="s-sidebar-group">
|
|
3
|
+
<slot />
|
|
4
|
+
</div>
|
|
5
|
+
</template>
|
|
6
|
+
|
|
7
|
+
<script setup lang="ts">
|
|
8
|
+
import {
|
|
9
|
+
computed,
|
|
10
|
+
onBeforeUnmount,
|
|
11
|
+
onMounted,
|
|
12
|
+
provide,
|
|
13
|
+
ref,
|
|
14
|
+
shallowRef,
|
|
15
|
+
watch,
|
|
16
|
+
} from 'vue';
|
|
17
|
+
|
|
18
|
+
import { useOwnedAttrs } from '../../../internal/ownedAttrs';
|
|
19
|
+
import {
|
|
20
|
+
sidebarGroupContextKey,
|
|
21
|
+
type RegisteredSidebarSection,
|
|
22
|
+
type SidebarSectionRegistration,
|
|
23
|
+
type SidebarSectionStyle,
|
|
24
|
+
} from '../_internal/sidebarGroupContext';
|
|
25
|
+
import {
|
|
26
|
+
assertSidebarSectionsState,
|
|
27
|
+
createSidebarSectionsTransition,
|
|
28
|
+
sidebarSectionsStateEqual,
|
|
29
|
+
type SectionState,
|
|
30
|
+
type SidebarGroupEmits,
|
|
31
|
+
type SidebarSectionReplacement,
|
|
32
|
+
type SidebarSectionsState,
|
|
33
|
+
} from './sidebar';
|
|
34
|
+
|
|
35
|
+
defineOptions({ inheritAttrs: false });
|
|
36
|
+
|
|
37
|
+
const props = defineProps<{
|
|
38
|
+
sectionsState: SidebarSectionsState;
|
|
39
|
+
}>();
|
|
40
|
+
|
|
41
|
+
const emit = defineEmits<SidebarGroupEmits>();
|
|
42
|
+
|
|
43
|
+
const ownedAttrs = useOwnedAttrs({ component: 'SSidebarGroup', owner: 'sidebar group root' });
|
|
44
|
+
const groupRef = ref<HTMLDivElement | null>(null);
|
|
45
|
+
const registrations = shallowRef<readonly SidebarSectionRegistration[]>([]);
|
|
46
|
+
let resizeObserver: ResizeObserver | null = null;
|
|
47
|
+
let mounted = false;
|
|
48
|
+
let geometryRevision = 0;
|
|
49
|
+
let activeResizePair: Readonly<{ topKey: string; bottomKey: string }> | null = null;
|
|
50
|
+
|
|
51
|
+
assertSidebarSectionsState(props.sectionsState);
|
|
52
|
+
|
|
53
|
+
function currentState(): SidebarSectionsState {
|
|
54
|
+
assertSidebarSectionsState(props.sectionsState);
|
|
55
|
+
return props.sectionsState;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function measuredHeight(element: HTMLElement, coordinate: string): number {
|
|
59
|
+
const height = element.offsetHeight;
|
|
60
|
+
if (!Number.isFinite(height) || height < 0) {
|
|
61
|
+
throw new RangeError(`SSidebarGroup: ${coordinate}.offsetHeight must be a non-negative finite number`);
|
|
62
|
+
}
|
|
63
|
+
return height;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function measuredContainerHeight(): number {
|
|
67
|
+
const element = groupRef.value;
|
|
68
|
+
if (!element) throw new Error('SSidebarGroup: mounted group element is unavailable');
|
|
69
|
+
const height = element.clientHeight;
|
|
70
|
+
if (!Number.isFinite(height) || height < 0) {
|
|
71
|
+
throw new RangeError('SSidebarGroup: group clientHeight must be a non-negative finite number');
|
|
72
|
+
}
|
|
73
|
+
return height;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function compareRegistrations(
|
|
77
|
+
left: SidebarSectionRegistration,
|
|
78
|
+
right: SidebarSectionRegistration,
|
|
79
|
+
): number {
|
|
80
|
+
const position = left.element.compareDocumentPosition(right.element);
|
|
81
|
+
if (position & Node.DOCUMENT_POSITION_DISCONNECTED) {
|
|
82
|
+
throw new Error(
|
|
83
|
+
`SSidebarGroup: registered sections '${left.key}' and '${right.key}' are not in one DOM tree`,
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
if (position & Node.DOCUMENT_POSITION_FOLLOWING) return -1;
|
|
87
|
+
if (position & Node.DOCUMENT_POSITION_PRECEDING) return 1;
|
|
88
|
+
throw new Error(`SSidebarGroup: registered sections '${left.key}' and '${right.key}' have ambiguous DOM order`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const orderedSections = computed<readonly RegisteredSidebarSection[]>(() => {
|
|
92
|
+
const state = currentState();
|
|
93
|
+
return [...registrations.value]
|
|
94
|
+
.sort(compareRegistrations)
|
|
95
|
+
.map((registration) => {
|
|
96
|
+
const section = state[registration.key];
|
|
97
|
+
if (!section) {
|
|
98
|
+
throw new Error(
|
|
99
|
+
`SSidebarGroup: registered section '${registration.key}' has no controlled state`,
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
key: registration.key,
|
|
104
|
+
element: registration.element,
|
|
105
|
+
state: section,
|
|
106
|
+
};
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
function registeredSection(key: string): RegisteredSidebarSection {
|
|
111
|
+
const section = orderedSections.value.find((candidate) => candidate.key === key);
|
|
112
|
+
if (!section) {
|
|
113
|
+
throw new Error(`SSidebarGroup: mounted geometry is unavailable for section '${key}'`);
|
|
114
|
+
}
|
|
115
|
+
return section;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function emitTransition(replacements: readonly SidebarSectionReplacement[]): void {
|
|
119
|
+
const state = currentState();
|
|
120
|
+
const next = createSidebarSectionsTransition(state, replacements);
|
|
121
|
+
if (!sidebarSectionsStateEqual(next, state)) emit('update-sections', next);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function scheduleGeometryReconciliation(): void {
|
|
125
|
+
const revision = ++geometryRevision;
|
|
126
|
+
queueMicrotask(() => {
|
|
127
|
+
if (!mounted || revision !== geometryRevision || activeResizePair !== null) return;
|
|
128
|
+
reconcileContainerGeometry();
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function reconcileContainerGeometry(): void {
|
|
133
|
+
const containerHeight = measuredContainerHeight();
|
|
134
|
+
if (containerHeight === 0) return;
|
|
135
|
+
const sections = orderedSections.value;
|
|
136
|
+
const openSections = sections.filter((section) => section.state.isOpen);
|
|
137
|
+
if (openSections.length === 0) return;
|
|
138
|
+
const flexibleIndex = openSections.findIndex((section) => section.state.size === null);
|
|
139
|
+
if (flexibleIndex < 0) {
|
|
140
|
+
throw new Error('SSidebarGroup: controlled state has no flexible open section');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const closedHeight = sections
|
|
144
|
+
.filter((section) => !section.state.isOpen)
|
|
145
|
+
.reduce((sum, section) => (
|
|
146
|
+
sum + measuredHeight(section.element, `sections.${section.key}`)
|
|
147
|
+
), 0);
|
|
148
|
+
const fixedSections = openSections.filter((_, index) => index !== flexibleIndex);
|
|
149
|
+
const fixedHeight = fixedSections.reduce((sum, section) => {
|
|
150
|
+
if (section.state.size === null) {
|
|
151
|
+
throw new Error(`SSidebarGroup: section '${section.key}' unexpectedly owns flexible size`);
|
|
152
|
+
}
|
|
153
|
+
return sum + section.state.size;
|
|
154
|
+
}, 0);
|
|
155
|
+
const flexible = openSections[flexibleIndex]!;
|
|
156
|
+
let overflow = closedHeight + fixedHeight + flexible.state.minSize - containerHeight;
|
|
157
|
+
if (overflow <= 0) return;
|
|
158
|
+
|
|
159
|
+
const orderedCandidates = openSections
|
|
160
|
+
.map((section, index) => ({ section, index }))
|
|
161
|
+
.filter(({ index }) => index !== flexibleIndex)
|
|
162
|
+
.map(({ section, index }) => ({
|
|
163
|
+
section,
|
|
164
|
+
distance: Math.abs(index - flexibleIndex),
|
|
165
|
+
}))
|
|
166
|
+
.sort((left, right) => left.distance - right.distance);
|
|
167
|
+
const replacements: SidebarSectionReplacement[] = [];
|
|
168
|
+
for (const candidate of orderedCandidates) {
|
|
169
|
+
if (overflow <= 0) break;
|
|
170
|
+
const size = candidate.section.state.size;
|
|
171
|
+
if (size === null) {
|
|
172
|
+
throw new Error(`SSidebarGroup: section '${candidate.section.key}' unexpectedly owns flexible size`);
|
|
173
|
+
}
|
|
174
|
+
const available = size - candidate.section.state.minSize;
|
|
175
|
+
if (available <= 0) continue;
|
|
176
|
+
const reduction = Math.min(available, overflow);
|
|
177
|
+
replacements.push({
|
|
178
|
+
key: candidate.section.key,
|
|
179
|
+
state: { ...candidate.section.state, size: size - reduction },
|
|
180
|
+
});
|
|
181
|
+
overflow -= reduction;
|
|
182
|
+
}
|
|
183
|
+
if (replacements.length > 0) emitTransition(replacements);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function registerSection(registration: SidebarSectionRegistration): () => void {
|
|
187
|
+
if (typeof registration.key !== 'string' || registration.key.trim().length === 0) {
|
|
188
|
+
throw new TypeError('SSidebarGroup: registered section key must be non-empty');
|
|
189
|
+
}
|
|
190
|
+
if (!(registration.element instanceof HTMLElement)) {
|
|
191
|
+
throw new TypeError('SSidebarGroup: registered section element must be an HTMLElement');
|
|
192
|
+
}
|
|
193
|
+
if (!Object.hasOwn(currentState(), registration.key)) {
|
|
194
|
+
throw new Error(`SSidebarGroup: registered section '${registration.key}' has no controlled state`);
|
|
195
|
+
}
|
|
196
|
+
if (registrations.value.some((current) => current.key === registration.key)) {
|
|
197
|
+
throw new Error(`SSidebarGroup: duplicate registered section key '${registration.key}'`);
|
|
198
|
+
}
|
|
199
|
+
if (registrations.value.some((current) => current.element === registration.element)) {
|
|
200
|
+
throw new Error('SSidebarGroup: one section element cannot be registered under multiple keys');
|
|
201
|
+
}
|
|
202
|
+
const storedRegistration = Object.freeze({ ...registration });
|
|
203
|
+
registrations.value = Object.freeze([...registrations.value, storedRegistration]);
|
|
204
|
+
scheduleGeometryReconciliation();
|
|
205
|
+
let active = true;
|
|
206
|
+
return () => {
|
|
207
|
+
if (!active) return;
|
|
208
|
+
active = false;
|
|
209
|
+
registrations.value = Object.freeze(
|
|
210
|
+
registrations.value.filter((current) => current !== storedRegistration),
|
|
211
|
+
);
|
|
212
|
+
scheduleGeometryReconciliation();
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function nearestOpenSection(
|
|
217
|
+
source: RegisteredSidebarSection,
|
|
218
|
+
candidates: readonly RegisteredSidebarSection[],
|
|
219
|
+
): RegisteredSidebarSection {
|
|
220
|
+
const sections = orderedSections.value;
|
|
221
|
+
const sourceIndex = sections.findIndex((section) => section.key === source.key);
|
|
222
|
+
const ordered = [...candidates].sort((left, right) => {
|
|
223
|
+
const leftDistance = Math.abs(sections.findIndex((section) => section.key === left.key) - sourceIndex);
|
|
224
|
+
const rightDistance = Math.abs(sections.findIndex((section) => section.key === right.key) - sourceIndex);
|
|
225
|
+
return leftDistance - rightDistance;
|
|
226
|
+
});
|
|
227
|
+
const target = ordered[0];
|
|
228
|
+
if (!target) throw new Error(`SSidebarGroup: no open neighbor exists for section '${source.key}'`);
|
|
229
|
+
return target;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function toggleSection(key: string): void {
|
|
233
|
+
const state = currentState();
|
|
234
|
+
const current = state[key];
|
|
235
|
+
if (!current) throw new Error(`SSidebarGroup: cannot toggle unknown section '${key}'`);
|
|
236
|
+
const openSections = Object.entries(state)
|
|
237
|
+
.filter(([, section]) => section.isOpen)
|
|
238
|
+
.map(([sectionKey]) => sectionKey);
|
|
239
|
+
|
|
240
|
+
if (current.isOpen) {
|
|
241
|
+
if (current.size === null && openSections.length > 1) {
|
|
242
|
+
const source = registeredSection(key);
|
|
243
|
+
const neighbors = orderedSections.value.filter(
|
|
244
|
+
(section) => section.key !== key && section.state.isOpen,
|
|
245
|
+
);
|
|
246
|
+
const nextFlexible = nearestOpenSection(source, neighbors);
|
|
247
|
+
emitTransition([
|
|
248
|
+
{
|
|
249
|
+
key,
|
|
250
|
+
state: {
|
|
251
|
+
...current,
|
|
252
|
+
size: Math.max(current.minSize, measuredHeight(source.element, `sections.${key}`)),
|
|
253
|
+
},
|
|
254
|
+
},
|
|
255
|
+
{ key: nextFlexible.key, state: { ...nextFlexible.state, size: null } },
|
|
256
|
+
]);
|
|
257
|
+
}
|
|
258
|
+
emit('toggle-section', key);
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (openSections.length === 0) {
|
|
263
|
+
if (current.size !== null) emitTransition([{ key, state: { ...current, size: null } }]);
|
|
264
|
+
emit('toggle-section', key);
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const source = registeredSection(key);
|
|
269
|
+
let nextSize = current.size ?? current.minSize;
|
|
270
|
+
const containerHeight = measuredContainerHeight();
|
|
271
|
+
if (containerHeight > 0) {
|
|
272
|
+
const occupiedMinimum = orderedSections.value.reduce((sum, section) => {
|
|
273
|
+
if (section.key === key) return sum;
|
|
274
|
+
if (section.state.isOpen) return sum + section.state.minSize;
|
|
275
|
+
return sum + measuredHeight(section.element, `sections.${section.key}`);
|
|
276
|
+
}, 0);
|
|
277
|
+
nextSize = Math.min(nextSize, Math.max(current.minSize, containerHeight - occupiedMinimum));
|
|
278
|
+
}
|
|
279
|
+
void source;
|
|
280
|
+
if (current.size !== nextSize) {
|
|
281
|
+
emitTransition([{ key, state: { ...current, size: nextSize } }]);
|
|
282
|
+
}
|
|
283
|
+
emit('toggle-section', key);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function getSectionStyle(key: string): SidebarSectionStyle {
|
|
287
|
+
const state = currentState()[key];
|
|
288
|
+
if (!state) throw new Error(`SSidebarGroup: cannot style unknown section '${key}'`);
|
|
289
|
+
if (!state.isOpen) {
|
|
290
|
+
return Object.freeze({ flexGrow: 0, flexShrink: 0, flexBasis: 'auto', overflow: 'hidden' });
|
|
291
|
+
}
|
|
292
|
+
if (state.size !== null) {
|
|
293
|
+
return Object.freeze({
|
|
294
|
+
flexBasis: `${state.size}px`,
|
|
295
|
+
flexGrow: 0,
|
|
296
|
+
flexShrink: 0,
|
|
297
|
+
minHeight: `${state.minSize}px`,
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
return Object.freeze({
|
|
301
|
+
flexGrow: 1,
|
|
302
|
+
flexShrink: 1,
|
|
303
|
+
flexBasis: '0%',
|
|
304
|
+
minHeight: `${state.minSize}px`,
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function resizePair(
|
|
309
|
+
topKey: string,
|
|
310
|
+
bottomKey: string,
|
|
311
|
+
): readonly [RegisteredSidebarSection, RegisteredSidebarSection] {
|
|
312
|
+
const sections = orderedSections.value;
|
|
313
|
+
const topIndex = sections.findIndex((section) => section.key === topKey);
|
|
314
|
+
if (topIndex < 0) throw new Error(`SSidebarGroup: unknown resize top section '${topKey}'`);
|
|
315
|
+
const bottomIndex = sections.findIndex((section) => section.key === bottomKey);
|
|
316
|
+
if (bottomIndex < 0) throw new Error(`SSidebarGroup: unknown resize bottom section '${bottomKey}'`);
|
|
317
|
+
const top = sections[topIndex]!;
|
|
318
|
+
const bottom = sections[bottomIndex]!;
|
|
319
|
+
if (!top.state.isOpen || !bottom.state.isOpen) {
|
|
320
|
+
throw new Error('SSidebarGroup: both resize sections must be open');
|
|
321
|
+
}
|
|
322
|
+
const nextOpen = sections.find((section, index) => index > topIndex && section.state.isOpen);
|
|
323
|
+
if (!nextOpen || nextOpen.key !== bottomKey || bottomIndex <= topIndex) {
|
|
324
|
+
throw new Error(
|
|
325
|
+
`SSidebarGroup: '${topKey}' and '${bottomKey}' must be consecutive open sections`,
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
return [top, bottom];
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function beginResize(topKey: string, bottomKey: string): void {
|
|
332
|
+
resizePair(topKey, bottomKey);
|
|
333
|
+
if (activeResizePair !== null) {
|
|
334
|
+
throw new Error(
|
|
335
|
+
`SSidebarGroup: resize '${activeResizePair.topKey}'/'${activeResizePair.bottomKey}' is already active`,
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
activeResizePair = Object.freeze({ topKey, bottomKey });
|
|
339
|
+
geometryRevision += 1;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function resizeSections(topKey: string, bottomKey: string, requestedTopSize: number): void {
|
|
343
|
+
if (!Number.isFinite(requestedTopSize)) {
|
|
344
|
+
throw new RangeError('SSidebarGroup: requestedTopSize must be a finite number');
|
|
345
|
+
}
|
|
346
|
+
if (
|
|
347
|
+
activeResizePair !== null
|
|
348
|
+
&& (activeResizePair.topKey !== topKey || activeResizePair.bottomKey !== bottomKey)
|
|
349
|
+
) {
|
|
350
|
+
throw new Error('SSidebarGroup: resize update does not match the active section pair');
|
|
351
|
+
}
|
|
352
|
+
const [top, bottom] = resizePair(topKey, bottomKey);
|
|
353
|
+
const topHeight = Math.max(
|
|
354
|
+
top.state.minSize,
|
|
355
|
+
measuredHeight(top.element, `sections.${top.key}`),
|
|
356
|
+
);
|
|
357
|
+
const bottomHeight = Math.max(
|
|
358
|
+
bottom.state.minSize,
|
|
359
|
+
measuredHeight(bottom.element, `sections.${bottom.key}`),
|
|
360
|
+
);
|
|
361
|
+
const minimumPairSize = top.state.minSize + bottom.state.minSize;
|
|
362
|
+
const measuredPairSize = Math.max(minimumPairSize, topHeight + bottomHeight);
|
|
363
|
+
const containerHeight = measuredContainerHeight();
|
|
364
|
+
const occupiedByOthers = orderedSections.value.reduce((sum, section) => {
|
|
365
|
+
if (section.key === topKey || section.key === bottomKey) return sum;
|
|
366
|
+
return sum + measuredHeight(section.element, `sections.${section.key}`);
|
|
367
|
+
}, 0);
|
|
368
|
+
const availablePairSize = containerHeight > 0
|
|
369
|
+
? Math.max(minimumPairSize, containerHeight - occupiedByOthers)
|
|
370
|
+
: measuredPairSize;
|
|
371
|
+
const pairSize = Math.max(minimumPairSize, Math.min(measuredPairSize, availablePairSize));
|
|
372
|
+
const maximumTopSize = pairSize - bottom.state.minSize;
|
|
373
|
+
const nextTopSize = Math.min(maximumTopSize, Math.max(top.state.minSize, requestedTopSize));
|
|
374
|
+
const nextBottomSize = pairSize - nextTopSize;
|
|
375
|
+
const flexibleOwner = top.state.size === null ? bottom.key : (
|
|
376
|
+
bottom.state.size === null ? bottom.key : null
|
|
377
|
+
);
|
|
378
|
+
const nextTop: SectionState = { ...top.state, size: nextTopSize };
|
|
379
|
+
const nextBottom: SectionState = {
|
|
380
|
+
...bottom.state,
|
|
381
|
+
size: flexibleOwner === bottom.key ? null : nextBottomSize,
|
|
382
|
+
};
|
|
383
|
+
if (
|
|
384
|
+
nextTop.size === top.state.size
|
|
385
|
+
&& nextBottom.size === bottom.state.size
|
|
386
|
+
) return;
|
|
387
|
+
emitTransition([
|
|
388
|
+
{ key: top.key, state: nextTop },
|
|
389
|
+
{ key: bottom.key, state: nextBottom },
|
|
390
|
+
]);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function finishResize(topKey: string, bottomKey: string): void {
|
|
394
|
+
if (activeResizePair === null) {
|
|
395
|
+
throw new Error('SSidebarGroup: cannot finish a resize that is not active');
|
|
396
|
+
}
|
|
397
|
+
if (activeResizePair.topKey !== topKey || activeResizePair.bottomKey !== bottomKey) {
|
|
398
|
+
throw new Error('SSidebarGroup: resize end does not match the active section pair');
|
|
399
|
+
}
|
|
400
|
+
activeResizePair = null;
|
|
401
|
+
scheduleGeometryReconciliation();
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
provide(sidebarGroupContextKey, {
|
|
405
|
+
sectionsState: computed(currentState),
|
|
406
|
+
orderedSections,
|
|
407
|
+
registerSection,
|
|
408
|
+
toggleSection,
|
|
409
|
+
getSectionStyle,
|
|
410
|
+
beginResize,
|
|
411
|
+
resizeSections,
|
|
412
|
+
finishResize,
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
watch(
|
|
416
|
+
() => props.sectionsState,
|
|
417
|
+
() => {
|
|
418
|
+
currentState();
|
|
419
|
+
scheduleGeometryReconciliation();
|
|
420
|
+
},
|
|
421
|
+
{ deep: true, flush: 'post' },
|
|
422
|
+
);
|
|
423
|
+
|
|
424
|
+
onMounted(() => {
|
|
425
|
+
mounted = true;
|
|
426
|
+
const element = groupRef.value;
|
|
427
|
+
if (!element) throw new Error('SSidebarGroup: group element is unavailable during mount');
|
|
428
|
+
resizeObserver = new ResizeObserver(scheduleGeometryReconciliation);
|
|
429
|
+
resizeObserver.observe(element);
|
|
430
|
+
scheduleGeometryReconciliation();
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
onBeforeUnmount(() => {
|
|
434
|
+
mounted = false;
|
|
435
|
+
geometryRevision += 1;
|
|
436
|
+
activeResizePair = null;
|
|
437
|
+
resizeObserver?.disconnect();
|
|
438
|
+
resizeObserver = null;
|
|
439
|
+
registrations.value = Object.freeze([]);
|
|
440
|
+
});
|
|
441
|
+
</script>
|
|
442
|
+
|
|
443
|
+
<style lang="postcss" src="./SSidebarGroup.css" scoped></style>
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
@reference "../../../styles/reference.css";
|
|
2
|
+
|
|
3
|
+
.s-sidebar-section-wrapper {
|
|
4
|
+
@apply flex flex-col;
|
|
5
|
+
min-height: 0;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
.s-sidebar-section {
|
|
9
|
+
@apply flex flex-col;
|
|
10
|
+
@apply min-h-0 flex-grow w-full;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.s-sidebar-section-wrapper + .s-sidebar-section-wrapper .s-sidebar-section {
|
|
14
|
+
@apply border-t border-surface-200 dark:border-surface-700;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.s-sidebar-section-wrapper--resizable-before .s-sidebar-section {
|
|
18
|
+
@apply border-t-0;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.s-sidebar-section-header {
|
|
22
|
+
@apply flex w-full items-center gap-1 px-2 h-7;
|
|
23
|
+
@apply text-surface-600 dark:text-surface-300;
|
|
24
|
+
@apply shrink-0;
|
|
25
|
+
}
|
|
26
|
+
.s-sidebar-section-header--mobile {
|
|
27
|
+
@apply h-6 px-1.5;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.s-sidebar-section-trigger {
|
|
31
|
+
@apply min-w-0 flex-1;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.s-sidebar-section-actions {
|
|
35
|
+
@apply flex shrink-0 items-center gap-1 ml-auto;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.s-sidebar-section-content {
|
|
39
|
+
@apply text-sm text-surface-700 dark:text-surface-300;
|
|
40
|
+
@apply flex-grow overflow-y-auto min-h-0;
|
|
41
|
+
}
|