@pgcorp/ui-kit 0.3.0 → 0.4.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 +36 -0
- package/docs/public-api.md +8 -0
- package/package.json +1 -1
- package/src/components/layout/SAppShell.vue +3 -2
- package/src/components/shared/complex/STree.vue +29 -4
- package/src/components/shared/complex/treeAdapter.ts +2 -1
- package/src/components/shared/complex/types.ts +1 -12
- package/src/components/shared/containers/SLeftSidebar.vue +7 -2
- package/src/components/shared/containers/SPageHeader.css +5 -0
- package/src/components/shared/containers/SPageHeader.vue +1 -1
- package/src/components/shared/containers/SPopover.vue +2 -1
- package/src/components/shared/containers/SSidebarGroup.vue +10 -5
- package/src/components/shared/containers/sidebar.ts +2 -1
- package/src/components/shared/controls/SButton.vue +10 -5
- package/src/components/shared/controls/SCombobox.vue +47 -5
- package/src/components/shared/controls/SInteractiveSurface.vue +2 -1
- package/src/components/shared/controls/SLink.ts +1 -17
- package/src/components/shared/controls/SLink.vue +26 -2
- package/src/components/shared/data-display/SDocBlock.vue +10 -10
- package/src/components/shared/data-display/SLinkedSystemsList.vue +3 -3
- package/src/components/shared/data-display/STable.css +39 -0
- package/src/components/shared/data-display/STable.vue +454 -67
- package/src/components/shared/data-display/STooltip.vue +2 -1
- package/src/components/shared/data-display/SVirtualList.ts +1 -16
- package/src/components/shared/data-display/SVirtualList.vue +18 -2
- package/src/components/shared/data-display/json.ts +1 -1
- package/src/components/shared/data-display/table.ts +4 -29
- package/src/components/shared/database/SDataGrid.vue +29 -8
- package/src/components/shared/database/SSqlEditor.vue +3 -2
- package/src/components/shared/database/dataGrid.ts +1 -17
- package/src/components/shared/navigation/STabs.vue +2 -1
- package/src/composables/uiPreferencesReset.ts +2 -1
- package/src/composables/useClipboard.ts +2 -1
- package/src/composables/useSidebarPanelState.ts +3 -2
- package/src/internal/es2020.ts +50 -0
- package/src/internal/inlineTokenEditorContract.ts +4 -3
- package/src/internal/pointerInteractionLease.ts +2 -1
- package/src/internal/semanticSizing.ts +2 -2
- package/src/styles/tokens.css +1 -0
package/README.md
CHANGED
|
@@ -113,6 +113,42 @@ TypeScript показывают доступные subpaths при импорт
|
|
|
113
113
|
The complete machine-readable catalog is declared in `package.json#exports`.
|
|
114
114
|
IDEs and TypeScript expose the available subpaths during import.
|
|
115
115
|
|
|
116
|
+
### Реестры и свободный ввод / Registries and custom values
|
|
117
|
+
|
|
118
|
+
`STable` владеет служебными колонками выбора и раскрытия. Consumer передаёт
|
|
119
|
+
controlled keys и доменный контент, не копирует checkbox, disclosure или CSS:
|
|
120
|
+
|
|
121
|
+
`STable` owns selection and disclosure columns. Consumers provide controlled
|
|
122
|
+
keys and domain content instead of copying checkboxes, disclosures, or CSS:
|
|
123
|
+
|
|
124
|
+
```vue
|
|
125
|
+
<STable
|
|
126
|
+
v-model:selected-row-keys="selectedRowKeys"
|
|
127
|
+
v-model:expanded-row-keys="expandedRowKeys"
|
|
128
|
+
:columns="columns"
|
|
129
|
+
:data="rows"
|
|
130
|
+
:get-row-key="(row) => row.id"
|
|
131
|
+
:get-row-label="(row) => row.name"
|
|
132
|
+
selection-mode="multiple"
|
|
133
|
+
aria-label="Поставщики"
|
|
134
|
+
@row-contextmenu="openRowMenu"
|
|
135
|
+
>
|
|
136
|
+
<template #row-details="{ row }">
|
|
137
|
+
<SupplierDetails :supplier="row" />
|
|
138
|
+
</template>
|
|
139
|
+
</STable>
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Для служебных столбцов доступны `headerPresentation: 'assistive'`,
|
|
143
|
+
`width: '2xs'` и `width: 'content'`. `SCombobox` принимает новое строковое
|
|
144
|
+
значение через `allow-custom-value`; `SPageHeader` сочетает `appearance="bare"`
|
|
145
|
+
с `title-presentation="assistive"` для доступного заголовка без визуальной полосы.
|
|
146
|
+
|
|
147
|
+
Utility columns support `headerPresentation: 'assistive'`, `width: '2xs'`, and
|
|
148
|
+
`width: 'content'`. `SCombobox` accepts a new string value with
|
|
149
|
+
`allow-custom-value`; `SPageHeader` combines `appearance="bare"` with
|
|
150
|
+
`title-presentation="assistive"` for an accessible zero-chrome heading.
|
|
151
|
+
|
|
116
152
|
## Темы и стили / Themes and styles
|
|
117
153
|
|
|
118
154
|
Для обычного приложения подключайте полный style entrypoint один раз:
|
package/docs/public-api.md
CHANGED
|
@@ -18,6 +18,10 @@
|
|
|
18
18
|
|
|
19
19
|
Компонентный contract выражается через typed props, slots, events и exposed
|
|
20
20
|
methods. Внутренний DOM, CSS-классы и private modules не являются API.
|
|
21
|
+
Публичные source-SFC объявляют runtime props и events локально: consumer Vue
|
|
22
|
+
compiler регистрирует их без межфайлового filesystem type resolver.
|
|
23
|
+
Публикуемый production-source компилируется с `target/lib: ES2020`, совпадающим
|
|
24
|
+
с baseline `@vue/tsconfig/tsconfig.dom.json`, и не требует скрытых полифиллов.
|
|
21
25
|
|
|
22
26
|
## English
|
|
23
27
|
|
|
@@ -37,3 +41,7 @@ Primary groups:
|
|
|
37
41
|
|
|
38
42
|
Component contracts are expressed through typed props, slots, events, and exposed
|
|
39
43
|
methods. Internal DOM, CSS classes, and private modules are not API.
|
|
44
|
+
Public source SFCs declare runtime props and events locally, so the consumer Vue
|
|
45
|
+
compiler registers them without a cross-file filesystem type resolver.
|
|
46
|
+
Published production source compiles with the `target/lib: ES2020` baseline from
|
|
47
|
+
`@vue/tsconfig/tsconfig.dom.json` and does not require implicit polyfills.
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { computed, getCurrentInstance, useSlots } from 'vue'
|
|
3
3
|
import type { RouteLocationRaw } from 'vue-router'
|
|
4
|
+
import { hasOwn } from '../../internal/es2020'
|
|
4
5
|
import { useOwnedAttrs } from '../../internal/ownedAttrs'
|
|
5
6
|
import { validateNonEmptyString } from '../../internal/runtimeContract'
|
|
6
7
|
import SBadge from '../shared/data-display/SBadge.vue'
|
|
@@ -98,8 +99,8 @@ function toKebabCase(value: string): string {
|
|
|
98
99
|
function hasExplicitProp(propName: string): boolean {
|
|
99
100
|
const vnodeProps = componentInstance.vnode.props
|
|
100
101
|
return vnodeProps !== null && (
|
|
101
|
-
|
|
102
|
-
||
|
|
102
|
+
hasOwn(vnodeProps, propName)
|
|
103
|
+
|| hasOwn(vnodeProps, toKebabCase(propName))
|
|
103
104
|
)
|
|
104
105
|
}
|
|
105
106
|
|
|
@@ -32,8 +32,36 @@
|
|
|
32
32
|
</ul>
|
|
33
33
|
</template>
|
|
34
34
|
|
|
35
|
+
<script lang="ts">
|
|
36
|
+
import type {
|
|
37
|
+
SelectionKey,
|
|
38
|
+
TreeActionsLayout,
|
|
39
|
+
TreeDensity,
|
|
40
|
+
TreeExpandControl,
|
|
41
|
+
TreeModel,
|
|
42
|
+
TreeRowVariant,
|
|
43
|
+
TreeSelection,
|
|
44
|
+
TreeSelectionRail,
|
|
45
|
+
} from './types'
|
|
46
|
+
|
|
47
|
+
/** Публичный контракт дерева принадлежит source-SFC runtime. / The public tree contract is owned by the source-SFC runtime. */
|
|
48
|
+
export interface STreeProps<T extends object, K extends SelectionKey = SelectionKey> {
|
|
49
|
+
nodes: readonly T[]
|
|
50
|
+
model: TreeModel<T, K>
|
|
51
|
+
label: string
|
|
52
|
+
selection: TreeSelection<K>
|
|
53
|
+
expanded?: ReadonlySet<K>
|
|
54
|
+
density?: TreeDensity
|
|
55
|
+
actionsLayout?: TreeActionsLayout
|
|
56
|
+
expandControl?: TreeExpandControl
|
|
57
|
+
selectionRail?: TreeSelectionRail
|
|
58
|
+
rowVariant?: TreeRowVariant
|
|
59
|
+
}
|
|
60
|
+
</script>
|
|
61
|
+
|
|
35
62
|
<script setup lang="ts" generic="T extends object, K extends SelectionKey">
|
|
36
63
|
import { computed, nextTick, provide, ref, watch } from 'vue'
|
|
64
|
+
import { lastItem } from '../../../internal/es2020'
|
|
37
65
|
import { useOwnedAttrs } from '../../../internal/ownedAttrs'
|
|
38
66
|
import { assertSelectionKey, selectionKeyToken } from '../_internal/useSelectionRoving'
|
|
39
67
|
import STreeNode from '../_internal/STreeNode.vue'
|
|
@@ -46,12 +74,9 @@ import {
|
|
|
46
74
|
import { validateTreeModel } from './treeAdapter'
|
|
47
75
|
import type {
|
|
48
76
|
STreeNodeContentScope,
|
|
49
|
-
STreeProps,
|
|
50
|
-
SelectionKey,
|
|
51
77
|
TreeActivationOrigin,
|
|
52
78
|
TreeNodeActivationEvent,
|
|
53
79
|
TreeNodeEvent,
|
|
54
|
-
TreeSelection,
|
|
55
80
|
} from './types'
|
|
56
81
|
|
|
57
82
|
defineOptions({ inheritAttrs: false })
|
|
@@ -172,7 +197,7 @@ function handleTreeItemKeydown(item: RegisteredTreeItem<T, K>, event: KeyboardEv
|
|
|
172
197
|
}
|
|
173
198
|
if (event.key === 'End') {
|
|
174
199
|
event.preventDefault()
|
|
175
|
-
void focusItem(items
|
|
200
|
+
void focusItem(lastItem(items))
|
|
176
201
|
return
|
|
177
202
|
}
|
|
178
203
|
if (event.key === 'ArrowRight' && item.hasChildren()) {
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
type SelectionIdentity,
|
|
6
6
|
type SelectionKey,
|
|
7
7
|
} from '../_internal/useSelectionRoving'
|
|
8
|
+
import { withErrorCause } from '../../../internal/es2020'
|
|
8
9
|
import type { TreeModel } from './types'
|
|
9
10
|
|
|
10
11
|
export interface FlatTreeAdapter<T extends object, K extends SelectionKey> {
|
|
@@ -130,7 +131,7 @@ function readModelValue<R>(
|
|
|
130
131
|
try {
|
|
131
132
|
return reader()
|
|
132
133
|
} catch (error) {
|
|
133
|
-
throw new TypeError(`STree: model.${field} failed at ${coordinate}
|
|
134
|
+
throw withErrorCause(new TypeError(`STree: model.${field} failed at ${coordinate}`), error)
|
|
134
135
|
}
|
|
135
136
|
}
|
|
136
137
|
|
|
@@ -51,18 +51,7 @@ export type TreeExpandControl = 'button' | 'row'
|
|
|
51
51
|
export type TreeSelectionRail = 'hidden' | 'visible'
|
|
52
52
|
export type TreeRowVariant = 'default' | 'table'
|
|
53
53
|
|
|
54
|
-
export
|
|
55
|
-
nodes: readonly T[]
|
|
56
|
-
model: TreeModel<T, K>
|
|
57
|
-
label: string
|
|
58
|
-
selection: TreeSelection<K>
|
|
59
|
-
expanded?: ReadonlySet<K>
|
|
60
|
-
density?: TreeDensity
|
|
61
|
-
actionsLayout?: TreeActionsLayout
|
|
62
|
-
expandControl?: TreeExpandControl
|
|
63
|
-
selectionRail?: TreeSelectionRail
|
|
64
|
-
rowVariant?: TreeRowVariant
|
|
65
|
-
}
|
|
54
|
+
export type { STreeProps } from './STree.vue'
|
|
66
55
|
|
|
67
56
|
export interface TreeNodeEvent<T extends object, K extends SelectionKey = SelectionKey> {
|
|
68
57
|
node: T
|
|
@@ -33,7 +33,7 @@ import { type FunctionalComponent, type HTMLAttributes, type VNodeProps } from '
|
|
|
33
33
|
import SPanel from './SPanel.vue';
|
|
34
34
|
import SSidebarGroup from './SSidebarGroup.vue';
|
|
35
35
|
import type { SAsyncStateContract } from '../feedback/SAsyncState.vue';
|
|
36
|
-
import type {
|
|
36
|
+
import type { SidebarSectionsState } from './sidebar';
|
|
37
37
|
import { useOwnedAttrs } from '../../../internal/ownedAttrs';
|
|
38
38
|
|
|
39
39
|
defineOptions({ inheritAttrs: false });
|
|
@@ -48,6 +48,11 @@ export interface Props {
|
|
|
48
48
|
scrollBehavior?: 'auto' | 'body' | 'self' | 'child';
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
interface Emits {
|
|
52
|
+
'update-sections': [state: SidebarSectionsState];
|
|
53
|
+
'toggle-section': [key: string];
|
|
54
|
+
}
|
|
55
|
+
|
|
51
56
|
withDefaults(defineProps<Props>(), {
|
|
52
57
|
title: undefined,
|
|
53
58
|
icon: undefined,
|
|
@@ -56,7 +61,7 @@ withDefaults(defineProps<Props>(), {
|
|
|
56
61
|
scrollBehavior: 'child',
|
|
57
62
|
});
|
|
58
63
|
|
|
59
|
-
const emit = defineEmits<
|
|
64
|
+
const emit = defineEmits<Emits>();
|
|
60
65
|
const ownedAttrs = useOwnedAttrs({ component: 'SLeftSidebar', owner: 'left-sidebar root' });
|
|
61
66
|
</script>
|
|
62
67
|
|
|
@@ -8,7 +8,7 @@ withDefaults(defineProps<{
|
|
|
8
8
|
description?: string
|
|
9
9
|
eyebrow?: string
|
|
10
10
|
headingLevel?: 1 | 2 | 3
|
|
11
|
-
appearance?: 'plain' | 'panel'
|
|
11
|
+
appearance?: 'bare' | 'plain' | 'panel'
|
|
12
12
|
size?: 'compact' | 'default'
|
|
13
13
|
actionsLayout?: 'responsive' | 'inline' | 'stacked'
|
|
14
14
|
titlePresentation?: 'visible' | 'assistive'
|
|
@@ -93,6 +93,7 @@ import SButton, {
|
|
|
93
93
|
} from '../controls/SButton.vue';
|
|
94
94
|
import { useFloatingPosition, type FloatingPlacement } from '../../../composables/useFloatingPosition';
|
|
95
95
|
import { findRelativeTabTarget, getFocusableElements } from '../../../internal/focusNavigation';
|
|
96
|
+
import { lastItem } from '../../../internal/es2020';
|
|
96
97
|
import { resolveInteractiveElement } from '../../../internal/interactiveElement';
|
|
97
98
|
import { registerLayer, type LayerRegistration } from '../../../internal/layerStack';
|
|
98
99
|
import { useOwnedAttrs } from '../../../internal/ownedAttrs';
|
|
@@ -495,7 +496,7 @@ function handlePanelTab(event: KeyboardEvent): void {
|
|
|
495
496
|
const focusable = getFocusableElements(panel);
|
|
496
497
|
const active = document.activeElement;
|
|
497
498
|
const first = focusable[0];
|
|
498
|
-
const last = focusable
|
|
499
|
+
const last = lastItem(focusable);
|
|
499
500
|
const leavesPanel = focusable.length === 0
|
|
500
501
|
|| (event.shiftKey ? active === panel || active === first : active === last);
|
|
501
502
|
if (!leavesPanel) return;
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
} from 'vue';
|
|
17
17
|
|
|
18
18
|
import { useOwnedAttrs } from '../../../internal/ownedAttrs';
|
|
19
|
+
import { hasOwn, lastItem } from '../../../internal/es2020';
|
|
19
20
|
import {
|
|
20
21
|
sidebarGroupContextKey,
|
|
21
22
|
type RegisteredSidebarSection,
|
|
@@ -27,7 +28,6 @@ import {
|
|
|
27
28
|
createSidebarSectionsTransition,
|
|
28
29
|
sidebarSectionsStateEqual,
|
|
29
30
|
type SectionState,
|
|
30
|
-
type SidebarGroupEmits,
|
|
31
31
|
type SidebarSectionReplacement,
|
|
32
32
|
type SidebarSectionsState,
|
|
33
33
|
} from './sidebar';
|
|
@@ -38,7 +38,12 @@ const props = defineProps<{
|
|
|
38
38
|
sectionsState: SidebarSectionsState;
|
|
39
39
|
}>();
|
|
40
40
|
|
|
41
|
-
|
|
41
|
+
interface Emits {
|
|
42
|
+
'update-sections': [state: SidebarSectionsState];
|
|
43
|
+
'toggle-section': [key: string];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const emit = defineEmits<Emits>();
|
|
42
47
|
|
|
43
48
|
const ownedAttrs = useOwnedAttrs({ component: 'SSidebarGroup', owner: 'sidebar group root' });
|
|
44
49
|
const groupRef = ref<HTMLDivElement | null>(null);
|
|
@@ -147,7 +152,7 @@ function fitOpenSectionSizes(
|
|
|
147
152
|
|
|
148
153
|
let delta = targetSize - [...sizes.values()].reduce((sum, size) => sum + size, 0);
|
|
149
154
|
if (delta > 0) {
|
|
150
|
-
const growTarget = flexible ?? openSections
|
|
155
|
+
const growTarget = flexible ?? lastItem(openSections);
|
|
151
156
|
if (growTarget) sizes.set(growTarget.key, sizes.get(growTarget.key)! + delta);
|
|
152
157
|
delta = 0;
|
|
153
158
|
}
|
|
@@ -200,7 +205,7 @@ function registerSection(registration: SidebarSectionRegistration): () => void {
|
|
|
200
205
|
if (!(registration.element instanceof HTMLElement)) {
|
|
201
206
|
throw new TypeError('SSidebarGroup: registered section element must be an HTMLElement');
|
|
202
207
|
}
|
|
203
|
-
if (!
|
|
208
|
+
if (!hasOwn(currentState(), registration.key)) {
|
|
204
209
|
throw new Error(`SSidebarGroup: registered section '${registration.key}' has no controlled state`);
|
|
205
210
|
}
|
|
206
211
|
if (registrations.value.some((current) => current.key === registration.key)) {
|
|
@@ -371,7 +376,7 @@ function resizeSections(topKey: string, bottomKey: string, requestedTopSize: num
|
|
|
371
376
|
);
|
|
372
377
|
const delta = requestedTopSize - currentUpperSize;
|
|
373
378
|
const shrinking = delta > 0 ? [...lower] : [...upper].reverse();
|
|
374
|
-
const growing = delta > 0 ? upper
|
|
379
|
+
const growing = delta > 0 ? lastItem(upper) : lower[0];
|
|
375
380
|
let transferable = Math.abs(delta);
|
|
376
381
|
transferable = Math.min(
|
|
377
382
|
transferable,
|
|
@@ -163,7 +163,7 @@ export function createSidebarSectionsTransition(
|
|
|
163
163
|
if (typeof replacement.key !== 'string' || replacement.key.trim().length === 0) {
|
|
164
164
|
throw new TypeError(`SSidebarGroup: replacements[${index}].key must be a non-empty string`);
|
|
165
165
|
}
|
|
166
|
-
if (!
|
|
166
|
+
if (!hasOwn(current, replacement.key)) {
|
|
167
167
|
throw new Error(`SSidebarGroup: cannot replace unknown section '${replacement.key}'`);
|
|
168
168
|
}
|
|
169
169
|
if (replacementsByKey.has(replacement.key)) {
|
|
@@ -204,3 +204,4 @@ export function sidebarSectionsStateEqual(
|
|
|
204
204
|
&& leftSection.minSize === rightSection.minSize;
|
|
205
205
|
});
|
|
206
206
|
}
|
|
207
|
+
import { hasOwn } from '../../../internal/es2020';
|
|
@@ -72,8 +72,8 @@ import { useTreeActionRegistration } from '../_internal/treeRuntime'
|
|
|
72
72
|
import SBadge, { type BadgeSeverity } from '../data-display/SBadge.vue'
|
|
73
73
|
import SProgressIndicator, { type SProgressIndicatorSize } from '../data-display/SProgressIndicator.vue'
|
|
74
74
|
import type {
|
|
75
|
-
|
|
76
|
-
|
|
75
|
+
LinkBrowsingTarget,
|
|
76
|
+
LinkRouterTarget,
|
|
77
77
|
} from './SLink'
|
|
78
78
|
|
|
79
79
|
defineOptions({ inheritAttrs: false })
|
|
@@ -132,9 +132,14 @@ export type SButtonActionProps = SButtonPresentationProps & {
|
|
|
132
132
|
type?: SButtonNativeType
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
135
|
+
interface SButtonNavigationOptions {
|
|
136
|
+
target?: LinkBrowsingTarget
|
|
137
|
+
rel?: string
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export type SButtonNavigationProps = SButtonPresentationProps & SButtonNavigationOptions & (
|
|
141
|
+
| { href: string; to?: never; type?: never }
|
|
142
|
+
| { href?: never; to: LinkRouterTarget; type?: never }
|
|
138
143
|
)
|
|
139
144
|
|
|
140
145
|
/** Strict action/navigation XOR with a shared button presentation. */
|
|
@@ -62,7 +62,7 @@ export type SComboboxOptionGroup<TKey extends PublicSelectionKey = PublicSelecti
|
|
|
62
62
|
export type SComboboxEntry<TKey extends PublicSelectionKey = PublicSelectionKey> = PublicSelectionEntry<TKey>
|
|
63
63
|
export type SComboboxFilterMode = 'local' | 'manual'
|
|
64
64
|
|
|
65
|
-
|
|
65
|
+
interface SharedProps<TKey extends PublicSelectionKey> {
|
|
66
66
|
modelValue: TKey | null
|
|
67
67
|
search: string
|
|
68
68
|
options: readonly SComboboxEntry<TKey>[]
|
|
@@ -79,6 +79,18 @@ export interface Props<TKey extends PublicSelectionKey = PublicSelectionKey> {
|
|
|
79
79
|
id?: string
|
|
80
80
|
size?: 'sm' | 'md' | 'lg'
|
|
81
81
|
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Custom values require a string-compatible model because the editable text is the committed identity.
|
|
85
|
+
* Свободные значения требуют string-compatible model, потому что введённый текст становится identity.
|
|
86
|
+
*/
|
|
87
|
+
export type Props<TKey extends PublicSelectionKey = PublicSelectionKey> = SharedProps<TKey> & {
|
|
88
|
+
allowCustomValue?: string extends TKey ? boolean : false
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
type ComponentProps<TKey extends PublicSelectionKey> = SharedProps<TKey> & {
|
|
92
|
+
allowCustomValue?: boolean
|
|
93
|
+
}
|
|
82
94
|
</script>
|
|
83
95
|
|
|
84
96
|
<script setup lang="ts" generic="TKey extends SelectionKey = string">
|
|
@@ -106,7 +118,7 @@ import SListbox, { type SListboxMoveIntent } from './SListbox.vue'
|
|
|
106
118
|
|
|
107
119
|
defineOptions({ inheritAttrs: false })
|
|
108
120
|
|
|
109
|
-
const props = withDefaults(defineProps<
|
|
121
|
+
const props = withDefaults(defineProps<ComponentProps<TKey>>(), {
|
|
110
122
|
filterMode: 'local',
|
|
111
123
|
label: undefined,
|
|
112
124
|
placeholder: 'Начните вводить для поиска',
|
|
@@ -119,6 +131,7 @@ const props = withDefaults(defineProps<Props<TKey>>(), {
|
|
|
119
131
|
emptyLabel: 'Нет доступных вариантов',
|
|
120
132
|
id: undefined,
|
|
121
133
|
size: 'md',
|
|
134
|
+
allowCustomValue: false,
|
|
122
135
|
})
|
|
123
136
|
const emit = defineEmits<{
|
|
124
137
|
'update:modelValue': [value: TKey | null]
|
|
@@ -156,13 +169,18 @@ const contract = computed(() => ({
|
|
|
156
169
|
invalid: validateBoolean('SCombobox', 'invalid', props.invalid),
|
|
157
170
|
emptyLabel: validateNonEmptyString('SCombobox', 'emptyLabel', props.emptyLabel),
|
|
158
171
|
size: validateExactString('SCombobox', 'size', props.size, ['sm', 'md', 'lg'] as const),
|
|
172
|
+
allowCustomValue: validateBoolean('SCombobox', 'allowCustomValue', props.allowCustomValue),
|
|
159
173
|
label: validateOptionalSelectionString('SCombobox', 'label', props.label),
|
|
160
174
|
helpText: validateOptionalSelectionString('SCombobox', 'helpText', props.helpText),
|
|
161
175
|
errorMessage: validateOptionalSelectionString('SCombobox', 'errorMessage', props.errorMessage),
|
|
162
176
|
}))
|
|
163
177
|
const inventory = computed(() => {
|
|
164
178
|
const resolved = resolveSelectionInventory<TKey>('SCombobox', props.options)
|
|
165
|
-
|
|
179
|
+
if (contract.value.allowCustomValue && typeof props.modelValue === 'string') {
|
|
180
|
+
selectionKeyToken('SCombobox', props.modelValue, 'modelValue')
|
|
181
|
+
} else {
|
|
182
|
+
validateSelectionModel('SCombobox', 'single', props.modelValue, resolved)
|
|
183
|
+
}
|
|
166
184
|
return resolved
|
|
167
185
|
})
|
|
168
186
|
|
|
@@ -231,6 +249,29 @@ const { anchorRef, panelRef, floatingStyle } = useFloatingPosition({
|
|
|
231
249
|
|
|
232
250
|
type CloseReason = 'selection' | 'escape' | 'tab' | 'outside'
|
|
233
251
|
|
|
252
|
+
function customValueFromSearch(): TKey | null {
|
|
253
|
+
if (!contract.value.allowCustomValue) return null
|
|
254
|
+
const value = validatedSearch.value.trim()
|
|
255
|
+
if (value.length === 0) return null
|
|
256
|
+
|
|
257
|
+
if (props.modelValue !== null) {
|
|
258
|
+
const currentToken = selectionKeyToken('SCombobox', props.modelValue, 'modelValue')
|
|
259
|
+
const currentOption = inventory.value.optionsByToken.get(currentToken)
|
|
260
|
+
if (currentOption?.option.label.trim() === value) return null
|
|
261
|
+
if (typeof props.modelValue === 'string' && props.modelValue === value) return null
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return value as TKey
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function commitCustomValue(): boolean {
|
|
268
|
+
const value = customValueFromSearch()
|
|
269
|
+
if (value === null) return false
|
|
270
|
+
emit('update:modelValue', value)
|
|
271
|
+
emit('update:search', String(value))
|
|
272
|
+
return true
|
|
273
|
+
}
|
|
274
|
+
|
|
234
275
|
function inputElement(): HTMLInputElement {
|
|
235
276
|
const api = inputRef.value
|
|
236
277
|
if (!api) throw new Error('SCombobox: SInputText API is unavailable')
|
|
@@ -239,7 +280,7 @@ function inputElement(): HTMLInputElement {
|
|
|
239
280
|
|
|
240
281
|
function close(reason: CloseReason): void {
|
|
241
282
|
if (!isOpen.value) return
|
|
242
|
-
|
|
283
|
+
if (reason === 'tab' || reason === 'outside') commitCustomValue()
|
|
243
284
|
isOpen.value = false
|
|
244
285
|
activeValue.value = null
|
|
245
286
|
layerRegistration?.unregister({ restoreFocus: false })
|
|
@@ -305,7 +346,8 @@ async function onInputKeydown(event: KeyboardEvent): Promise<void> {
|
|
|
305
346
|
}
|
|
306
347
|
if (event.key === 'Enter' && isOpen.value) {
|
|
307
348
|
event.preventDefault()
|
|
308
|
-
listboxRef.value?.selectActive()
|
|
349
|
+
if (activeValue.value !== null) listboxRef.value?.selectActive()
|
|
350
|
+
else if (commitCustomValue()) close('selection')
|
|
309
351
|
return
|
|
310
352
|
}
|
|
311
353
|
if (event.key === 'Escape' && isOpen.value) {
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { computed, watchEffect, type VNodeChild } from 'vue'
|
|
3
3
|
import { RouterLink, type RouteLocationRaw } from 'vue-router'
|
|
4
4
|
import type { STooltipTriggerBinding } from '../data-display/STooltip.vue'
|
|
5
|
+
import { hasOwn } from '../../../internal/es2020'
|
|
5
6
|
import { resolveLinkTarget } from '../../../internal/linkTarget'
|
|
6
7
|
import { useOwnedAttrs } from '../../../internal/ownedAttrs'
|
|
7
8
|
import {
|
|
@@ -195,7 +196,7 @@ const validatedTooltipTrigger = computed<STooltipTriggerBinding | undefined>(()
|
|
|
195
196
|
}
|
|
196
197
|
const record = candidate as Record<string, unknown>
|
|
197
198
|
const unexpected = Object.keys(record).filter((key) => !TOOLTIP_TRIGGER_KEYS.includes(key as typeof TOOLTIP_TRIGGER_KEYS[number]))
|
|
198
|
-
const missing = TOOLTIP_TRIGGER_KEYS.filter((key) => !
|
|
199
|
+
const missing = TOOLTIP_TRIGGER_KEYS.filter((key) => !hasOwn(record, key))
|
|
199
200
|
if (unexpected.length > 0 || missing.length > 0) {
|
|
200
201
|
throw new Error(`SInteractiveSurface: tooltipTrigger имеет недопустимый набор ключей; missing=${missing.join(',')}; unexpected=${unexpected.join(',')}. / SInteractiveSurface: tooltipTrigger has an invalid key set; missing=${missing.join(',')}; unexpected=${unexpected.join(',')}.`)
|
|
201
202
|
}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import type { Component } from 'vue'
|
|
2
1
|
import type {
|
|
3
2
|
HistoryState,
|
|
4
3
|
LocationQueryRaw,
|
|
@@ -67,19 +66,4 @@ export type RouterLinkDestination = LinkDestinationOptions & {
|
|
|
67
66
|
/** Строгий XOR-контракт обычного destination. / Strict XOR contract for an ordinary destination. */
|
|
68
67
|
export type LinkDestination = HrefLinkDestination | RouterLinkDestination
|
|
69
68
|
|
|
70
|
-
|
|
71
|
-
export interface LinkPresentationProps {
|
|
72
|
-
readonly variant?: 'default' | 'subtle' | 'brand' | 'navigation' | 'bottom-navigation' | 'document'
|
|
73
|
-
readonly external?: boolean
|
|
74
|
-
readonly active?: boolean
|
|
75
|
-
readonly disabled?: boolean
|
|
76
|
-
readonly width?: 'fit' | 'full'
|
|
77
|
-
readonly size?: 'inherit' | 'sm' | 'md'
|
|
78
|
-
readonly underline?: 'hover' | 'always' | 'none'
|
|
79
|
-
readonly wrap?: 'normal' | 'anywhere'
|
|
80
|
-
readonly contentKind?: 'text' | 'identifier'
|
|
81
|
-
readonly leadingIcon?: Component
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/** Полный discriminated public props contract SLink. / Complete discriminated public SLink props contract. */
|
|
85
|
-
export type SLinkProps = LinkDestination & LinkPresentationProps
|
|
69
|
+
export type { LinkPresentationProps, Props as SLinkProps } from './SLink.vue'
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import {
|
|
3
|
+
type Component,
|
|
3
4
|
computed,
|
|
4
5
|
type VNodeChild,
|
|
5
6
|
} from 'vue'
|
|
@@ -12,13 +13,36 @@ import {
|
|
|
12
13
|
type PassiveContentContract,
|
|
13
14
|
} from '../../../internal/passiveContentContract'
|
|
14
15
|
import type {
|
|
15
|
-
|
|
16
|
+
LinkBrowsingTarget,
|
|
17
|
+
LinkRouterTarget,
|
|
16
18
|
} from './SLink'
|
|
17
19
|
|
|
18
20
|
defineOptions({ inheritAttrs: false })
|
|
19
21
|
|
|
22
|
+
/** Canonical presentation/state API owned by SLink. */
|
|
23
|
+
export interface LinkPresentationProps {
|
|
24
|
+
readonly variant?: 'default' | 'subtle' | 'brand' | 'navigation' | 'bottom-navigation' | 'document'
|
|
25
|
+
readonly external?: boolean
|
|
26
|
+
readonly active?: boolean
|
|
27
|
+
readonly disabled?: boolean
|
|
28
|
+
readonly width?: 'fit' | 'full'
|
|
29
|
+
readonly size?: 'inherit' | 'sm' | 'md'
|
|
30
|
+
readonly underline?: 'hover' | 'always' | 'none'
|
|
31
|
+
readonly wrap?: 'normal' | 'anywhere'
|
|
32
|
+
readonly contentKind?: 'text' | 'identifier'
|
|
33
|
+
readonly leadingIcon?: Component
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface LinkDestinationOptions {
|
|
37
|
+
readonly target?: LinkBrowsingTarget
|
|
38
|
+
readonly rel?: string
|
|
39
|
+
}
|
|
40
|
+
|
|
20
41
|
/** Exact href/to XOR plus the canonical link presentation API. */
|
|
21
|
-
export type Props =
|
|
42
|
+
export type Props = LinkPresentationProps & LinkDestinationOptions & (
|
|
43
|
+
| { readonly href: string; readonly to?: never }
|
|
44
|
+
| { readonly href?: never; readonly to: LinkRouterTarget }
|
|
45
|
+
)
|
|
22
46
|
|
|
23
47
|
const props = withDefaults(defineProps<Props>(), {
|
|
24
48
|
href: undefined,
|
|
@@ -88,20 +88,20 @@ useInteractiveLeafRegistration({ owner: 'SDocBlock' });
|
|
|
88
88
|
|
|
89
89
|
function escapeHighlightedCode(code: string): string {
|
|
90
90
|
return code
|
|
91
|
-
.
|
|
92
|
-
.
|
|
93
|
-
.
|
|
94
|
-
.
|
|
95
|
-
.
|
|
91
|
+
.replace(/&/gu, '&')
|
|
92
|
+
.replace(/</gu, '<')
|
|
93
|
+
.replace(/>/gu, '>')
|
|
94
|
+
.replace(/"/gu, '"')
|
|
95
|
+
.replace(/'/gu, ''');
|
|
96
96
|
}
|
|
97
97
|
|
|
98
98
|
function escapeHtmlAttribute(value: string): string {
|
|
99
99
|
return value
|
|
100
|
-
.
|
|
101
|
-
.
|
|
102
|
-
.
|
|
103
|
-
.
|
|
104
|
-
.
|
|
100
|
+
.replace(/&/gu, '&')
|
|
101
|
+
.replace(/</gu, '<')
|
|
102
|
+
.replace(/>/gu, '>')
|
|
103
|
+
.replace(/"/gu, '"')
|
|
104
|
+
.replace(/'/gu, ''');
|
|
105
105
|
}
|
|
106
106
|
|
|
107
107
|
const markdownRenderer = new marked.Renderer();
|
|
@@ -113,6 +113,7 @@ export interface Slots<T extends LinkedSystemListItem = LinkedSystemListItem> {
|
|
|
113
113
|
|
|
114
114
|
<script setup lang="ts" generic="T extends LinkedSystemListItem = LinkedSystemListItem">
|
|
115
115
|
import { computed, watchEffect } from 'vue'
|
|
116
|
+
import { withErrorCause } from '../../../internal/es2020'
|
|
116
117
|
import { useOwnedAttrs } from '../../../internal/ownedAttrs'
|
|
117
118
|
import {
|
|
118
119
|
validateBoolean,
|
|
@@ -169,11 +170,10 @@ function validateExternalUrl(value: unknown, index: number): void {
|
|
|
169
170
|
try {
|
|
170
171
|
parsedUrl = new URL(externalUrl)
|
|
171
172
|
} catch (error: unknown) {
|
|
172
|
-
throw new TypeError(
|
|
173
|
+
throw withErrorCause(new TypeError(
|
|
173
174
|
`SLinkedSystemsList: ${coordinate} должен быть абсолютным HTTP(S) URL или null; получено ${JSON.stringify(externalUrl)}. `
|
|
174
175
|
+ `/ SLinkedSystemsList: ${coordinate} must be an absolute HTTP(S) URL or null; received ${JSON.stringify(externalUrl)}.`,
|
|
175
|
-
|
|
176
|
-
)
|
|
176
|
+
), error)
|
|
177
177
|
}
|
|
178
178
|
if (
|
|
179
179
|
(parsedUrl.protocol === 'http:' || parsedUrl.protocol === 'https:')
|