@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,105 @@
|
|
|
1
|
+
export type DialogSurfaceModalSize = 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl' | 'wide' | 'full'
|
|
2
|
+
export type DialogSurfaceDrawerSize = 'sm' | 'md' | 'lg'
|
|
3
|
+
export type DialogSurfaceTone = 'default' | 'subtle'
|
|
4
|
+
export type DialogSurfaceScrollBehavior = 'page' | 'body'
|
|
5
|
+
|
|
6
|
+
export interface ModalDialogSurfacePresentation {
|
|
7
|
+
kind: 'modal'
|
|
8
|
+
size: DialogSurfaceModalSize
|
|
9
|
+
fullscreen: boolean
|
|
10
|
+
surface: DialogSurfaceTone
|
|
11
|
+
scrollBehavior: DialogSurfaceScrollBehavior
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface DrawerDialogSurfacePresentation {
|
|
15
|
+
kind: 'drawer'
|
|
16
|
+
size: DialogSurfaceDrawerSize
|
|
17
|
+
mobileFullScreen: boolean
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type DialogSurfacePresentation =
|
|
21
|
+
| ModalDialogSurfacePresentation
|
|
22
|
+
| DrawerDialogSurfacePresentation
|
|
23
|
+
|
|
24
|
+
export type DialogSurfaceContractErrorCode =
|
|
25
|
+
| 'invalid-presentation'
|
|
26
|
+
| 'invalid-size'
|
|
27
|
+
| 'invalid-surface'
|
|
28
|
+
| 'invalid-scroll-behavior'
|
|
29
|
+
|
|
30
|
+
export class DialogSurfaceContractError extends Error {
|
|
31
|
+
readonly code: DialogSurfaceContractErrorCode
|
|
32
|
+
|
|
33
|
+
constructor(code: DialogSurfaceContractErrorCode, message: string) {
|
|
34
|
+
super(message)
|
|
35
|
+
this.name = 'DialogSurfaceContractError'
|
|
36
|
+
this.code = code
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const modalKeys = new Set(['kind', 'size', 'fullscreen', 'surface', 'scrollBehavior'])
|
|
41
|
+
const drawerKeys = new Set(['kind', 'size', 'mobileFullScreen'])
|
|
42
|
+
const modalSizes = new Set<DialogSurfaceModalSize>(['sm', 'md', 'lg', 'xl', '2xl', '3xl', 'wide', 'full'])
|
|
43
|
+
const drawerSizes = new Set<DialogSurfaceDrawerSize>(['sm', 'md', 'lg'])
|
|
44
|
+
const surfaceTones = new Set<DialogSurfaceTone>(['default', 'subtle'])
|
|
45
|
+
const scrollBehaviors = new Set<DialogSurfaceScrollBehavior>(['page', 'body'])
|
|
46
|
+
|
|
47
|
+
const assertExactKeys = (
|
|
48
|
+
presentation: Record<string, unknown>,
|
|
49
|
+
kind: DialogSurfacePresentation['kind'],
|
|
50
|
+
allowedKeys: ReadonlySet<string>,
|
|
51
|
+
): void => {
|
|
52
|
+
const unexpectedKey = Object.keys(presentation).find((key) => !allowedKeys.has(key))
|
|
53
|
+
if (unexpectedKey) {
|
|
54
|
+
throw new DialogSurfaceContractError(
|
|
55
|
+
'invalid-presentation',
|
|
56
|
+
`SDialogSurface presentation kind="${kind}" содержит недопустимое поле ${unexpectedKey}`,
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Проверяет закрытый discriminated contract приватной dialog surface без fallback-веток.
|
|
63
|
+
* Validates the private dialog surface discriminated contract without fallback branches.
|
|
64
|
+
*/
|
|
65
|
+
export function assertDialogSurfacePresentation(
|
|
66
|
+
presentation: DialogSurfacePresentation,
|
|
67
|
+
): asserts presentation is DialogSurfacePresentation {
|
|
68
|
+
const candidate = presentation as unknown as Record<string, unknown>
|
|
69
|
+
|
|
70
|
+
if (candidate.kind === 'modal') {
|
|
71
|
+
assertExactKeys(candidate, 'modal', modalKeys)
|
|
72
|
+
if (!modalSizes.has(candidate.size as DialogSurfaceModalSize)) {
|
|
73
|
+
throw new DialogSurfaceContractError('invalid-size', `Недопустимый размер SModal: ${String(candidate.size)}`)
|
|
74
|
+
}
|
|
75
|
+
if (typeof candidate.fullscreen !== 'boolean') {
|
|
76
|
+
throw new DialogSurfaceContractError('invalid-presentation', 'SModal presentation требует boolean fullscreen')
|
|
77
|
+
}
|
|
78
|
+
if (!surfaceTones.has(candidate.surface as DialogSurfaceTone)) {
|
|
79
|
+
throw new DialogSurfaceContractError('invalid-surface', `Недопустимая поверхность SModal: ${String(candidate.surface)}`)
|
|
80
|
+
}
|
|
81
|
+
if (!scrollBehaviors.has(candidate.scrollBehavior as DialogSurfaceScrollBehavior)) {
|
|
82
|
+
throw new DialogSurfaceContractError(
|
|
83
|
+
'invalid-scroll-behavior',
|
|
84
|
+
`Недопустимый scroll behavior SModal: ${String(candidate.scrollBehavior)}`,
|
|
85
|
+
)
|
|
86
|
+
}
|
|
87
|
+
return
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (candidate.kind === 'drawer') {
|
|
91
|
+
assertExactKeys(candidate, 'drawer', drawerKeys)
|
|
92
|
+
if (!drawerSizes.has(candidate.size as DialogSurfaceDrawerSize)) {
|
|
93
|
+
throw new DialogSurfaceContractError('invalid-size', `Недопустимый размер SDrawer: ${String(candidate.size)}`)
|
|
94
|
+
}
|
|
95
|
+
if (typeof candidate.mobileFullScreen !== 'boolean') {
|
|
96
|
+
throw new DialogSurfaceContractError('invalid-presentation', 'SDrawer presentation требует boolean mobileFullScreen')
|
|
97
|
+
}
|
|
98
|
+
return
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
throw new DialogSurfaceContractError(
|
|
102
|
+
'invalid-presentation',
|
|
103
|
+
`Неизвестный kind SDialogSurface: ${String(candidate.kind)}`,
|
|
104
|
+
)
|
|
105
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { Ref } from 'vue'
|
|
2
|
+
|
|
3
|
+
import { getFocusableElements } from '../../../internal/focusNavigation'
|
|
4
|
+
|
|
5
|
+
export function trapFocusInsidePanel(event: KeyboardEvent, panelRef: Ref<HTMLElement | null>, isOpen: boolean): void {
|
|
6
|
+
if (event.key !== 'Tab' || !isOpen) {
|
|
7
|
+
return
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const panel = panelRef.value
|
|
11
|
+
if (!panel) {
|
|
12
|
+
return
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const visibleFocusable = getFocusableElements(panel, { requireLayout: true })
|
|
16
|
+
|
|
17
|
+
if (visibleFocusable.length === 0) {
|
|
18
|
+
event.preventDefault()
|
|
19
|
+
panel.focus()
|
|
20
|
+
return
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const firstElement = visibleFocusable[0]
|
|
24
|
+
const lastElement = visibleFocusable[visibleFocusable.length - 1]
|
|
25
|
+
const activeElement = document.activeElement as HTMLElement | null
|
|
26
|
+
const focusIsOutsideSequence = activeElement === panel || !panel.contains(activeElement)
|
|
27
|
+
|
|
28
|
+
if (event.shiftKey) {
|
|
29
|
+
if (activeElement === firstElement || focusIsOutsideSequence) {
|
|
30
|
+
event.preventDefault()
|
|
31
|
+
lastElement?.focus()
|
|
32
|
+
}
|
|
33
|
+
return
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (activeElement === lastElement || focusIsOutsideSequence) {
|
|
37
|
+
event.preventDefault()
|
|
38
|
+
firstElement?.focus()
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { InjectionKey } from 'vue'
|
|
2
|
+
|
|
3
|
+
export interface ListboxOptionRegistration {
|
|
4
|
+
token: string
|
|
5
|
+
element: HTMLElement
|
|
6
|
+
activate: () => void
|
|
7
|
+
disabled: () => boolean
|
|
8
|
+
label: () => string
|
|
9
|
+
selected: () => boolean
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface ListboxContext {
|
|
13
|
+
register: (registration: ListboxOptionRegistration) => () => void
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const listboxContextKey: InjectionKey<ListboxContext> = Symbol('s-listbox-context')
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export type SMarkerTone = 'surface' | 'primary' | 'success' | 'blue' | 'sky' | 'indigo' | 'emerald' | 'green' | 'teal' | 'amber' | 'orange' | 'rose' | 'pink' | 'fuchsia' | 'cyan' | 'lime'
|
|
2
|
+
export type SMarker = { tone: SMarkerTone } | { tone: 'custom'; color: `#${string}` }
|
|
3
|
+
export type SCustomMarker = Extract<SMarker, { tone: 'custom' }>
|
|
4
|
+
|
|
5
|
+
export function createCustomMarker(color: string): SCustomMarker {
|
|
6
|
+
if (!/^#[\da-f]{6}$/iu.test(color)) {
|
|
7
|
+
throw new Error('SMarker: custom color must be a six-digit hex value')
|
|
8
|
+
}
|
|
9
|
+
return { tone: 'custom', color: color as `#${string}` }
|
|
10
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { InjectionKey, Ref } from 'vue'
|
|
2
|
+
|
|
3
|
+
export type MenuDensity = 'compact' | 'comfortable'
|
|
4
|
+
|
|
5
|
+
export interface MenuItemRegistration {
|
|
6
|
+
readonly entryId: string
|
|
7
|
+
readonly label: () => string
|
|
8
|
+
readonly hasSubmenu: () => boolean
|
|
9
|
+
readonly element: () => HTMLElement
|
|
10
|
+
readonly setTabStop: (active: boolean) => void
|
|
11
|
+
readonly focus: () => void
|
|
12
|
+
readonly isFocused: () => boolean
|
|
13
|
+
readonly ownsEventTarget: (target: EventTarget | null) => boolean
|
|
14
|
+
readonly activate: (event: KeyboardEvent) => void
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface MenuContext {
|
|
18
|
+
density: Readonly<Ref<MenuDensity>>
|
|
19
|
+
register: (registration: MenuItemRegistration) => () => void
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const menuContextKey: InjectionKey<MenuContext> = Symbol('s-menu-context')
|
|
23
|
+
export const menuDepthKey: InjectionKey<number> = Symbol('s-menu-depth')
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import { validateBoolean, validateNonEmptyString } from '../../../internal/runtimeContract'
|
|
2
|
+
import { createCustomMarker, type SMarker } from './markerContract'
|
|
3
|
+
import {
|
|
4
|
+
selectionKeyToken,
|
|
5
|
+
validateSelectionIdentities,
|
|
6
|
+
type SelectionKey,
|
|
7
|
+
} from './useSelectionRoving'
|
|
8
|
+
|
|
9
|
+
/** Одна типизированная option общего selection owner. / One typed option of the shared selection owner. */
|
|
10
|
+
export interface SelectionOption<TKey extends SelectionKey = SelectionKey> {
|
|
11
|
+
value: TKey
|
|
12
|
+
label: string
|
|
13
|
+
title?: string
|
|
14
|
+
marker?: SMarker
|
|
15
|
+
disabled?: boolean
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Группа options с собственной стабильной identity. / An option group with its own stable identity. */
|
|
19
|
+
export interface SelectionOptionGroup<TKey extends SelectionKey = SelectionKey> {
|
|
20
|
+
groupId: string
|
|
21
|
+
label: string
|
|
22
|
+
options: readonly SelectionOption<TKey>[]
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type SelectionEntry<TKey extends SelectionKey = SelectionKey> =
|
|
26
|
+
| SelectionOption<TKey>
|
|
27
|
+
| SelectionOptionGroup<TKey>
|
|
28
|
+
export type SelectionMode = 'single' | 'multiple'
|
|
29
|
+
export type SelectionModelValue<
|
|
30
|
+
TKey extends SelectionKey,
|
|
31
|
+
TMode extends SelectionMode,
|
|
32
|
+
> = TMode extends 'multiple' ? readonly TKey[] : TKey | null
|
|
33
|
+
|
|
34
|
+
export interface ResolvedSelectionOption<TKey extends SelectionKey> {
|
|
35
|
+
option: SelectionOption<TKey>
|
|
36
|
+
token: string
|
|
37
|
+
coordinate: string
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export type ResolvedSelectionEntry<TKey extends SelectionKey> =
|
|
41
|
+
| { kind: 'option'; record: ResolvedSelectionOption<TKey>; token: string }
|
|
42
|
+
| {
|
|
43
|
+
kind: 'group'
|
|
44
|
+
groupId: string
|
|
45
|
+
label: string
|
|
46
|
+
records: readonly ResolvedSelectionOption<TKey>[]
|
|
47
|
+
token: string
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface ResolvedSelectionInventory<TKey extends SelectionKey> {
|
|
51
|
+
entries: readonly ResolvedSelectionEntry<TKey>[]
|
|
52
|
+
options: readonly ResolvedSelectionOption<TKey>[]
|
|
53
|
+
optionsByToken: ReadonlyMap<string, ResolvedSelectionOption<TKey>>
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const OPTION_KEYS = new Set(['value', 'label', 'title', 'marker', 'disabled'])
|
|
57
|
+
const GROUP_KEYS = new Set(['groupId', 'label', 'options'])
|
|
58
|
+
const MARKER_TONES = [
|
|
59
|
+
'surface', 'primary', 'success', 'blue', 'sky', 'indigo', 'emerald', 'green', 'teal',
|
|
60
|
+
'amber', 'orange', 'rose', 'pink', 'fuchsia', 'cyan', 'lime', 'custom',
|
|
61
|
+
] as const
|
|
62
|
+
|
|
63
|
+
function assertRecord(owner: string, coordinate: string, value: unknown): asserts value is Record<string, unknown> {
|
|
64
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
65
|
+
throw new TypeError(`${owner}: ${coordinate} must be an option object or an option group`)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function assertExactKeys(
|
|
70
|
+
owner: string,
|
|
71
|
+
coordinate: string,
|
|
72
|
+
value: Record<string, unknown>,
|
|
73
|
+
keys: ReadonlySet<string>,
|
|
74
|
+
): void {
|
|
75
|
+
const unsupported = Object.keys(value).find((key) => !keys.has(key))
|
|
76
|
+
if (unsupported !== undefined) {
|
|
77
|
+
throw new TypeError(`${owner}: ${coordinate}.${unsupported} is not part of the selection contract`)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Проверяет optional selection text без coercion. / Validates optional selection text without coercion. */
|
|
82
|
+
export function validateOptionalSelectionString(
|
|
83
|
+
owner: string,
|
|
84
|
+
coordinate: string,
|
|
85
|
+
value: unknown,
|
|
86
|
+
): string | undefined {
|
|
87
|
+
if (value === undefined) return undefined
|
|
88
|
+
if (typeof value !== 'string') {
|
|
89
|
+
throw new TypeError(`${owner}: ${coordinate} must be a string when provided`)
|
|
90
|
+
}
|
|
91
|
+
return value
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Проверяет exact marker payload на selection boundary. / Validates an exact marker payload at a selection boundary. */
|
|
95
|
+
export function validateSelectionMarker(
|
|
96
|
+
owner: string,
|
|
97
|
+
coordinate: string,
|
|
98
|
+
marker: SMarker | undefined,
|
|
99
|
+
): SMarker | undefined {
|
|
100
|
+
if (marker === undefined) return undefined
|
|
101
|
+
assertRecord(owner, coordinate, marker)
|
|
102
|
+
const tone = marker.tone
|
|
103
|
+
if (typeof tone !== 'string' || !MARKER_TONES.some((candidate) => candidate === tone)) {
|
|
104
|
+
throw new TypeError(`${owner}: ${coordinate}.tone is not a supported marker tone`)
|
|
105
|
+
}
|
|
106
|
+
const expectedKeys = tone === 'custom' ? new Set(['tone', 'color']) : new Set(['tone'])
|
|
107
|
+
assertExactKeys(owner, coordinate, marker, expectedKeys)
|
|
108
|
+
if (tone === 'custom') createCustomMarker(validateNonEmptyString(owner, `${coordinate}.color`, marker.color))
|
|
109
|
+
return marker
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function resolveOption<TKey extends SelectionKey>(
|
|
113
|
+
owner: string,
|
|
114
|
+
value: SelectionOption<TKey>,
|
|
115
|
+
coordinate: string,
|
|
116
|
+
): ResolvedSelectionOption<TKey> {
|
|
117
|
+
assertRecord(owner, coordinate, value)
|
|
118
|
+
assertExactKeys(owner, coordinate, value, OPTION_KEYS)
|
|
119
|
+
const key = value.value
|
|
120
|
+
const token = selectionKeyToken(owner, key, `${coordinate}.value`)
|
|
121
|
+
const label = validateNonEmptyString(owner, `${coordinate}.label`, value.label)
|
|
122
|
+
const title = validateOptionalSelectionString(owner, `${coordinate}.title`, value.title)
|
|
123
|
+
if (value.disabled !== undefined) validateBoolean(owner, `${coordinate}.disabled`, value.disabled)
|
|
124
|
+
const marker = validateSelectionMarker(owner, `${coordinate}.marker`, value.marker)
|
|
125
|
+
const option: SelectionOption<TKey> = {
|
|
126
|
+
value: key,
|
|
127
|
+
label,
|
|
128
|
+
...(title === undefined ? {} : { title }),
|
|
129
|
+
...(marker === undefined ? {} : { marker }),
|
|
130
|
+
...(value.disabled === undefined ? {} : { disabled: value.disabled }),
|
|
131
|
+
}
|
|
132
|
+
return {
|
|
133
|
+
option,
|
|
134
|
+
token,
|
|
135
|
+
coordinate,
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Проверяет весь inventory и возвращает новый типизированный контейнер без непроверенных полей.
|
|
141
|
+
* Validates the complete inventory and returns a new typed container without unchecked fields.
|
|
142
|
+
*/
|
|
143
|
+
export function resolveSelectionInventory<TKey extends SelectionKey>(
|
|
144
|
+
owner: string,
|
|
145
|
+
value: readonly SelectionEntry<TKey>[],
|
|
146
|
+
): ResolvedSelectionInventory<TKey> {
|
|
147
|
+
if (!Array.isArray(value)) throw new TypeError(`${owner}: options must be an array`)
|
|
148
|
+
const typedEntries: readonly SelectionEntry<TKey>[] = value
|
|
149
|
+
const entries: ResolvedSelectionEntry<TKey>[] = []
|
|
150
|
+
const options: ResolvedSelectionOption<TKey>[] = []
|
|
151
|
+
const optionIdentities: Array<{ key: TKey; coordinate: string }> = []
|
|
152
|
+
const groupIdentities: Array<{ key: string; coordinate: string }> = []
|
|
153
|
+
|
|
154
|
+
for (const [entryIndex, entryValue] of typedEntries.entries()) {
|
|
155
|
+
const coordinate = `options[${entryIndex}]`
|
|
156
|
+
assertRecord(owner, coordinate, entryValue)
|
|
157
|
+
if ('groupId' in entryValue) {
|
|
158
|
+
assertExactKeys(owner, coordinate, entryValue, GROUP_KEYS)
|
|
159
|
+
const groupId = validateNonEmptyString(owner, `${coordinate}.groupId`, entryValue.groupId)
|
|
160
|
+
const label = validateNonEmptyString(owner, `${coordinate}.label`, entryValue.label)
|
|
161
|
+
if (!Array.isArray(entryValue.options)) {
|
|
162
|
+
throw new TypeError(`${owner}: ${coordinate}.options must be an array`)
|
|
163
|
+
}
|
|
164
|
+
if (entryValue.options.length === 0) {
|
|
165
|
+
throw new Error(`${owner}: ${coordinate}.options must contain at least one option`)
|
|
166
|
+
}
|
|
167
|
+
groupIdentities.push({ key: groupId, coordinate: `${coordinate}.groupId` })
|
|
168
|
+
const records = entryValue.options.map((option, optionIndex) => {
|
|
169
|
+
const optionCoordinate = `${coordinate}.options[${optionIndex}]`
|
|
170
|
+
const record = resolveOption<TKey>(owner, option, optionCoordinate)
|
|
171
|
+
optionIdentities.push({ key: record.option.value, coordinate: `${optionCoordinate}.value` })
|
|
172
|
+
options.push(record)
|
|
173
|
+
return record
|
|
174
|
+
})
|
|
175
|
+
entries.push({
|
|
176
|
+
kind: 'group',
|
|
177
|
+
groupId,
|
|
178
|
+
label,
|
|
179
|
+
records,
|
|
180
|
+
token: `group:${selectionKeyToken(owner, groupId, `${coordinate}.groupId`)}`,
|
|
181
|
+
})
|
|
182
|
+
continue
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const record = resolveOption<TKey>(owner, entryValue, coordinate)
|
|
186
|
+
optionIdentities.push({ key: record.option.value, coordinate: `${coordinate}.value` })
|
|
187
|
+
options.push(record)
|
|
188
|
+
entries.push({ kind: 'option', record, token: `option:${record.token}` })
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
validateSelectionIdentities(`${owner} groups`, groupIdentities)
|
|
192
|
+
validateSelectionIdentities(owner, optionIdentities)
|
|
193
|
+
return {
|
|
194
|
+
entries,
|
|
195
|
+
options,
|
|
196
|
+
optionsByToken: new Map(options.map((record) => [record.token, record])),
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** Проверяет controlled model относительно точного inventory. / Validates the controlled model against the exact inventory. */
|
|
201
|
+
export function validateSelectionModel<TKey extends SelectionKey>(
|
|
202
|
+
owner: string,
|
|
203
|
+
mode: SelectionMode,
|
|
204
|
+
modelValue: TKey | null | readonly TKey[] | undefined,
|
|
205
|
+
inventory: ResolvedSelectionInventory<TKey>,
|
|
206
|
+
): ReadonlySet<string> {
|
|
207
|
+
if (modelValue === undefined) throw new TypeError(`${owner}: modelValue is required and cannot be undefined`)
|
|
208
|
+
if (mode === 'multiple') {
|
|
209
|
+
if (!Array.isArray(modelValue)) {
|
|
210
|
+
throw new TypeError(`${owner}: multiple selection requires modelValue to be an array`)
|
|
211
|
+
}
|
|
212
|
+
const tokens = validateSelectionIdentities(`${owner} modelValue`, modelValue.map((key, index) => ({
|
|
213
|
+
key,
|
|
214
|
+
coordinate: `modelValue[${index}]`,
|
|
215
|
+
})))
|
|
216
|
+
for (const token of tokens.keys()) {
|
|
217
|
+
if (!inventory.optionsByToken.has(token)) {
|
|
218
|
+
throw new Error(`${owner}: modelValue does not match every option`)
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return new Set(tokens.keys())
|
|
222
|
+
}
|
|
223
|
+
if (isSelectionModelList(modelValue)) {
|
|
224
|
+
throw new TypeError(`${owner}: single selection requires modelValue to be a selection key or null`)
|
|
225
|
+
}
|
|
226
|
+
if (modelValue === null) return new Set()
|
|
227
|
+
const token = selectionKeyToken(owner, modelValue, 'modelValue')
|
|
228
|
+
if (!inventory.optionsByToken.has(token)) {
|
|
229
|
+
throw new Error(`${owner}: modelValue does not match any option`)
|
|
230
|
+
}
|
|
231
|
+
return new Set([token])
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** Narrowing helper for the exact multiple-selection model branch. */
|
|
235
|
+
export function isSelectionModelList<TKey extends SelectionKey>(
|
|
236
|
+
value: TKey | null | readonly TKey[],
|
|
237
|
+
): value is readonly TKey[] {
|
|
238
|
+
return Array.isArray(value)
|
|
239
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { ComputedRef, InjectionKey } from 'vue';
|
|
2
|
+
|
|
3
|
+
import type { SectionState, SidebarSectionsState } from '../containers/sidebar';
|
|
4
|
+
|
|
5
|
+
/** Stable mounted descriptor registered by one SSidebarSection. */
|
|
6
|
+
export interface SidebarSectionRegistration {
|
|
7
|
+
readonly key: string;
|
|
8
|
+
readonly element: HTMLElement;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** DOM-ordered registered section with its exact controlled state. */
|
|
12
|
+
export interface RegisteredSidebarSection {
|
|
13
|
+
readonly key: string;
|
|
14
|
+
readonly element: HTMLElement;
|
|
15
|
+
readonly state: Readonly<SectionState>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export type SidebarSectionStyle = Readonly<Record<string, string | number>>;
|
|
19
|
+
|
|
20
|
+
export interface SidebarGroupContext {
|
|
21
|
+
readonly sectionsState: ComputedRef<SidebarSectionsState>;
|
|
22
|
+
readonly orderedSections: ComputedRef<readonly RegisteredSidebarSection[]>;
|
|
23
|
+
registerSection(registration: SidebarSectionRegistration): () => void;
|
|
24
|
+
toggleSection(key: string): void;
|
|
25
|
+
getSectionStyle(key: string): SidebarSectionStyle;
|
|
26
|
+
beginResize(topKey: string, bottomKey: string): void;
|
|
27
|
+
resizeSections(topKey: string, bottomKey: string, requestedTopSize: number): void;
|
|
28
|
+
finishResize(topKey: string, bottomKey: string): void;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const sidebarGroupContextKey: InjectionKey<SidebarGroupContext> = Symbol('SidebarGroupContext');
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { ComputedRef, InjectionKey } from 'vue'
|
|
2
|
+
|
|
3
|
+
export type STabsActivationMode = 'automatic' | 'manual'
|
|
4
|
+
export type STabRegistrationKey = string
|
|
5
|
+
export type STabPanelRegistrationKey = string
|
|
6
|
+
export type STabListRegistrationKey = symbol
|
|
7
|
+
export type STabPanelsRegistrationKey = symbol
|
|
8
|
+
|
|
9
|
+
export interface STabRegistration {
|
|
10
|
+
readonly tabKey: string
|
|
11
|
+
readonly disabled: () => boolean
|
|
12
|
+
readonly element: () => HTMLElement | null
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface STabPanelRegistration {
|
|
16
|
+
readonly tabKey: string
|
|
17
|
+
readonly element: () => HTMLElement | null
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface STabState {
|
|
21
|
+
readonly active: boolean
|
|
22
|
+
readonly disabled: boolean
|
|
23
|
+
readonly tabIndex: 0 | -1
|
|
24
|
+
readonly tabId: string
|
|
25
|
+
readonly panelId: string | undefined
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface STabPanelState {
|
|
29
|
+
readonly active: boolean
|
|
30
|
+
readonly panelId: string
|
|
31
|
+
readonly tabId: string | undefined
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface STabsContext {
|
|
35
|
+
readonly activeKey: ComputedRef<string | null>
|
|
36
|
+
readonly activationMode: ComputedRef<STabsActivationMode>
|
|
37
|
+
readonly closeButtonMode: ComputedRef<'reserve' | 'overlay'>
|
|
38
|
+
readonly tabDensity: ComputedRef<'compact' | 'default' | 'comfortable'>
|
|
39
|
+
readonly registerTab: (registration: STabRegistration) => STabRegistrationKey
|
|
40
|
+
readonly unregisterTab: (key: STabRegistrationKey) => void
|
|
41
|
+
readonly notifyTabOrderChanged: () => void
|
|
42
|
+
readonly getTabState: (key: STabRegistrationKey) => STabState
|
|
43
|
+
readonly getActiveTabElement: () => HTMLElement | null
|
|
44
|
+
readonly activateTab: (key: STabRegistrationKey) => void
|
|
45
|
+
readonly focusTab: (key: STabRegistrationKey) => void
|
|
46
|
+
readonly registerPanel: (registration: STabPanelRegistration) => STabPanelRegistrationKey
|
|
47
|
+
readonly unregisterPanel: (key: STabPanelRegistrationKey) => void
|
|
48
|
+
readonly notifyPanelOrderChanged: () => void
|
|
49
|
+
readonly getPanelState: (key: STabPanelRegistrationKey) => STabPanelState
|
|
50
|
+
readonly handleTabKeydown: (key: STabRegistrationKey, event: KeyboardEvent) => void
|
|
51
|
+
readonly registerTabList: (scrollActiveIntoCenter: () => void) => STabListRegistrationKey
|
|
52
|
+
readonly unregisterTabList: (key: STabListRegistrationKey) => void
|
|
53
|
+
readonly registerPanels: () => STabPanelsRegistrationKey
|
|
54
|
+
readonly unregisterPanels: (key: STabPanelsRegistrationKey) => void
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export const tabsContextKey: InjectionKey<STabsContext> = Symbol('STabsContext')
|