@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,37 @@
|
|
|
1
|
+
export type ClipboardCopyResult =
|
|
2
|
+
| { status: 'copied'; method: 'clipboard-api' }
|
|
3
|
+
| { status: 'empty' }
|
|
4
|
+
| { status: 'unavailable'; reason: 'clipboard-api-missing' }
|
|
5
|
+
| { status: 'failed'; error: Error | DOMException }
|
|
6
|
+
|
|
7
|
+
export interface ClipboardService {
|
|
8
|
+
copyText: (value: string) => Promise<ClipboardCopyResult>
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Предоставляет единый типизированный контракт копирования текста.
|
|
13
|
+
* Provides the canonical typed text-copying contract.
|
|
14
|
+
*/
|
|
15
|
+
export function useClipboard(): ClipboardService {
|
|
16
|
+
const copyText = async (value: string): Promise<ClipboardCopyResult> => {
|
|
17
|
+
if (!value) return { status: 'empty' }
|
|
18
|
+
|
|
19
|
+
const clipboard = typeof navigator === 'undefined' ? undefined : navigator.clipboard
|
|
20
|
+
if (!clipboard?.writeText) {
|
|
21
|
+
return { status: 'unavailable', reason: 'clipboard-api-missing' }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
await clipboard.writeText(value)
|
|
26
|
+
return { status: 'copied', method: 'clipboard-api' }
|
|
27
|
+
} catch (cause) {
|
|
28
|
+
const isDomException = typeof DOMException !== 'undefined' && cause instanceof DOMException
|
|
29
|
+
const error = cause instanceof Error || isDomException
|
|
30
|
+
? cause
|
|
31
|
+
: new Error('Clipboard API rejected writeText without an Error object', { cause })
|
|
32
|
+
return { status: 'failed', error }
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return { copyText }
|
|
37
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import {
|
|
2
|
+
nextTick,
|
|
3
|
+
onBeforeUnmount,
|
|
4
|
+
ref,
|
|
5
|
+
watch,
|
|
6
|
+
type ComponentPublicInstance,
|
|
7
|
+
type Ref,
|
|
8
|
+
} from 'vue'
|
|
9
|
+
import { trapFocusInsidePanel } from '../components/shared/_internal/focusTrap'
|
|
10
|
+
import { registerLayer, type LayerRegistration } from '../internal/layerStack'
|
|
11
|
+
|
|
12
|
+
export interface DialogOverlayOptions {
|
|
13
|
+
open: Readonly<Ref<boolean>>
|
|
14
|
+
closable: Readonly<Ref<boolean>>
|
|
15
|
+
requestClose: () => void
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface DialogOverlayController {
|
|
19
|
+
panelRef: Ref<HTMLElement | null>
|
|
20
|
+
setPanelElement: (element: Element | ComponentPublicInstance | null) => void
|
|
21
|
+
backdropStyle: Readonly<Ref<Record<string, string>>>
|
|
22
|
+
surfaceStyle: Readonly<Ref<Record<string, string>>>
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Владеет modal-регистрацией, focus trap, body lock и восстановлением фокуса.
|
|
27
|
+
* Owns modal registration, focus trapping, body lock, and focus restoration.
|
|
28
|
+
*/
|
|
29
|
+
export function useDialogOverlay(options: DialogOverlayOptions): DialogOverlayController {
|
|
30
|
+
const panelRef = ref<HTMLElement | null>(null)
|
|
31
|
+
const backdropStyle = ref<Record<string, string>>({})
|
|
32
|
+
const surfaceStyle = ref<Record<string, string>>({})
|
|
33
|
+
let registration: LayerRegistration | null = null
|
|
34
|
+
|
|
35
|
+
const setPanelElement = (element: Element | ComponentPublicInstance | null): void => {
|
|
36
|
+
if (element === null || element instanceof HTMLElement) {
|
|
37
|
+
panelRef.value = element
|
|
38
|
+
return
|
|
39
|
+
}
|
|
40
|
+
throw new TypeError('Dialog overlay panel ref обязан указывать на HTMLElement')
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const registerOpen = (): void => {
|
|
44
|
+
if (registration) return
|
|
45
|
+
const restoreTarget = typeof document !== 'undefined' && document.activeElement instanceof HTMLElement
|
|
46
|
+
? document.activeElement
|
|
47
|
+
: null
|
|
48
|
+
registration = registerLayer({
|
|
49
|
+
kind: 'dialog',
|
|
50
|
+
root: () => panelRef.value,
|
|
51
|
+
modal: true,
|
|
52
|
+
restoreTarget,
|
|
53
|
+
dismissOnEscape: () => options.closable.value,
|
|
54
|
+
dismissOnOutsidePointer: () => options.closable.value,
|
|
55
|
+
trapFocus: (event) => trapFocusInsidePanel(event, panelRef, true),
|
|
56
|
+
requestDismiss: () => options.requestClose(),
|
|
57
|
+
})
|
|
58
|
+
backdropStyle.value = { zIndex: registration.zIndex }
|
|
59
|
+
surfaceStyle.value = {
|
|
60
|
+
zIndex: `calc(${registration.zIndex} + var(--s-layer-overlay-surface-offset))`,
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const unregisterOpen = (restoreFocus = true): void => {
|
|
65
|
+
registration?.unregister({ restoreFocus })
|
|
66
|
+
registration = null
|
|
67
|
+
backdropStyle.value = {}
|
|
68
|
+
surfaceStyle.value = {}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
watch(options.open, async (open, wasOpen) => {
|
|
72
|
+
if (open) {
|
|
73
|
+
registerOpen()
|
|
74
|
+
await nextTick()
|
|
75
|
+
if (options.open.value) panelRef.value?.focus()
|
|
76
|
+
return
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
unregisterOpen(Boolean(wasOpen))
|
|
80
|
+
}, { immediate: true })
|
|
81
|
+
|
|
82
|
+
onBeforeUnmount(() => unregisterOpen(false))
|
|
83
|
+
|
|
84
|
+
return { panelRef, setPanelElement, backdropStyle, surfaceStyle }
|
|
85
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { computed, ref, toValue, useId, type MaybeRefOrGetter, type Ref } from 'vue'
|
|
2
|
+
|
|
3
|
+
export interface UseDisclosureOptions {
|
|
4
|
+
id?: string
|
|
5
|
+
open?: Readonly<Ref<boolean>>
|
|
6
|
+
defaultOpen?: boolean
|
|
7
|
+
disabled?: MaybeRefOrGetter<boolean>
|
|
8
|
+
onUpdate?: (open: boolean) => void
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Canonical controlled/uncontrolled state and ID contract for button disclosures. */
|
|
12
|
+
export function useDisclosure(options: UseDisclosureOptions = {}) {
|
|
13
|
+
if (options.id !== undefined && options.id.trim().length === 0) {
|
|
14
|
+
throw new Error('useDisclosure: id must be a non-empty string')
|
|
15
|
+
}
|
|
16
|
+
if (options.open && !options.onUpdate) {
|
|
17
|
+
throw new Error('useDisclosure: controlled open requires onUpdate')
|
|
18
|
+
}
|
|
19
|
+
if (options.open && options.defaultOpen !== undefined) {
|
|
20
|
+
throw new Error('useDisclosure: controlled open cannot be combined with defaultOpen')
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const generatedId = useId()
|
|
24
|
+
const baseId = options.id ?? `s-disclosure-${generatedId}`
|
|
25
|
+
const internalOpen = ref(options.defaultOpen ?? false)
|
|
26
|
+
const isControlled = options.open !== undefined
|
|
27
|
+
const isDisabled = computed(() => toValue(options.disabled ?? false))
|
|
28
|
+
const isOpen = computed(() => isControlled ? options.open!.value : internalOpen.value)
|
|
29
|
+
const triggerId = computed(() => `${baseId}-trigger`)
|
|
30
|
+
const panelId = computed(() => `${baseId}-panel`)
|
|
31
|
+
|
|
32
|
+
const setOpen = (nextOpen: boolean): void => {
|
|
33
|
+
if (isDisabled.value || nextOpen === isOpen.value) return
|
|
34
|
+
if (isControlled) options.onUpdate!(nextOpen)
|
|
35
|
+
else internalOpen.value = nextOpen
|
|
36
|
+
}
|
|
37
|
+
const toggle = (): void => setOpen(!isOpen.value)
|
|
38
|
+
|
|
39
|
+
return { isOpen, isDisabled, triggerId, panelId, setOpen, toggle }
|
|
40
|
+
}
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import { nextTick, onBeforeUnmount, onMounted, ref, watch, type Ref } from 'vue'
|
|
2
|
+
|
|
3
|
+
export type FloatingSide = 'top' | 'bottom' | 'left' | 'right'
|
|
4
|
+
export type FloatingAlignment = 'start' | 'center' | 'end'
|
|
5
|
+
export type FloatingPlacement = `${FloatingSide}-${FloatingAlignment}`
|
|
6
|
+
export type FloatingWidthMatch = 'none' | 'min' | 'exact'
|
|
7
|
+
|
|
8
|
+
export interface FloatingPositionOptions {
|
|
9
|
+
active: Readonly<Ref<boolean>>
|
|
10
|
+
placement: () => FloatingPlacement
|
|
11
|
+
anchorElement?: () => HTMLElement | null
|
|
12
|
+
anchorRect?: () => DOMRect | null
|
|
13
|
+
panelRef?: Ref<HTMLElement | null>
|
|
14
|
+
offset?: () => number
|
|
15
|
+
boundaryPadding?: () => number
|
|
16
|
+
flip?: () => boolean
|
|
17
|
+
shift?: () => boolean
|
|
18
|
+
widthMatch?: () => FloatingWidthMatch
|
|
19
|
+
zIndex?: () => string
|
|
20
|
+
scrollStrategy?: 'reposition' | 'close'
|
|
21
|
+
requestClose?: () => void
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface FloatingPositionController {
|
|
25
|
+
anchorRef: Ref<HTMLElement | null>
|
|
26
|
+
panelRef: Ref<HTMLElement | null>
|
|
27
|
+
floatingStyle: Ref<Record<string, string>>
|
|
28
|
+
arrowStyle: Ref<Record<string, string>>
|
|
29
|
+
resolvedPlacement: Ref<FloatingPlacement>
|
|
30
|
+
coordinates: Ref<FloatingCoordinates>
|
|
31
|
+
updatePosition: () => void
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Координаты floating-поверхности во viewport. / Floating-surface viewport coordinates. */
|
|
35
|
+
export interface FloatingCoordinates {
|
|
36
|
+
left: number
|
|
37
|
+
top: number
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const oppositeSide: Record<FloatingSide, FloatingSide> = {
|
|
41
|
+
top: 'bottom',
|
|
42
|
+
bottom: 'top',
|
|
43
|
+
left: 'right',
|
|
44
|
+
right: 'left',
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function splitPlacement(placement: FloatingPlacement): [FloatingSide, FloatingAlignment] {
|
|
48
|
+
return placement.split('-') as [FloatingSide, FloatingAlignment]
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function alignedOffset(start: number, anchorSize: number, panelSize: number, alignment: FloatingAlignment): number {
|
|
52
|
+
if (alignment === 'start') return start
|
|
53
|
+
if (alignment === 'end') return start + anchorSize - panelSize
|
|
54
|
+
return start + (anchorSize - panelSize) / 2
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function coordinatesFor(
|
|
58
|
+
side: FloatingSide,
|
|
59
|
+
alignment: FloatingAlignment,
|
|
60
|
+
anchor: DOMRect,
|
|
61
|
+
panelWidth: number,
|
|
62
|
+
panelHeight: number,
|
|
63
|
+
offset: number,
|
|
64
|
+
): FloatingCoordinates {
|
|
65
|
+
if (side === 'top' || side === 'bottom') {
|
|
66
|
+
return {
|
|
67
|
+
left: alignedOffset(anchor.left, anchor.width, panelWidth, alignment),
|
|
68
|
+
top: side === 'top' ? anchor.top - panelHeight - offset : anchor.bottom + offset,
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
left: side === 'left' ? anchor.left - panelWidth - offset : anchor.right + offset,
|
|
73
|
+
top: alignedOffset(anchor.top, anchor.height, panelHeight, alignment),
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function mainAxisOverflow(
|
|
78
|
+
side: FloatingSide,
|
|
79
|
+
coordinates: FloatingCoordinates,
|
|
80
|
+
panelWidth: number,
|
|
81
|
+
panelHeight: number,
|
|
82
|
+
viewportWidth: number,
|
|
83
|
+
viewportHeight: number,
|
|
84
|
+
padding: number,
|
|
85
|
+
): number {
|
|
86
|
+
if (side === 'top') return Math.max(0, padding - coordinates.top)
|
|
87
|
+
if (side === 'bottom') return Math.max(0, coordinates.top + panelHeight - (viewportHeight - padding))
|
|
88
|
+
if (side === 'left') return Math.max(0, padding - coordinates.left)
|
|
89
|
+
return Math.max(0, coordinates.left + panelWidth - (viewportWidth - padding))
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function clampArrowCoordinate(coordinate: number, panelSize: number): number {
|
|
93
|
+
const inset = Math.min(10, panelSize / 2)
|
|
94
|
+
return Math.max(inset, Math.min(coordinate, panelSize - inset))
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Единый владелец geometry/collision/observer-контракта floating surfaces.
|
|
99
|
+
* Canonical owner of floating-surface geometry, collision and observers.
|
|
100
|
+
*/
|
|
101
|
+
export function useFloatingPosition(options: FloatingPositionOptions): FloatingPositionController {
|
|
102
|
+
const anchorRef = ref<HTMLElement | null>(null)
|
|
103
|
+
const panelRef = options.panelRef ?? ref<HTMLElement | null>(null)
|
|
104
|
+
const floatingStyle = ref<Record<string, string>>({})
|
|
105
|
+
const arrowStyle = ref<Record<string, string>>({})
|
|
106
|
+
const resolvedPlacement = ref<FloatingPlacement>(options.placement())
|
|
107
|
+
const resolvedCoordinates = ref<FloatingCoordinates>({ left: -10000, top: -10000 })
|
|
108
|
+
let resizeObserver: ResizeObserver | null = null
|
|
109
|
+
let listenersAttached = false
|
|
110
|
+
|
|
111
|
+
const updatePosition = (): void => {
|
|
112
|
+
if (!options.active.value || typeof window === 'undefined') return
|
|
113
|
+
const anchorElement = options.anchorElement?.() ?? anchorRef.value
|
|
114
|
+
const panelElement = panelRef.value
|
|
115
|
+
const anchor = options.anchorRect?.() ?? anchorElement?.getBoundingClientRect() ?? null
|
|
116
|
+
if (!anchor || !panelElement) return
|
|
117
|
+
|
|
118
|
+
const panel = panelElement.getBoundingClientRect()
|
|
119
|
+
const panelWidth = panel.width || panelElement.offsetWidth
|
|
120
|
+
const panelHeight = panel.height || panelElement.offsetHeight
|
|
121
|
+
const offset = options.offset?.() ?? 6
|
|
122
|
+
const padding = options.boundaryPadding?.() ?? 8
|
|
123
|
+
const viewportWidth = window.innerWidth
|
|
124
|
+
const viewportHeight = window.innerHeight
|
|
125
|
+
const [preferredSide, alignment] = splitPlacement(options.placement())
|
|
126
|
+
let side = preferredSide
|
|
127
|
+
let coordinates = coordinatesFor(side, alignment, anchor, panelWidth, panelHeight, offset)
|
|
128
|
+
|
|
129
|
+
if (options.flip?.() ?? true) {
|
|
130
|
+
const alternateSide = oppositeSide[side]
|
|
131
|
+
const alternateCoordinates = coordinatesFor(alternateSide, alignment, anchor, panelWidth, panelHeight, offset)
|
|
132
|
+
const preferredOverflow = mainAxisOverflow(
|
|
133
|
+
side, coordinates, panelWidth, panelHeight, viewportWidth, viewportHeight, padding,
|
|
134
|
+
)
|
|
135
|
+
const alternateOverflow = mainAxisOverflow(
|
|
136
|
+
alternateSide, alternateCoordinates, panelWidth, panelHeight, viewportWidth, viewportHeight, padding,
|
|
137
|
+
)
|
|
138
|
+
if (alternateOverflow < preferredOverflow) {
|
|
139
|
+
side = alternateSide
|
|
140
|
+
coordinates = alternateCoordinates
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (options.shift?.() ?? true) {
|
|
145
|
+
coordinates.left = Math.max(padding, Math.min(coordinates.left, viewportWidth - panelWidth - padding))
|
|
146
|
+
coordinates.top = Math.max(padding, Math.min(coordinates.top, viewportHeight - panelHeight - padding))
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const placement = `${side}-${alignment}` as FloatingPlacement
|
|
150
|
+
resolvedPlacement.value = placement
|
|
151
|
+
resolvedCoordinates.value = {
|
|
152
|
+
left: Math.round(coordinates.left),
|
|
153
|
+
top: Math.round(coordinates.top),
|
|
154
|
+
}
|
|
155
|
+
const widthMatch = options.widthMatch?.() ?? 'none'
|
|
156
|
+
floatingStyle.value = {
|
|
157
|
+
position: 'fixed',
|
|
158
|
+
left: `${resolvedCoordinates.value.left}px`,
|
|
159
|
+
top: `${resolvedCoordinates.value.top}px`,
|
|
160
|
+
zIndex: options.zIndex?.() ?? 'var(--s-layer-floating)',
|
|
161
|
+
...(widthMatch === 'min' ? { minWidth: `${Math.round(anchor.width)}px` } : {}),
|
|
162
|
+
...(widthMatch === 'exact' ? { width: `${Math.round(anchor.width)}px` } : {}),
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (side === 'top' || side === 'bottom') {
|
|
166
|
+
const anchorCenter = anchor.left + anchor.width / 2 - coordinates.left
|
|
167
|
+
arrowStyle.value = {
|
|
168
|
+
left: `${Math.round(clampArrowCoordinate(anchorCenter, panelWidth))}px`,
|
|
169
|
+
}
|
|
170
|
+
} else {
|
|
171
|
+
const anchorCenter = anchor.top + anchor.height / 2 - coordinates.top
|
|
172
|
+
arrowStyle.value = {
|
|
173
|
+
top: `${Math.round(clampArrowCoordinate(anchorCenter, panelHeight))}px`,
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const handleScroll = (): void => {
|
|
179
|
+
if (!options.active.value) return
|
|
180
|
+
if (options.scrollStrategy === 'close') {
|
|
181
|
+
options.requestClose?.()
|
|
182
|
+
return
|
|
183
|
+
}
|
|
184
|
+
updatePosition()
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const attachListeners = (): void => {
|
|
188
|
+
if (listenersAttached || typeof window === 'undefined') return
|
|
189
|
+
listenersAttached = true
|
|
190
|
+
window.addEventListener('resize', updatePosition, { passive: true })
|
|
191
|
+
window.addEventListener('scroll', handleScroll, { passive: true, capture: true })
|
|
192
|
+
if (typeof ResizeObserver !== 'undefined') {
|
|
193
|
+
resizeObserver = new ResizeObserver(updatePosition)
|
|
194
|
+
const observedAnchor = options.anchorElement?.() ?? anchorRef.value
|
|
195
|
+
if (observedAnchor) resizeObserver.observe(observedAnchor)
|
|
196
|
+
if (panelRef.value) resizeObserver.observe(panelRef.value)
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const detachListeners = (): void => {
|
|
201
|
+
if (!listenersAttached || typeof window === 'undefined') return
|
|
202
|
+
listenersAttached = false
|
|
203
|
+
window.removeEventListener('resize', updatePosition)
|
|
204
|
+
window.removeEventListener('scroll', handleScroll, true)
|
|
205
|
+
resizeObserver?.disconnect()
|
|
206
|
+
resizeObserver = null
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
watch(options.active, async (active) => {
|
|
210
|
+
if (!active) {
|
|
211
|
+
detachListeners()
|
|
212
|
+
floatingStyle.value = {}
|
|
213
|
+
arrowStyle.value = {}
|
|
214
|
+
resolvedCoordinates.value = { left: -10000, top: -10000 }
|
|
215
|
+
return
|
|
216
|
+
}
|
|
217
|
+
floatingStyle.value = {
|
|
218
|
+
position: 'fixed',
|
|
219
|
+
left: '-10000px',
|
|
220
|
+
top: '-10000px',
|
|
221
|
+
zIndex: options.zIndex?.() ?? 'var(--s-layer-floating)',
|
|
222
|
+
}
|
|
223
|
+
await nextTick()
|
|
224
|
+
attachListeners()
|
|
225
|
+
updatePosition()
|
|
226
|
+
}, { immediate: true })
|
|
227
|
+
|
|
228
|
+
onMounted(() => {
|
|
229
|
+
if (options.active.value) {
|
|
230
|
+
attachListeners()
|
|
231
|
+
updatePosition()
|
|
232
|
+
}
|
|
233
|
+
})
|
|
234
|
+
onBeforeUnmount(detachListeners)
|
|
235
|
+
|
|
236
|
+
return {
|
|
237
|
+
anchorRef,
|
|
238
|
+
panelRef,
|
|
239
|
+
floatingStyle,
|
|
240
|
+
arrowStyle,
|
|
241
|
+
resolvedPlacement,
|
|
242
|
+
coordinates: resolvedCoordinates,
|
|
243
|
+
updatePosition,
|
|
244
|
+
}
|
|
245
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { useNotifierStore } from '../stores/useNotifierStore'
|
|
2
|
+
import type { NotificationInput, Severity } from '../stores/useNotifierStore'
|
|
3
|
+
|
|
4
|
+
export type NotifyOptions = Pick<NotificationInput, 'title' | 'message' | 'duration'>
|
|
5
|
+
|
|
6
|
+
type NotifyArg = NotifyOptions | string
|
|
7
|
+
|
|
8
|
+
// This composable provides a clean API to trigger notifications from any component.
|
|
9
|
+
export function useNotifier() {
|
|
10
|
+
const store = useNotifierStore()
|
|
11
|
+
|
|
12
|
+
const notify = (severity: Severity, options: NotifyArg) => {
|
|
13
|
+
const payload: NotifyOptions = typeof options === 'string' ? { message: options } : options
|
|
14
|
+
store.add({
|
|
15
|
+
severity,
|
|
16
|
+
...payload,
|
|
17
|
+
})
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
info: (options: NotifyArg) => notify('info', options),
|
|
22
|
+
success: (options: NotifyArg) => notify('success', options),
|
|
23
|
+
warn: (options: NotifyArg) => notify('warn', options),
|
|
24
|
+
danger: (options: NotifyArg) => notify('danger', options),
|
|
25
|
+
}
|
|
26
|
+
}
|