@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,152 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div
|
|
3
|
+
v-bind="ownedAttrs.bindings()"
|
|
4
|
+
class="s-message"
|
|
5
|
+
:data-severity="validatedSeverity"
|
|
6
|
+
:data-size="validatedSize"
|
|
7
|
+
:role="liveAttributes.role"
|
|
8
|
+
:aria-live="liveAttributes.ariaLive"
|
|
9
|
+
:aria-atomic="liveAttributes.ariaAtomic"
|
|
10
|
+
>
|
|
11
|
+
<div class="s-message-icon">
|
|
12
|
+
<component
|
|
13
|
+
:is="iconComponent"
|
|
14
|
+
class="s-message-icon-graphic"
|
|
15
|
+
aria-hidden="true"
|
|
16
|
+
/>
|
|
17
|
+
</div>
|
|
18
|
+
<div class="s-message-content">
|
|
19
|
+
<span v-if="title" class="s-message-title">{{ title }}</span>
|
|
20
|
+
<div class="s-message-body">
|
|
21
|
+
<slot />
|
|
22
|
+
</div>
|
|
23
|
+
</div>
|
|
24
|
+
<div v-if="dismissAction" class="s-message-close">
|
|
25
|
+
<SButton
|
|
26
|
+
label=""
|
|
27
|
+
size="sm"
|
|
28
|
+
appearance="ghost"
|
|
29
|
+
severity="secondary"
|
|
30
|
+
shape="circle"
|
|
31
|
+
icon-only
|
|
32
|
+
:leading-icon="X"
|
|
33
|
+
:title="dismissAction.label"
|
|
34
|
+
:aria-label="dismissAction.label"
|
|
35
|
+
:disabled="dismissAction.disabled"
|
|
36
|
+
@click="requestDismiss"
|
|
37
|
+
/>
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
</template>
|
|
41
|
+
|
|
42
|
+
<script setup lang="ts">
|
|
43
|
+
import { computed } from 'vue'
|
|
44
|
+
import { X } from '../../icons/sputnigUiIcons'
|
|
45
|
+
import { useOwnedAttrs } from '../../../internal/ownedAttrs'
|
|
46
|
+
import {
|
|
47
|
+
resolveFeedbackLive,
|
|
48
|
+
resolveFeedbackPresentation,
|
|
49
|
+
type FeedbackLiveMode,
|
|
50
|
+
type FeedbackSeverity,
|
|
51
|
+
} from '../../../internal/feedbackPresentation'
|
|
52
|
+
import SButton from '../controls/SButton.vue'
|
|
53
|
+
import {
|
|
54
|
+
validateBoolean,
|
|
55
|
+
validateExactString,
|
|
56
|
+
validateNonEmptyString,
|
|
57
|
+
} from '../../../internal/runtimeContract'
|
|
58
|
+
|
|
59
|
+
defineOptions({ inheritAttrs: false })
|
|
60
|
+
|
|
61
|
+
export type Severity = FeedbackSeverity
|
|
62
|
+
|
|
63
|
+
/** Семантический размер message surface. / Semantic message-surface size. */
|
|
64
|
+
export type SMessageSize = 'sm' | 'md'
|
|
65
|
+
|
|
66
|
+
/** Управляемое dismiss-действие, которое только отправляет intent родителю. / Controlled dismiss action that only emits intent to the parent. */
|
|
67
|
+
export interface SMessageDismissAction {
|
|
68
|
+
label: string
|
|
69
|
+
disabled: boolean
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Typed payload управляемого dismiss intent. / Typed payload of a controlled dismiss intent. */
|
|
73
|
+
export interface SMessageDismissEvent {
|
|
74
|
+
source: 'close-button'
|
|
75
|
+
nativeEvent: MouseEvent
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const MESSAGE_SEVERITIES = ['info', 'success', 'warn', 'danger'] as const satisfies readonly Severity[]
|
|
79
|
+
const MESSAGE_LIVE_MODES = ['off', 'polite', 'assertive', 'auto'] as const satisfies readonly FeedbackLiveMode[]
|
|
80
|
+
const MESSAGE_SIZES = ['sm', 'md'] as const satisfies readonly SMessageSize[]
|
|
81
|
+
const DEFAULT_DISMISS_LABEL = 'Закрыть'
|
|
82
|
+
|
|
83
|
+
export interface Props {
|
|
84
|
+
severity?: Severity
|
|
85
|
+
title?: string
|
|
86
|
+
closable?: boolean
|
|
87
|
+
dismissLabel?: string
|
|
88
|
+
dismissDisabled?: boolean
|
|
89
|
+
live?: FeedbackLiveMode
|
|
90
|
+
/** Семантический размер и padding сообщения. / Semantic message size and padding. */
|
|
91
|
+
size?: SMessageSize
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
95
|
+
severity: 'info',
|
|
96
|
+
title: undefined,
|
|
97
|
+
closable: false,
|
|
98
|
+
dismissLabel: DEFAULT_DISMISS_LABEL,
|
|
99
|
+
dismissDisabled: false,
|
|
100
|
+
live: 'off',
|
|
101
|
+
size: 'md',
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
const emit = defineEmits<{
|
|
105
|
+
dismiss: [event: SMessageDismissEvent]
|
|
106
|
+
}>()
|
|
107
|
+
const ownedAttrs = useOwnedAttrs({ component: 'SMessage', owner: 'message root' })
|
|
108
|
+
|
|
109
|
+
const validatedSeverity = computed<Severity>(() => validateExactString(
|
|
110
|
+
'SMessage',
|
|
111
|
+
'severity',
|
|
112
|
+
props.severity,
|
|
113
|
+
MESSAGE_SEVERITIES,
|
|
114
|
+
))
|
|
115
|
+
const validatedLive = computed<FeedbackLiveMode>(() => validateExactString(
|
|
116
|
+
'SMessage',
|
|
117
|
+
'live',
|
|
118
|
+
props.live,
|
|
119
|
+
MESSAGE_LIVE_MODES,
|
|
120
|
+
))
|
|
121
|
+
const validatedSize = computed<SMessageSize>(() => validateExactString(
|
|
122
|
+
'SMessage',
|
|
123
|
+
'size',
|
|
124
|
+
props.size,
|
|
125
|
+
MESSAGE_SIZES,
|
|
126
|
+
))
|
|
127
|
+
const dismissAction = computed<SMessageDismissAction | undefined>(() => {
|
|
128
|
+
const closable = validateBoolean('SMessage', 'closable', props.closable)
|
|
129
|
+
const disabled = validateBoolean('SMessage', 'dismissDisabled', props.dismissDisabled)
|
|
130
|
+
const label = validateNonEmptyString('SMessage', 'dismissLabel', props.dismissLabel)
|
|
131
|
+
if (!closable) {
|
|
132
|
+
if (disabled || label !== DEFAULT_DISMISS_LABEL) {
|
|
133
|
+
throw new TypeError(
|
|
134
|
+
'SMessage: dismissLabel/dismissDisabled применяются только при closable=true. '
|
|
135
|
+
+ '/ SMessage: dismissLabel/dismissDisabled apply only when closable=true.',
|
|
136
|
+
)
|
|
137
|
+
}
|
|
138
|
+
return undefined
|
|
139
|
+
}
|
|
140
|
+
return { label, disabled }
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
const presentation = computed(() => resolveFeedbackPresentation(validatedSeverity.value))
|
|
144
|
+
const iconComponent = computed(() => presentation.value.icon)
|
|
145
|
+
const liveAttributes = computed(() => resolveFeedbackLive(validatedSeverity.value, validatedLive.value))
|
|
146
|
+
|
|
147
|
+
function requestDismiss(nativeEvent: MouseEvent): void {
|
|
148
|
+
emit('dismiss', { source: 'close-button', nativeEvent })
|
|
149
|
+
}
|
|
150
|
+
</script>
|
|
151
|
+
|
|
152
|
+
<style lang="postcss" src="./SMessage.css" scoped></style>
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
@reference "../../../styles/reference.css";
|
|
2
|
+
|
|
3
|
+
.s-metric-card__layout { @apply flex min-h-0 min-w-0 w-full flex-1 flex-col gap-3 text-left; }
|
|
4
|
+
.s-metric-card__layout[data-centered='true'] { @apply items-center justify-center text-center; }
|
|
5
|
+
.s-metric-card__icon { @apply shrink-0 text-primary-500; }
|
|
6
|
+
.s-metric-card__icon-glyph { @apply h-8 w-8; }
|
|
7
|
+
.s-metric-card__body { @apply min-w-0; }
|
|
8
|
+
.s-metric-card__heading { @apply flex min-w-0 items-start justify-between gap-3; }
|
|
9
|
+
.s-metric-card__layout[data-centered='true'] .s-metric-card__heading { @apply justify-center; }
|
|
10
|
+
.s-metric-card__title { @apply min-w-0 text-xs font-medium uppercase tracking-[0.18em] text-surface-500 dark:text-surface-400; }
|
|
11
|
+
.s-metric-card__value { @apply mt-2 min-w-0 font-semibold tracking-tight text-surface-900 dark:text-surface-50; }
|
|
12
|
+
.s-metric-card__value[data-size='md'] { @apply text-2xl; }
|
|
13
|
+
.s-metric-card__value[data-size='lg'] { @apply text-[1.9rem] leading-tight; }
|
|
14
|
+
.s-metric-card__value[data-size='xl'] { @apply text-[2.2rem] leading-none; }
|
|
15
|
+
.s-metric-card__description { @apply mt-2 min-w-0 text-sm text-surface-500 dark:text-surface-400; }
|
|
16
|
+
.s-metric-card__value[data-overflow='wrap'], .s-metric-card__description[data-overflow='wrap'] {
|
|
17
|
+
@apply whitespace-normal;
|
|
18
|
+
overflow-wrap: anywhere;
|
|
19
|
+
}
|
|
20
|
+
.s-metric-card__value[data-overflow='clamp-2'], .s-metric-card__description[data-overflow='clamp-2'] {
|
|
21
|
+
@apply min-w-0 overflow-hidden;
|
|
22
|
+
display: -webkit-box;
|
|
23
|
+
-webkit-box-orient: vertical;
|
|
24
|
+
-webkit-line-clamp: 2;
|
|
25
|
+
overflow-wrap: anywhere;
|
|
26
|
+
word-break: break-word;
|
|
27
|
+
}
|
|
28
|
+
.s-metric-card__value[data-overflow='truncate'], .s-metric-card__description[data-overflow='truncate'] {
|
|
29
|
+
@apply min-w-0 overflow-hidden text-ellipsis whitespace-nowrap;
|
|
30
|
+
}
|
|
31
|
+
.s-metric-card__progress { @apply mt-auto space-y-2; }
|
|
32
|
+
.s-metric-card__progress-label { @apply text-xs text-surface-500; }
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, watchEffect } from 'vue'
|
|
3
|
+
import type { Component } from 'vue'
|
|
4
|
+
import { useOwnedAttrs } from '../../../internal/ownedAttrs'
|
|
5
|
+
import type { InteractiveSurfaceContract, InteractiveSurfacePadding, InteractiveSurfaceRadius, InteractiveSurfaceTone } from '../controls/SInteractiveSurface.vue'
|
|
6
|
+
import SActionCard from './SActionCard.vue'
|
|
7
|
+
import SBadge from './SBadge.vue'
|
|
8
|
+
import type { BadgeSeverity } from './SBadge.vue'
|
|
9
|
+
import SProgressBar from './SProgressBar.vue'
|
|
10
|
+
|
|
11
|
+
defineOptions({ inheritAttrs: false })
|
|
12
|
+
|
|
13
|
+
export type MetricOverflow = 'wrap' | 'clamp-2' | 'truncate'
|
|
14
|
+
export type MetricValueSize = 'md' | 'lg' | 'xl'
|
|
15
|
+
|
|
16
|
+
export interface Props {
|
|
17
|
+
title: string
|
|
18
|
+
value?: string | number
|
|
19
|
+
description?: string
|
|
20
|
+
descriptionTitle?: string
|
|
21
|
+
icon?: Component
|
|
22
|
+
centered?: boolean
|
|
23
|
+
density?: 'compact' | 'comfortable'
|
|
24
|
+
tone?: InteractiveSurfaceTone
|
|
25
|
+
surface?: InteractiveSurfaceContract
|
|
26
|
+
selected?: boolean
|
|
27
|
+
fill?: boolean
|
|
28
|
+
padding?: InteractiveSurfacePadding
|
|
29
|
+
radius?: InteractiveSurfaceRadius
|
|
30
|
+
valueSize?: MetricValueSize
|
|
31
|
+
valueOverflow?: MetricOverflow
|
|
32
|
+
descriptionOverflow?: MetricOverflow
|
|
33
|
+
badgeLabel?: string
|
|
34
|
+
badgeSeverity?: BadgeSeverity
|
|
35
|
+
progressValue?: number | null
|
|
36
|
+
progressLabel?: string
|
|
37
|
+
progressSeverity?: 'primary' | 'success' | 'info' | 'warn' | 'danger' | 'neutral'
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
41
|
+
value: undefined,
|
|
42
|
+
description: undefined,
|
|
43
|
+
descriptionTitle: undefined,
|
|
44
|
+
icon: undefined,
|
|
45
|
+
centered: false,
|
|
46
|
+
density: 'comfortable',
|
|
47
|
+
tone: 'default',
|
|
48
|
+
surface: undefined,
|
|
49
|
+
selected: false,
|
|
50
|
+
fill: false,
|
|
51
|
+
padding: undefined,
|
|
52
|
+
radius: 'xl',
|
|
53
|
+
valueSize: undefined,
|
|
54
|
+
valueOverflow: 'wrap',
|
|
55
|
+
descriptionOverflow: 'wrap',
|
|
56
|
+
badgeLabel: undefined,
|
|
57
|
+
badgeSeverity: 'secondary',
|
|
58
|
+
progressValue: null,
|
|
59
|
+
progressLabel: undefined,
|
|
60
|
+
progressSeverity: 'primary',
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
const emit = defineEmits<{ activate: [event: MouseEvent] }>()
|
|
64
|
+
const ownedAttrs = useOwnedAttrs({ component: 'SMetricCard', owner: 'metric card surface' })
|
|
65
|
+
const resolvedProgressValue = computed(() => props.progressValue)
|
|
66
|
+
const resolvedProgressLabel = computed(() => props.progressLabel ?? `${props.progressValue}%`)
|
|
67
|
+
const progressAccessibility = computed(() => ({
|
|
68
|
+
mode: 'label' as const,
|
|
69
|
+
label: `${props.title}: ${resolvedProgressLabel.value}`,
|
|
70
|
+
}))
|
|
71
|
+
const resolvedValueSize = computed(() => props.valueSize ?? (props.density === 'compact' ? 'md' : 'xl'))
|
|
72
|
+
const resolvedSurface = computed<InteractiveSurfaceContract>(() => props.surface ?? { mode: 'static' })
|
|
73
|
+
const resolvedPadding = computed<InteractiveSurfacePadding>(() => props.padding ?? (props.density === 'compact' ? 'sm' : 'md'))
|
|
74
|
+
|
|
75
|
+
watchEffect(() => {
|
|
76
|
+
if (props.progressValue === null) return
|
|
77
|
+
if (!Number.isFinite(props.progressValue) || props.progressValue < 0 || props.progressValue > 100) {
|
|
78
|
+
throw new RangeError('SMetricCard: progressValue должен быть конечным числом от 0 до 100')
|
|
79
|
+
}
|
|
80
|
+
})
|
|
81
|
+
</script>
|
|
82
|
+
|
|
83
|
+
<template>
|
|
84
|
+
<SActionCard
|
|
85
|
+
v-bind="ownedAttrs.bindings()"
|
|
86
|
+
:surface="resolvedSurface"
|
|
87
|
+
:selected="selected"
|
|
88
|
+
:tone="tone"
|
|
89
|
+
:density="density"
|
|
90
|
+
:padding="resolvedPadding"
|
|
91
|
+
:radius="radius"
|
|
92
|
+
:fill="fill ? 'container' : 'content'"
|
|
93
|
+
@activate="emit('activate', $event)"
|
|
94
|
+
>
|
|
95
|
+
<div class="s-metric-card__layout" :data-centered="centered || undefined">
|
|
96
|
+
<div v-if="icon || $slots.icon" class="s-metric-card__icon" aria-hidden="true">
|
|
97
|
+
<slot name="icon"><component :is="icon" class="s-metric-card__icon-glyph" /></slot>
|
|
98
|
+
</div>
|
|
99
|
+
<div class="s-metric-card__body">
|
|
100
|
+
<div class="s-metric-card__heading">
|
|
101
|
+
<div class="s-metric-card__title">{{ title }}</div>
|
|
102
|
+
<SBadge v-if="badgeLabel" :severity="badgeSeverity">{{ badgeLabel }}</SBadge>
|
|
103
|
+
</div>
|
|
104
|
+
<div v-if="value !== undefined" class="s-metric-card__value" :data-size="resolvedValueSize" :data-overflow="valueOverflow">{{ value }}</div>
|
|
105
|
+
<div v-if="description" class="s-metric-card__description" :data-overflow="descriptionOverflow" :title="descriptionTitle">{{ description }}</div>
|
|
106
|
+
</div>
|
|
107
|
+
<div v-if="resolvedProgressValue !== null" class="s-metric-card__progress">
|
|
108
|
+
<SProgressBar :accessibility="progressAccessibility" :value="resolvedProgressValue" :severity="progressSeverity" />
|
|
109
|
+
<div class="s-metric-card__progress-label">{{ resolvedProgressLabel }}</div>
|
|
110
|
+
</div>
|
|
111
|
+
<slot />
|
|
112
|
+
</div>
|
|
113
|
+
<template v-if="$slots.actions" #actions><slot name="actions" /></template>
|
|
114
|
+
</SActionCard>
|
|
115
|
+
</template>
|
|
116
|
+
|
|
117
|
+
<style lang="postcss" src="./SMetricCard.css" scoped></style>
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
@reference "../../../styles/reference.css";
|
|
2
|
+
|
|
3
|
+
.s-progress-bar {
|
|
4
|
+
@apply w-full h-4 rounded-full;
|
|
5
|
+
background-color: var(--s-progress-bar-track-surface);
|
|
6
|
+
@apply overflow-hidden relative;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.s-progress-bar[data-size='xs'] { @apply h-1.5; }
|
|
10
|
+
.s-progress-bar[data-size='sm'] { @apply h-2; }
|
|
11
|
+
|
|
12
|
+
.s-progress-bar-value {
|
|
13
|
+
@apply h-full rounded-full;
|
|
14
|
+
@apply transition-all ease-in-out;
|
|
15
|
+
transition-duration: var(--s-motion-deliberate);
|
|
16
|
+
@apply flex items-center justify-center;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.s-progress-bar-label {
|
|
20
|
+
color: var(--s-progress-bar-label-foreground);
|
|
21
|
+
@apply text-xs font-semibold;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/* Severity Colors */
|
|
25
|
+
.s-progress-bar-value[data-severity='primary'] {
|
|
26
|
+
background-color: var(--s-progress-bar-fill-primary);
|
|
27
|
+
}
|
|
28
|
+
.s-progress-bar-value[data-severity='success'] {
|
|
29
|
+
background-color: var(--s-progress-bar-fill-success);
|
|
30
|
+
}
|
|
31
|
+
.s-progress-bar-value[data-severity='info'] {
|
|
32
|
+
background-color: var(--s-progress-bar-fill-info);
|
|
33
|
+
}
|
|
34
|
+
.s-progress-bar-value[data-severity='warn'] {
|
|
35
|
+
background-color: var(--s-progress-bar-fill-warn);
|
|
36
|
+
}
|
|
37
|
+
.s-progress-bar-value[data-severity='danger'] {
|
|
38
|
+
background-color: var(--s-progress-bar-fill-danger);
|
|
39
|
+
}
|
|
40
|
+
.s-progress-bar-value[data-severity='neutral'] {
|
|
41
|
+
background-color: var(--s-progress-bar-fill-neutral);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/* --- Indeterminate Mode --- */
|
|
45
|
+
.s-progress-bar-value--indeterminate {
|
|
46
|
+
@apply absolute h-full w-full top-0 left-0;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.s-progress-bar-value--indeterminate::before {
|
|
50
|
+
content: '';
|
|
51
|
+
@apply absolute h-full top-0;
|
|
52
|
+
width: 40%;
|
|
53
|
+
border-radius: var(--s-radius-pill);
|
|
54
|
+
animation: s-progress-bar-indeterminate-anim 2s infinite ease-in-out;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
@keyframes s-progress-bar-indeterminate-anim {
|
|
58
|
+
0% {
|
|
59
|
+
left: -50%;
|
|
60
|
+
}
|
|
61
|
+
100% {
|
|
62
|
+
left: 100%;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/* Indeterminate Severity Colors */
|
|
67
|
+
.s-progress-bar-value--indeterminate[data-severity='primary']::before {
|
|
68
|
+
background-color: var(--s-progress-bar-fill-primary);
|
|
69
|
+
}
|
|
70
|
+
.s-progress-bar-value--indeterminate[data-severity='success']::before {
|
|
71
|
+
background-color: var(--s-progress-bar-fill-success);
|
|
72
|
+
}
|
|
73
|
+
.s-progress-bar-value--indeterminate[data-severity='info']::before {
|
|
74
|
+
background-color: var(--s-progress-bar-fill-info);
|
|
75
|
+
}
|
|
76
|
+
.s-progress-bar-value--indeterminate[data-severity='warn']::before {
|
|
77
|
+
background-color: var(--s-progress-bar-fill-warn);
|
|
78
|
+
}
|
|
79
|
+
.s-progress-bar-value--indeterminate[data-severity='danger']::before {
|
|
80
|
+
background-color: var(--s-progress-bar-fill-danger);
|
|
81
|
+
}
|
|
82
|
+
.s-progress-bar-value--indeterminate[data-severity='neutral']::before {
|
|
83
|
+
background-color: var(--s-progress-bar-indeterminate-neutral-fill);
|
|
84
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div
|
|
3
|
+
v-bind="ownedAttrs.bindings()"
|
|
4
|
+
class="s-progress-bar"
|
|
5
|
+
:class="{ 's-progress-bar--indeterminate': mode === 'indeterminate' }"
|
|
6
|
+
:role="accessibility.mode === 'decorative' ? undefined : 'progressbar'"
|
|
7
|
+
:aria-label="accessibility.mode === 'label' ? accessibility.label : undefined"
|
|
8
|
+
:aria-labelledby="accessibility.mode === 'labelledby' ? accessibility.id : undefined"
|
|
9
|
+
:aria-hidden="accessibility.mode === 'decorative' || undefined"
|
|
10
|
+
:aria-valuenow="accessibility.mode !== 'decorative' && mode === 'determinate' ? value : undefined"
|
|
11
|
+
:data-size="size"
|
|
12
|
+
:aria-valuemin="accessibility.mode === 'decorative' ? undefined : 0"
|
|
13
|
+
:aria-valuemax="accessibility.mode === 'decorative' ? undefined : 100"
|
|
14
|
+
>
|
|
15
|
+
<div
|
|
16
|
+
v-if="mode === 'determinate'"
|
|
17
|
+
class="s-progress-bar-value"
|
|
18
|
+
:data-severity="severity"
|
|
19
|
+
:style="{ width: `${value}%` }"
|
|
20
|
+
>
|
|
21
|
+
<span v-if="showValue" class="s-progress-bar-label">
|
|
22
|
+
<slot :value="value">{{ value }}%</slot>
|
|
23
|
+
</span>
|
|
24
|
+
</div>
|
|
25
|
+
<div
|
|
26
|
+
v-else
|
|
27
|
+
class="s-progress-bar-value--indeterminate"
|
|
28
|
+
:data-severity="severity"
|
|
29
|
+
></div>
|
|
30
|
+
</div>
|
|
31
|
+
</template>
|
|
32
|
+
|
|
33
|
+
<script setup lang="ts">
|
|
34
|
+
import { watchEffect } from 'vue';
|
|
35
|
+
import { useOwnedAttrs } from '../../../internal/ownedAttrs';
|
|
36
|
+
|
|
37
|
+
defineOptions({ inheritAttrs: false });
|
|
38
|
+
|
|
39
|
+
export type Severity = 'primary' | 'success' | 'info' | 'warn' | 'danger' | 'neutral';
|
|
40
|
+
export type ProgressBarMode = 'determinate' | 'indeterminate';
|
|
41
|
+
export type SProgressBarAccessibility =
|
|
42
|
+
| { readonly mode: 'decorative' }
|
|
43
|
+
| { readonly mode: 'label'; readonly label: string }
|
|
44
|
+
| { readonly mode: 'labelledby'; readonly id: string };
|
|
45
|
+
|
|
46
|
+
export interface Props {
|
|
47
|
+
/** Обязательное accessible-name решение либо явный decorative режим. / Required accessible-name decision or explicit decorative mode. */
|
|
48
|
+
accessibility: SProgressBarAccessibility;
|
|
49
|
+
value?: number;
|
|
50
|
+
severity?: Severity;
|
|
51
|
+
showValue?: boolean;
|
|
52
|
+
mode?: ProgressBarMode;
|
|
53
|
+
size?: 'xs' | 'sm' | 'md';
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
57
|
+
value: 0,
|
|
58
|
+
severity: 'primary',
|
|
59
|
+
showValue: false,
|
|
60
|
+
mode: 'determinate',
|
|
61
|
+
size: 'md',
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const ownedAttrs = useOwnedAttrs({ component: 'SProgressBar', owner: 'progressbar root' });
|
|
65
|
+
|
|
66
|
+
watchEffect(() => {
|
|
67
|
+
if (!Number.isFinite(props.value) || props.value < 0 || props.value > 100) {
|
|
68
|
+
throw new RangeError('SProgressBar: value должен быть конечным числом от 0 до 100');
|
|
69
|
+
}
|
|
70
|
+
if (!['determinate', 'indeterminate'].includes(props.mode)) {
|
|
71
|
+
throw new TypeError(`SProgressBar: неизвестный mode "${String(props.mode)}"`);
|
|
72
|
+
}
|
|
73
|
+
if (!['primary', 'success', 'info', 'warn', 'danger', 'neutral'].includes(props.severity)) {
|
|
74
|
+
throw new TypeError(`SProgressBar: неизвестный severity "${String(props.severity)}"`);
|
|
75
|
+
}
|
|
76
|
+
if (!['xs', 'sm', 'md'].includes(props.size)) {
|
|
77
|
+
throw new TypeError(`SProgressBar: неизвестный size "${String(props.size)}"`);
|
|
78
|
+
}
|
|
79
|
+
if (props.showValue && props.mode !== 'determinate') {
|
|
80
|
+
throw new Error('SProgressBar: showValue разрешён только в determinate mode');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const contract: unknown = props.accessibility;
|
|
84
|
+
if (typeof contract !== 'object' || contract === null || Array.isArray(contract)) {
|
|
85
|
+
throw new TypeError('SProgressBar: accessibility должен быть discriminated object');
|
|
86
|
+
}
|
|
87
|
+
const record = contract as Record<string, unknown>;
|
|
88
|
+
const allowedKeys = record.mode === 'label'
|
|
89
|
+
? ['mode', 'label']
|
|
90
|
+
: record.mode === 'labelledby'
|
|
91
|
+
? ['mode', 'id']
|
|
92
|
+
: ['mode'];
|
|
93
|
+
const unexpected = Object.keys(record).filter((key) => !allowedKeys.includes(key));
|
|
94
|
+
if (unexpected.length > 0) {
|
|
95
|
+
throw new Error(`SProgressBar: accessibility.mode="${String(record.mode)}" не допускает поля ${unexpected.join(', ')}`);
|
|
96
|
+
}
|
|
97
|
+
if (record.mode === 'decorative') return;
|
|
98
|
+
if (record.mode === 'label') {
|
|
99
|
+
if (typeof record.label !== 'string' || record.label.trim().length === 0) {
|
|
100
|
+
throw new TypeError('SProgressBar: accessibility.label должен быть непустой строкой');
|
|
101
|
+
}
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
if (record.mode === 'labelledby') {
|
|
105
|
+
if (typeof record.id !== 'string' || record.id.trim().length === 0 || /\s/u.test(record.id)) {
|
|
106
|
+
throw new TypeError('SProgressBar: accessibility.id должен быть одним непустым DOM id');
|
|
107
|
+
}
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
throw new TypeError(`SProgressBar: неизвестный accessibility.mode "${String(record.mode)}"`);
|
|
111
|
+
});
|
|
112
|
+
</script>
|
|
113
|
+
|
|
114
|
+
<style lang="postcss" src="./SProgressBar.css" scoped></style>
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
@reference "../../../styles/reference.css";
|
|
2
|
+
|
|
3
|
+
.s-progress-indicator {
|
|
4
|
+
display: inline-flex;
|
|
5
|
+
flex: 0 0 auto;
|
|
6
|
+
align-items: center;
|
|
7
|
+
justify-content: center;
|
|
8
|
+
width: var(--s-progress-indicator-size);
|
|
9
|
+
height: var(--s-progress-indicator-size);
|
|
10
|
+
color: currentColor;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.s-progress-indicator[data-show-label='true'] {
|
|
14
|
+
width: auto;
|
|
15
|
+
max-width: 100%;
|
|
16
|
+
height: auto;
|
|
17
|
+
gap: var(--spacing-2);
|
|
18
|
+
justify-content: flex-start;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.s-progress-indicator[data-size='xs'] { --s-progress-indicator-size: var(--s-icon-size-xs); }
|
|
22
|
+
.s-progress-indicator[data-size='sm'] { --s-progress-indicator-size: var(--s-icon-size-sm); }
|
|
23
|
+
.s-progress-indicator[data-size='md'] { --s-progress-indicator-size: var(--s-icon-size-md); }
|
|
24
|
+
.s-progress-indicator[data-size='lg'] { --s-progress-indicator-size: var(--s-icon-size-lg); }
|
|
25
|
+
|
|
26
|
+
.s-progress-indicator[data-tone='neutral'] { color: var(--s-progress-indicator-foreground-neutral); }
|
|
27
|
+
.s-progress-indicator[data-tone='info'] { color: var(--s-progress-indicator-foreground-info); }
|
|
28
|
+
.s-progress-indicator[data-tone='success'] { color: var(--s-progress-indicator-foreground-success); }
|
|
29
|
+
.s-progress-indicator[data-tone='warn'] { color: var(--s-progress-indicator-foreground-warn); }
|
|
30
|
+
.s-progress-indicator[data-tone='danger'] { color: var(--s-progress-indicator-foreground-danger); }
|
|
31
|
+
|
|
32
|
+
.s-progress-indicator__graphic {
|
|
33
|
+
width: 100%;
|
|
34
|
+
height: 100%;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.s-progress-indicator[data-show-label='true'] .s-progress-indicator__graphic {
|
|
38
|
+
flex: 0 0 var(--s-progress-indicator-size);
|
|
39
|
+
width: var(--s-progress-indicator-size);
|
|
40
|
+
height: var(--s-progress-indicator-size);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.s-progress-indicator__label {
|
|
44
|
+
min-width: 0;
|
|
45
|
+
overflow-wrap: anywhere;
|
|
46
|
+
color: currentColor;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.s-progress-indicator[data-motion='spin'] .s-progress-indicator__graphic {
|
|
50
|
+
animation: s-progress-indicator-spin calc(var(--s-motion-deliberate) * 4) linear infinite;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
@keyframes s-progress-indicator-spin {
|
|
54
|
+
to { transform: rotate(360deg); }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
@media (prefers-reduced-motion: reduce) {
|
|
58
|
+
.s-progress-indicator__graphic { animation: none; }
|
|
59
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<span
|
|
3
|
+
v-bind="ownedAttrs.bindings()"
|
|
4
|
+
class="s-progress-indicator"
|
|
5
|
+
:data-size="size"
|
|
6
|
+
:data-tone="tone"
|
|
7
|
+
:data-motion="presentation.motion"
|
|
8
|
+
:data-show-label="validatedShowLabel || undefined"
|
|
9
|
+
:role="liveAttributes.role"
|
|
10
|
+
:aria-live="liveAttributes.ariaLive"
|
|
11
|
+
:aria-atomic="liveAttributes.ariaAtomic"
|
|
12
|
+
:aria-label="accessibleLabel"
|
|
13
|
+
:aria-hidden="accessibility.mode === 'decorative' || undefined"
|
|
14
|
+
>
|
|
15
|
+
<component
|
|
16
|
+
:is="presentation.icon"
|
|
17
|
+
class="s-progress-indicator__graphic"
|
|
18
|
+
:data-sputnig-ui-icon="presentation.iconName"
|
|
19
|
+
data-sputnig-ui-icon-color-mode="auto"
|
|
20
|
+
data-sputnig-ui-icon-set="classic"
|
|
21
|
+
/>
|
|
22
|
+
<span v-if="validatedShowLabel && accessibility.mode === 'status'" class="s-progress-indicator__label">
|
|
23
|
+
{{ accessibility.label }}
|
|
24
|
+
</span>
|
|
25
|
+
</span>
|
|
26
|
+
</template>
|
|
27
|
+
|
|
28
|
+
<script setup lang="ts">
|
|
29
|
+
import { computed, watchEffect } from 'vue'
|
|
30
|
+
|
|
31
|
+
import { resolveFeedbackIndicator, resolveFeedbackLive, type FeedbackLiveMode } from '../../../internal/feedbackPresentation'
|
|
32
|
+
import { useOwnedAttrs } from '../../../internal/ownedAttrs'
|
|
33
|
+
import { validateBoolean } from '../../../internal/runtimeContract'
|
|
34
|
+
|
|
35
|
+
defineOptions({ inheritAttrs: false })
|
|
36
|
+
|
|
37
|
+
export type SProgressIndicatorSize = 'xs' | 'sm' | 'md' | 'lg'
|
|
38
|
+
export type SProgressIndicatorTone = 'current' | 'neutral' | 'info' | 'success' | 'warn' | 'danger'
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Декоративный progress принадлежит labelled parent; standalone status требует собственного имени.
|
|
42
|
+
* Decorative progress belongs to a labelled parent; a standalone status requires its own name.
|
|
43
|
+
*/
|
|
44
|
+
export type SProgressIndicatorAccessibility =
|
|
45
|
+
| { readonly mode: 'decorative' }
|
|
46
|
+
| {
|
|
47
|
+
readonly mode: 'status'
|
|
48
|
+
readonly label: string
|
|
49
|
+
readonly live?: Exclude<FeedbackLiveMode, 'off'>
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface Props {
|
|
53
|
+
accessibility: SProgressIndicatorAccessibility
|
|
54
|
+
size?: SProgressIndicatorSize
|
|
55
|
+
tone?: SProgressIndicatorTone
|
|
56
|
+
/** Показывает status label рядом с индикатором. / Shows the status label next to the indicator. */
|
|
57
|
+
showLabel?: boolean
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
61
|
+
size: 'sm',
|
|
62
|
+
tone: 'current',
|
|
63
|
+
showLabel: false,
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
const ownedAttrs = useOwnedAttrs({ component: 'SProgressIndicator', owner: 'progress indicator root' })
|
|
67
|
+
const presentation = resolveFeedbackIndicator('progress')
|
|
68
|
+
const validatedShowLabel = computed(() => validateBoolean('SProgressIndicator', 'showLabel', props.showLabel))
|
|
69
|
+
const feedbackTone = computed(() => props.tone === 'current' ? 'info' : props.tone)
|
|
70
|
+
const accessibleLabel = computed(() => props.accessibility.mode === 'status'
|
|
71
|
+
? props.accessibility.label
|
|
72
|
+
: undefined)
|
|
73
|
+
const liveAttributes = computed(() => props.accessibility.mode === 'status'
|
|
74
|
+
? resolveFeedbackLive(feedbackTone.value, props.accessibility.live ?? 'auto')
|
|
75
|
+
: {})
|
|
76
|
+
|
|
77
|
+
watchEffect(() => {
|
|
78
|
+
const contract: unknown = props.accessibility
|
|
79
|
+
if (typeof contract !== 'object' || contract === null || Array.isArray(contract)) {
|
|
80
|
+
throw new TypeError('SProgressIndicator: accessibility должен быть discriminated object')
|
|
81
|
+
}
|
|
82
|
+
const record = contract as Record<string, unknown>
|
|
83
|
+
if (validatedShowLabel.value && record.mode === 'decorative') {
|
|
84
|
+
throw new Error('SProgressIndicator: showLabel требует accessibility.mode="status" / showLabel requires accessibility.mode="status"')
|
|
85
|
+
}
|
|
86
|
+
const allowedKeys = record.mode === 'decorative' ? ['mode'] : ['mode', 'label', 'live']
|
|
87
|
+
const unexpected = Object.keys(record).filter((key) => !allowedKeys.includes(key))
|
|
88
|
+
if (unexpected.length > 0) {
|
|
89
|
+
throw new Error(`SProgressIndicator: accessibility.mode="${String(record.mode)}" не допускает поля ${unexpected.join(', ')}`)
|
|
90
|
+
}
|
|
91
|
+
if (record.mode === 'decorative') return
|
|
92
|
+
if (record.mode !== 'status') {
|
|
93
|
+
throw new TypeError(`SProgressIndicator: неизвестный accessibility.mode "${String(record.mode)}"`)
|
|
94
|
+
}
|
|
95
|
+
if (typeof record.label !== 'string' || record.label.trim().length === 0) {
|
|
96
|
+
throw new TypeError('SProgressIndicator: accessibility.label должен быть непустой строкой для mode="status"')
|
|
97
|
+
}
|
|
98
|
+
if (record.live !== undefined && !['auto', 'polite', 'assertive'].includes(String(record.live))) {
|
|
99
|
+
throw new TypeError(`SProgressIndicator: accessibility.live имеет недопустимое значение "${String(record.live)}"`)
|
|
100
|
+
}
|
|
101
|
+
})
|
|
102
|
+
</script>
|
|
103
|
+
|
|
104
|
+
<style lang="postcss" src="./SProgressIndicator.css" scoped></style>
|