@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,326 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, watchEffect, type VNodeChild } from 'vue'
|
|
3
|
+
import { RouterLink, type RouteLocationRaw } from 'vue-router'
|
|
4
|
+
import type { STooltipTriggerBinding } from '../data-display/STooltip.vue'
|
|
5
|
+
import { resolveLinkTarget } from '../../../internal/linkTarget'
|
|
6
|
+
import { useOwnedAttrs } from '../../../internal/ownedAttrs'
|
|
7
|
+
import {
|
|
8
|
+
PassiveContentBoundary,
|
|
9
|
+
useInteractiveLeafRegistration,
|
|
10
|
+
type PassiveContentContract,
|
|
11
|
+
} from '../../../internal/passiveContentContract'
|
|
12
|
+
|
|
13
|
+
defineOptions({ inheritAttrs: false })
|
|
14
|
+
|
|
15
|
+
export type InteractiveSurfaceTone = 'default' | 'primary' | 'success' | 'danger'
|
|
16
|
+
export type InteractiveSurfaceDensity = 'compact' | 'comfortable'
|
|
17
|
+
export type InteractiveSurfacePadding = 'none' | 'sm' | 'md'
|
|
18
|
+
export type InteractiveSurfaceRadius = 'md' | 'lg' | 'xl'
|
|
19
|
+
export type InteractiveSurfaceFill = 'content' | 'container'
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Единственный semantic-контракт поверхности: passive, action или ровно одна link-target.
|
|
23
|
+
* The sole surface semantics contract: passive, action, or exactly one link target.
|
|
24
|
+
*/
|
|
25
|
+
export type InteractiveSurfaceContract =
|
|
26
|
+
| { readonly mode: 'static' }
|
|
27
|
+
| {
|
|
28
|
+
readonly mode: 'action'
|
|
29
|
+
readonly accessibleLabel: string
|
|
30
|
+
readonly disabled?: boolean
|
|
31
|
+
readonly pressed?: boolean
|
|
32
|
+
}
|
|
33
|
+
| ({
|
|
34
|
+
readonly mode: 'link'
|
|
35
|
+
readonly accessibleLabel: string
|
|
36
|
+
readonly disabled?: boolean
|
|
37
|
+
readonly target?: '_self' | '_blank' | '_parent' | '_top'
|
|
38
|
+
readonly rel?: string
|
|
39
|
+
} & (
|
|
40
|
+
| { readonly href: string; readonly to?: never }
|
|
41
|
+
| { readonly href?: never; readonly to: RouteLocationRaw }
|
|
42
|
+
))
|
|
43
|
+
/** Разрешённые semantic-режимы поверхности. / Supported semantic surface modes. */
|
|
44
|
+
export type InteractiveSurfaceMode = InteractiveSurfaceContract['mode']
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Контракт большой интерактивной поверхности с пассивным содержимым.
|
|
48
|
+
* Contract for a large interactive surface whose visual content remains passive.
|
|
49
|
+
*/
|
|
50
|
+
export interface Props {
|
|
51
|
+
surface: InteractiveSurfaceContract
|
|
52
|
+
selected?: boolean
|
|
53
|
+
tone?: InteractiveSurfaceTone
|
|
54
|
+
density?: InteractiveSurfaceDensity
|
|
55
|
+
padding?: InteractiveSurfacePadding
|
|
56
|
+
radius?: InteractiveSurfaceRadius
|
|
57
|
+
fill?: InteractiveSurfaceFill
|
|
58
|
+
contentAlign?: 'start' | 'center'
|
|
59
|
+
/** Полный binding явного STooltip trigger, направляемый на activation owner. / Complete explicit STooltip trigger binding routed to the activation owner. */
|
|
60
|
+
tooltipTrigger?: STooltipTriggerBinding
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
64
|
+
selected: false,
|
|
65
|
+
tone: 'default',
|
|
66
|
+
density: 'comfortable',
|
|
67
|
+
padding: 'md',
|
|
68
|
+
radius: 'lg',
|
|
69
|
+
fill: 'content',
|
|
70
|
+
contentAlign: 'start',
|
|
71
|
+
tooltipTrigger: undefined,
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
const emit = defineEmits<{
|
|
75
|
+
activate: [event: MouseEvent]
|
|
76
|
+
}>()
|
|
77
|
+
defineSlots<{
|
|
78
|
+
/** Пассивное flow-содержимое поверхности. / Passive flow content of the surface. */
|
|
79
|
+
default?: () => VNodeChild
|
|
80
|
+
}>()
|
|
81
|
+
|
|
82
|
+
const ownedAttrs = useOwnedAttrs({ component: 'SInteractiveSurface', owner: 'interactive surface owner' })
|
|
83
|
+
|
|
84
|
+
const assertOptionalBoolean = (surface: Record<string, unknown>, field: 'disabled' | 'pressed'): void => {
|
|
85
|
+
if (surface[field] !== undefined && typeof surface[field] !== 'boolean') {
|
|
86
|
+
throw new TypeError(`SInteractiveSurface: surface.${field} должен быть boolean`)
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const assertExactSurfaceKeys = (surface: Record<string, unknown>, allowed: readonly string[]): void => {
|
|
91
|
+
const allowedKeys = new Set(allowed)
|
|
92
|
+
const unexpected = Object.keys(surface).filter((key) => !allowedKeys.has(key))
|
|
93
|
+
if (unexpected.length > 0) {
|
|
94
|
+
throw new Error(`SInteractiveSurface: surface mode="${String(surface.mode)}" не допускает поля ${unexpected.join(', ')}`)
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const validatedSurface = computed<InteractiveSurfaceContract>(() => {
|
|
99
|
+
const candidate: unknown = props.surface
|
|
100
|
+
if (typeof candidate !== 'object' || candidate === null || Array.isArray(candidate)) {
|
|
101
|
+
throw new TypeError('SInteractiveSurface: surface должен быть discriminated object')
|
|
102
|
+
}
|
|
103
|
+
const surface = candidate as Record<string, unknown>
|
|
104
|
+
if (surface.mode === 'static') {
|
|
105
|
+
assertExactSurfaceKeys(surface, ['mode'])
|
|
106
|
+
return { mode: 'static' }
|
|
107
|
+
}
|
|
108
|
+
if (surface.mode === 'action') {
|
|
109
|
+
assertExactSurfaceKeys(surface, ['mode', 'accessibleLabel', 'disabled', 'pressed'])
|
|
110
|
+
assertOptionalBoolean(surface, 'disabled')
|
|
111
|
+
assertOptionalBoolean(surface, 'pressed')
|
|
112
|
+
if (typeof surface.accessibleLabel !== 'string' || !surface.accessibleLabel.trim()) {
|
|
113
|
+
throw new TypeError('SInteractiveSurface: accessibleLabel обязателен для mode="action"')
|
|
114
|
+
}
|
|
115
|
+
return {
|
|
116
|
+
mode: 'action',
|
|
117
|
+
accessibleLabel: surface.accessibleLabel,
|
|
118
|
+
disabled: surface.disabled as boolean | undefined,
|
|
119
|
+
pressed: surface.pressed as boolean | undefined,
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
if (surface.mode === 'link') {
|
|
123
|
+
assertExactSurfaceKeys(surface, ['mode', 'accessibleLabel', 'disabled', 'target', 'rel', 'href', 'to'])
|
|
124
|
+
assertOptionalBoolean(surface, 'disabled')
|
|
125
|
+
if (typeof surface.accessibleLabel !== 'string' || !surface.accessibleLabel.trim()) {
|
|
126
|
+
throw new TypeError('SInteractiveSurface: accessibleLabel обязателен для mode="link"')
|
|
127
|
+
}
|
|
128
|
+
const target = surface.target ?? '_self'
|
|
129
|
+
if (!['_self', '_blank', '_parent', '_top'].includes(String(target))) {
|
|
130
|
+
throw new TypeError('SInteractiveSurface: surface.target имеет недопустимое значение')
|
|
131
|
+
}
|
|
132
|
+
if (surface.rel !== undefined && typeof surface.rel !== 'string') {
|
|
133
|
+
throw new TypeError('SInteractiveSurface: surface.rel должен быть string')
|
|
134
|
+
}
|
|
135
|
+
if ('href' in surface && (typeof surface.href !== 'string' || surface.href.length === 0)) {
|
|
136
|
+
throw new TypeError('SInteractiveSurface: surface.href должен быть непустой string')
|
|
137
|
+
}
|
|
138
|
+
if (
|
|
139
|
+
'to' in surface
|
|
140
|
+
&& !(
|
|
141
|
+
typeof surface.to === 'string'
|
|
142
|
+
|| (typeof surface.to === 'object' && surface.to !== null && !Array.isArray(surface.to))
|
|
143
|
+
)
|
|
144
|
+
) {
|
|
145
|
+
throw new TypeError('SInteractiveSurface: surface.to должен быть RouteLocationRaw')
|
|
146
|
+
}
|
|
147
|
+
const resolved = resolveLinkTarget({
|
|
148
|
+
href: typeof surface.href === 'string' ? surface.href : undefined,
|
|
149
|
+
to: surface.to as RouteLocationRaw | undefined,
|
|
150
|
+
target: target as '_self' | '_blank' | '_parent' | '_top',
|
|
151
|
+
rel: surface.rel as string | undefined,
|
|
152
|
+
owner: 'SInteractiveSurface',
|
|
153
|
+
})
|
|
154
|
+
return resolved.kind === 'anchor'
|
|
155
|
+
? {
|
|
156
|
+
mode: 'link',
|
|
157
|
+
accessibleLabel: surface.accessibleLabel,
|
|
158
|
+
disabled: surface.disabled as boolean | undefined,
|
|
159
|
+
href: resolved.bindings.href,
|
|
160
|
+
target: resolved.bindings.target,
|
|
161
|
+
rel: resolved.bindings.rel,
|
|
162
|
+
}
|
|
163
|
+
: {
|
|
164
|
+
mode: 'link',
|
|
165
|
+
accessibleLabel: surface.accessibleLabel,
|
|
166
|
+
disabled: surface.disabled as boolean | undefined,
|
|
167
|
+
to: resolved.bindings.to,
|
|
168
|
+
target: resolved.bindings.target,
|
|
169
|
+
rel: resolved.bindings.rel,
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
throw new TypeError(`SInteractiveSurface: неизвестный surface.mode "${String(surface.mode)}"`)
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
const TOOLTIP_TRIGGER_KEYS = [
|
|
176
|
+
'ref',
|
|
177
|
+
'aria-describedby',
|
|
178
|
+
'onPointerenter',
|
|
179
|
+
'onPointerleave',
|
|
180
|
+
'onPointerdown',
|
|
181
|
+
'onPointerup',
|
|
182
|
+
'onPointercancel',
|
|
183
|
+
'onFocusin',
|
|
184
|
+
'onFocusout',
|
|
185
|
+
] as const
|
|
186
|
+
|
|
187
|
+
const validatedTooltipTrigger = computed<STooltipTriggerBinding | undefined>(() => {
|
|
188
|
+
const candidate: unknown = props.tooltipTrigger
|
|
189
|
+
if (candidate === undefined) return undefined
|
|
190
|
+
if (typeof candidate !== 'object' || candidate === null || Array.isArray(candidate)) {
|
|
191
|
+
throw new TypeError('SInteractiveSurface: tooltipTrigger должен быть полным STooltip trigger binding. / SInteractiveSurface: tooltipTrigger must be a complete STooltip trigger binding.')
|
|
192
|
+
}
|
|
193
|
+
const record = candidate as Record<string, unknown>
|
|
194
|
+
const unexpected = Object.keys(record).filter((key) => !TOOLTIP_TRIGGER_KEYS.includes(key as typeof TOOLTIP_TRIGGER_KEYS[number]))
|
|
195
|
+
const missing = TOOLTIP_TRIGGER_KEYS.filter((key) => !Object.hasOwn(record, key))
|
|
196
|
+
if (unexpected.length > 0 || missing.length > 0) {
|
|
197
|
+
throw new Error(`SInteractiveSurface: tooltipTrigger имеет недопустимый набор ключей; missing=${missing.join(',')}; unexpected=${unexpected.join(',')}. / SInteractiveSurface: tooltipTrigger has an invalid key set; missing=${missing.join(',')}; unexpected=${unexpected.join(',')}.`)
|
|
198
|
+
}
|
|
199
|
+
if (record['aria-describedby'] !== undefined && typeof record['aria-describedby'] !== 'string') {
|
|
200
|
+
throw new TypeError('SInteractiveSurface: tooltipTrigger aria-describedby должен быть string или undefined. / SInteractiveSurface: tooltipTrigger aria-describedby must be a string or undefined.')
|
|
201
|
+
}
|
|
202
|
+
if (typeof record.ref !== 'function') {
|
|
203
|
+
throw new TypeError('SInteractiveSurface: tooltipTrigger.ref должен быть callback ref. / SInteractiveSurface: tooltipTrigger.ref must be a callback ref.')
|
|
204
|
+
}
|
|
205
|
+
for (const key of TOOLTIP_TRIGGER_KEYS) {
|
|
206
|
+
if (key !== 'ref' && key !== 'aria-describedby' && typeof record[key] !== 'function') {
|
|
207
|
+
throw new TypeError(`SInteractiveSurface: tooltipTrigger.${key} должен быть function. / SInteractiveSurface: tooltipTrigger.${key} must be a function.`)
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return candidate as STooltipTriggerBinding
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
const activationBindings = (): Readonly<Record<string, unknown>> => ({
|
|
214
|
+
...ownedAttrs.bindings(),
|
|
215
|
+
...(validatedTooltipTrigger.value ?? {}),
|
|
216
|
+
})
|
|
217
|
+
const surfaceMode = computed<InteractiveSurfaceMode>(() => validatedSurface.value.mode)
|
|
218
|
+
const surfaceDisabled = computed(() => validatedSurface.value.mode === 'static'
|
|
219
|
+
? false
|
|
220
|
+
: validatedSurface.value.disabled ?? false)
|
|
221
|
+
const surfacePressed = computed(() => validatedSurface.value.mode === 'action' ? validatedSurface.value.pressed : undefined)
|
|
222
|
+
const resolvedTarget = computed(() => {
|
|
223
|
+
const surface = validatedSurface.value
|
|
224
|
+
if (surface.mode !== 'link') return null
|
|
225
|
+
return resolveLinkTarget({
|
|
226
|
+
href: surface.href,
|
|
227
|
+
to: surface.to,
|
|
228
|
+
target: surface.target ?? '_self',
|
|
229
|
+
rel: surface.rel,
|
|
230
|
+
owner: 'SInteractiveSurface',
|
|
231
|
+
})
|
|
232
|
+
})
|
|
233
|
+
const linkComponent = computed(() => resolvedTarget.value?.kind === 'router' ? RouterLink : 'a')
|
|
234
|
+
const linkBindings = () => {
|
|
235
|
+
const target = resolvedTarget.value
|
|
236
|
+
if (!target) throw new Error('SInteractiveSurface: link bindings requested outside link surface')
|
|
237
|
+
return {
|
|
238
|
+
...activationBindings(),
|
|
239
|
+
...target.bindings,
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
const resolvedAccessibleLabel = computed(() => {
|
|
243
|
+
if (validatedSurface.value.mode === 'static') return undefined
|
|
244
|
+
return validatedSurface.value.accessibleLabel.trim()
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
watchEffect(() => {
|
|
248
|
+
if (validatedSurface.value.mode === 'static' && validatedTooltipTrigger.value) {
|
|
249
|
+
throw new Error('SInteractiveSurface: tooltipTrigger требует action или link surface. / SInteractiveSurface: tooltipTrigger requires an action or link surface.')
|
|
250
|
+
}
|
|
251
|
+
void validatedTooltipTrigger.value
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
useInteractiveLeafRegistration({
|
|
255
|
+
owner: 'SInteractiveSurface',
|
|
256
|
+
active: () => surfaceMode.value !== 'static',
|
|
257
|
+
})
|
|
258
|
+
const passiveContentContract: PassiveContentContract = {
|
|
259
|
+
owner: 'SInteractiveSurface',
|
|
260
|
+
region: 'passive content-region',
|
|
261
|
+
active: () => surfaceMode.value !== 'static',
|
|
262
|
+
guidance: 'Передайте дополнительное действие через sibling action-region. / Put the additional action in the sibling action region.',
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function activateLink(event: MouseEvent): void {
|
|
266
|
+
if (surfaceDisabled.value) {
|
|
267
|
+
event.preventDefault()
|
|
268
|
+
return
|
|
269
|
+
}
|
|
270
|
+
emit('activate', event)
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function activateAction(event: MouseEvent): void {
|
|
274
|
+
emit('activate', event)
|
|
275
|
+
}
|
|
276
|
+
</script>
|
|
277
|
+
|
|
278
|
+
<template>
|
|
279
|
+
<div
|
|
280
|
+
v-bind="surfaceMode === 'static' ? ownedAttrs.bindings() : undefined"
|
|
281
|
+
class="s-interactive-surface"
|
|
282
|
+
:data-mode="surfaceMode"
|
|
283
|
+
:data-tone="tone"
|
|
284
|
+
:data-density="density"
|
|
285
|
+
:data-padding="padding"
|
|
286
|
+
:data-radius="radius"
|
|
287
|
+
:data-fill="fill"
|
|
288
|
+
:data-selected="selected || undefined"
|
|
289
|
+
:data-pressed="surfacePressed"
|
|
290
|
+
:data-disabled="surfaceDisabled || undefined"
|
|
291
|
+
:data-content-align="contentAlign"
|
|
292
|
+
>
|
|
293
|
+
<button
|
|
294
|
+
v-if="surfaceMode === 'action'"
|
|
295
|
+
v-bind="activationBindings()"
|
|
296
|
+
class="s-interactive-surface__activation"
|
|
297
|
+
type="button"
|
|
298
|
+
:disabled="surfaceDisabled"
|
|
299
|
+
:aria-label="resolvedAccessibleLabel"
|
|
300
|
+
:aria-pressed="surfacePressed"
|
|
301
|
+
@click="activateAction"
|
|
302
|
+
>
|
|
303
|
+
<span class="s-interactive-surface__accessible-label">{{ resolvedAccessibleLabel }}</span>
|
|
304
|
+
</button>
|
|
305
|
+
<component
|
|
306
|
+
v-else-if="surfaceMode === 'link'"
|
|
307
|
+
:is="linkComponent"
|
|
308
|
+
v-bind="linkBindings()"
|
|
309
|
+
class="s-interactive-surface__activation"
|
|
310
|
+
:aria-label="resolvedAccessibleLabel"
|
|
311
|
+
:aria-current="selected ? 'page' : undefined"
|
|
312
|
+
:aria-disabled="surfaceDisabled || undefined"
|
|
313
|
+
:tabindex="surfaceDisabled ? -1 : undefined"
|
|
314
|
+
@click="activateLink"
|
|
315
|
+
>
|
|
316
|
+
<span class="s-interactive-surface__accessible-label">{{ resolvedAccessibleLabel }}</span>
|
|
317
|
+
</component>
|
|
318
|
+
<div class="s-interactive-surface__content" data-passive-content="">
|
|
319
|
+
<PassiveContentBoundary :contract="passiveContentContract">
|
|
320
|
+
<slot />
|
|
321
|
+
</PassiveContentBoundary>
|
|
322
|
+
</div>
|
|
323
|
+
</div>
|
|
324
|
+
</template>
|
|
325
|
+
|
|
326
|
+
<style lang="postcss" src="./SInteractiveSurface.css" scoped></style>
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
@reference "../../../styles/reference.css";
|
|
2
|
+
|
|
3
|
+
.s-link {
|
|
4
|
+
@apply inline-flex min-w-0 items-center rounded-sm font-medium text-primary-600 underline-offset-2;
|
|
5
|
+
@apply hover:text-primary-700;
|
|
6
|
+
@apply dark:text-primary-400 dark:hover:text-primary-300;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.s-link:focus-visible {
|
|
10
|
+
outline: var(--s-focus-ring-width) solid var(--s-focus-ring-color);
|
|
11
|
+
outline-offset: var(--s-focus-ring-offset);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.s-link[data-width='full'] {
|
|
15
|
+
@apply w-full;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.s-link[data-size='sm'] {
|
|
19
|
+
@apply text-sm;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.s-link[data-size='md'] {
|
|
23
|
+
@apply text-base;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.s-link[data-underline='hover'] {
|
|
27
|
+
@apply no-underline hover:underline;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.s-link[data-underline='always'] {
|
|
31
|
+
@apply underline;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.s-link[data-underline='none'] {
|
|
35
|
+
@apply no-underline hover:no-underline;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.s-link__icon {
|
|
39
|
+
@apply inline-flex shrink-0 items-center justify-center;
|
|
40
|
+
width: var(--s-icon-size-xs);
|
|
41
|
+
height: var(--s-icon-size-xs);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.s-link__icon-graphic {
|
|
45
|
+
@apply h-full w-full;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.s-link__content {
|
|
49
|
+
@apply min-w-0;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.s-link[data-wrap='anywhere'] .s-link__content {
|
|
53
|
+
overflow-wrap: anywhere;
|
|
54
|
+
word-break: break-word;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.s-link[data-content-kind='identifier'] {
|
|
58
|
+
@apply font-mono;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.s-link__trailing {
|
|
62
|
+
@apply ml-auto shrink-0;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.s-link[data-variant='subtle'] {
|
|
66
|
+
@apply text-surface-600 dark:text-surface-300;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.s-link[data-variant='document'] {
|
|
70
|
+
display: inline;
|
|
71
|
+
font: inherit;
|
|
72
|
+
@apply cursor-pointer text-primary-600 underline decoration-primary-300 underline-offset-2 transition-colors;
|
|
73
|
+
@apply hover:text-primary-700 hover:decoration-primary-500;
|
|
74
|
+
@apply dark:text-primary-400 dark:hover:text-primary-300;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.s-link[data-variant='document'] .s-link__content {
|
|
78
|
+
display: inline;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.s-link[data-variant='brand'] {
|
|
82
|
+
@apply gap-3 rounded-xl text-inherit no-underline transition-opacity hover:text-inherit hover:no-underline hover:opacity-90;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.s-link[data-variant='navigation'] {
|
|
86
|
+
@apply justify-between gap-2 rounded-2xl border border-transparent px-3 py-2.5 text-sm text-surface-700 no-underline transition;
|
|
87
|
+
@apply hover:border-surface-200 hover:bg-surface-100/80 hover:text-surface-900 hover:no-underline;
|
|
88
|
+
@apply dark:text-surface-200 dark:hover:border-surface-700 dark:hover:bg-surface-800/60 dark:hover:text-surface-0;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.s-link[data-variant='navigation'] .s-link__content {
|
|
92
|
+
@apply truncate;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.s-link[data-variant='navigation'][data-active='true'] {
|
|
96
|
+
@apply border-primary-200 bg-primary-50 text-primary-700;
|
|
97
|
+
@apply dark:border-primary-800 dark:bg-primary-950/50 dark:text-primary-200;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.s-link[data-variant='bottom-navigation'] {
|
|
101
|
+
@apply w-full justify-center gap-2 rounded-xl px-2 py-2 text-sm text-surface-600 no-underline transition;
|
|
102
|
+
@apply hover:bg-surface-100 hover:text-surface-900 hover:no-underline;
|
|
103
|
+
min-height: var(--s-control-hitbox-sm);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.s-link[data-variant='bottom-navigation'][data-active='true'] {
|
|
107
|
+
@apply bg-primary-50 text-primary-700;
|
|
108
|
+
@apply dark:bg-primary-950/50 dark:text-primary-200;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.s-link[data-disabled='true'] {
|
|
112
|
+
@apply cursor-not-allowed opacity-50;
|
|
113
|
+
pointer-events: auto;
|
|
114
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { Component } from 'vue'
|
|
2
|
+
import type {
|
|
3
|
+
HistoryState,
|
|
4
|
+
LocationQueryRaw,
|
|
5
|
+
LocationQueryValueRaw,
|
|
6
|
+
RouteParamsRawGeneric,
|
|
7
|
+
} from 'vue-router'
|
|
8
|
+
|
|
9
|
+
/** Допустимое scalar-значение params/query. / Supported scalar params/query value. */
|
|
10
|
+
export type LinkRouteScalar = LocationQueryValueRaw
|
|
11
|
+
|
|
12
|
+
/** Exact params/query container for the UI-kit destination boundary. */
|
|
13
|
+
export type LinkRouteDictionary = LocationQueryRaw
|
|
14
|
+
|
|
15
|
+
/** Exact named-route params accepted by Vue Router. */
|
|
16
|
+
export type LinkRouteParams = RouteParamsRawGeneric
|
|
17
|
+
|
|
18
|
+
/** Exact serializable History API state supported by the UI-kit destination boundary. */
|
|
19
|
+
export type LinkHistoryStateValue = string | number | boolean | null | undefined | LinkHistoryState | LinkHistoryStateValue[]
|
|
20
|
+
export interface LinkHistoryState extends HistoryState {
|
|
21
|
+
[key: number]: LinkHistoryStateValue
|
|
22
|
+
[key: string]: LinkHistoryStateValue
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface LinkRouterOptions {
|
|
26
|
+
readonly query?: LinkRouteDictionary
|
|
27
|
+
readonly hash?: string
|
|
28
|
+
readonly replace?: boolean
|
|
29
|
+
readonly force?: boolean
|
|
30
|
+
readonly state?: LinkHistoryState
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Намеренно суженный router destination: UI-kit требует явный named/path target.
|
|
35
|
+
* Intentionally narrowed router destination: the UI kit requires an explicit named/path target.
|
|
36
|
+
*/
|
|
37
|
+
export type LinkRouterLocation = LinkRouterOptions & (
|
|
38
|
+
| { readonly name: string | symbol; readonly path?: never; readonly params?: LinkRouteParams }
|
|
39
|
+
| { readonly path: string; readonly name?: never; readonly params?: never }
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
/** Router destination supported by the UI-kit boundary. */
|
|
43
|
+
export type LinkRouterTarget = string | LinkRouterLocation
|
|
44
|
+
|
|
45
|
+
export const LINK_BROWSING_TARGETS = ['_self', '_blank', '_parent', '_top'] as const
|
|
46
|
+
|
|
47
|
+
/** Допустимый browsing-context target ссылки. / Supported link browsing-context target. */
|
|
48
|
+
export type LinkBrowsingTarget = typeof LINK_BROWSING_TARGETS[number]
|
|
49
|
+
|
|
50
|
+
interface LinkDestinationOptions {
|
|
51
|
+
readonly target?: LinkBrowsingTarget
|
|
52
|
+
readonly rel?: string
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Native href destination; router target is structurally forbidden. */
|
|
56
|
+
export type HrefLinkDestination = LinkDestinationOptions & {
|
|
57
|
+
readonly href: string
|
|
58
|
+
readonly to?: never
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Explicit named/path router destination; native href is structurally forbidden. */
|
|
62
|
+
export type RouterLinkDestination = LinkDestinationOptions & {
|
|
63
|
+
readonly href?: never
|
|
64
|
+
readonly to: LinkRouterTarget
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Строгий XOR-контракт обычного destination. / Strict XOR contract for an ordinary destination. */
|
|
68
|
+
export type LinkDestination = HrefLinkDestination | RouterLinkDestination
|
|
69
|
+
|
|
70
|
+
/** Canonical presentation/state API owned by SLink. */
|
|
71
|
+
export interface LinkPresentationProps {
|
|
72
|
+
readonly variant?: 'default' | 'subtle' | 'brand' | 'navigation' | 'bottom-navigation' | 'document'
|
|
73
|
+
readonly external?: boolean
|
|
74
|
+
readonly active?: boolean
|
|
75
|
+
readonly disabled?: boolean
|
|
76
|
+
readonly width?: 'fit' | 'full'
|
|
77
|
+
readonly size?: 'inherit' | 'sm' | 'md'
|
|
78
|
+
readonly underline?: 'hover' | 'always' | 'none'
|
|
79
|
+
readonly wrap?: 'normal' | 'anywhere'
|
|
80
|
+
readonly contentKind?: 'text' | 'identifier'
|
|
81
|
+
readonly leadingIcon?: Component
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Полный discriminated public props contract SLink. / Complete discriminated public SLink props contract. */
|
|
85
|
+
export type SLinkProps = LinkDestination & LinkPresentationProps
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import {
|
|
3
|
+
computed,
|
|
4
|
+
type VNodeChild,
|
|
5
|
+
} from 'vue'
|
|
6
|
+
import { RouterLink } from 'vue-router'
|
|
7
|
+
import { resolveLinkTarget } from '../../../internal/linkTarget'
|
|
8
|
+
import { useOwnedAttrs } from '../../../internal/ownedAttrs'
|
|
9
|
+
import {
|
|
10
|
+
PassiveContentBoundary,
|
|
11
|
+
useInteractiveLeafRegistration,
|
|
12
|
+
type PassiveContentContract,
|
|
13
|
+
} from '../../../internal/passiveContentContract'
|
|
14
|
+
import type {
|
|
15
|
+
SLinkProps,
|
|
16
|
+
} from './SLink'
|
|
17
|
+
|
|
18
|
+
defineOptions({ inheritAttrs: false })
|
|
19
|
+
|
|
20
|
+
/** Exact href/to XOR plus the canonical link presentation API. */
|
|
21
|
+
export type Props = SLinkProps
|
|
22
|
+
|
|
23
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
24
|
+
href: undefined,
|
|
25
|
+
to: undefined,
|
|
26
|
+
target: '_self',
|
|
27
|
+
rel: undefined,
|
|
28
|
+
variant: 'default',
|
|
29
|
+
external: false,
|
|
30
|
+
active: false,
|
|
31
|
+
disabled: false,
|
|
32
|
+
width: 'fit',
|
|
33
|
+
size: 'inherit',
|
|
34
|
+
underline: 'hover',
|
|
35
|
+
wrap: 'normal',
|
|
36
|
+
contentKind: 'text',
|
|
37
|
+
leadingIcon: undefined,
|
|
38
|
+
})
|
|
39
|
+
const emit = defineEmits<{
|
|
40
|
+
click: [event: MouseEvent]
|
|
41
|
+
}>()
|
|
42
|
+
const ownedAttrs = useOwnedAttrs({ component: 'SLink', owner: 'native navigation link' })
|
|
43
|
+
const slots = defineSlots<{
|
|
44
|
+
/** Пассивный текст/inline-контент ссылки. / Passive text or inline link content. */
|
|
45
|
+
default?: () => VNodeChild
|
|
46
|
+
/** Пассивная trailing presentation, например badge. / Passive trailing presentation such as a badge. */
|
|
47
|
+
trailing?: () => VNodeChild
|
|
48
|
+
}>()
|
|
49
|
+
useInteractiveLeafRegistration({ owner: 'SLink' })
|
|
50
|
+
const passiveContentContract: PassiveContentContract = {
|
|
51
|
+
owner: 'SLink',
|
|
52
|
+
region: 'passive link content',
|
|
53
|
+
guidance: 'Вынесите control за границы ссылки. / Move the control outside the link.',
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const resolvedTarget = computed(() => resolveLinkTarget({
|
|
57
|
+
href: props.href,
|
|
58
|
+
to: props.to,
|
|
59
|
+
target: props.target,
|
|
60
|
+
rel: props.rel,
|
|
61
|
+
owner: 'SLink',
|
|
62
|
+
}))
|
|
63
|
+
|
|
64
|
+
const linkComponent = computed(() => {
|
|
65
|
+
if (props.disabled) return 'a'
|
|
66
|
+
return resolvedTarget.value.kind === 'router' ? RouterLink : 'a'
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
const linkBindings = () => ({
|
|
70
|
+
...ownedAttrs.bindings(),
|
|
71
|
+
...(props.disabled ? {} : resolvedTarget.value.bindings),
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
function handleClick(event: MouseEvent): void {
|
|
75
|
+
if (props.disabled) {
|
|
76
|
+
event.preventDefault()
|
|
77
|
+
event.stopImmediatePropagation()
|
|
78
|
+
return
|
|
79
|
+
}
|
|
80
|
+
emit('click', event)
|
|
81
|
+
}
|
|
82
|
+
</script>
|
|
83
|
+
|
|
84
|
+
<template>
|
|
85
|
+
<component
|
|
86
|
+
:is="linkComponent"
|
|
87
|
+
v-bind="linkBindings()"
|
|
88
|
+
class="s-link"
|
|
89
|
+
:data-variant="variant"
|
|
90
|
+
:data-external="external || undefined"
|
|
91
|
+
:data-active="active || undefined"
|
|
92
|
+
:data-disabled="disabled || undefined"
|
|
93
|
+
:data-width="width"
|
|
94
|
+
:data-size="size"
|
|
95
|
+
:data-underline="underline"
|
|
96
|
+
:data-wrap="wrap"
|
|
97
|
+
:data-content-kind="contentKind"
|
|
98
|
+
:aria-current="active ? 'page' : undefined"
|
|
99
|
+
:aria-disabled="disabled || undefined"
|
|
100
|
+
:tabindex="disabled ? -1 : undefined"
|
|
101
|
+
:role="disabled ? 'link' : undefined"
|
|
102
|
+
@click="handleClick"
|
|
103
|
+
>
|
|
104
|
+
<span v-if="leadingIcon" class="s-link__icon" aria-hidden="true">
|
|
105
|
+
<component :is="leadingIcon" class="s-link__icon-graphic" />
|
|
106
|
+
</span>
|
|
107
|
+
<PassiveContentBoundary :contract="passiveContentContract">
|
|
108
|
+
<span class="s-link__content"><slot /></span>
|
|
109
|
+
<span v-if="slots.trailing" class="s-link__trailing"><slot name="trailing" /></span>
|
|
110
|
+
</PassiveContentBoundary>
|
|
111
|
+
</component>
|
|
112
|
+
</template>
|
|
113
|
+
|
|
114
|
+
<style lang="postcss" src="./SLink.css" scoped></style>
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
@reference "../../../styles/reference.css";
|
|
2
|
+
|
|
3
|
+
.s-listbox {
|
|
4
|
+
@apply m-0 list-none;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
.s-listbox[aria-disabled='true'] {
|
|
8
|
+
@apply opacity-60;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.s-listbox[data-invalid='true'] {
|
|
12
|
+
outline: var(--s-focus-ring-width) solid var(--s-selection-invalid-outline-color);
|
|
13
|
+
outline-offset: var(--s-focus-ring-inset-offset);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.s-listbox__empty {
|
|
17
|
+
@apply px-3 py-2 text-sm text-surface-500 dark:text-surface-400;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.s-listbox[data-variant='select'] {
|
|
21
|
+
@apply mt-1 max-h-60 w-full overflow-auto rounded-md py-1 text-base;
|
|
22
|
+
background: var(--s-floating-surface);
|
|
23
|
+
color: var(--s-floating-foreground);
|
|
24
|
+
box-shadow: 0 0 0 1px var(--s-floating-outline), var(--s-elevation-floating);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.s-listbox[data-variant='color'] {
|
|
28
|
+
@apply grid grid-cols-5 gap-2 rounded-2xl border border-surface-200 bg-surface-0 p-2;
|
|
29
|
+
@apply dark:border-surface-700 dark:bg-surface-900;
|
|
30
|
+
box-shadow: var(--s-elevation-overlay);
|
|
31
|
+
}
|