@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
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
<div class="s-code-search-panel__field">
|
|
5
5
|
<SInputText
|
|
6
6
|
:model-value="searchValue"
|
|
7
|
-
:placeholder="findPlaceholder"
|
|
8
|
-
:aria-label="findPlaceholder"
|
|
7
|
+
:placeholder="labels.findPlaceholder"
|
|
8
|
+
:aria-label="labels.findPlaceholder"
|
|
9
9
|
:invalid="validatedSearchError !== undefined"
|
|
10
10
|
:error-message="validatedSearchError"
|
|
11
11
|
name="search"
|
|
@@ -22,8 +22,8 @@
|
|
|
22
22
|
size="sm"
|
|
23
23
|
icon-only
|
|
24
24
|
:leading-icon="ChevronUp"
|
|
25
|
-
:title="previousLabel"
|
|
26
|
-
:aria-label="previousLabel"
|
|
25
|
+
:title="labels.previousLabel"
|
|
26
|
+
:aria-label="labels.previousLabel"
|
|
27
27
|
@click="emit('previous')"
|
|
28
28
|
/>
|
|
29
29
|
|
|
@@ -33,8 +33,8 @@
|
|
|
33
33
|
size="sm"
|
|
34
34
|
icon-only
|
|
35
35
|
:leading-icon="ChevronDown"
|
|
36
|
-
:title="nextLabel"
|
|
37
|
-
:aria-label="nextLabel"
|
|
36
|
+
:title="labels.nextLabel"
|
|
37
|
+
:aria-label="labels.nextLabel"
|
|
38
38
|
@click="emit('next')"
|
|
39
39
|
/>
|
|
40
40
|
|
|
@@ -45,8 +45,8 @@
|
|
|
45
45
|
size="sm"
|
|
46
46
|
icon-only
|
|
47
47
|
:leading-icon="ScanSearch"
|
|
48
|
-
:title="selectAllLabel"
|
|
49
|
-
:aria-label="selectAllLabel"
|
|
48
|
+
:title="labels.selectAllLabel"
|
|
49
|
+
:aria-label="labels.selectAllLabel"
|
|
50
50
|
@click="emit('selectAll')"
|
|
51
51
|
/>
|
|
52
52
|
|
|
@@ -57,8 +57,8 @@
|
|
|
57
57
|
size="sm"
|
|
58
58
|
icon-only
|
|
59
59
|
:leading-icon="X"
|
|
60
|
-
:title="closeLabel"
|
|
61
|
-
:aria-label="closeLabel"
|
|
60
|
+
:title="labels.closeLabel"
|
|
61
|
+
:aria-label="labels.closeLabel"
|
|
62
62
|
@click="emit('close')"
|
|
63
63
|
/>
|
|
64
64
|
</div>
|
|
@@ -71,8 +71,8 @@
|
|
|
71
71
|
size="sm"
|
|
72
72
|
icon-only
|
|
73
73
|
:leading-icon="CaseSensitive"
|
|
74
|
-
:title="matchCaseLabel"
|
|
75
|
-
:aria-label="matchCaseLabel"
|
|
74
|
+
:title="labels.matchCaseLabel"
|
|
75
|
+
:aria-label="labels.matchCaseLabel"
|
|
76
76
|
:pressed="caseSensitive"
|
|
77
77
|
@click="emit('update:caseSensitive', !caseSensitive)"
|
|
78
78
|
/>
|
|
@@ -83,8 +83,8 @@
|
|
|
83
83
|
size="sm"
|
|
84
84
|
icon-only
|
|
85
85
|
:leading-icon="Regex"
|
|
86
|
-
:title="regexpLabel"
|
|
87
|
-
:aria-label="regexpLabel"
|
|
86
|
+
:title="labels.regexpLabel"
|
|
87
|
+
:aria-label="labels.regexpLabel"
|
|
88
88
|
:pressed="regexp"
|
|
89
89
|
@click="emit('update:regexp', !regexp)"
|
|
90
90
|
/>
|
|
@@ -95,8 +95,8 @@
|
|
|
95
95
|
size="sm"
|
|
96
96
|
icon-only
|
|
97
97
|
:leading-icon="WholeWord"
|
|
98
|
-
:title="wholeWordLabel"
|
|
99
|
-
:aria-label="wholeWordLabel"
|
|
98
|
+
:title="labels.wholeWordLabel"
|
|
99
|
+
:aria-label="labels.wholeWordLabel"
|
|
100
100
|
:pressed="wholeWord"
|
|
101
101
|
@click="emit('update:wholeWord', !wholeWord)"
|
|
102
102
|
/>
|
|
@@ -109,8 +109,8 @@
|
|
|
109
109
|
<div class="s-code-search-panel__field">
|
|
110
110
|
<SInputText
|
|
111
111
|
:model-value="replaceValue"
|
|
112
|
-
:placeholder="replacePlaceholder"
|
|
113
|
-
:aria-label="replacePlaceholder"
|
|
112
|
+
:placeholder="labels.replacePlaceholder"
|
|
113
|
+
:aria-label="labels.replacePlaceholder"
|
|
114
114
|
:invalid="validatedReplaceError !== undefined"
|
|
115
115
|
:error-message="validatedReplaceError"
|
|
116
116
|
name="replace"
|
|
@@ -126,8 +126,8 @@
|
|
|
126
126
|
size="sm"
|
|
127
127
|
icon-only
|
|
128
128
|
:leading-icon="Replace"
|
|
129
|
-
:title="replaceLabel"
|
|
130
|
-
:aria-label="replaceLabel"
|
|
129
|
+
:title="labels.replaceLabel"
|
|
130
|
+
:aria-label="labels.replaceLabel"
|
|
131
131
|
@click="emit('replace')"
|
|
132
132
|
/>
|
|
133
133
|
|
|
@@ -138,8 +138,8 @@
|
|
|
138
138
|
size="sm"
|
|
139
139
|
icon-only
|
|
140
140
|
:leading-icon="ReplaceAll"
|
|
141
|
-
:title="replaceAllLabel"
|
|
142
|
-
:aria-label="replaceAllLabel"
|
|
141
|
+
:title="labels.replaceAllLabel"
|
|
142
|
+
:aria-label="labels.replaceAllLabel"
|
|
143
143
|
@click="emit('replaceAll')"
|
|
144
144
|
/>
|
|
145
145
|
</div>
|
|
@@ -162,6 +162,7 @@ import {
|
|
|
162
162
|
} from '../../icons/sputnigUiIcons';
|
|
163
163
|
import { useOwnedAttrs } from '../../../internal/ownedAttrs';
|
|
164
164
|
import { validateNonEmptyString } from '../../../internal/runtimeContract';
|
|
165
|
+
import { useUiKitLocale } from '../../../composables/useUiKitLocale';
|
|
165
166
|
|
|
166
167
|
import SButton from '../controls/SButton.vue';
|
|
167
168
|
import SInputText from '../controls/SInputText.vue';
|
|
@@ -192,20 +193,34 @@ export interface Props {
|
|
|
192
193
|
|
|
193
194
|
const props = withDefaults(defineProps<Props>(), {
|
|
194
195
|
canReplace: true,
|
|
195
|
-
findPlaceholder:
|
|
196
|
-
replacePlaceholder:
|
|
197
|
-
nextLabel:
|
|
198
|
-
previousLabel:
|
|
199
|
-
selectAllLabel:
|
|
200
|
-
matchCaseLabel:
|
|
201
|
-
regexpLabel:
|
|
202
|
-
wholeWordLabel:
|
|
203
|
-
replaceLabel:
|
|
204
|
-
replaceAllLabel:
|
|
205
|
-
closeLabel:
|
|
196
|
+
findPlaceholder: undefined,
|
|
197
|
+
replacePlaceholder: undefined,
|
|
198
|
+
nextLabel: undefined,
|
|
199
|
+
previousLabel: undefined,
|
|
200
|
+
selectAllLabel: undefined,
|
|
201
|
+
matchCaseLabel: undefined,
|
|
202
|
+
regexpLabel: undefined,
|
|
203
|
+
wholeWordLabel: undefined,
|
|
204
|
+
replaceLabel: undefined,
|
|
205
|
+
replaceAllLabel: undefined,
|
|
206
|
+
closeLabel: undefined,
|
|
206
207
|
searchError: undefined,
|
|
207
208
|
replaceError: undefined,
|
|
208
209
|
});
|
|
210
|
+
const { t } = useUiKitLocale();
|
|
211
|
+
const labels = computed(() => ({
|
|
212
|
+
findPlaceholder: validateNonEmptyString('SCodeSearchPanel', 'findPlaceholder', props.findPlaceholder ?? t('editor.find')),
|
|
213
|
+
replacePlaceholder: validateNonEmptyString('SCodeSearchPanel', 'replacePlaceholder', props.replacePlaceholder ?? t('editor.replace')),
|
|
214
|
+
nextLabel: validateNonEmptyString('SCodeSearchPanel', 'nextLabel', props.nextLabel ?? t('editor.nextMatchShort')),
|
|
215
|
+
previousLabel: validateNonEmptyString('SCodeSearchPanel', 'previousLabel', props.previousLabel ?? t('editor.previousMatchShort')),
|
|
216
|
+
selectAllLabel: validateNonEmptyString('SCodeSearchPanel', 'selectAllLabel', props.selectAllLabel ?? t('editor.selectAllMatches')),
|
|
217
|
+
matchCaseLabel: validateNonEmptyString('SCodeSearchPanel', 'matchCaseLabel', props.matchCaseLabel ?? t('editor.matchCase')),
|
|
218
|
+
regexpLabel: validateNonEmptyString('SCodeSearchPanel', 'regexpLabel', props.regexpLabel ?? t('editor.regularExpression')),
|
|
219
|
+
wholeWordLabel: validateNonEmptyString('SCodeSearchPanel', 'wholeWordLabel', props.wholeWordLabel ?? t('editor.wholeWord')),
|
|
220
|
+
replaceLabel: validateNonEmptyString('SCodeSearchPanel', 'replaceLabel', props.replaceLabel ?? t('editor.replaceOne')),
|
|
221
|
+
replaceAllLabel: validateNonEmptyString('SCodeSearchPanel', 'replaceAllLabel', props.replaceAllLabel ?? t('editor.replaceAll')),
|
|
222
|
+
closeLabel: validateNonEmptyString('SCodeSearchPanel', 'closeLabel', props.closeLabel ?? t('actions.close')),
|
|
223
|
+
}));
|
|
209
224
|
|
|
210
225
|
const emit = defineEmits<{
|
|
211
226
|
(e: 'update:searchValue', value: string): void;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
@reference "../../../styles/reference.css";
|
|
2
|
+
|
|
3
|
+
.s-code-workspace-editor {
|
|
4
|
+
@apply flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden bg-surface-0 dark:bg-surface-900;
|
|
5
|
+
width: 100%;
|
|
6
|
+
height: 100%;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.s-code-workspace-editor[data-empty='true'] {
|
|
10
|
+
@apply items-center justify-center;
|
|
11
|
+
min-height: var(--s-code-editor-viewport-compact);
|
|
12
|
+
padding: var(--s-space-section-gap);
|
|
13
|
+
}
|
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div
|
|
3
|
+
v-bind="ownedAttrs.bindings()"
|
|
4
|
+
class="s-code-workspace-editor"
|
|
5
|
+
:data-empty="records.length === 0 || undefined"
|
|
6
|
+
>
|
|
7
|
+
<SEmptyState
|
|
8
|
+
v-if="records.length === 0"
|
|
9
|
+
:title="resolvedEmptyTitle"
|
|
10
|
+
size="sm"
|
|
11
|
+
presentation="inline"
|
|
12
|
+
/>
|
|
13
|
+
<STabs
|
|
14
|
+
v-else
|
|
15
|
+
:model-value="validatedActiveKey ?? undefined"
|
|
16
|
+
:aria-label="validatedAriaLabel"
|
|
17
|
+
:controls-aria-label="resolvedTabActionsLabel"
|
|
18
|
+
tab-sizing="content"
|
|
19
|
+
tab-list-width="full"
|
|
20
|
+
tab-density="compact"
|
|
21
|
+
surface-radius="none"
|
|
22
|
+
grow
|
|
23
|
+
@update:model-value="activateDocument"
|
|
24
|
+
>
|
|
25
|
+
<template #list>
|
|
26
|
+
<STab
|
|
27
|
+
v-for="record in records"
|
|
28
|
+
:key="record.document.sessionKey"
|
|
29
|
+
:name="record.document.key"
|
|
30
|
+
:title="record.document.label"
|
|
31
|
+
>
|
|
32
|
+
{{ record.document.label }}
|
|
33
|
+
</STab>
|
|
34
|
+
</template>
|
|
35
|
+
<template v-if="activeRecord?.document.closable" #controls>
|
|
36
|
+
<SButton
|
|
37
|
+
label=""
|
|
38
|
+
:aria-label="closeLabel(activeRecord.document.label)"
|
|
39
|
+
:leading-icon="X"
|
|
40
|
+
appearance="ghost"
|
|
41
|
+
severity="secondary"
|
|
42
|
+
size="xs"
|
|
43
|
+
shape="square"
|
|
44
|
+
icon-only
|
|
45
|
+
@click="requestRelease(activeRecord.document)"
|
|
46
|
+
/>
|
|
47
|
+
</template>
|
|
48
|
+
<template #panels>
|
|
49
|
+
<STabPanel
|
|
50
|
+
v-for="record in records"
|
|
51
|
+
:key="record.document.sessionKey"
|
|
52
|
+
:name="record.document.key"
|
|
53
|
+
overflow="hidden"
|
|
54
|
+
>
|
|
55
|
+
<SCodeEditor
|
|
56
|
+
:key="`${record.document.sessionKey}:${record.epoch}`"
|
|
57
|
+
:ref="(instance) => setEditorRef(record.document.sessionKey, instance)"
|
|
58
|
+
:model-value="record.document.value"
|
|
59
|
+
:language="record.document.language"
|
|
60
|
+
:capabilities="record.document.capabilities"
|
|
61
|
+
:diagnostics="record.document.diagnostics"
|
|
62
|
+
:read-only="record.document.readOnly"
|
|
63
|
+
:disabled="record.document.disabled"
|
|
64
|
+
:invalid="record.document.invalid"
|
|
65
|
+
:placeholder="record.document.placeholder"
|
|
66
|
+
:variant="record.document.variant"
|
|
67
|
+
:aria-label="record.document.label"
|
|
68
|
+
viewport="fill"
|
|
69
|
+
@update:model-value="emitDocumentChange(record, $event)"
|
|
70
|
+
@selection-change="emitSelectionChange(record, $event)"
|
|
71
|
+
@view-state-change="emitViewStateChange(record, $event)"
|
|
72
|
+
/>
|
|
73
|
+
</STabPanel>
|
|
74
|
+
</template>
|
|
75
|
+
</STabs>
|
|
76
|
+
</div>
|
|
77
|
+
</template>
|
|
78
|
+
|
|
79
|
+
<script setup lang="ts">
|
|
80
|
+
import { computed, defineAsyncComponent, onBeforeUnmount, shallowRef, watch } from 'vue'
|
|
81
|
+
import { X } from '../../icons/sputnigUiIcons'
|
|
82
|
+
import { useOwnedAttrs } from '../../../internal/ownedAttrs'
|
|
83
|
+
import { useUiKitLocale } from '../../../composables/useUiKitLocale'
|
|
84
|
+
import { validateNonEmptyString } from '../../../internal/runtimeContract'
|
|
85
|
+
import SButton from '../controls/SButton.vue'
|
|
86
|
+
import STab from '../navigation/STab.vue'
|
|
87
|
+
import STabPanel from '../navigation/STabPanel.vue'
|
|
88
|
+
import STabs from '../navigation/STabs.vue'
|
|
89
|
+
import SEmptyState from './SEmptyState.vue'
|
|
90
|
+
import type {
|
|
91
|
+
SCodeEditorApi,
|
|
92
|
+
SCodeEditorSelectionRange,
|
|
93
|
+
SCodeEditorViewState,
|
|
94
|
+
} from './codeEditor'
|
|
95
|
+
import {
|
|
96
|
+
validateCodeWorkspaceActiveKey,
|
|
97
|
+
validateCodeWorkspaceDocuments,
|
|
98
|
+
type SCodeWorkspaceDocument,
|
|
99
|
+
type SCodeWorkspaceDocumentChange,
|
|
100
|
+
type SCodeWorkspaceDocumentKey,
|
|
101
|
+
type SCodeWorkspaceDocumentState,
|
|
102
|
+
type SCodeWorkspaceEditorApi,
|
|
103
|
+
type SCodeWorkspaceReleaseRequest,
|
|
104
|
+
type SCodeWorkspaceSelectionChange,
|
|
105
|
+
type SCodeWorkspaceSessionKey,
|
|
106
|
+
type SCodeWorkspaceViewStateChange,
|
|
107
|
+
} from './codeWorkspace'
|
|
108
|
+
|
|
109
|
+
defineOptions({ inheritAttrs: false })
|
|
110
|
+
|
|
111
|
+
const SCodeEditor = defineAsyncComponent(() => import('../../../internal/lazyCodeEditor'))
|
|
112
|
+
|
|
113
|
+
/** Keyed editor workspace. Consumer owns text/revisions; UI-kit owns mounted editor-session resources. */
|
|
114
|
+
export interface Props {
|
|
115
|
+
/** Current controlled document inventory. Sessions must be explicitly released before removal. */
|
|
116
|
+
documents: readonly SCodeWorkspaceDocument[]
|
|
117
|
+
/** Controlled active document key, or null for an empty inventory. */
|
|
118
|
+
activeKey: SCodeWorkspaceDocumentKey | null
|
|
119
|
+
/** Accessible name for the workspace tab list. */
|
|
120
|
+
ariaLabel: string
|
|
121
|
+
/** Optional empty-state title. */
|
|
122
|
+
emptyTitle?: string
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
interface SessionRecord {
|
|
126
|
+
readonly document: SCodeWorkspaceDocument
|
|
127
|
+
readonly epoch: number
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
131
|
+
emptyTitle: undefined,
|
|
132
|
+
})
|
|
133
|
+
const emit = defineEmits<{
|
|
134
|
+
'update:activeKey': [key: SCodeWorkspaceDocumentKey]
|
|
135
|
+
'document-change': [payload: SCodeWorkspaceDocumentChange]
|
|
136
|
+
'selection-change': [payload: SCodeWorkspaceSelectionChange]
|
|
137
|
+
'view-state-change': [payload: SCodeWorkspaceViewStateChange]
|
|
138
|
+
'release-request': [payload: SCodeWorkspaceReleaseRequest]
|
|
139
|
+
}>()
|
|
140
|
+
|
|
141
|
+
const ownedAttrs = useOwnedAttrs({
|
|
142
|
+
component: 'SCodeWorkspaceEditor',
|
|
143
|
+
owner: 'keyed code-editor workspace root',
|
|
144
|
+
})
|
|
145
|
+
const { t } = useUiKitLocale()
|
|
146
|
+
const records = shallowRef<readonly SessionRecord[]>([])
|
|
147
|
+
const releasedSessionKeys = new Set<SCodeWorkspaceSessionKey>()
|
|
148
|
+
const editorRefs = new Map<SCodeWorkspaceSessionKey, SCodeEditorApi>()
|
|
149
|
+
|
|
150
|
+
const validatedDocuments = computed(() => validateCodeWorkspaceDocuments(
|
|
151
|
+
'SCodeWorkspaceEditor',
|
|
152
|
+
props.documents,
|
|
153
|
+
))
|
|
154
|
+
const validatedActiveKey = computed(() => validateCodeWorkspaceActiveKey(
|
|
155
|
+
'SCodeWorkspaceEditor',
|
|
156
|
+
props.activeKey,
|
|
157
|
+
validatedDocuments.value,
|
|
158
|
+
))
|
|
159
|
+
const activeRecord = computed(() => records.value.find(
|
|
160
|
+
(record) => record.document.key === validatedActiveKey.value,
|
|
161
|
+
))
|
|
162
|
+
const validatedAriaLabel = computed(() => validateNonEmptyString(
|
|
163
|
+
'SCodeWorkspaceEditor',
|
|
164
|
+
'ariaLabel',
|
|
165
|
+
props.ariaLabel,
|
|
166
|
+
))
|
|
167
|
+
const resolvedEmptyTitle = computed(() => props.emptyTitle === undefined
|
|
168
|
+
? t('editor.workspaceEmpty')
|
|
169
|
+
: validateNonEmptyString('SCodeWorkspaceEditor', 'emptyTitle', props.emptyTitle))
|
|
170
|
+
const resolvedTabActionsLabel = computed(() => t('navigation.tabActions'))
|
|
171
|
+
|
|
172
|
+
function sameRevision(
|
|
173
|
+
left: SCodeWorkspaceDocument['revision'],
|
|
174
|
+
right: SCodeWorkspaceDocument['revision'],
|
|
175
|
+
): boolean {
|
|
176
|
+
return typeof left === typeof right && left === right
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function synchronizeSessions(documents: readonly SCodeWorkspaceDocument[]): void {
|
|
180
|
+
const previousBySession = new Map(records.value.map((record) => [record.document.sessionKey, record]))
|
|
181
|
+
const nextSessionKeys = new Set(documents.map((document) => document.sessionKey))
|
|
182
|
+
const missing = records.value.find((record) => !nextSessionKeys.has(record.document.sessionKey))
|
|
183
|
+
if (missing) {
|
|
184
|
+
throw new Error(
|
|
185
|
+
`SCodeWorkspaceEditor: session ${JSON.stringify(missing.document.sessionKey)} удалена из documents без releaseSession(). `
|
|
186
|
+
+ `/ SCodeWorkspaceEditor: session ${JSON.stringify(missing.document.sessionKey)} was removed from documents without releaseSession().`,
|
|
187
|
+
)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
records.value = documents.map((document) => {
|
|
191
|
+
if (releasedSessionKeys.has(document.sessionKey)) {
|
|
192
|
+
throw new Error(
|
|
193
|
+
`SCodeWorkspaceEditor: sessionKey ${JSON.stringify(document.sessionKey)} уже освобождён; откройте документ с новым sessionKey. `
|
|
194
|
+
+ `/ SCodeWorkspaceEditor: sessionKey ${JSON.stringify(document.sessionKey)} was released; reopen with a new sessionKey.`,
|
|
195
|
+
)
|
|
196
|
+
}
|
|
197
|
+
const previous = previousBySession.get(document.sessionKey)
|
|
198
|
+
if (!previous) return Object.freeze({ document, epoch: 0 })
|
|
199
|
+
if (previous.document.key !== document.key) {
|
|
200
|
+
throw new Error(
|
|
201
|
+
`SCodeWorkspaceEditor: sessionKey ${JSON.stringify(document.sessionKey)} не может менять document key. `
|
|
202
|
+
+ `/ SCodeWorkspaceEditor: sessionKey ${JSON.stringify(document.sessionKey)} cannot change document key.`,
|
|
203
|
+
)
|
|
204
|
+
}
|
|
205
|
+
const epoch = sameRevision(previous.document.revision, document.revision)
|
|
206
|
+
? previous.epoch
|
|
207
|
+
: previous.epoch + 1
|
|
208
|
+
return Object.freeze({ document, epoch })
|
|
209
|
+
})
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
watch(validatedDocuments, synchronizeSessions, { immediate: true })
|
|
213
|
+
watch(validatedActiveKey, () => undefined, { immediate: true })
|
|
214
|
+
|
|
215
|
+
function isCodeEditorApi(value: unknown): value is SCodeEditorApi {
|
|
216
|
+
if (typeof value !== 'object' || value === null) return false
|
|
217
|
+
const candidate = value as Partial<Record<keyof SCodeEditorApi, unknown>>
|
|
218
|
+
return typeof candidate.applySelection === 'function'
|
|
219
|
+
&& typeof candidate.revealRange === 'function'
|
|
220
|
+
&& typeof candidate.getSelection === 'function'
|
|
221
|
+
&& typeof candidate.captureViewState === 'function'
|
|
222
|
+
&& typeof candidate.applyViewState === 'function'
|
|
223
|
+
&& typeof candidate.focus === 'function'
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function setEditorRef(sessionKey: SCodeWorkspaceSessionKey, instance: unknown): void {
|
|
227
|
+
if (instance === null) {
|
|
228
|
+
editorRefs.delete(sessionKey)
|
|
229
|
+
return
|
|
230
|
+
}
|
|
231
|
+
if (!isCodeEditorApi(instance)) {
|
|
232
|
+
throw new TypeError('SCodeWorkspaceEditor: SCodeEditor ref does not implement the public SCodeEditorApi.')
|
|
233
|
+
}
|
|
234
|
+
editorRefs.set(sessionKey, instance)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function requireRecordByKey(key: SCodeWorkspaceDocumentKey): SessionRecord {
|
|
238
|
+
const validatedKey = validateNonEmptyString('SCodeWorkspaceEditor', 'document key', key)
|
|
239
|
+
const record = records.value.find((candidate) => candidate.document.key === validatedKey)
|
|
240
|
+
if (!record) {
|
|
241
|
+
throw new RangeError(
|
|
242
|
+
`SCodeWorkspaceEditor: document key ${JSON.stringify(validatedKey)} отсутствует. `
|
|
243
|
+
+ `/ SCodeWorkspaceEditor: document key ${JSON.stringify(validatedKey)} is unavailable.`,
|
|
244
|
+
)
|
|
245
|
+
}
|
|
246
|
+
return record
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function requireEditor(record: SessionRecord): SCodeEditorApi {
|
|
250
|
+
const editor = editorRefs.get(record.document.sessionKey)
|
|
251
|
+
if (!editor) {
|
|
252
|
+
throw new Error(
|
|
253
|
+
`SCodeWorkspaceEditor: editor session ${JSON.stringify(record.document.sessionKey)} не готова. `
|
|
254
|
+
+ `/ SCodeWorkspaceEditor: editor session ${JSON.stringify(record.document.sessionKey)} is not ready.`,
|
|
255
|
+
)
|
|
256
|
+
}
|
|
257
|
+
return editor
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function activateDocument(key: SCodeWorkspaceDocumentKey): void {
|
|
261
|
+
requireRecordByKey(key)
|
|
262
|
+
emit('update:activeKey', key)
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function closeLabel(label: string): string {
|
|
266
|
+
return t('editor.closeDocument', { name: label })
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function requestRelease(document: SCodeWorkspaceDocument): void {
|
|
270
|
+
emit('release-request', Object.freeze({
|
|
271
|
+
key: document.key,
|
|
272
|
+
sessionKey: document.sessionKey,
|
|
273
|
+
}))
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function emitDocumentChange(record: SessionRecord, value: string): void {
|
|
277
|
+
emit('document-change', Object.freeze({
|
|
278
|
+
key: record.document.key,
|
|
279
|
+
sessionKey: record.document.sessionKey,
|
|
280
|
+
revision: record.document.revision,
|
|
281
|
+
value,
|
|
282
|
+
}))
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function emitSelectionChange(record: SessionRecord, selection: SCodeEditorSelectionRange): void {
|
|
286
|
+
emit('selection-change', Object.freeze({
|
|
287
|
+
key: record.document.key,
|
|
288
|
+
sessionKey: record.document.sessionKey,
|
|
289
|
+
selection,
|
|
290
|
+
}))
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function emitViewStateChange(record: SessionRecord, viewState: SCodeEditorViewState): void {
|
|
294
|
+
emit('view-state-change', Object.freeze({
|
|
295
|
+
key: record.document.key,
|
|
296
|
+
sessionKey: record.document.sessionKey,
|
|
297
|
+
viewState,
|
|
298
|
+
}))
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function releaseSession(sessionKey: SCodeWorkspaceSessionKey): void {
|
|
302
|
+
const validatedSessionKey = validateNonEmptyString('SCodeWorkspaceEditor', 'sessionKey', sessionKey)
|
|
303
|
+
if (releasedSessionKeys.has(validatedSessionKey)) {
|
|
304
|
+
throw new Error(
|
|
305
|
+
`SCodeWorkspaceEditor: session ${JSON.stringify(validatedSessionKey)} уже освобождена. `
|
|
306
|
+
+ `/ SCodeWorkspaceEditor: session ${JSON.stringify(validatedSessionKey)} is already released.`,
|
|
307
|
+
)
|
|
308
|
+
}
|
|
309
|
+
if (!records.value.some((record) => record.document.sessionKey === validatedSessionKey)) {
|
|
310
|
+
throw new RangeError(
|
|
311
|
+
`SCodeWorkspaceEditor: session ${JSON.stringify(validatedSessionKey)} отсутствует. `
|
|
312
|
+
+ `/ SCodeWorkspaceEditor: session ${JSON.stringify(validatedSessionKey)} is unavailable.`,
|
|
313
|
+
)
|
|
314
|
+
}
|
|
315
|
+
releasedSessionKeys.add(validatedSessionKey)
|
|
316
|
+
editorRefs.delete(validatedSessionKey)
|
|
317
|
+
records.value = records.value.filter((record) => record.document.sessionKey !== validatedSessionKey)
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const api: SCodeWorkspaceEditorApi = {
|
|
321
|
+
focusActive: () => {
|
|
322
|
+
if (validatedActiveKey.value === null) {
|
|
323
|
+
throw new Error('SCodeWorkspaceEditor: пустой workspace не имеет active editor. / SCodeWorkspaceEditor: an empty workspace has no active editor.')
|
|
324
|
+
}
|
|
325
|
+
const record = requireRecordByKey(validatedActiveKey.value)
|
|
326
|
+
requireEditor(record).focus()
|
|
327
|
+
},
|
|
328
|
+
revealRange: (key, range) => {
|
|
329
|
+
const record = requireRecordByKey(key)
|
|
330
|
+
requireEditor(record).revealRange(range)
|
|
331
|
+
},
|
|
332
|
+
captureDocumentState: (key): SCodeWorkspaceDocumentState => {
|
|
333
|
+
const record = requireRecordByKey(key)
|
|
334
|
+
const editor = requireEditor(record)
|
|
335
|
+
return Object.freeze({
|
|
336
|
+
key: record.document.key,
|
|
337
|
+
sessionKey: record.document.sessionKey,
|
|
338
|
+
revision: record.document.revision,
|
|
339
|
+
selection: editor.getSelection(),
|
|
340
|
+
viewState: editor.captureViewState(),
|
|
341
|
+
})
|
|
342
|
+
},
|
|
343
|
+
releaseSession,
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
defineExpose<SCodeWorkspaceEditorApi>(api)
|
|
347
|
+
|
|
348
|
+
onBeforeUnmount(() => {
|
|
349
|
+
editorRefs.clear()
|
|
350
|
+
releasedSessionKeys.clear()
|
|
351
|
+
records.value = []
|
|
352
|
+
})
|
|
353
|
+
</script>
|
|
354
|
+
|
|
355
|
+
<style lang="postcss" src="./SCodeWorkspaceEditor.css" scoped></style>
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
@reference "../../../styles/reference.css";
|
|
2
|
+
|
|
3
|
+
.s-toast-container {
|
|
4
|
+
@apply fixed flex max-h-[100dvh] min-w-0 w-full max-w-md flex-col overflow-y-auto overscroll-contain p-4;
|
|
5
|
+
z-index: var(--s-layer-root-toast);
|
|
6
|
+
pointer-events: none;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.s-toast-container[data-position^='top-'] {
|
|
10
|
+
inset-block-start: 0;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.s-toast-container[data-position^='bottom-'] {
|
|
14
|
+
inset-block-end: 0;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.s-toast-container[data-position$='-start'] {
|
|
18
|
+
inset-inline-start: 0;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.s-toast-container[data-position$='-end'] {
|
|
22
|
+
inset-inline-end: 0;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.s-toast-container[data-position$='-center'] {
|
|
26
|
+
inset-inline: 0;
|
|
27
|
+
margin-inline: auto;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.s-toast-transition-wrapper {
|
|
31
|
+
@apply relative flex flex-col gap-2;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.s-toast-container[data-position$='-start'] .s-toast-transition-wrapper {
|
|
35
|
+
align-items: flex-start;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.s-toast-container[data-position$='-center'] .s-toast-transition-wrapper {
|
|
39
|
+
align-items: center;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.s-toast-container[data-position$='-end'] .s-toast-transition-wrapper {
|
|
43
|
+
align-items: flex-end;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.notification-transition-enter-active {
|
|
47
|
+
transition:
|
|
48
|
+
opacity var(--s-motion-standard) ease-out,
|
|
49
|
+
transform var(--s-motion-standard) ease-out;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.notification-transition-enter-from {
|
|
53
|
+
opacity: 0;
|
|
54
|
+
transform: translate3d(0, 0.75rem, 0) scale(0.98);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.notification-transition-enter-to {
|
|
58
|
+
opacity: 1;
|
|
59
|
+
transform: translate3d(0, 0, 0) scale(1);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.notification-transition-leave-active {
|
|
63
|
+
position: absolute;
|
|
64
|
+
transition:
|
|
65
|
+
opacity var(--s-motion-fast) ease-in,
|
|
66
|
+
transform var(--s-motion-fast) ease-in;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.notification-transition-leave-from {
|
|
70
|
+
opacity: 1;
|
|
71
|
+
transform: translate3d(0, 0, 0) scale(1);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.notification-transition-leave-to {
|
|
75
|
+
opacity: 0;
|
|
76
|
+
transform: translate3d(0, 0.5rem, 0) scale(0.98);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.notification-transition-move {
|
|
80
|
+
transition: transform var(--s-motion-standard) ease;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
@media (prefers-reduced-motion: reduce) {
|
|
84
|
+
.notification-transition-enter-active,
|
|
85
|
+
.notification-transition-leave-active,
|
|
86
|
+
.notification-transition-move {
|
|
87
|
+
transition-duration: 0.01ms;
|
|
88
|
+
}
|
|
89
|
+
}
|