@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,363 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<SPanel
|
|
3
|
+
v-bind="ownedAttrs.bindings()"
|
|
4
|
+
:title="validatedTitle || undefined"
|
|
5
|
+
:title-id="validatedTitle ? generatedTitleId : undefined"
|
|
6
|
+
:footer-id="hasStatus ? generatedStatusId : undefined"
|
|
7
|
+
:heading-level="3"
|
|
8
|
+
:state="panelState"
|
|
9
|
+
no-content-padding
|
|
10
|
+
content-layout="block"
|
|
11
|
+
scroll-behavior="child"
|
|
12
|
+
surface-radius="md"
|
|
13
|
+
surface-overflow="clip"
|
|
14
|
+
>
|
|
15
|
+
<template v-if="$slots.actions" #actions><slot name="actions" /></template>
|
|
16
|
+
|
|
17
|
+
<SCodeEditor
|
|
18
|
+
ref="codeEditor"
|
|
19
|
+
:model-value="modelValue"
|
|
20
|
+
:language="sqlLanguage"
|
|
21
|
+
:capabilities="editorCapabilities"
|
|
22
|
+
variant="sql"
|
|
23
|
+
:viewport="validatedViewport"
|
|
24
|
+
:autofocus="validatedAutofocus"
|
|
25
|
+
:placeholder="validatedPlaceholder"
|
|
26
|
+
:read-only="validatedReadOnly"
|
|
27
|
+
:disabled="validatedDisabled"
|
|
28
|
+
:aria-label="resolvedAccessibleName.ariaLabel"
|
|
29
|
+
:aria-labelledby="resolvedAccessibleName.ariaLabelledby"
|
|
30
|
+
:aria-describedby="hasStatus ? generatedStatusId : undefined"
|
|
31
|
+
:invalid="statusHasError"
|
|
32
|
+
:next-selection-pos="nextSelectionPos"
|
|
33
|
+
@update:model-value="onUpdate"
|
|
34
|
+
@cursor-activity="emitCursorActivity"
|
|
35
|
+
@selection-applied="emit('selection-applied', $event)"
|
|
36
|
+
@ready="emit('ready', $event)"
|
|
37
|
+
/>
|
|
38
|
+
|
|
39
|
+
<template v-if="hasStatus" #footer>
|
|
40
|
+
<slot name="status" :diagnostics="validatedDiagnostics" :error-message="validatedErrorMessage">
|
|
41
|
+
<SStatus
|
|
42
|
+
:label="statusLabel"
|
|
43
|
+
:tone="statusTone"
|
|
44
|
+
size="sm"
|
|
45
|
+
:live="validatedErrorMessage ? 'assertive' : 'polite'"
|
|
46
|
+
/>
|
|
47
|
+
</slot>
|
|
48
|
+
</template>
|
|
49
|
+
</SPanel>
|
|
50
|
+
</template>
|
|
51
|
+
|
|
52
|
+
<script setup lang="ts">
|
|
53
|
+
import { computed, ref, useId, useSlots } from 'vue'
|
|
54
|
+
import { useOwnedAttrs } from '../../../internal/ownedAttrs'
|
|
55
|
+
import { provideTrustedCodeEditorExtensions } from '../../../internal/codeEditorExtensions'
|
|
56
|
+
import { validateCodeEditorViewport } from '../../../internal/codeEditorContract'
|
|
57
|
+
import {
|
|
58
|
+
validateBoolean,
|
|
59
|
+
validateBoundedInteger,
|
|
60
|
+
validateDomId,
|
|
61
|
+
validateExactString,
|
|
62
|
+
validateNonEmptyString,
|
|
63
|
+
} from '../../../internal/runtimeContract'
|
|
64
|
+
import { autocompletion, type Completion, type CompletionContext } from '@codemirror/autocomplete'
|
|
65
|
+
import { linter, type Diagnostic as CodeMirrorDiagnostic } from '@codemirror/lint'
|
|
66
|
+
import { EditorState, type Extension } from '@codemirror/state'
|
|
67
|
+
import SCodeEditor, {
|
|
68
|
+
type SCodeEditorCapabilities,
|
|
69
|
+
type SCodeEditorCursorActivity,
|
|
70
|
+
type SCodeEditorLanguage,
|
|
71
|
+
type SCodeEditorReadyPayload,
|
|
72
|
+
type SCodeEditorSelectionAppliedPayload,
|
|
73
|
+
type SCodeEditorViewport,
|
|
74
|
+
} from '../data-display/SCodeEditor.vue'
|
|
75
|
+
import SPanel from '../containers/SPanel.vue'
|
|
76
|
+
import SStatus, { type StatusTone } from '../data-display/SStatus.vue'
|
|
77
|
+
|
|
78
|
+
defineOptions({ inheritAttrs: false })
|
|
79
|
+
|
|
80
|
+
interface SSqlEditorDiagnosticBase {
|
|
81
|
+
severity: 'error' | 'warning' | 'info'
|
|
82
|
+
message: string
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Exact SQL diagnostic range через offsets либо line/column. / Exact SQL diagnostic range expressed either as offsets or line/column. */
|
|
86
|
+
export type SSqlEditorDiagnostic = SSqlEditorDiagnosticBase & (
|
|
87
|
+
| { from: number; to?: number; line?: never; column?: never }
|
|
88
|
+
| { line: number; column: number; from?: never; to?: never }
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
/** Typed completion item для private SQL extension adapter. / Typed completion item transformed by the private SQL extension adapter. */
|
|
92
|
+
export interface SSqlEditorCompletionItem {
|
|
93
|
+
label: string
|
|
94
|
+
apply?: string
|
|
95
|
+
detail?: string
|
|
96
|
+
kind?: 'keyword' | 'schema' | 'table' | 'view' | 'column' | 'function'
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Public SQL-editor adapter contract; CodeMirror extensions остаются private. / Public SQL-editor adapter contract; CodeMirror extensions remain private. */
|
|
100
|
+
export interface Props {
|
|
101
|
+
modelValue: string
|
|
102
|
+
title?: string
|
|
103
|
+
placeholder?: string
|
|
104
|
+
readOnly?: boolean
|
|
105
|
+
disabled?: boolean
|
|
106
|
+
loading?: boolean
|
|
107
|
+
errorMessage?: string
|
|
108
|
+
diagnostics?: readonly SSqlEditorDiagnostic[]
|
|
109
|
+
completionItems?: readonly SSqlEditorCompletionItem[]
|
|
110
|
+
showLineNumbers?: boolean
|
|
111
|
+
autofocus?: boolean
|
|
112
|
+
viewport?: SCodeEditorViewport
|
|
113
|
+
tabSize?: number
|
|
114
|
+
ariaLabel?: string
|
|
115
|
+
ariaLabelledby?: string
|
|
116
|
+
nextSelectionPos?: number | null
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
120
|
+
title: undefined,
|
|
121
|
+
placeholder: 'select * from public.table_name;',
|
|
122
|
+
readOnly: false,
|
|
123
|
+
disabled: false,
|
|
124
|
+
loading: false,
|
|
125
|
+
errorMessage: '',
|
|
126
|
+
diagnostics: () => [],
|
|
127
|
+
completionItems: () => [],
|
|
128
|
+
showLineNumbers: true,
|
|
129
|
+
autofocus: false,
|
|
130
|
+
viewport: 'standard',
|
|
131
|
+
tabSize: 2,
|
|
132
|
+
ariaLabel: undefined,
|
|
133
|
+
ariaLabelledby: undefined,
|
|
134
|
+
nextSelectionPos: null,
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
const ownedAttrs = useOwnedAttrs({ component: 'SSqlEditor', owner: 'SQL editor adapter root' })
|
|
138
|
+
const slots = useSlots()
|
|
139
|
+
|
|
140
|
+
const emit = defineEmits<{
|
|
141
|
+
(e: 'update:modelValue', value: string): void
|
|
142
|
+
(e: 'cursor-activity', payload: { pos: number; line: number; column: number }): void
|
|
143
|
+
(e: 'selection-applied', payload: SCodeEditorSelectionAppliedPayload): void
|
|
144
|
+
(e: 'ready', payload: SCodeEditorReadyPayload): void
|
|
145
|
+
}>()
|
|
146
|
+
|
|
147
|
+
const codeEditor = ref<InstanceType<typeof SCodeEditor> | null>(null)
|
|
148
|
+
|
|
149
|
+
const validatedReadOnly = computed(() => validateBoolean('SSqlEditor', 'readOnly', props.readOnly))
|
|
150
|
+
const validatedDisabled = computed(() => validateBoolean('SSqlEditor', 'disabled', props.disabled))
|
|
151
|
+
const validatedLoading = computed(() => validateBoolean('SSqlEditor', 'loading', props.loading))
|
|
152
|
+
const validatedAutofocus = computed(() => validateBoolean('SSqlEditor', 'autofocus', props.autofocus))
|
|
153
|
+
const validatedShowLineNumbers = computed(() => validateBoolean('SSqlEditor', 'showLineNumbers', props.showLineNumbers))
|
|
154
|
+
const validatedViewport = computed(() => validateCodeEditorViewport('SSqlEditor', props.viewport))
|
|
155
|
+
const validatedTitle = computed(() => props.title === undefined ? '' : validateNonEmptyString('SSqlEditor', 'title', props.title))
|
|
156
|
+
const validatedPlaceholder = computed(() => validateNonEmptyString('SSqlEditor', 'placeholder', props.placeholder))
|
|
157
|
+
const validatedErrorMessage = computed(() => {
|
|
158
|
+
if (typeof props.errorMessage !== 'string') {
|
|
159
|
+
throw new TypeError('SSqlEditor: errorMessage должен быть строкой. / SSqlEditor: errorMessage must be a string.')
|
|
160
|
+
}
|
|
161
|
+
return props.errorMessage === '' ? '' : validateNonEmptyString('SSqlEditor', 'errorMessage', props.errorMessage)
|
|
162
|
+
})
|
|
163
|
+
const resolvedAccessibleName = computed(() => {
|
|
164
|
+
if (validatedTitle.value !== '') {
|
|
165
|
+
if (props.ariaLabel !== undefined || props.ariaLabelledby !== undefined) {
|
|
166
|
+
throw new TypeError(
|
|
167
|
+
'SSqlEditor: title уже задаёт accessible name; не передавайте ariaLabel/ariaLabelledby. '
|
|
168
|
+
+ '/ SSqlEditor: title already provides the accessible name; do not provide ariaLabel/ariaLabelledby.',
|
|
169
|
+
)
|
|
170
|
+
}
|
|
171
|
+
return { ariaLabel: undefined, ariaLabelledby: generatedTitleId }
|
|
172
|
+
}
|
|
173
|
+
if (props.ariaLabel !== undefined && props.ariaLabelledby !== undefined) {
|
|
174
|
+
throw new TypeError('SSqlEditor: задайте только ariaLabel или ariaLabelledby. / SSqlEditor: provide only ariaLabel or ariaLabelledby.')
|
|
175
|
+
}
|
|
176
|
+
if (props.ariaLabelledby !== undefined) {
|
|
177
|
+
return { ariaLabel: undefined, ariaLabelledby: validateDomId('SSqlEditor', 'ariaLabelledby', props.ariaLabelledby) }
|
|
178
|
+
}
|
|
179
|
+
return {
|
|
180
|
+
ariaLabel: props.ariaLabel === undefined ? 'SQL editor' : validateNonEmptyString('SSqlEditor', 'ariaLabel', props.ariaLabel),
|
|
181
|
+
ariaLabelledby: undefined,
|
|
182
|
+
}
|
|
183
|
+
})
|
|
184
|
+
const generatedTitleId = `s-sql-editor-title-${useId()}`
|
|
185
|
+
const generatedStatusId = `s-sql-editor-status-${useId()}`
|
|
186
|
+
const panelState = computed(() => validatedLoading.value
|
|
187
|
+
? { status: 'loading' as const, message: 'Загрузка SQL editor...' }
|
|
188
|
+
: { status: 'ready' as const })
|
|
189
|
+
|
|
190
|
+
const onUpdate = (value: string) => {
|
|
191
|
+
emit('update:modelValue', value)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const completionSource = (context: CompletionContext) => {
|
|
195
|
+
const word = context.matchBefore(/[A-Za-zА-Яа-я0-9_.$"]*/)
|
|
196
|
+
if (!word || (word.from === word.to && !context.explicit)) {
|
|
197
|
+
return null
|
|
198
|
+
}
|
|
199
|
+
const options: Completion[] = validatedCompletionItems.value.map((item) => ({
|
|
200
|
+
label: item.label,
|
|
201
|
+
apply: item.apply ?? item.label,
|
|
202
|
+
detail: item.detail,
|
|
203
|
+
type: completionType(item.kind),
|
|
204
|
+
}))
|
|
205
|
+
return {
|
|
206
|
+
from: word.from,
|
|
207
|
+
options,
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const completionType = (kind: SSqlEditorCompletionItem['kind']) => {
|
|
212
|
+
switch (kind) {
|
|
213
|
+
case 'schema':
|
|
214
|
+
return 'namespace'
|
|
215
|
+
case 'table':
|
|
216
|
+
case 'view':
|
|
217
|
+
return 'class'
|
|
218
|
+
case 'column':
|
|
219
|
+
return 'property'
|
|
220
|
+
case 'function':
|
|
221
|
+
return 'function'
|
|
222
|
+
case 'keyword':
|
|
223
|
+
default:
|
|
224
|
+
return 'keyword'
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const diagnosticsExtension = computed(() =>
|
|
229
|
+
linter((view) =>
|
|
230
|
+
validatedDiagnostics.value.map((diagnostic): CodeMirrorDiagnostic => {
|
|
231
|
+
const { from, to } = resolveDiagnosticRange(view.state.doc, diagnostic)
|
|
232
|
+
return {
|
|
233
|
+
from,
|
|
234
|
+
to,
|
|
235
|
+
severity: diagnosticSeverity(diagnostic.severity),
|
|
236
|
+
message: diagnostic.message,
|
|
237
|
+
}
|
|
238
|
+
}),
|
|
239
|
+
),
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
const resolveDiagnosticRange = (
|
|
243
|
+
doc: EditorState['doc'],
|
|
244
|
+
diagnostic: SSqlEditorDiagnostic,
|
|
245
|
+
) : { from: number; to: number } => {
|
|
246
|
+
if ('from' in diagnostic) {
|
|
247
|
+
const from = validateBoundedInteger('SSqlEditor', 'diagnostics[].from', diagnostic.from, { minimum: 0, maximum: doc.length })
|
|
248
|
+
const to = diagnostic.to === undefined
|
|
249
|
+
? Math.min(from + 1, doc.length)
|
|
250
|
+
: validateBoundedInteger('SSqlEditor', 'diagnostics[].to', diagnostic.to, { minimum: from, maximum: doc.length })
|
|
251
|
+
return { from, to }
|
|
252
|
+
}
|
|
253
|
+
const lineNumber = validateBoundedInteger('SSqlEditor', 'diagnostics[].line', diagnostic.line, { minimum: 1, maximum: doc.lines })
|
|
254
|
+
const line = doc.line(lineNumber)
|
|
255
|
+
const column = validateBoundedInteger('SSqlEditor', 'diagnostics[].column', diagnostic.column, { minimum: 1, maximum: line.length + 1 })
|
|
256
|
+
const from = line.from + column - 1
|
|
257
|
+
return { from, to: Math.min(from + 1, doc.length) }
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const diagnosticSeverity = (severity: SSqlEditorDiagnostic['severity']): CodeMirrorDiagnostic['severity'] => {
|
|
261
|
+
if (severity === 'info') return 'info'
|
|
262
|
+
if (severity === 'warning') return 'warning'
|
|
263
|
+
return 'error'
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function assertPlainExactRecord(owner: string, value: unknown, allowed: readonly string[]): Record<string, unknown> {
|
|
267
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value) || Object.getPrototypeOf(value) !== Object.prototype) {
|
|
268
|
+
throw new TypeError(`${owner}: значение должно быть plain object. / ${owner}: value must be a plain object.`)
|
|
269
|
+
}
|
|
270
|
+
const record = value as Record<string, unknown>
|
|
271
|
+
const unexpected = Object.keys(record).filter((key) => !allowed.includes(key))
|
|
272
|
+
if (unexpected.length > 0) throw new TypeError(`${owner}: неизвестные поля ${unexpected.join(', ')}. / ${owner}: unknown fields ${unexpected.join(', ')}.`)
|
|
273
|
+
return record
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const validatedCompletionItems = computed<SSqlEditorCompletionItem[]>(() => {
|
|
277
|
+
if (!Array.isArray(props.completionItems)) throw new TypeError('SSqlEditor: completionItems должен быть массивом. / SSqlEditor: completionItems must be an array.')
|
|
278
|
+
return props.completionItems.map((candidate, index) => {
|
|
279
|
+
const record = assertPlainExactRecord(`SSqlEditor.completionItems[${index}]`, candidate, ['label', 'apply', 'detail', 'kind'])
|
|
280
|
+
return {
|
|
281
|
+
label: validateNonEmptyString('SSqlEditor', `completionItems[${index}].label`, record.label),
|
|
282
|
+
...(record.apply === undefined ? {} : { apply: validateNonEmptyString('SSqlEditor', `completionItems[${index}].apply`, record.apply) }),
|
|
283
|
+
...(record.detail === undefined ? {} : { detail: validateNonEmptyString('SSqlEditor', `completionItems[${index}].detail`, record.detail) }),
|
|
284
|
+
...(record.kind === undefined ? {} : { kind: validateExactString('SSqlEditor', `completionItems[${index}].kind`, record.kind, ['keyword', 'schema', 'table', 'view', 'column', 'function'] as const) }),
|
|
285
|
+
}
|
|
286
|
+
})
|
|
287
|
+
})
|
|
288
|
+
|
|
289
|
+
const validatedDiagnostics = computed<SSqlEditorDiagnostic[]>(() => {
|
|
290
|
+
if (!Array.isArray(props.diagnostics)) throw new TypeError('SSqlEditor: diagnostics должен быть массивом. / SSqlEditor: diagnostics must be an array.')
|
|
291
|
+
return props.diagnostics.map((candidate, index) => {
|
|
292
|
+
const runtimeCandidate: unknown = candidate
|
|
293
|
+
const record = assertPlainExactRecord(`SSqlEditor.diagnostics[${index}]`, runtimeCandidate, ['severity', 'message', 'from', 'to', 'line', 'column'])
|
|
294
|
+
const hasOffset = Object.hasOwn(record, 'from')
|
|
295
|
+
const hasLine = Object.hasOwn(record, 'line') || Object.hasOwn(record, 'column')
|
|
296
|
+
if (hasOffset === hasLine) throw new TypeError(`SSqlEditor: diagnostics[${index}] должен задавать ровно один range: from/to или line/column. / SSqlEditor: diagnostics[${index}] must provide exactly one range: from/to or line/column.`)
|
|
297
|
+
const allowedRangeKeys = hasOffset
|
|
298
|
+
? ['severity', 'message', 'from', 'to']
|
|
299
|
+
: ['severity', 'message', 'line', 'column']
|
|
300
|
+
const unexpectedRangeKeys = Object.keys(record).filter((key) => !allowedRangeKeys.includes(key))
|
|
301
|
+
if (unexpectedRangeKeys.length > 0) throw new TypeError(`SSqlEditor.diagnostics[${index}]: неизвестные поля ${unexpectedRangeKeys.join(', ')}. / SSqlEditor.diagnostics[${index}]: unknown fields ${unexpectedRangeKeys.join(', ')}.`)
|
|
302
|
+
const common = {
|
|
303
|
+
severity: validateExactString('SSqlEditor', `diagnostics[${index}].severity`, record.severity, ['error', 'warning', 'info'] as const),
|
|
304
|
+
message: validateNonEmptyString('SSqlEditor', `diagnostics[${index}].message`, record.message),
|
|
305
|
+
}
|
|
306
|
+
if (hasOffset) {
|
|
307
|
+
if (!Number.isInteger(record.from) || (record.to !== undefined && !Number.isInteger(record.to))) throw new TypeError(`SSqlEditor: diagnostics[${index}] offsets должны быть целыми числами. / SSqlEditor: diagnostics[${index}] offsets must be integers.`)
|
|
308
|
+
return { ...common, from: record.from as number, ...(record.to === undefined ? {} : { to: record.to as number }) }
|
|
309
|
+
}
|
|
310
|
+
if (!Number.isInteger(record.line) || !Number.isInteger(record.column)) throw new TypeError(`SSqlEditor: diagnostics[${index}] line/column должны быть целыми числами. / SSqlEditor: diagnostics[${index}] line/column must be integers.`)
|
|
311
|
+
return { ...common, line: record.line as number, column: record.column as number }
|
|
312
|
+
})
|
|
313
|
+
})
|
|
314
|
+
const statusLabel = computed(() => validatedErrorMessage.value || `${validatedDiagnostics.value.length} диагностик`)
|
|
315
|
+
const hasStatus = computed(() => (
|
|
316
|
+
validatedErrorMessage.value !== ''
|
|
317
|
+
|| validatedDiagnostics.value.length > 0
|
|
318
|
+
|| Boolean(slots.status)
|
|
319
|
+
))
|
|
320
|
+
const statusHasError = computed(() => (
|
|
321
|
+
validatedErrorMessage.value !== ''
|
|
322
|
+
|| validatedDiagnostics.value.some((diagnostic) => diagnostic.severity === 'error')
|
|
323
|
+
))
|
|
324
|
+
const statusTone = computed<StatusTone>(() => {
|
|
325
|
+
if (statusHasError.value) return 'danger'
|
|
326
|
+
if (validatedDiagnostics.value.some((diagnostic) => diagnostic.severity === 'warning')) return 'warn'
|
|
327
|
+
return 'info'
|
|
328
|
+
})
|
|
329
|
+
|
|
330
|
+
const sqlLanguage: SCodeEditorLanguage = { mode: 'explicit', id: 'sql' }
|
|
331
|
+
const editorCapabilities = computed<SCodeEditorCapabilities>(() => ({
|
|
332
|
+
lineNumbers: validatedShowLineNumbers.value,
|
|
333
|
+
indentation: { tabSize: props.tabSize, keyBehavior: 'indent' },
|
|
334
|
+
cursorActivity: 'always',
|
|
335
|
+
}))
|
|
336
|
+
|
|
337
|
+
const extensions = computed<readonly Extension[]>(() => {
|
|
338
|
+
const result: Extension[] = []
|
|
339
|
+
result.push(autocompletion({ override: [completionSource] }))
|
|
340
|
+
result.push(diagnosticsExtension.value)
|
|
341
|
+
return result
|
|
342
|
+
})
|
|
343
|
+
|
|
344
|
+
provideTrustedCodeEditorExtensions('SSqlEditor', extensions)
|
|
345
|
+
|
|
346
|
+
const emitCursorActivity = (payload: SCodeEditorCursorActivity): void => {
|
|
347
|
+
emit('cursor-activity', { pos: payload.pos, line: payload.line, column: payload.column })
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const applySelection = (pos: number): void => {
|
|
351
|
+
const editor = codeEditor.value
|
|
352
|
+
if (!editor) throw new Error('SSqlEditor: editor не готов; дождитесь ready. / SSqlEditor: editor is not ready; wait for ready.')
|
|
353
|
+
editor.applySelection(pos)
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const focus = (): void => {
|
|
357
|
+
const editor = codeEditor.value
|
|
358
|
+
if (!editor) throw new Error('SSqlEditor: editor не готов; дождитесь ready. / SSqlEditor: editor is not ready; wait for ready.')
|
|
359
|
+
editor.focus()
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
defineExpose({ applySelection, focus })
|
|
363
|
+
</script>
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ScrollableViewportMaxBlockSize,
|
|
3
|
+
TableColumnInlineSize,
|
|
4
|
+
TableColumnMinInlineSize,
|
|
5
|
+
} from '../../../internal/semanticSizing'
|
|
6
|
+
import type { STableAlignment } from '../data-display/table'
|
|
7
|
+
|
|
8
|
+
export type SDataGridColumnKey = string
|
|
9
|
+
export type SDataGridCellKind = 'text' | 'number' | 'boolean' | 'json' | 'datetime' | 'binary'
|
|
10
|
+
export type SDataGridRowState = 'clean' | 'inserted' | 'updated' | 'deleted'
|
|
11
|
+
|
|
12
|
+
export interface SDataGridColumn<Key extends SDataGridColumnKey = SDataGridColumnKey> {
|
|
13
|
+
key: Key
|
|
14
|
+
label: string
|
|
15
|
+
type?: SDataGridCellKind
|
|
16
|
+
width?: TableColumnInlineSize
|
|
17
|
+
minWidth?: TableColumnMinInlineSize
|
|
18
|
+
editable?: boolean
|
|
19
|
+
align?: STableAlignment
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface SDataGridCell {
|
|
23
|
+
value: unknown
|
|
24
|
+
display: string
|
|
25
|
+
kind?: SDataGridCellKind
|
|
26
|
+
pending?: boolean
|
|
27
|
+
readonly?: boolean
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface SDataGridRow<Key extends SDataGridColumnKey = SDataGridColumnKey> {
|
|
31
|
+
id: string
|
|
32
|
+
cells: Record<Key, SDataGridCell>
|
|
33
|
+
state?: SDataGridRowState
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface SDataGridEditPayload<Key extends SDataGridColumnKey = SDataGridColumnKey> {
|
|
37
|
+
rowId: string
|
|
38
|
+
columnKey: Key
|
|
39
|
+
value: unknown
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export type SDataGridCommitInput =
|
|
43
|
+
| { source: 'draft' }
|
|
44
|
+
| { source: 'value'; value: unknown }
|
|
45
|
+
|
|
46
|
+
export interface SDataGridProps<Key extends SDataGridColumnKey = SDataGridColumnKey> {
|
|
47
|
+
columns: readonly SDataGridColumn<Key>[]
|
|
48
|
+
rows: readonly SDataGridRow<Key>[]
|
|
49
|
+
title?: string
|
|
50
|
+
loading?: boolean
|
|
51
|
+
disabled?: boolean
|
|
52
|
+
readOnly?: boolean
|
|
53
|
+
errorMessage?: string
|
|
54
|
+
emptyMessage?: string
|
|
55
|
+
selectedRowId?: string
|
|
56
|
+
maxHeight?: ScrollableViewportMaxBlockSize
|
|
57
|
+
/** Accessible name без visible title. / Accessible name when there is no visible title. */
|
|
58
|
+
ariaLabel?: string
|
|
59
|
+
/** DOM id external visible title. / DOM id of an external visible title. */
|
|
60
|
+
ariaLabelledby?: string
|
|
61
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
@reference "../../../styles/reference.css";
|
|
2
|
+
|
|
3
|
+
.s-async-state,
|
|
4
|
+
.s-async-state__surface {
|
|
5
|
+
@apply min-w-0;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
.s-async-state--block,
|
|
9
|
+
.s-async-state__surface {
|
|
10
|
+
@apply w-full;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.s-async-state--overlay {
|
|
14
|
+
@apply absolute inset-0 flex items-center justify-center bg-surface-0/90 p-4 backdrop-blur-sm dark:bg-surface-900/90;
|
|
15
|
+
z-index: var(--s-layer-sticky);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.s-async-state__cell {
|
|
19
|
+
@apply p-0;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.s-async-state__loading {
|
|
23
|
+
@apply flex min-h-24 min-w-0 w-full items-center justify-center whitespace-normal p-4 text-center text-sm text-surface-500 dark:text-surface-400;
|
|
24
|
+
overflow-wrap: anywhere;
|
|
25
|
+
gap: var(--s-space-control-gap);
|
|
26
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<slot v-if="validatedState.status === 'ready' && assertReadyAttrs()" />
|
|
3
|
+
|
|
4
|
+
<tr
|
|
5
|
+
v-else-if="validatedPresentation === 'table-row'"
|
|
6
|
+
class="s-async-state s-async-state--table-row"
|
|
7
|
+
:data-state="validatedState.status"
|
|
8
|
+
>
|
|
9
|
+
<td :colspan="validatedColspan" class="s-async-state__cell">
|
|
10
|
+
<div
|
|
11
|
+
v-bind="ownedAttrs.bindings()"
|
|
12
|
+
class="s-async-state__surface"
|
|
13
|
+
:data-state="validatedState.status"
|
|
14
|
+
:role="surfaceLiveAttributes.role"
|
|
15
|
+
:aria-live="surfaceLiveAttributes.ariaLive"
|
|
16
|
+
:aria-atomic="surfaceLiveAttributes.ariaAtomic"
|
|
17
|
+
:aria-busy="validatedState.status === 'loading' || undefined"
|
|
18
|
+
>
|
|
19
|
+
<AsyncStateBody />
|
|
20
|
+
</div>
|
|
21
|
+
</td>
|
|
22
|
+
</tr>
|
|
23
|
+
|
|
24
|
+
<div
|
|
25
|
+
v-else
|
|
26
|
+
v-bind="ownedAttrs.bindings()"
|
|
27
|
+
class="s-async-state"
|
|
28
|
+
:class="`s-async-state--${validatedPresentation}`"
|
|
29
|
+
:data-state="validatedState.status"
|
|
30
|
+
:role="surfaceLiveAttributes.role"
|
|
31
|
+
:aria-live="surfaceLiveAttributes.ariaLive"
|
|
32
|
+
:aria-atomic="surfaceLiveAttributes.ariaAtomic"
|
|
33
|
+
:aria-busy="validatedState.status === 'loading' || undefined"
|
|
34
|
+
>
|
|
35
|
+
<AsyncStateBody />
|
|
36
|
+
</div>
|
|
37
|
+
</template>
|
|
38
|
+
|
|
39
|
+
<script setup lang="ts">
|
|
40
|
+
import { Fragment, computed, h, type Slots, type VNodeChild } from 'vue'
|
|
41
|
+
|
|
42
|
+
import { resolveAsyncFeedbackPresentation } from '../../../internal/feedbackPresentation'
|
|
43
|
+
import { useOwnedAttrs } from '../../../internal/ownedAttrs'
|
|
44
|
+
import SEmptyState from '../data-display/SEmptyState.vue'
|
|
45
|
+
import SMessage from '../data-display/SMessage.vue'
|
|
46
|
+
import SProgressIndicator from '../data-display/SProgressIndicator.vue'
|
|
47
|
+
|
|
48
|
+
defineOptions({ inheritAttrs: false })
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Единственное состояние асинхронной поверхности; error всегда содержит диагностику, endpoint допустим только для error.
|
|
52
|
+
* The sole async-surface state; error always carries diagnostics, and endpoint is valid only for error.
|
|
53
|
+
*/
|
|
54
|
+
export type SAsyncStateContract =
|
|
55
|
+
| { status: 'ready' }
|
|
56
|
+
| { status: 'loading'; message?: string }
|
|
57
|
+
| { status: 'empty'; message?: string }
|
|
58
|
+
| { status: 'error'; message: string; endpoint?: string }
|
|
59
|
+
export type SAsyncStatePresentation = 'block' | 'table-row' | 'overlay'
|
|
60
|
+
|
|
61
|
+
interface Props {
|
|
62
|
+
state: SAsyncStateContract
|
|
63
|
+
presentation?: SAsyncStatePresentation
|
|
64
|
+
/** Обязателен и допустим только для table-row. / Required and valid only for table-row. */
|
|
65
|
+
colspan?: number
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
69
|
+
presentation: 'block',
|
|
70
|
+
colspan: undefined,
|
|
71
|
+
})
|
|
72
|
+
const slots = defineSlots<{
|
|
73
|
+
default?: () => VNodeChild
|
|
74
|
+
loading?: (props: { message: string; state: Extract<SAsyncStateContract, { status: 'loading' }> }) => VNodeChild
|
|
75
|
+
empty?: (props: { message: string; state: Extract<SAsyncStateContract, { status: 'empty' }> }) => VNodeChild
|
|
76
|
+
error?: (props: { message: string; state: Extract<SAsyncStateContract, { status: 'error' }> }) => VNodeChild
|
|
77
|
+
}>()
|
|
78
|
+
|
|
79
|
+
const ownedAttrs = useOwnedAttrs({
|
|
80
|
+
component: 'SAsyncState',
|
|
81
|
+
owner: 'rendered async-state surface',
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
const stateAllowedKeys: Readonly<Record<SAsyncStateContract['status'], readonly string[]>> = {
|
|
85
|
+
ready: ['status'],
|
|
86
|
+
loading: ['status', 'message'],
|
|
87
|
+
empty: ['status', 'message'],
|
|
88
|
+
error: ['status', 'message', 'endpoint'],
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const assertNonEmptyOptionalString = (record: Record<string, unknown>, key: 'message' | 'endpoint') => {
|
|
92
|
+
if (!(key in record)) return
|
|
93
|
+
if (typeof record[key] !== 'string' || record[key].trim().length === 0) {
|
|
94
|
+
throw new TypeError(`SAsyncState: state.${key} должен быть непустой строкой при наличии.`)
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const validateState = (state: unknown): SAsyncStateContract => {
|
|
99
|
+
if (typeof state !== 'object' || state === null || Array.isArray(state)) {
|
|
100
|
+
throw new TypeError('SAsyncState: state должен быть объектом discriminated union.')
|
|
101
|
+
}
|
|
102
|
+
const record = state as Record<string, unknown>
|
|
103
|
+
const status = record.status
|
|
104
|
+
if (!['ready', 'loading', 'empty', 'error'].includes(String(status))) {
|
|
105
|
+
throw new TypeError(`SAsyncState: неизвестный state.status "${String(status)}".`)
|
|
106
|
+
}
|
|
107
|
+
const typedStatus = status as SAsyncStateContract['status']
|
|
108
|
+
const unexpected = Object.keys(record).filter((key) => !stateAllowedKeys[typedStatus].includes(key))
|
|
109
|
+
if (unexpected.length > 0) {
|
|
110
|
+
throw new TypeError(`SAsyncState: state.status="${typedStatus}" не допускает поля ${unexpected.join(', ')}.`)
|
|
111
|
+
}
|
|
112
|
+
if (typedStatus === 'error') {
|
|
113
|
+
if (typeof record.message !== 'string' || record.message.trim().length === 0) {
|
|
114
|
+
throw new TypeError('SAsyncState: state.message должен быть непустой строкой для status="error".')
|
|
115
|
+
}
|
|
116
|
+
assertNonEmptyOptionalString(record, 'endpoint')
|
|
117
|
+
} else if (typedStatus === 'loading' || typedStatus === 'empty') {
|
|
118
|
+
assertNonEmptyOptionalString(record, 'message')
|
|
119
|
+
}
|
|
120
|
+
return state as SAsyncStateContract
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const validatedState = computed(() => validateState(props.state))
|
|
124
|
+
const validatedPresentation = computed<SAsyncStatePresentation>(() => {
|
|
125
|
+
const colspan = props.colspan
|
|
126
|
+
if (!['block', 'table-row', 'overlay'].includes(String(props.presentation))) {
|
|
127
|
+
throw new TypeError(`SAsyncState: неизвестный presentation "${String(props.presentation)}".`)
|
|
128
|
+
}
|
|
129
|
+
if (props.presentation !== 'table-row' && colspan !== undefined) {
|
|
130
|
+
throw new TypeError('SAsyncState: colspan допустим только для presentation="table-row".')
|
|
131
|
+
}
|
|
132
|
+
if (props.presentation === 'table-row' && colspan === undefined) {
|
|
133
|
+
throw new TypeError('SAsyncState: presentation="table-row" требует colspan.')
|
|
134
|
+
}
|
|
135
|
+
if (props.presentation === 'table-row'
|
|
136
|
+
&& (typeof colspan !== 'number' || !Number.isSafeInteger(colspan) || colspan <= 0)) {
|
|
137
|
+
throw new TypeError('SAsyncState: colspan должен быть целым числом больше 0.')
|
|
138
|
+
}
|
|
139
|
+
return props.presentation
|
|
140
|
+
})
|
|
141
|
+
const validatedColspan = computed(() => {
|
|
142
|
+
return validatedPresentation.value === 'table-row' ? props.colspan : undefined
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
const assertReadyAttrs = (): boolean => {
|
|
146
|
+
ownedAttrs.reject('ready state отдаёт consumer content без UI-kit root / ready state renders consumer content without a UI-kit root')
|
|
147
|
+
return true
|
|
148
|
+
}
|
|
149
|
+
const statePresentation = computed(() => {
|
|
150
|
+
const state = validatedState.value
|
|
151
|
+
return state.status === 'ready' ? undefined : resolveAsyncFeedbackPresentation(state.status)
|
|
152
|
+
})
|
|
153
|
+
const resolvedMessage = computed(() => {
|
|
154
|
+
const state = validatedState.value
|
|
155
|
+
if (state.status === 'ready') return ''
|
|
156
|
+
return state.message ?? statePresentation.value?.fallbackMessage ?? ''
|
|
157
|
+
})
|
|
158
|
+
const customStateSlot = computed(() => {
|
|
159
|
+
const state = validatedState.value
|
|
160
|
+
return state.status === 'ready' ? undefined : slots[state.status] as Slots[string] | undefined
|
|
161
|
+
})
|
|
162
|
+
const surfaceLiveAttributes = computed(() => {
|
|
163
|
+
const state = validatedState.value
|
|
164
|
+
if (state.status === 'ready') return {}
|
|
165
|
+
return state.status === 'loading' || customStateSlot.value
|
|
166
|
+
? statePresentation.value?.live ?? {}
|
|
167
|
+
: {}
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
const AsyncStateBody = () => {
|
|
171
|
+
const state = validatedState.value
|
|
172
|
+
if (state.status === 'ready') return null
|
|
173
|
+
const stateSlot = customStateSlot.value
|
|
174
|
+
if (stateSlot) {
|
|
175
|
+
return h(Fragment, stateSlot({ message: resolvedMessage.value, state }))
|
|
176
|
+
}
|
|
177
|
+
if (state.status === 'error') {
|
|
178
|
+
return h(SMessage, { severity: 'danger', live: 'auto', size: 'sm' }, () => resolvedMessage.value)
|
|
179
|
+
}
|
|
180
|
+
if (state.status === 'empty') {
|
|
181
|
+
return h(SEmptyState, { title: resolvedMessage.value, size: 'sm' })
|
|
182
|
+
}
|
|
183
|
+
return h('div', { class: 's-async-state__loading' }, [
|
|
184
|
+
h(SProgressIndicator, { accessibility: { mode: 'decorative' }, size: 'sm', tone: 'info' }),
|
|
185
|
+
h('span', resolvedMessage.value),
|
|
186
|
+
])
|
|
187
|
+
}
|
|
188
|
+
</script>
|
|
189
|
+
|
|
190
|
+
<style lang="postcss" src="./SAsyncState.css" scoped></style>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './_internal/graphViewportContract'
|