@pgcorp/ui-kit 0.9.0 → 0.10.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/README.md +188 -9
- package/docs/getting-started.md +135 -0
- package/docs/public-api.md +53 -4
- package/docs/rich-document.md +216 -0
- package/docs/scroll-viewport.md +167 -0
- package/package.json +103 -50
- package/src/components/layout/SAppShell.vue +3 -1
- package/src/components/layout/SDockRegion.vue +5 -3
- package/src/components/layout/SWorkbenchPanelShell.vue +4 -2
- package/src/components/shared/_internal/SDialogSurface.css +5 -0
- package/src/components/shared/_internal/SDialogSurface.vue +3 -1
- package/src/components/shared/_internal/SToastItem.css +4 -0
- package/src/components/shared/_internal/SToastItem.vue +44 -8
- package/src/components/shared/_internal/STreeNode.vue +24 -11
- package/src/components/shared/complex/SKanbanLane.vue +4 -2
- package/src/components/shared/containers/SCloudChromeRow.vue +3 -2
- package/src/components/shared/containers/SDrawer.vue +5 -2
- package/src/components/shared/containers/SModal.vue +5 -2
- package/src/components/shared/containers/SPanel.vue +4 -2
- package/src/components/shared/containers/SPopover.vue +7 -3
- package/src/components/shared/containers/SUiKitLocaleProvider.vue +57 -0
- package/src/components/shared/controls/SButton.css +14 -10
- package/src/components/shared/controls/SCombobox.vue +8 -6
- package/src/components/shared/controls/SComboboxTrigger.css +7 -4
- package/src/components/shared/controls/SComboboxTrigger.vue +5 -2
- package/src/components/shared/controls/SContextToggleButton.vue +6 -4
- package/src/components/shared/controls/SDragHandle.vue +8 -4
- package/src/components/shared/controls/SDropdownMenu.vue +7 -4
- package/src/components/shared/controls/SInputText.css +5 -4
- package/src/components/shared/controls/SListbox.vue +4 -2
- package/src/components/shared/controls/SSelect.vue +4 -2
- package/src/components/shared/controls/_internal/SInlineTokenSurface.vue +4 -2
- package/src/components/shared/data-display/SChart.vue +7 -5
- package/src/components/shared/data-display/SChip.vue +3 -1
- package/src/components/shared/data-display/SCodeBlock.css +4 -0
- package/src/components/shared/data-display/SCodeBlock.vue +42 -20
- package/src/components/shared/data-display/SCodeEditor.css +17 -0
- package/src/components/shared/data-display/SCodeEditor.vue +173 -8
- package/src/components/shared/data-display/SCodeSearchPanel.vue +48 -33
- package/src/components/shared/data-display/SCodeWorkspaceEditor.css +13 -0
- package/src/components/shared/data-display/SCodeWorkspaceEditor.vue +355 -0
- package/src/components/shared/data-display/SControlledToastViewport.css +89 -0
- package/src/components/shared/data-display/SControlledToastViewport.vue +205 -0
- package/src/components/shared/data-display/SCopyField.vue +12 -10
- package/src/components/shared/data-display/SDiffViewer.css +122 -0
- package/src/components/shared/data-display/SDiffViewer.vue +499 -0
- package/src/components/shared/data-display/SDocBlock.vue +99 -71
- package/src/components/shared/data-display/SExpandableText.vue +10 -8
- package/src/components/shared/data-display/SJsonTree.vue +4 -2
- package/src/components/shared/data-display/SLinkedSystemsList.vue +4 -2
- package/src/components/shared/data-display/SMessage.vue +6 -6
- package/src/components/shared/data-display/SRichDocument.css +8 -0
- package/src/components/shared/data-display/SRichDocument.vue +65 -0
- package/src/components/shared/data-display/SScrollViewport.css +53 -0
- package/src/components/shared/data-display/SScrollViewport.vue +470 -0
- package/src/components/shared/data-display/STable.vue +38 -19
- package/src/components/shared/data-display/SToastContainer.vue +22 -17
- package/src/components/shared/data-display/_internal/SCodeTokenLine.css +72 -0
- package/src/components/shared/data-display/_internal/SCodeTokenLine.vue +79 -0
- package/src/components/shared/data-display/_internal/SRichBlockNode.css +85 -0
- package/src/components/shared/data-display/_internal/SRichBlockNode.vue +282 -0
- package/src/components/shared/data-display/_internal/SRichInlineNode.css +6 -0
- package/src/components/shared/data-display/_internal/SRichInlineNode.vue +76 -0
- package/src/components/shared/data-display/_internal/SRichMath.css +42 -0
- package/src/components/shared/data-display/_internal/SRichMath.vue +102 -0
- package/src/components/shared/data-display/codeEditor.ts +200 -0
- package/src/components/shared/data-display/codeWorkspace.ts +233 -0
- package/src/components/shared/data-display/diff.ts +541 -0
- package/src/components/shared/data-display/richDocument.ts +876 -0
- package/src/components/shared/data-display/scrollViewport.ts +47 -0
- package/src/components/shared/data-display/toast.ts +194 -0
- package/src/components/shared/database/SDataGrid.vue +7 -4
- package/src/components/shared/database/SSqlEditor.vue +39 -39
- package/src/components/shared/feedback/SAsyncState.vue +3 -1
- package/src/components/shared/graph/SGraphViewport.vue +11 -9
- package/src/components/shared/navigation/SBreadcrumbs.vue +8 -5
- package/src/components/shared/navigation/SCatalogNavigator.vue +26 -20
- package/src/components/shared/navigation/STabList.vue +10 -6
- package/src/components/shared/navigation/STabs.vue +15 -11
- package/src/components/shared/navigation/SWizardSteps.vue +4 -2
- package/src/components/shared/persona/SPersonaProjectPicker.vue +31 -27
- package/src/components/shared/persona/SPersonaRunWorkbench.vue +39 -33
- package/src/components/shared/persona/_internal/SPersonaRunConsole.vue +8 -6
- package/src/components/shared/persona/_internal/labels.ts +66 -57
- package/src/components/shared/persona/presentation.ts +140 -110
- package/src/composables/useFloatingPosition.ts +39 -2
- package/src/composables/useUiKitLocale.ts +17 -0
- package/src/internal/codeHighlight.ts +110 -0
- package/src/internal/diffEngine.ts +464 -0
- package/src/internal/diffRuntime.ts +189 -0
- package/src/internal/feedbackPresentation.ts +33 -14
- package/src/internal/localeContext.ts +22 -0
- package/src/internal/ownedAttrs.ts +8 -0
- package/src/internal/runtimeContract.ts +11 -9
- package/src/internal/semanticSizing.ts +13 -0
- package/src/locales/catalog.ts +634 -0
- package/src/locales/locale.ts +472 -0
- package/src/locales/messages.generated.ts +1826 -0
- package/src/locales/messages.ts +150 -0
- package/src/stores/useNotifierStore.ts +61 -44
- package/src/styles/tokens.css +20 -2
- package/src/theme.ts +59 -32
- package/src/workers/diff.worker.ts +73 -0
- package/src/workers/diffProtocol.ts +155 -0
- package/src/components/shared/data-display/SToastContainer.css +0 -53
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
} from 'vue'
|
|
11
11
|
|
|
12
12
|
import { X } from '../../../icons/sputnigUiIcons'
|
|
13
|
+
import { useUiKitLocale } from '../../../../composables/useUiKitLocale'
|
|
13
14
|
import type { FieldSurfaceBindings } from '../../../../internal/fieldSurface'
|
|
14
15
|
import type { InlineTokenEditorSegmentContract } from '../../../../internal/inlineTokenEditorContract'
|
|
15
16
|
import type { OwnedAttributeBindings } from '../../../../internal/ownedAttrs'
|
|
@@ -121,6 +122,7 @@ export default defineComponent({
|
|
|
121
122
|
),
|
|
122
123
|
},
|
|
123
124
|
setup(props, { emit }) {
|
|
125
|
+
const { t } = useUiKitLocale()
|
|
124
126
|
useInteractiveLeafRegistration({
|
|
125
127
|
owner: 'SInlineTokenEditor',
|
|
126
128
|
active: () => !props.disabled,
|
|
@@ -143,7 +145,7 @@ export default defineComponent({
|
|
|
143
145
|
const bindings: InlineTokenActionButtonBindings = action === 'open'
|
|
144
146
|
? {
|
|
145
147
|
label: segment.label,
|
|
146
|
-
'aria-label':
|
|
148
|
+
'aria-label': t('forms.openToken', { label: segment.title ?? segment.label }),
|
|
147
149
|
...(segment.title === undefined ? {} : { title: segment.title }),
|
|
148
150
|
size: 'xs',
|
|
149
151
|
width: 'fit',
|
|
@@ -155,7 +157,7 @@ export default defineComponent({
|
|
|
155
157
|
}
|
|
156
158
|
: {
|
|
157
159
|
leadingIcon: X,
|
|
158
|
-
'aria-label':
|
|
160
|
+
'aria-label': t('forms.removeToken', { label: segment.label }),
|
|
159
161
|
size: 'xs',
|
|
160
162
|
appearance: 'ghost',
|
|
161
163
|
shape: 'circle',
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { computed, onBeforeUnmount, onMounted, ref, shallowRef, watchEffect } from 'vue'
|
|
3
|
+
import { useUiKitLocale } from '../../../composables/useUiKitLocale'
|
|
3
4
|
import { useOwnedAttrs } from '../../../internal/ownedAttrs'
|
|
4
5
|
import { resolveChartModel } from '../../../internal/chartGeometry'
|
|
5
6
|
import type { ChartDimensions } from '../../../internal/chartGeometry'
|
|
@@ -76,9 +77,10 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
76
77
|
centerLabel: undefined,
|
|
77
78
|
centerValue: undefined,
|
|
78
79
|
dataTablePresentation: 'assistive',
|
|
79
|
-
emptyLabel:
|
|
80
|
+
emptyLabel: undefined,
|
|
80
81
|
valueFormatter: undefined,
|
|
81
82
|
})
|
|
83
|
+
const { t } = useUiKitLocale()
|
|
82
84
|
|
|
83
85
|
const ownedAttrs = useOwnedAttrs({ component: 'SChart', owner: 'chart root' })
|
|
84
86
|
const viewportElement = ref<HTMLElement | null>(null)
|
|
@@ -147,7 +149,7 @@ const resolvedValueFormatter = computed(() => {
|
|
|
147
149
|
return props.valueFormatter
|
|
148
150
|
})
|
|
149
151
|
const validatedEmptyLabel = computed(() => validateNonEmptyString(
|
|
150
|
-
'SChart', 'emptyLabel', props.emptyLabel,
|
|
152
|
+
'SChart', 'emptyLabel', props.emptyLabel ?? t('chart.empty'),
|
|
151
153
|
))
|
|
152
154
|
const validatedXAxisLabel = computed(() => props.xAxisLabel === undefined
|
|
153
155
|
? undefined
|
|
@@ -223,7 +225,7 @@ const showVerticalGrid = computed(() => validatedGrid.value === 'vertical' || va
|
|
|
223
225
|
const showTable = computed(() => validatedTablePresentation.value !== 'none' && !model.value.empty)
|
|
224
226
|
const tableClass = computed(() => validatedTablePresentation.value === 'assistive' ? 's-chart__data-table--assistive' : undefined)
|
|
225
227
|
const emptyTitle = computed(() => model.value.emptyReason === 'zero-total'
|
|
226
|
-
? '
|
|
228
|
+
? t('chart.noNonZeroValues')
|
|
227
229
|
: validatedEmptyLabel.value)
|
|
228
230
|
const accessibleLabel = computed(() => accessibility.value.mode === 'label' ? accessibility.value.label : undefined)
|
|
229
231
|
const accessibleDescription = computed(() => accessibility.value.mode === 'label' ? accessibility.value.description : undefined)
|
|
@@ -471,12 +473,12 @@ watchEffect(() => {
|
|
|
471
473
|
v-if="showTable"
|
|
472
474
|
class="s-chart__data-table"
|
|
473
475
|
:class="tableClass"
|
|
474
|
-
:aria-label="accessibility.mode === 'label' ?
|
|
476
|
+
:aria-label="accessibility.mode === 'label' ? t('chart.dataFor', { chart: accessibility.label }) : undefined"
|
|
475
477
|
:aria-labelledby="accessibility.mode === 'labelledby' ? accessibility.id : undefined"
|
|
476
478
|
>
|
|
477
479
|
<thead>
|
|
478
480
|
<tr>
|
|
479
|
-
<th scope="col"
|
|
481
|
+
<th scope="col">{{ t('chart.category') }}</th>
|
|
480
482
|
<th v-for="item in series" :key="item.key" scope="col">{{ item.label }}</th>
|
|
481
483
|
</tr>
|
|
482
484
|
</thead>
|
|
@@ -41,6 +41,7 @@
|
|
|
41
41
|
<script setup lang="ts">
|
|
42
42
|
import { computed, type Component } from 'vue'
|
|
43
43
|
import { X } from '../../icons/sputnigUiIcons'
|
|
44
|
+
import { useUiKitLocale } from '../../../composables/useUiKitLocale'
|
|
44
45
|
import { useOwnedAttrs } from '../../../internal/ownedAttrs'
|
|
45
46
|
import {
|
|
46
47
|
PILL_ICON_CLASS,
|
|
@@ -89,6 +90,7 @@ const emit = defineEmits<{
|
|
|
89
90
|
activate: [event: MouseEvent]
|
|
90
91
|
}>()
|
|
91
92
|
const ownedAttrs = useOwnedAttrs({ component: 'SChip', owner: 'chip root' })
|
|
93
|
+
const { t } = useUiKitLocale()
|
|
92
94
|
|
|
93
95
|
const validatedLabel = computed(() => validateNonEmptyString('SChip', 'label', props.label))
|
|
94
96
|
const validatedSeverity = computed(() => validateExactString(
|
|
@@ -120,7 +122,7 @@ const resolvedCloseLabel = computed(() => {
|
|
|
120
122
|
validateBoolean('SChip', 'closable', props.closable)
|
|
121
123
|
validateBoolean('SChip', 'disabled', props.disabled)
|
|
122
124
|
return props.closeLabel === undefined
|
|
123
|
-
?
|
|
125
|
+
? t('actions.removeChip', { label: validatedLabel.value })
|
|
124
126
|
: validateNonEmptyString('SChip', 'closeLabel', props.closeLabel)
|
|
125
127
|
})
|
|
126
128
|
</script>
|
|
@@ -111,6 +111,10 @@
|
|
|
111
111
|
grid-template-columns: max-content minmax(0, 1fr);
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
+
.s-code-block-plain__line[data-line-numbers='hidden'] {
|
|
115
|
+
display: block;
|
|
116
|
+
}
|
|
117
|
+
|
|
114
118
|
.s-code-block-plain__line-number {
|
|
115
119
|
min-width: 2.5rem;
|
|
116
120
|
padding-inline-end: 0.75rem;
|
|
@@ -22,8 +22,8 @@
|
|
|
22
22
|
size="xs"
|
|
23
23
|
:leading-icon="Copy"
|
|
24
24
|
icon-only
|
|
25
|
-
title="
|
|
26
|
-
aria-label="
|
|
25
|
+
:title="t('clipboard.copyCode')"
|
|
26
|
+
:aria-label="t('clipboard.copyCode')"
|
|
27
27
|
label=""
|
|
28
28
|
@click="copyCode"
|
|
29
29
|
/>
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
@ready="onReady"
|
|
48
48
|
/>
|
|
49
49
|
<pre
|
|
50
|
-
v-else
|
|
50
|
+
v-else-if="validatedPresentation === 'plain'"
|
|
51
51
|
:ref="setPlainSurface"
|
|
52
52
|
class="s-code-block-plain"
|
|
53
53
|
:data-viewport="validatedViewport"
|
|
@@ -61,11 +61,30 @@
|
|
|
61
61
|
:key="index"
|
|
62
62
|
class="s-code-block-plain__line"
|
|
63
63
|
><span class="s-code-block-plain__line-number" aria-hidden="true">{{ index + 1 }}</span><span class="s-code-block-plain__line-content">{{ line || '\u200b' }}</span></span></code></pre>
|
|
64
|
+
<pre
|
|
65
|
+
v-else
|
|
66
|
+
:ref="setPlainSurface"
|
|
67
|
+
class="s-code-block-plain"
|
|
68
|
+
:data-viewport="validatedViewport"
|
|
69
|
+
:aria-label="props.ariaLabel"
|
|
70
|
+
:aria-labelledby="props.ariaLabelledby"
|
|
71
|
+
:style="{ tabSize: validatedTabSize }"
|
|
72
|
+
tabindex="0"
|
|
73
|
+
><span class="s-code-block-plain__lines"><span
|
|
74
|
+
v-for="(line, index) in plainLines"
|
|
75
|
+
:key="index"
|
|
76
|
+
class="s-code-block-plain__line"
|
|
77
|
+
:data-line-numbers="validatedShowLineNumbers ? 'visible' : 'hidden'"
|
|
78
|
+
><span v-if="validatedShowLineNumbers" class="s-code-block-plain__line-number" aria-hidden="true">{{ index + 1 }}</span><span class="s-code-block-plain__line-content"><SCodeTokenLine
|
|
79
|
+
:text="line || '\u200b'"
|
|
80
|
+
:language="validatedLanguage"
|
|
81
|
+
variant="code-block"
|
|
82
|
+
/></span></span></span></pre>
|
|
64
83
|
</div>
|
|
65
84
|
<SAsyncState
|
|
66
85
|
v-if="validatedErrorMessage !== undefined || (validatedPresentation === 'editor' && !isReady)"
|
|
67
86
|
:state="validatedErrorMessage === undefined
|
|
68
|
-
? { status: 'loading', message: '
|
|
87
|
+
? { status: 'loading', message: t('editor.loading') }
|
|
69
88
|
: { status: 'error', message: validatedErrorMessage }"
|
|
70
89
|
presentation="overlay"
|
|
71
90
|
/>
|
|
@@ -80,6 +99,7 @@ import { validateCodeEditorLanguage, validateCodeEditorViewport } from '../../..
|
|
|
80
99
|
import { resolveCodeLanguageKey } from '../../../internal/codeLanguageIdentity';
|
|
81
100
|
import { useNotifier } from '../../../composables/useNotifier';
|
|
82
101
|
import { useClipboard } from '../../../composables/useClipboard';
|
|
102
|
+
import { useUiKitLocale } from '../../../composables/useUiKitLocale';
|
|
83
103
|
import SButton from '../controls/SButton.vue';
|
|
84
104
|
import type {
|
|
85
105
|
SCodeEditorCapabilities,
|
|
@@ -91,6 +111,7 @@ import type {
|
|
|
91
111
|
} from './SCodeEditor.vue';
|
|
92
112
|
import { Copy } from '../../icons/sputnigUiIcons';
|
|
93
113
|
import SAsyncState from '../feedback/SAsyncState.vue';
|
|
114
|
+
import SCodeTokenLine from './_internal/SCodeTokenLine.vue';
|
|
94
115
|
|
|
95
116
|
defineOptions({ inheritAttrs: false });
|
|
96
117
|
|
|
@@ -102,7 +123,7 @@ type AccessibleName =
|
|
|
102
123
|
export type Props = AccessibleName & {
|
|
103
124
|
code: string;
|
|
104
125
|
language: SCodeEditorLanguage;
|
|
105
|
-
presentation?: 'editor' | 'plain';
|
|
126
|
+
presentation?: 'editor' | 'plain' | 'highlighted';
|
|
106
127
|
showHeader?: boolean;
|
|
107
128
|
showLineNumbers?: boolean;
|
|
108
129
|
readOnly?: boolean;
|
|
@@ -150,7 +171,7 @@ const validatedShowHeader = computed(() => validateBoolean('SCodeBlock', 'showHe
|
|
|
150
171
|
const validatedShowLineNumbers = computed(() => validateBoolean('SCodeBlock', 'showLineNumbers', props.showLineNumbers));
|
|
151
172
|
const validatedReadOnly = computed(() => validateBoolean('SCodeBlock', 'readOnly', props.readOnly));
|
|
152
173
|
const validatedPresentation = computed(() => validateExactString(
|
|
153
|
-
'SCodeBlock', 'presentation', props.presentation, ['editor', 'plain'] as const,
|
|
174
|
+
'SCodeBlock', 'presentation', props.presentation, ['editor', 'plain', 'highlighted'] as const,
|
|
154
175
|
));
|
|
155
176
|
const validatedSurfaceRadius = computed(() => validateExactString(
|
|
156
177
|
'SCodeBlock', 'surfaceRadius', props.surfaceRadius, ['default', 'none', 'xl'] as const,
|
|
@@ -200,7 +221,7 @@ const emitPlainReady = (): void => {
|
|
|
200
221
|
|
|
201
222
|
const syncPresentation = (): void => {
|
|
202
223
|
if (!mounted) return;
|
|
203
|
-
if (validatedPresentation.value
|
|
224
|
+
if (validatedPresentation.value !== 'editor') emitPlainReady();
|
|
204
225
|
else isReady.value = false;
|
|
205
226
|
};
|
|
206
227
|
|
|
@@ -213,14 +234,14 @@ watch(validatedPresentation, syncPresentation);
|
|
|
213
234
|
watch(
|
|
214
235
|
[validatedPresentation, validatedReadOnly],
|
|
215
236
|
([presentation, readOnly]) => {
|
|
216
|
-
if (presentation
|
|
217
|
-
throw new TypeError(
|
|
237
|
+
if (presentation !== 'editor' && !readOnly) {
|
|
238
|
+
throw new TypeError(`SCodeBlock: presentation=${JSON.stringify(presentation)} поддерживает только readOnly=true. / SCodeBlock: presentation=${JSON.stringify(presentation)} supports readOnly=true only.`);
|
|
218
239
|
}
|
|
219
|
-
if (presentation
|
|
220
|
-
throw new TypeError(
|
|
240
|
+
if (presentation !== 'editor' && props.interceptKeys !== undefined) {
|
|
241
|
+
throw new TypeError(`SCodeBlock: presentation=${JSON.stringify(presentation)} не принимает interceptKeys. / SCodeBlock: presentation=${JSON.stringify(presentation)} does not accept interceptKeys.`);
|
|
221
242
|
}
|
|
222
|
-
if (presentation
|
|
223
|
-
throw new TypeError(
|
|
243
|
+
if (presentation !== 'editor' && props.nextSelectionPos !== null) {
|
|
244
|
+
throw new TypeError(`SCodeBlock: presentation=${JSON.stringify(presentation)} не принимает nextSelectionPos. / SCodeBlock: presentation=${JSON.stringify(presentation)} does not accept nextSelectionPos.`);
|
|
224
245
|
}
|
|
225
246
|
},
|
|
226
247
|
{ immediate: true },
|
|
@@ -234,8 +255,8 @@ const setPlainSurface = (element: Element | ComponentPublicInstance | null): voi
|
|
|
234
255
|
};
|
|
235
256
|
|
|
236
257
|
const applySelection = (position: number): void => {
|
|
237
|
-
if (validatedPresentation.value
|
|
238
|
-
throw new Error(
|
|
258
|
+
if (validatedPresentation.value !== 'editor') {
|
|
259
|
+
throw new Error(`SCodeBlock: applySelection недоступен в presentation=${JSON.stringify(validatedPresentation.value)}. / SCodeBlock: applySelection is unavailable in presentation=${JSON.stringify(validatedPresentation.value)}.`);
|
|
239
260
|
}
|
|
240
261
|
const editor = codeEditor.value;
|
|
241
262
|
if (!editor) throw new Error('SCodeBlock: editor не готов; дождитесь ready. / SCodeBlock: editor is not ready; wait for ready.');
|
|
@@ -243,7 +264,7 @@ const applySelection = (position: number): void => {
|
|
|
243
264
|
};
|
|
244
265
|
|
|
245
266
|
const focus = (): void => {
|
|
246
|
-
if (validatedPresentation.value
|
|
267
|
+
if (validatedPresentation.value !== 'editor') {
|
|
247
268
|
const surface = plainSurface.value;
|
|
248
269
|
if (!surface) throw new Error('SCodeBlock: plain surface не готов; дождитесь ready. / SCodeBlock: plain surface is not ready; wait for ready.');
|
|
249
270
|
surface.focus();
|
|
@@ -256,19 +277,20 @@ const focus = (): void => {
|
|
|
256
277
|
|
|
257
278
|
defineExpose({ applySelection, focus });
|
|
258
279
|
const notifier = useNotifier();
|
|
280
|
+
const { t } = useUiKitLocale();
|
|
259
281
|
const { copyText } = useClipboard();
|
|
260
282
|
const copyCode = async () => {
|
|
261
283
|
const result = await copyText(props.code);
|
|
262
284
|
if (result.status === 'copied') {
|
|
263
|
-
notifier.success({ message: '
|
|
285
|
+
notifier.success({ message: t('clipboard.codeCopied') });
|
|
264
286
|
return;
|
|
265
287
|
}
|
|
266
288
|
notifier.danger({
|
|
267
289
|
message: result.status === 'unavailable'
|
|
268
|
-
? '
|
|
290
|
+
? t('clipboard.codeCopyUnavailable')
|
|
269
291
|
: result.status === 'empty'
|
|
270
|
-
? '
|
|
271
|
-
:
|
|
292
|
+
? t('clipboard.emptyCode')
|
|
293
|
+
: `${t('clipboard.codeCopyFailed')} ${result.error.message}`,
|
|
272
294
|
});
|
|
273
295
|
};
|
|
274
296
|
</script>
|
|
@@ -87,6 +87,23 @@
|
|
|
87
87
|
@apply text-surface-400 dark:text-surface-500;
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
+
.s-code-editor :deep(.cm-lintRange-error) {
|
|
91
|
+
text-decoration-color: var(--s-code-editor-diagnostic-error);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.s-code-editor :deep(.cm-lintRange-warning) {
|
|
95
|
+
text-decoration-color: var(--s-code-editor-diagnostic-warning);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.s-code-editor :deep(.cm-lintRange-info),
|
|
99
|
+
.s-code-editor :deep(.cm-lintRange-hint) {
|
|
100
|
+
text-decoration-color: var(--s-code-editor-diagnostic-info);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.s-code-editor__diagnostics {
|
|
104
|
+
@apply sr-only;
|
|
105
|
+
}
|
|
106
|
+
|
|
90
107
|
.s-code-editor :deep(.cm-tooltip) {
|
|
91
108
|
@apply rounded-md border border-surface-200 bg-surface-0 text-surface-900 shadow-lg;
|
|
92
109
|
@apply dark:border-surface-700 dark:bg-surface-800 dark:text-surface-100;
|
|
@@ -13,13 +13,28 @@
|
|
|
13
13
|
@ready="onReady"
|
|
14
14
|
@update:model-value="onUpdate"
|
|
15
15
|
/>
|
|
16
|
+
<div
|
|
17
|
+
v-if="validatedDiagnostics.length > 0"
|
|
18
|
+
:id="diagnosticsOwnerId"
|
|
19
|
+
class="s-code-editor__diagnostics"
|
|
20
|
+
role="status"
|
|
21
|
+
aria-live="polite"
|
|
22
|
+
aria-atomic="true"
|
|
23
|
+
>
|
|
24
|
+
<ul>
|
|
25
|
+
<li v-for="diagnostic in validatedDiagnostics" :key="diagnostic.id">
|
|
26
|
+
{{ diagnostic.message }}
|
|
27
|
+
</li>
|
|
28
|
+
</ul>
|
|
29
|
+
</div>
|
|
16
30
|
</template>
|
|
17
31
|
|
|
18
32
|
<script setup lang="ts">
|
|
19
|
-
import { computed, ref, watch } from 'vue'
|
|
33
|
+
import { computed, onBeforeUnmount, ref, shallowRef, useId, watch } from 'vue'
|
|
20
34
|
import { Codemirror } from 'vue-codemirror'
|
|
21
35
|
import { oneDark } from '@codemirror/theme-one-dark'
|
|
22
36
|
import { EditorSelection, EditorState, type Extension } from '@codemirror/state'
|
|
37
|
+
import { lintGutter, setDiagnostics, type Diagnostic as CodeMirrorDiagnostic } from '@codemirror/lint'
|
|
23
38
|
import { EditorView, lineNumbers, placeholder as codeMirrorPlaceholder } from '@codemirror/view'
|
|
24
39
|
import { useOwnedAttrs } from '../../../internal/ownedAttrs'
|
|
25
40
|
import { useInteractiveLeafRegistration } from '../../../internal/passiveContentContract'
|
|
@@ -42,6 +57,18 @@ import {
|
|
|
42
57
|
validateCodeEditorViewport,
|
|
43
58
|
} from '../../../internal/codeEditorContract'
|
|
44
59
|
import { getCodeMirrorLanguageExtension, resolveCodeLanguageKey } from '../codeLanguages'
|
|
60
|
+
import {
|
|
61
|
+
type SCodeEditorDiagnostic,
|
|
62
|
+
type SCodeEditorDiagnosticId,
|
|
63
|
+
type SCodeEditorApi,
|
|
64
|
+
type SCodeEditorRange,
|
|
65
|
+
type SCodeEditorSelectionRange,
|
|
66
|
+
type SCodeEditorSelectionSnapshot,
|
|
67
|
+
type SCodeEditorViewState,
|
|
68
|
+
validateCodeEditorDiagnostics,
|
|
69
|
+
validateCodeEditorRange,
|
|
70
|
+
validateCodeEditorViewState,
|
|
71
|
+
} from './codeEditor'
|
|
45
72
|
|
|
46
73
|
defineOptions({ inheritAttrs: false })
|
|
47
74
|
|
|
@@ -92,6 +119,8 @@ export type Props = AccessibleName & {
|
|
|
92
119
|
viewport?: SCodeEditorViewport
|
|
93
120
|
variant?: SCodeEditorVariant
|
|
94
121
|
nextSelectionPos?: number | null
|
|
122
|
+
/** Controlled diagnostics expressed only through serializable UI-kit ranges. */
|
|
123
|
+
diagnostics?: readonly SCodeEditorDiagnostic<SCodeEditorDiagnosticId>[]
|
|
95
124
|
/** DOM id supplemental status/diagnostic owner. / DOM id of a supplemental status/diagnostic owner. */
|
|
96
125
|
ariaDescribedby?: string
|
|
97
126
|
/** Помечает editor content невалидным без владения presentation диагностики. / Marks the editor content as invalid without owning diagnostic presentation. */
|
|
@@ -106,6 +135,7 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
106
135
|
viewport: 'content',
|
|
107
136
|
variant: 'code',
|
|
108
137
|
nextSelectionPos: null,
|
|
138
|
+
diagnostics: () => [],
|
|
109
139
|
ariaDescribedby: undefined,
|
|
110
140
|
invalid: false,
|
|
111
141
|
})
|
|
@@ -113,6 +143,8 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
113
143
|
const emit = defineEmits<{
|
|
114
144
|
'update:modelValue': [value: string]
|
|
115
145
|
'cursor-activity': [payload: SCodeEditorCursorActivity]
|
|
146
|
+
'selection-change': [payload: SCodeEditorSelectionRange]
|
|
147
|
+
'view-state-change': [payload: SCodeEditorViewState]
|
|
116
148
|
'selection-applied': [payload: SCodeEditorSelectionAppliedPayload]
|
|
117
149
|
ready: [payload: SCodeEditorReadyPayload]
|
|
118
150
|
}>()
|
|
@@ -122,8 +154,12 @@ useInteractiveLeafRegistration({ owner: 'SCodeEditor' })
|
|
|
122
154
|
|
|
123
155
|
const runtimeTheme = useRuntimeTheme()
|
|
124
156
|
const trustedChannel = useTrustedCodeEditorExtensions()
|
|
125
|
-
const editorView =
|
|
157
|
+
const editorView = shallowRef<EditorView | null>(null)
|
|
126
158
|
const pendingControlledSelection = ref<number | null>(null)
|
|
159
|
+
const diagnosticsOwnerId = `s-code-editor-diagnostics-${useId()}`
|
|
160
|
+
let scrollOwner: HTMLElement | null = null
|
|
161
|
+
let lastSelection: SCodeEditorSelectionRange | null = null
|
|
162
|
+
let lastViewState: SCodeEditorViewState | null = null
|
|
127
163
|
|
|
128
164
|
const validatedModelValue = computed(() => {
|
|
129
165
|
if (typeof props.modelValue === 'string') return props.modelValue
|
|
@@ -137,6 +173,11 @@ const validatedReadOnly = computed(() => validateBoolean('SCodeEditor', 'readOnl
|
|
|
137
173
|
const validatedDisabled = computed(() => validateBoolean('SCodeEditor', 'disabled', props.disabled))
|
|
138
174
|
const validatedInvalid = computed(() => validateBoolean('SCodeEditor', 'invalid', props.invalid))
|
|
139
175
|
const isLocked = computed(() => validatedReadOnly.value || validatedDisabled.value)
|
|
176
|
+
const validatedDiagnostics = computed(() => validateCodeEditorDiagnostics(
|
|
177
|
+
'SCodeEditor',
|
|
178
|
+
props.diagnostics,
|
|
179
|
+
validatedModelValue.value.length,
|
|
180
|
+
))
|
|
140
181
|
|
|
141
182
|
const resolvedLanguage = computed((): SCodeEditorLanguage => {
|
|
142
183
|
const language = validateCodeEditorLanguage('SCodeEditor', props.language)
|
|
@@ -166,9 +207,12 @@ const contentAttributes = computed((): Record<string, string> => {
|
|
|
166
207
|
} else {
|
|
167
208
|
throw new TypeError('SCodeEditor: требуется ariaLabel или ariaLabelledby. / SCodeEditor: ariaLabel or ariaLabelledby is required.')
|
|
168
209
|
}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
210
|
+
attributes.tabindex = validatedDisabled.value ? '-1' : '0'
|
|
211
|
+
const describedBy = props.ariaDescribedby === undefined
|
|
212
|
+
? []
|
|
213
|
+
: [validateDomId('SCodeEditor', 'ariaDescribedby', props.ariaDescribedby)]
|
|
214
|
+
if (validatedDiagnostics.value.length > 0) describedBy.push(diagnosticsOwnerId)
|
|
215
|
+
if (describedBy.length > 0) attributes['aria-describedby'] = describedBy.join(' ')
|
|
172
216
|
if (validatedInvalid.value) attributes['aria-invalid'] = 'true'
|
|
173
217
|
if (validatedReadOnly.value) attributes['aria-readonly'] = 'true'
|
|
174
218
|
if (validatedDisabled.value) attributes['aria-disabled'] = 'true'
|
|
@@ -197,6 +241,7 @@ const resolvedExtensions = computed<Extension[]>(() => {
|
|
|
197
241
|
if (languageExtension) result.push(languageExtension)
|
|
198
242
|
if (runtimeTheme.value.mode === 'dark') result.push(oneDark)
|
|
199
243
|
if (capabilities.lineNumbers) result.push(lineNumbers())
|
|
244
|
+
if (validatedDiagnostics.value.length > 0) result.push(lintGutter())
|
|
200
245
|
if (props.placeholder !== undefined) result.push(codeMirrorPlaceholder(validateNonEmptyString('SCodeEditor', 'placeholder', props.placeholder)))
|
|
201
246
|
result.push(EditorState.readOnly.of(isLocked.value))
|
|
202
247
|
result.push(EditorView.editable.of(!isLocked.value))
|
|
@@ -211,6 +256,7 @@ const resolvedExtensions = computed<Extension[]>(() => {
|
|
|
211
256
|
}
|
|
212
257
|
result.push(EditorView.updateListener.of((update) => {
|
|
213
258
|
editorView.value = update.view
|
|
259
|
+
if (update.selectionSet || update.docChanged) emitSelectionAndViewState(update.view)
|
|
214
260
|
if ((isLocked.value && capabilities.cursorActivity !== 'always') || (!update.selectionSet && !update.docChanged)) return
|
|
215
261
|
const pos = update.state.selection.main.head
|
|
216
262
|
const line = update.state.doc.lineAt(pos)
|
|
@@ -238,6 +284,93 @@ const dispatchSelection = (position: number, source: SCodeEditorSelectionApplied
|
|
|
238
284
|
emit('selection-applied', { position: validated, source })
|
|
239
285
|
}
|
|
240
286
|
|
|
287
|
+
const requireEditorView = (): EditorView => {
|
|
288
|
+
const view = editorView.value
|
|
289
|
+
if (!view) throw new Error('SCodeEditor: editor не готов; дождитесь ready. / SCodeEditor: editor is not ready; wait for ready.')
|
|
290
|
+
return view
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const freezeSelection = (view: EditorView): SCodeEditorSelectionRange => {
|
|
294
|
+
const range = view.state.selection.main
|
|
295
|
+
return Object.freeze({
|
|
296
|
+
anchor: range.anchor,
|
|
297
|
+
head: range.head,
|
|
298
|
+
from: range.from,
|
|
299
|
+
to: range.to,
|
|
300
|
+
empty: range.empty,
|
|
301
|
+
})
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const captureViewStateFrom = (view: EditorView): SCodeEditorViewState => Object.freeze({
|
|
305
|
+
selection: freezeSelection(view),
|
|
306
|
+
scrollTop: view.scrollDOM.scrollTop,
|
|
307
|
+
scrollLeft: view.scrollDOM.scrollLeft,
|
|
308
|
+
})
|
|
309
|
+
|
|
310
|
+
const selectionsEqual = (
|
|
311
|
+
left: SCodeEditorSelectionRange | null,
|
|
312
|
+
right: SCodeEditorSelectionRange,
|
|
313
|
+
): boolean => left !== null
|
|
314
|
+
&& left.anchor === right.anchor
|
|
315
|
+
&& left.head === right.head
|
|
316
|
+
&& left.from === right.from
|
|
317
|
+
&& left.to === right.to
|
|
318
|
+
&& left.empty === right.empty
|
|
319
|
+
|
|
320
|
+
const viewStatesEqual = (left: SCodeEditorViewState | null, right: SCodeEditorViewState): boolean =>
|
|
321
|
+
left !== null
|
|
322
|
+
&& selectionsEqual(left.selection, right.selection)
|
|
323
|
+
&& left.scrollTop === right.scrollTop
|
|
324
|
+
&& left.scrollLeft === right.scrollLeft
|
|
325
|
+
|
|
326
|
+
const emitSelectionAndViewState = (view: EditorView): void => {
|
|
327
|
+
const selection = freezeSelection(view)
|
|
328
|
+
if (!selectionsEqual(lastSelection, selection)) {
|
|
329
|
+
lastSelection = selection
|
|
330
|
+
emit('selection-change', selection)
|
|
331
|
+
}
|
|
332
|
+
const viewState = captureViewStateFrom(view)
|
|
333
|
+
if (!viewStatesEqual(lastViewState, viewState)) {
|
|
334
|
+
lastViewState = viewState
|
|
335
|
+
emit('view-state-change', viewState)
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const emitScrollViewState = (): void => {
|
|
340
|
+
const view = editorView.value
|
|
341
|
+
if (!view) return
|
|
342
|
+
const viewState = captureViewStateFrom(view)
|
|
343
|
+
if (viewStatesEqual(lastViewState, viewState)) return
|
|
344
|
+
lastViewState = viewState
|
|
345
|
+
emit('view-state-change', viewState)
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const detachScrollOwner = (): void => {
|
|
349
|
+
scrollOwner?.removeEventListener('scroll', emitScrollViewState)
|
|
350
|
+
scrollOwner = null
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const attachScrollOwner = (view: EditorView): void => {
|
|
354
|
+
detachScrollOwner()
|
|
355
|
+
scrollOwner = view.scrollDOM
|
|
356
|
+
scrollOwner.addEventListener('scroll', emitScrollViewState, { passive: true })
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const toCodeMirrorDiagnostics = (
|
|
360
|
+
diagnostics: readonly SCodeEditorDiagnostic<SCodeEditorDiagnosticId>[],
|
|
361
|
+
): readonly CodeMirrorDiagnostic[] => diagnostics.map((diagnostic) => ({
|
|
362
|
+
from: diagnostic.from,
|
|
363
|
+
to: diagnostic.to,
|
|
364
|
+
severity: diagnostic.severity,
|
|
365
|
+
message: diagnostic.message,
|
|
366
|
+
...(diagnostic.source === undefined ? {} : { source: diagnostic.source }),
|
|
367
|
+
}))
|
|
368
|
+
|
|
369
|
+
const syncDiagnostics = (view: EditorView): void => {
|
|
370
|
+
const diagnostics = validateCodeEditorDiagnostics('SCodeEditor', props.diagnostics, view.state.doc.length)
|
|
371
|
+
view.dispatch(setDiagnostics(view.state, toCodeMirrorDiagnostics(diagnostics)))
|
|
372
|
+
}
|
|
373
|
+
|
|
241
374
|
const onUpdate = (value: string): void => {
|
|
242
375
|
if (typeof value !== 'string') {
|
|
243
376
|
throw new TypeError('SCodeEditor: CodeMirror update payload должен быть строкой. / SCodeEditor: CodeMirror update payload must be a string.')
|
|
@@ -250,6 +383,8 @@ const onReady = (payload: unknown): void => {
|
|
|
250
383
|
throw new TypeError('SCodeEditor: CodeMirror ready payload должен содержать EditorView. / SCodeEditor: CodeMirror ready payload must contain EditorView.')
|
|
251
384
|
}
|
|
252
385
|
editorView.value = (payload as { view: EditorView }).view
|
|
386
|
+
attachScrollOwner(editorView.value)
|
|
387
|
+
syncDiagnostics(editorView.value)
|
|
253
388
|
const readyPayload: SCodeEditorReadyPayload = {
|
|
254
389
|
documentLength: editorView.value.state.doc.length,
|
|
255
390
|
viewport: validatedViewport.value,
|
|
@@ -276,11 +411,41 @@ watch(() => props.nextSelectionPos, (position) => {
|
|
|
276
411
|
dispatchSelection(position, 'controlled')
|
|
277
412
|
}, { immediate: true })
|
|
278
413
|
|
|
279
|
-
|
|
414
|
+
watch(validatedDiagnostics, () => {
|
|
415
|
+
if (editorView.value) syncDiagnostics(editorView.value)
|
|
416
|
+
})
|
|
417
|
+
|
|
418
|
+
onBeforeUnmount(() => {
|
|
419
|
+
detachScrollOwner()
|
|
420
|
+
editorView.value = null
|
|
421
|
+
lastSelection = null
|
|
422
|
+
lastViewState = null
|
|
423
|
+
})
|
|
424
|
+
|
|
425
|
+
defineExpose<SCodeEditorApi>({
|
|
280
426
|
applySelection: (position: number) => dispatchSelection(position, 'imperative'),
|
|
427
|
+
revealRange: (range: SCodeEditorRange) => {
|
|
428
|
+
const view = requireEditorView()
|
|
429
|
+
const validated = validateCodeEditorRange('SCodeEditor', 'revealRange', range, view.state.doc.length)
|
|
430
|
+
const effect = EditorView.scrollIntoView(EditorSelection.range(validated.from, validated.to), { y: 'nearest' })
|
|
431
|
+
view.dispatch({ effects: effect })
|
|
432
|
+
},
|
|
433
|
+
getSelection: (): SCodeEditorSelectionSnapshot => {
|
|
434
|
+
const view = requireEditorView()
|
|
435
|
+
const selection = freezeSelection(view)
|
|
436
|
+
return Object.freeze({ ...selection, text: view.state.doc.sliceString(selection.from, selection.to) })
|
|
437
|
+
},
|
|
438
|
+
captureViewState: (): SCodeEditorViewState => captureViewStateFrom(requireEditorView()),
|
|
439
|
+
applyViewState: (viewState: SCodeEditorViewState) => {
|
|
440
|
+
const view = requireEditorView()
|
|
441
|
+
const validated = validateCodeEditorViewState('SCodeEditor', viewState, view.state.doc.length)
|
|
442
|
+
view.dispatch({ selection: EditorSelection.range(validated.selection.anchor, validated.selection.head) })
|
|
443
|
+
view.scrollDOM.scrollTop = validated.scrollTop
|
|
444
|
+
view.scrollDOM.scrollLeft = validated.scrollLeft
|
|
445
|
+
emitSelectionAndViewState(view)
|
|
446
|
+
},
|
|
281
447
|
focus: () => {
|
|
282
|
-
|
|
283
|
-
editorView.value.focus()
|
|
448
|
+
requireEditorView().focus()
|
|
284
449
|
},
|
|
285
450
|
})
|
|
286
451
|
</script>
|