@pgcorp/ui-kit 0.2.0 → 0.3.1

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.
Files changed (50) hide show
  1. package/README.md +5 -5
  2. package/docs/public-api.md +8 -0
  3. package/package.json +15 -3
  4. package/src/components/layout/SAppShell.vue +3 -2
  5. package/src/components/layout/SStack.css +9 -3
  6. package/src/components/layout/SStack.vue +10 -2
  7. package/src/components/shared/complex/STree.vue +29 -4
  8. package/src/components/shared/complex/treeAdapter.ts +2 -1
  9. package/src/components/shared/complex/types.ts +1 -12
  10. package/src/components/shared/containers/SLeftSidebar.vue +7 -2
  11. package/src/components/shared/containers/SPageHeader.css +11 -0
  12. package/src/components/shared/containers/SPageHeader.vue +7 -1
  13. package/src/components/shared/containers/SPopover.vue +2 -1
  14. package/src/components/shared/containers/SSidebarGroup.vue +10 -5
  15. package/src/components/shared/containers/sidebar.ts +2 -1
  16. package/src/components/shared/controls/SButton.css +13 -0
  17. package/src/components/shared/controls/SButton.vue +135 -29
  18. package/src/components/shared/controls/SCombobox.css +9 -0
  19. package/src/components/shared/controls/SCombobox.vue +322 -0
  20. package/src/components/shared/controls/SInputText.vue +15 -0
  21. package/src/components/shared/controls/SInteractiveSurface.vue +2 -1
  22. package/src/components/shared/controls/SLink.ts +1 -17
  23. package/src/components/shared/controls/SLink.vue +26 -2
  24. package/src/components/shared/controls/SListbox.vue +99 -1
  25. package/src/components/shared/controls/SListboxOption.vue +15 -2
  26. package/src/components/shared/controls/SSwitch.css +48 -0
  27. package/src/components/shared/controls/SSwitch.vue +109 -0
  28. package/src/components/shared/controls/_internal/SInlineTokenSurface.vue +6 -3
  29. package/src/components/shared/data-display/SChip.css +59 -0
  30. package/src/components/shared/data-display/SChip.vue +92 -0
  31. package/src/components/shared/data-display/SDocBlock.vue +10 -10
  32. package/src/components/shared/data-display/SLinkedSystemsList.vue +3 -3
  33. package/src/components/shared/data-display/STable.vue +150 -14
  34. package/src/components/shared/data-display/STooltip.vue +2 -1
  35. package/src/components/shared/data-display/SVirtualList.ts +1 -16
  36. package/src/components/shared/data-display/SVirtualList.vue +18 -2
  37. package/src/components/shared/data-display/json.ts +1 -1
  38. package/src/components/shared/data-display/table.ts +9 -28
  39. package/src/components/shared/database/SDataGrid.vue +29 -8
  40. package/src/components/shared/database/SSqlEditor.vue +3 -2
  41. package/src/components/shared/database/dataGrid.ts +1 -17
  42. package/src/components/shared/navigation/SBottomNav.vue +3 -0
  43. package/src/components/shared/navigation/STabs.vue +2 -1
  44. package/src/composables/uiPreferencesReset.ts +2 -1
  45. package/src/composables/useClipboard.ts +2 -1
  46. package/src/composables/useSidebarPanelState.ts +3 -2
  47. package/src/internal/es2020.ts +50 -0
  48. package/src/internal/inlineTokenEditorContract.ts +4 -3
  49. package/src/internal/ownedAttrs.ts +24 -2
  50. package/src/internal/pointerInteractionLease.ts +2 -1
@@ -0,0 +1,92 @@
1
+ <template>
2
+ <span
3
+ v-bind="ownedAttrs.bindings()"
4
+ class="s-chip"
5
+ :data-severity="validatedSeverity"
6
+ :data-size="validatedSize"
7
+ :data-closable="closable || undefined"
8
+ :data-disabled="disabled || undefined"
9
+ >
10
+ <span v-if="leadingIcon" class="s-chip__icon" aria-hidden="true">
11
+ <component :is="leadingIcon" class="s-chip__icon-graphic" />
12
+ </span>
13
+ <span class="s-chip__label">{{ validatedLabel }}</span>
14
+ <SButton
15
+ v-if="closable"
16
+ label=""
17
+ appearance="ghost"
18
+ severity="secondary"
19
+ size="xs"
20
+ density="compact"
21
+ spacing="tight"
22
+ shape="circle"
23
+ icon-only
24
+ :leading-icon="X"
25
+ :disabled="disabled"
26
+ :aria-label="resolvedCloseLabel"
27
+ :title="resolvedCloseLabel"
28
+ @click.stop="emit('close', $event)"
29
+ />
30
+ </span>
31
+ </template>
32
+
33
+ <script setup lang="ts">
34
+ import { computed, type Component } from 'vue'
35
+ import { X } from '../../icons/sputnigUiIcons'
36
+ import { useOwnedAttrs } from '../../../internal/ownedAttrs'
37
+ import {
38
+ validateBoolean,
39
+ validateExactString,
40
+ validateNonEmptyString,
41
+ } from '../../../internal/runtimeContract'
42
+ import SButton from '../controls/SButton.vue'
43
+ import type { BadgeSeverity } from './SBadge.vue'
44
+
45
+ defineOptions({ inheritAttrs: false })
46
+
47
+ export type SChipSeverity = BadgeSeverity
48
+ export type SChipSize = 'sm' | 'md'
49
+
50
+ export interface Props {
51
+ label: string
52
+ severity?: SChipSeverity
53
+ size?: SChipSize
54
+ leadingIcon?: Component
55
+ closable?: boolean
56
+ closeLabel?: string
57
+ disabled?: boolean
58
+ }
59
+
60
+ const props = withDefaults(defineProps<Props>(), {
61
+ severity: 'secondary',
62
+ size: 'md',
63
+ leadingIcon: undefined,
64
+ closable: false,
65
+ closeLabel: undefined,
66
+ disabled: false,
67
+ })
68
+ const emit = defineEmits<{
69
+ close: [event: MouseEvent]
70
+ }>()
71
+ const ownedAttrs = useOwnedAttrs({ component: 'SChip', owner: 'chip root' })
72
+
73
+ const validatedLabel = computed(() => validateNonEmptyString('SChip', 'label', props.label))
74
+ const validatedSeverity = computed(() => validateExactString(
75
+ 'SChip',
76
+ 'severity',
77
+ props.severity,
78
+ ['primary', 'secondary', 'success', 'info', 'warn', 'danger', 'contrast'] as const,
79
+ ))
80
+ const validatedSize = computed(() => validateExactString(
81
+ 'SChip', 'size', props.size, ['sm', 'md'] as const,
82
+ ))
83
+ const resolvedCloseLabel = computed(() => {
84
+ validateBoolean('SChip', 'closable', props.closable)
85
+ validateBoolean('SChip', 'disabled', props.disabled)
86
+ return props.closeLabel === undefined
87
+ ? `Удалить «${validatedLabel.value}»`
88
+ : validateNonEmptyString('SChip', 'closeLabel', props.closeLabel)
89
+ })
90
+ </script>
91
+
92
+ <style lang="postcss" src="./SChip.css" scoped></style>
@@ -88,20 +88,20 @@ useInteractiveLeafRegistration({ owner: 'SDocBlock' });
88
88
 
89
89
  function escapeHighlightedCode(code: string): string {
90
90
  return code
91
- .replaceAll('&', '&amp;')
92
- .replaceAll('<', '&lt;')
93
- .replaceAll('>', '&gt;')
94
- .replaceAll('"', '&quot;')
95
- .replaceAll("'", '&#39;');
91
+ .replace(/&/gu, '&amp;')
92
+ .replace(/</gu, '&lt;')
93
+ .replace(/>/gu, '&gt;')
94
+ .replace(/"/gu, '&quot;')
95
+ .replace(/'/gu, '&#39;');
96
96
  }
97
97
 
98
98
  function escapeHtmlAttribute(value: string): string {
99
99
  return value
100
- .replaceAll('&', '&amp;')
101
- .replaceAll('<', '&lt;')
102
- .replaceAll('>', '&gt;')
103
- .replaceAll('"', '&quot;')
104
- .replaceAll("'", '&#39;');
100
+ .replace(/&/gu, '&amp;')
101
+ .replace(/</gu, '&lt;')
102
+ .replace(/>/gu, '&gt;')
103
+ .replace(/"/gu, '&quot;')
104
+ .replace(/'/gu, '&#39;');
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
- { cause: error },
176
- )
176
+ ), error)
177
177
  }
178
178
  if (
179
179
  (parsedUrl.protocol === 'http:' || parsedUrl.protocol === 'https:')
@@ -23,10 +23,30 @@
23
23
  :data-width="column.width"
24
24
  :data-min-width="column.minWidth"
25
25
  :data-align="column.align ?? 'left'"
26
+ :data-sortable="column.sortable || undefined"
27
+ :aria-sort="columnAriaSort(column)"
26
28
  >
27
- <slot name="column" :column="column">
28
- <slot :name="`header-${column.field}`" :column="column">{{ column.header }}</slot>
29
- </slot>
29
+ <SButton
30
+ v-if="column.sortable"
31
+ :aria-label="sortActionLabel(column)"
32
+ appearance="text"
33
+ severity="secondary"
34
+ size="xs"
35
+ density="compact"
36
+ width="full"
37
+ :content-align="column.align === 'right' ? 'end' : column.align === 'center' ? 'center' : 'start'"
38
+ :trailing-icon="sortIcon(column)"
39
+ @click="requestSort(column)"
40
+ >
41
+ <slot name="column" :column="column">
42
+ <slot :name="`header-${column.field}`" :column="column">{{ column.header }}</slot>
43
+ </slot>
44
+ </SButton>
45
+ <template v-else>
46
+ <slot name="column" :column="column">
47
+ <slot :name="`header-${column.field}`" :column="column">{{ column.header }}</slot>
48
+ </slot>
49
+ </template>
30
50
  </th>
31
51
  </tr>
32
52
  </thead>
@@ -47,6 +67,10 @@
47
67
  :key="resolvedRowKeys.get(row)"
48
68
  :data-state="resolveRowState(row)"
49
69
  :aria-selected="resolvedRowKeys.get(row) === validatedSelectedRowKey ? 'true' : undefined"
70
+ tabindex="0"
71
+ @click="onRowClick($event, row, rowIndex)"
72
+ @keydown.enter="onRowKeydown($event, row, rowIndex)"
73
+ @keydown.space.prevent="onRowKeydown($event, row, rowIndex)"
50
74
  >
51
75
  <td
52
76
  v-for="column in validatedColumns"
@@ -80,6 +104,19 @@
80
104
  </template>
81
105
 
82
106
  <script lang="ts">
107
+ import type { ScrollableViewportMaxBlockSize } from '../../../internal/semanticSizing'
108
+ import type {
109
+ STableCellState,
110
+ STableColumn,
111
+ STableColumnContent,
112
+ STableDensity,
113
+ STableMinWidth,
114
+ STableRowKey,
115
+ STableRowState,
116
+ STableSort,
117
+ STableSurface,
118
+ } from './table'
119
+
83
120
  export type {
84
121
  STableAlignment,
85
122
  STableCellState,
@@ -87,16 +124,49 @@ export type {
87
124
  STableColumnContent,
88
125
  STableDensity,
89
126
  STableMinWidth,
90
- STableProps,
91
127
  STableRowKey,
92
128
  STableRowState,
129
+ STableSort,
130
+ STableSortDirection,
93
131
  STableSurface,
94
132
  } from './table'
133
+
134
+ /** Публичный generic-контракт таблицы принадлежит source-SFC runtime. / The public generic table contract is owned by the source-SFC runtime. */
135
+ export interface STableProps<
136
+ Row extends object,
137
+ RowKey extends STableRowKey,
138
+ Field extends string,
139
+ > {
140
+ columns: readonly STableColumn<Row, Field>[]
141
+ data: readonly Row[]
142
+ getRowKey: (row: Row) => RowKey
143
+ loading?: boolean
144
+ loadingMessage?: string
145
+ errorMessage?: string
146
+ emptyMessage?: string
147
+ selectedRowKey?: RowKey
148
+ sort?: STableSort<Field> | null
149
+ maxHeight?: ScrollableViewportMaxBlockSize
150
+ density?: STableDensity
151
+ surface?: STableSurface
152
+ minWidth?: STableMinWidth
153
+ rowState?: (row: Row) => STableRowState
154
+ cellState?: (row: Row, column: STableColumn<Row, Field>) => STableCellState
155
+ /** Accessible name без visible owner. / Accessible name when there is no visible owner. */
156
+ ariaLabel?: string
157
+ /** DOM id visible owner, mutually exclusive with ariaLabel. / DOM id of the visible owner, mutually exclusive with ariaLabel. */
158
+ ariaLabelledby?: string
159
+ /** DOM id supplemental status/diagnostic owner. / DOM id of a supplemental status/diagnostic owner. */
160
+ ariaDescribedby?: string
161
+ }
95
162
  </script>
96
163
 
97
164
  <script setup lang="ts" generic="Row extends object = Record<string, unknown>, RowKey extends STableRowKey = STableRowKey, Field extends string = Extract<keyof Row, string>">
98
165
  import { computed, useSlots } from 'vue'
166
+ import { ChevronDown, ChevronsUpDown, ChevronUp } from '../../icons/sputnigUiIcons'
167
+ import { hasOwn } from '../../../internal/es2020'
99
168
  import { useOwnedAttrs } from '../../../internal/ownedAttrs'
169
+ import { useInteractiveLeafRegistration } from '../../../internal/passiveContentContract'
100
170
  import {
101
171
  validateScrollableViewportMaxBlockSize,
102
172
  validateTableColumnInlineSize,
@@ -109,15 +179,7 @@ import {
109
179
  validateNonEmptyString,
110
180
  } from '../../../internal/runtimeContract'
111
181
  import SAsyncState from '../feedback/SAsyncState.vue'
112
- import type {
113
- STableCellState,
114
- STableColumn,
115
- STableColumnContent,
116
- STableProps,
117
- STableRowKey,
118
- STableRowState,
119
- } from './table'
120
-
182
+ import SButton from '../controls/SButton.vue'
121
183
  defineOptions({ inheritAttrs: false })
122
184
 
123
185
  const props = withDefaults(defineProps<STableProps<Row, RowKey, Field>>(), {
@@ -126,6 +188,7 @@ const props = withDefaults(defineProps<STableProps<Row, RowKey, Field>>(), {
126
188
  errorMessage: '',
127
189
  emptyMessage: 'Нет данных для отображения',
128
190
  selectedRowKey: undefined,
191
+ sort: null,
129
192
  maxHeight: 'none',
130
193
  density: 'default',
131
194
  surface: 'bordered',
@@ -136,6 +199,11 @@ const props = withDefaults(defineProps<STableProps<Row, RowKey, Field>>(), {
136
199
  ariaLabelledby: undefined,
137
200
  ariaDescribedby: undefined,
138
201
  })
202
+ const emit = defineEmits<{
203
+ 'row-click': [row: Row, index: number]
204
+ 'update:sort': [value: STableSort<Field> | null]
205
+ sort: [value: STableSort<Field> | null]
206
+ }>()
139
207
  const slots = useSlots()
140
208
 
141
209
  const TABLE_DENSITIES = ['default', 'compact'] as const
@@ -147,6 +215,22 @@ const TABLE_CELL_STATES = ['default', 'pending'] as const
147
215
  const TABLE_COLUMN_CONTENT = ['field', 'value', 'slot'] as const
148
216
 
149
217
  const ownedAttrs = useOwnedAttrs({ component: 'STable', owner: 'table scroll surface' })
218
+ useInteractiveLeafRegistration({ owner: 'STable' })
219
+
220
+ function eventBelongsToNestedControl(event: Event): boolean {
221
+ const owner = event.currentTarget
222
+ return event.composedPath().some((candidate) => candidate !== owner && candidate instanceof HTMLElement && (
223
+ candidate.matches('button, a, input, select, textarea, [role="button"], [role="link"]')
224
+ ))
225
+ }
226
+
227
+ function onRowClick(event: MouseEvent, row: Row, rowIndex: number): void {
228
+ if (!eventBelongsToNestedControl(event)) emit('row-click', row, rowIndex)
229
+ }
230
+
231
+ function onRowKeydown(event: KeyboardEvent, row: Row, rowIndex: number): void {
232
+ if (event.target === event.currentTarget) emit('row-click', row, rowIndex)
233
+ }
150
234
  const validatedLoading = computed(() => validateBoolean('STable', 'loading', props.loading))
151
235
  const resolvedAccessibleName = computed(() => {
152
236
  if (props.ariaLabel !== undefined && props.ariaLabelledby !== undefined) {
@@ -207,6 +291,9 @@ const validatedColumns = computed<readonly STableColumn<Row, Field>[]>(() => {
207
291
  if (column.align !== undefined) {
208
292
  validateExactString('STable', `column ${JSON.stringify(field)}.align`, column.align, TABLE_ALIGNMENTS)
209
293
  }
294
+ if (column.sortable !== undefined) {
295
+ validateBoolean('STable', `column ${JSON.stringify(field)}.sortable`, column.sortable)
296
+ }
210
297
  if (column.width !== undefined) {
211
298
  validateTableColumnInlineSize('STable', `column ${JSON.stringify(field)}.width`, column.width)
212
299
  }
@@ -241,6 +328,54 @@ const validatedColumns = computed<readonly STableColumn<Row, Field>[]>(() => {
241
328
  return props.columns
242
329
  })
243
330
 
331
+ const validatedSort = computed<STableSort<Field> | null>(() => {
332
+ if (props.sort === null || props.sort === undefined) return null
333
+ const key = validateNonEmptyString('STable', 'sort.key', props.sort.key) as Field
334
+ const column = validatedColumns.value.find((candidate) => candidate.field === key)
335
+ if (!column?.sortable) {
336
+ throw new TypeError(`STable: sort key ${JSON.stringify(key)} должен ссылаться на sortable column. / STable: sort key ${JSON.stringify(key)} must reference a sortable column.`)
337
+ }
338
+ return {
339
+ key,
340
+ direction: validateExactString(
341
+ 'STable',
342
+ 'sort.direction',
343
+ props.sort.direction,
344
+ ['ascending', 'descending'] as const,
345
+ ),
346
+ }
347
+ })
348
+
349
+ const columnAriaSort = (column: STableColumn<Row, Field>): 'none' | 'ascending' | 'descending' | undefined => {
350
+ if (!column.sortable) return undefined
351
+ return validatedSort.value?.key === column.field ? validatedSort.value.direction : 'none'
352
+ }
353
+
354
+ const sortIcon = (column: STableColumn<Row, Field>) => {
355
+ const direction = columnAriaSort(column)
356
+ return direction === 'ascending' ? ChevronUp : direction === 'descending' ? ChevronDown : ChevronsUpDown
357
+ }
358
+
359
+ const sortActionLabel = (column: STableColumn<Row, Field>): string => {
360
+ const direction = columnAriaSort(column)
361
+ if (direction === 'ascending') return `${column.header}: сортировать по убыванию`
362
+ if (direction === 'descending') return `${column.header}: сбросить сортировку`
363
+ return `${column.header}: сортировать по возрастанию`
364
+ }
365
+
366
+ function requestSort(column: STableColumn<Row, Field>): void {
367
+ if (!column.sortable) return
368
+ const current = validatedSort.value
369
+ const key = column.field as Field
370
+ const next: STableSort<Field> | null = current?.key !== key
371
+ ? { key, direction: 'ascending' }
372
+ : current.direction === 'ascending'
373
+ ? { key, direction: 'descending' }
374
+ : null
375
+ emit('update:sort', next)
376
+ emit('sort', next)
377
+ }
378
+
244
379
  function describeRowKey(key: STableRowKey): string {
245
380
  return typeof key === 'string' ? JSON.stringify(key) : String(key)
246
381
  }
@@ -283,12 +418,13 @@ const resolvedRowKeys = computed<ReadonlyMap<Row, RowKey>>(() => {
283
418
 
284
419
  const validatedRows = computed<readonly Row[]>(() => {
285
420
  const columns = validatedColumns.value
421
+ void validatedSort.value
286
422
  void resolvedRowKeys.value
287
423
  void validatedSelectedRowKey.value
288
424
  for (const row of props.data) {
289
425
  for (const column of columns) {
290
426
  const content = column.content ?? (column.value === undefined ? 'field' : 'value')
291
- if (content === 'field' && !Object.hasOwn(row, column.field)) {
427
+ if (content === 'field' && !hasOwn(row, column.field)) {
292
428
  throw new TypeError(
293
429
  `STable: column ${JSON.stringify(column.field)} не известен row; добавьте typed field или value resolver. `
294
430
  + `/ STable: column ${JSON.stringify(column.field)} is unknown to row; add a typed field or value resolver.`,
@@ -69,6 +69,7 @@ import {
69
69
  type VNodeRef,
70
70
  } from 'vue';
71
71
  import { mergeIdReferences, useOwnedAttrs } from '../../../internal/ownedAttrs';
72
+ import { hasOwn } from '../../../internal/es2020';
72
73
  import {
73
74
  resolveInteractiveElement,
74
75
  type ElementRefTarget,
@@ -202,7 +203,7 @@ function assertDenseArray(coordinate: string, value: unknown): readonly unknown[
202
203
  throw triggerContractError(`${coordinate} должен быть массивом. ${coordinate} must be an array.`);
203
204
  }
204
205
  for (let index = 0; index < value.length; index += 1) {
205
- if (!Object.hasOwn(value, index)) {
206
+ if (!hasOwn(value, index)) {
206
207
  throw triggerContractError(`${coordinate}[${index}] отсутствует в sparse array. ${coordinate}[${index}] is missing from a sparse array.`);
207
208
  }
208
209
  }
@@ -1,19 +1,4 @@
1
1
  /** Ключ элемента виртуального списка. / Virtual-list item key. */
2
2
  export type SVirtualListItemKey = string | number
3
3
 
4
- /**
5
- * Публичный контракт виртуального списка с измеряемыми строками.
6
- * Public contract for a virtual list with measured rows.
7
- */
8
- export interface SVirtualListProps<Item = unknown> {
9
- items: readonly Item[]
10
- /** Оценочная высота строки до фактического измерения ResizeObserver. / Estimated row height before ResizeObserver measurement. */
11
- itemSize: number
12
- overscan?: number
13
- height?: number | null
14
- /** Устойчивый доменный ключ; индекс массива не является допустимым fallback. / Stable domain key; an array index is not a valid fallback. */
15
- itemKey: (item: Item) => SVirtualListItemKey
16
- itemLabel: (item: Item, index: number) => string
17
- accessibleLabel: string
18
- externalScrollTarget?: HTMLElement | null
19
- }
4
+ export type { SVirtualListProps } from './SVirtualList.vue'
@@ -35,6 +35,24 @@
35
35
  </div>
36
36
  </template>
37
37
 
38
+ <script lang="ts">
39
+ import type { SVirtualListItemKey } from './SVirtualList'
40
+
41
+ /** Публичный контракт virtual list принадлежит source-SFC runtime. / The public virtual-list contract is owned by the source-SFC runtime. */
42
+ export interface SVirtualListProps<Item = unknown> {
43
+ items: readonly Item[]
44
+ /** Оценочная высота строки до фактического измерения. / Estimated row height before measurement. */
45
+ itemSize: number
46
+ overscan?: number
47
+ height?: number | null
48
+ /** Устойчивый доменный ключ; индекс не является fallback. / Stable domain key; the index is not a fallback. */
49
+ itemKey: (item: Item) => SVirtualListItemKey
50
+ itemLabel: (item: Item, index: number) => string
51
+ accessibleLabel: string
52
+ externalScrollTarget?: HTMLElement | null
53
+ }
54
+ </script>
55
+
38
56
  <script setup lang="ts" generic="T">
39
57
  import {
40
58
  computed,
@@ -47,8 +65,6 @@ import {
47
65
  } from 'vue'
48
66
  import { useOwnedAttrs } from '../../../internal/ownedAttrs'
49
67
  import { useInteractiveLeafRegistration } from '../../../internal/passiveContentContract'
50
- import type { SVirtualListItemKey, SVirtualListProps } from './SVirtualList'
51
-
52
68
  defineOptions({ inheritAttrs: false })
53
69
 
54
70
  type Alignment = 'auto' | 'start' | 'center' | 'end'
@@ -12,7 +12,7 @@ export type JsonValue = JsonScalar | JsonObject | JsonArray
12
12
 
13
13
  /** Escapes one RFC 6901 reference token. / Экранирует один reference token по RFC 6901. */
14
14
  export function escapeJsonPointerToken(token: string): string {
15
- return token.replaceAll('~', '~0').replaceAll('/', '~1')
15
+ return token.replace(/~/gu, '~0').replace(/\//gu, '~1')
16
16
  }
17
17
 
18
18
  /** Appends a reference token to an RFC 6901 JSON Pointer. / Добавляет reference token к JSON Pointer по RFC 6901. */
@@ -1,5 +1,4 @@
1
1
  import type {
2
- ScrollableViewportMaxBlockSize,
3
2
  TableColumnInlineSize,
4
3
  TableColumnMinInlineSize,
5
4
  } from '../../../internal/semanticSizing'
@@ -12,6 +11,13 @@ export type STableSurface = 'bordered' | 'plain'
12
11
  export type STableMinWidth = 'content' | 'md' | 'lg'
13
12
  export type STableDensity = 'default' | 'compact'
14
13
  export type STableColumnContent = 'field' | 'value' | 'slot'
14
+ export type STableSortDirection = 'ascending' | 'descending'
15
+
16
+ /** Controlled sort state; consumer owns data ordering. / Управляемое состояние сортировки; порядок данных принадлежит consumer. */
17
+ export interface STableSort<Field extends string = string> {
18
+ key: Field
19
+ direction: STableSortDirection
20
+ }
15
21
 
16
22
  interface STableColumnBase<Field extends string> {
17
23
  field: Field
@@ -19,6 +25,7 @@ interface STableColumnBase<Field extends string> {
19
25
  width?: TableColumnInlineSize
20
26
  minWidth?: TableColumnMinInlineSize
21
27
  align?: STableAlignment
28
+ sortable?: boolean
22
29
  }
23
30
 
24
31
  type STableFieldColumn<Field extends string> = STableColumnBase<Field> & {
@@ -47,30 +54,4 @@ export type STableColumn<
47
54
  | STableSlotColumn<Field>
48
55
  | (Field extends Extract<keyof Row, string> ? STableFieldColumn<Field> : never)
49
56
 
50
- /** Публичный generic-контракт таблицы. / Public generic table contract. */
51
- export interface STableProps<
52
- Row extends object,
53
- RowKey extends STableRowKey,
54
- Field extends string,
55
- > {
56
- columns: readonly STableColumn<Row, Field>[]
57
- data: readonly Row[]
58
- getRowKey: (row: Row) => RowKey
59
- loading?: boolean
60
- loadingMessage?: string
61
- errorMessage?: string
62
- emptyMessage?: string
63
- selectedRowKey?: RowKey
64
- maxHeight?: ScrollableViewportMaxBlockSize
65
- density?: STableDensity
66
- surface?: STableSurface
67
- minWidth?: STableMinWidth
68
- rowState?: (row: Row) => STableRowState
69
- cellState?: (row: Row, column: STableColumn<Row, Field>) => STableCellState
70
- /** Accessible name без visible owner. / Accessible name when there is no visible owner. */
71
- ariaLabel?: string
72
- /** DOM id visible owner, mutually exclusive with ariaLabel. / DOM id of the visible owner, mutually exclusive with ariaLabel. */
73
- ariaLabelledby?: string
74
- /** DOM id supplemental status/diagnostic owner. / DOM id of a supplemental status/diagnostic owner. */
75
- ariaDescribedby?: string
76
- }
57
+ export type { STableProps } from './STable.vue'
@@ -83,6 +83,13 @@
83
83
  </template>
84
84
 
85
85
  <script lang="ts">
86
+ import type { ScrollableViewportMaxBlockSize } from '../../../internal/semanticSizing'
87
+ import type {
88
+ SDataGridColumn,
89
+ SDataGridColumnKey,
90
+ SDataGridRow,
91
+ } from './dataGrid'
92
+
86
93
  export type {
87
94
  SDataGridCell,
88
95
  SDataGridCellKind,
@@ -90,14 +97,32 @@ export type {
90
97
  SDataGridColumnKey,
91
98
  SDataGridCommitInput,
92
99
  SDataGridEditPayload,
93
- SDataGridProps,
94
100
  SDataGridRow,
95
101
  SDataGridRowState,
96
102
  } from './dataGrid'
103
+
104
+ /** Публичный контракт data grid принадлежит source-SFC runtime. / The public data-grid contract is owned by the source-SFC runtime. */
105
+ export interface SDataGridProps<Key extends SDataGridColumnKey = SDataGridColumnKey> {
106
+ columns: readonly SDataGridColumn<Key>[]
107
+ rows: readonly SDataGridRow<Key>[]
108
+ title?: string
109
+ loading?: boolean
110
+ disabled?: boolean
111
+ readOnly?: boolean
112
+ errorMessage?: string
113
+ emptyMessage?: string
114
+ selectedRowId?: string
115
+ maxHeight?: ScrollableViewportMaxBlockSize
116
+ /** Accessible name без visible title. / Accessible name when there is no visible title. */
117
+ ariaLabel?: string
118
+ /** DOM id external visible title. / DOM id of an external visible title. */
119
+ ariaLabelledby?: string
120
+ }
97
121
  </script>
98
122
 
99
123
  <script setup lang="ts" generic="ColumnKey extends string = string">
100
124
  import { computed, reactive, useId, useSlots, watch } from 'vue'
125
+ import { hasOwn } from '../../../internal/es2020'
101
126
  import { useOwnedAttrs } from '../../../internal/ownedAttrs'
102
127
  import {
103
128
  validateScrollableViewportMaxBlockSize,
@@ -121,12 +146,8 @@ import type {
121
146
  } from '../data-display/table'
122
147
  import type {
123
148
  SDataGridCell,
124
- SDataGridColumn,
125
- SDataGridColumnKey,
126
149
  SDataGridCommitInput,
127
150
  SDataGridEditPayload,
128
- SDataGridProps,
129
- SDataGridRow,
130
151
  } from './dataGrid'
131
152
 
132
153
  defineOptions({ inheritAttrs: false })
@@ -266,7 +287,7 @@ const validatedGrid = computed<ValidatedGrid<ColumnKey>>(() => {
266
287
  }
267
288
  }
268
289
  for (const [key, column] of columnByKey) {
269
- if (!Object.hasOwn(row.cells, key)) {
290
+ if (!hasOwn(row.cells, key)) {
270
291
  throw new TypeError(
271
292
  `SDataGrid: row ${JSON.stringify(rowId)} не содержит cell ${JSON.stringify(key)}. `
272
293
  + `/ SDataGrid: row ${JSON.stringify(rowId)} is missing cell ${JSON.stringify(key)}.`,
@@ -358,7 +379,7 @@ function resolveColumn(key: string): SDataGridColumn<ColumnKey> {
358
379
 
359
380
  function resolveCell(row: SDataGridRow<ColumnKey>, columnKeyValue: string): SDataGridCell {
360
381
  const columnKey = resolveColumn(columnKeyValue).key
361
- if (Object.hasOwn(row.cells, columnKey)) return row.cells[columnKey]
382
+ if (hasOwn(row.cells, columnKey)) return row.cells[columnKey]
362
383
  throw new TypeError(
363
384
  `SDataGrid: row ${JSON.stringify(row.id)} не содержит cell ${JSON.stringify(columnKey)}. `
364
385
  + `/ SDataGrid: row ${JSON.stringify(row.id)} is missing cell ${JSON.stringify(columnKey)}.`,
@@ -404,7 +425,7 @@ function editCell(row: SDataGridRow<ColumnKey>, column: SDataGridColumn<ColumnKe
404
425
  function validateCommitInput(input: unknown): SDataGridCommitInput {
405
426
  assertRecord(input, 'commit input')
406
427
  const source = validateExactString('SDataGrid', 'commit input.source', input.source, COMMIT_SOURCES)
407
- const hasValue = Object.hasOwn(input, 'value')
428
+ const hasValue = hasOwn(input, 'value')
408
429
  if (source === 'draft' && hasValue) {
409
430
  throw new TypeError(
410
431
  'SDataGrid: draft commit не должен содержать value. / SDataGrid: draft commit must not contain value.',
@@ -51,6 +51,7 @@
51
51
 
52
52
  <script setup lang="ts">
53
53
  import { computed, ref, useId, useSlots } from 'vue'
54
+ import { hasOwn } from '../../../internal/es2020'
54
55
  import { useOwnedAttrs } from '../../../internal/ownedAttrs'
55
56
  import { provideTrustedCodeEditorExtensions } from '../../../internal/codeEditorExtensions'
56
57
  import { validateCodeEditorViewport } from '../../../internal/codeEditorContract'
@@ -291,8 +292,8 @@ const validatedDiagnostics = computed<SSqlEditorDiagnostic[]>(() => {
291
292
  return props.diagnostics.map((candidate, index) => {
292
293
  const runtimeCandidate: unknown = candidate
293
294
  const record = assertPlainExactRecord(`SSqlEditor.diagnostics[${index}]`, runtimeCandidate, ['severity', 'message', 'from', 'to', 'line', 'column'])
294
- const hasOffset = Object.hasOwn(record, 'from')
295
- const hasLine = Object.hasOwn(record, 'line') || Object.hasOwn(record, 'column')
295
+ const hasOffset = hasOwn(record, 'from')
296
+ const hasLine = hasOwn(record, 'line') || hasOwn(record, 'column')
296
297
  if (hasOffset === hasLine) throw new TypeError(`SSqlEditor: diagnostics[${index}] должен задавать ровно один range: from/to или line/column. / SSqlEditor: diagnostics[${index}] must provide exactly one range: from/to or line/column.`)
297
298
  const allowedRangeKeys = hasOffset
298
299
  ? ['severity', 'message', 'from', 'to']