@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
@@ -1,5 +1,4 @@
1
1
  import type {
2
- ScrollableViewportMaxBlockSize,
3
2
  TableColumnInlineSize,
4
3
  TableColumnMinInlineSize,
5
4
  } from '../../../internal/semanticSizing'
@@ -43,19 +42,4 @@ export type SDataGridCommitInput =
43
42
  | { source: 'draft' }
44
43
  | { source: 'value'; value: unknown }
45
44
 
46
- export interface SDataGridProps<Key extends SDataGridColumnKey = SDataGridColumnKey> {
47
- columns: readonly SDataGridColumn<Key>[]
48
- rows: readonly SDataGridRow<Key>[]
49
- title?: string
50
- loading?: boolean
51
- disabled?: boolean
52
- readOnly?: boolean
53
- errorMessage?: string
54
- emptyMessage?: string
55
- selectedRowId?: string
56
- maxHeight?: ScrollableViewportMaxBlockSize
57
- /** Accessible name без visible title. / Accessible name when there is no visible title. */
58
- ariaLabel?: string
59
- /** DOM id external visible title. / DOM id of an external visible title. */
60
- ariaLabelledby?: string
61
- }
45
+ export type { SDataGridProps } from './SDataGrid.vue'
@@ -270,6 +270,9 @@ const resolveOverflowTrigger = (item: BottomNavOverflowItem): HTMLButtonElement
270
270
  throw new Error(`SBottomNav: overflow trigger '${item.id}' is unavailable`);
271
271
  }
272
272
  const trigger = api.getElement();
273
+ if (!(trigger instanceof HTMLButtonElement)) {
274
+ throw new TypeError(`SBottomNav: overflow trigger '${item.id}' must be an action button`);
275
+ }
273
276
  const expectedId = overflowTriggerId(item);
274
277
  if (trigger.id !== expectedId) {
275
278
  throw new Error(
@@ -58,6 +58,7 @@ import {
58
58
  type VNodeChild,
59
59
  } from 'vue'
60
60
  import { useOwnedAttrs } from '../../../internal/ownedAttrs'
61
+ import { lastItem } from '../../../internal/es2020'
61
62
  import {
62
63
  tabsContextKey,
63
64
  type STabListRegistrationKey,
@@ -425,7 +426,7 @@ function handleTabKeydown(key: STabRegistrationKey, event: KeyboardEvent): void
425
426
  }
426
427
  let next: TabRecord | undefined
427
428
  if (event.key === 'Home') next = enabled[0]
428
- else if (event.key === 'End') next = enabled.at(-1)
429
+ else if (event.key === 'End') next = lastItem(enabled)
429
430
  else if (event.key === 'ArrowLeft') next = enabled[(currentIndex - 1 + enabled.length) % enabled.length]
430
431
  else if (event.key === 'ArrowRight') next = enabled[(currentIndex + 1) % enabled.length]
431
432
  if (!next) return
@@ -45,8 +45,9 @@ export function createUiPreferencesResetController(): UiPreferencesResetControll
45
45
  throw errors[0];
46
46
  }
47
47
  if (errors.length > 1) {
48
- throw new AggregateError(errors, 'Не все owners UI-предпочтений смогли обработать сброс.');
48
+ throw createAggregateError(errors, 'Не все owners UI-предпочтений смогли обработать сброс.');
49
49
  }
50
50
  },
51
51
  };
52
52
  }
53
+ import { createAggregateError } from '../internal/es2020';
@@ -28,10 +28,11 @@ export function useClipboard(): ClipboardService {
28
28
  const isDomException = typeof DOMException !== 'undefined' && cause instanceof DOMException
29
29
  const error = cause instanceof Error || isDomException
30
30
  ? cause
31
- : new Error('Clipboard API rejected writeText without an Error object', { cause })
31
+ : withErrorCause(new Error('Clipboard API rejected writeText without an Error object'), cause)
32
32
  return { status: 'failed', error }
33
33
  }
34
34
  }
35
35
 
36
36
  return { copyText }
37
37
  }
38
+ import { withErrorCause } from '../internal/es2020'
@@ -1,4 +1,5 @@
1
1
  import { computed, getCurrentScope, onScopeDispose, reactive, readonly, watch } from 'vue';
2
+ import { hasOwn, lastItem } from '../internal/es2020';
2
3
 
3
4
  import {
4
5
  assertSidebarSectionsState,
@@ -284,7 +285,7 @@ function preferredFlexibleSectionKey(
284
285
  return config?.defaultIsOpen === true && config.defaultSize === null;
285
286
  });
286
287
  if (defaultFlexibleKey && state[defaultFlexibleKey]?.isOpen) return defaultFlexibleKey;
287
- return openKeys.at(-1) ?? null;
288
+ return lastItem(openKeys) ?? null;
288
289
  }
289
290
 
290
291
  /**
@@ -327,7 +328,7 @@ function decodePersistedDocument(
327
328
  if (!isPlainRecord(value)) {
328
329
  throw new SidebarPanelStateContractError('$', 'expected an object');
329
330
  }
330
- if (Object.hasOwn(value, 'version') || Object.hasOwn(value, 'sections')) {
331
+ if (hasOwn(value, 'version') || hasOwn(value, 'sections')) {
331
332
  assertExactKeys(value, ['sections', 'version'], '$');
332
333
  if (value.version !== SIDEBAR_PANEL_STATE_SCHEMA_VERSION) {
333
334
  throw new SidebarPanelStateContractError(
@@ -0,0 +1,50 @@
1
+ /** Проверяет собственное свойство без зависимости от ES2022 Object.hasOwn. / Checks an own property without the ES2022 Object.hasOwn API. */
2
+ export function hasOwn(object: object, key: PropertyKey): boolean {
3
+ return Object.prototype.hasOwnProperty.call(object, key)
4
+ }
5
+
6
+ /** Возвращает последний элемент без зависимости от ES2022 Array.at. / Returns the last item without the ES2022 Array.at API. */
7
+ export function lastItem<T>(items: readonly T[]): T | undefined {
8
+ return items.length > 0 ? items[items.length - 1] : undefined
9
+ }
10
+
11
+ /** Ошибка с ES2022-compatible cause, доступная в ES2020 runtime. / An ES2022-compatible error cause for ES2020 runtimes. */
12
+ export type ErrorWithCause<T extends Error = Error> = T & { cause: unknown }
13
+
14
+ /** Добавляет стандартный non-enumerable cause без требования ES2022 lib. / Adds the standard non-enumerable cause without requiring the ES2022 lib. */
15
+ export function withErrorCause<T extends Error>(error: T, cause: unknown): ErrorWithCause<T> {
16
+ Object.defineProperty(error, 'cause', {
17
+ configurable: true,
18
+ value: cause,
19
+ writable: true,
20
+ })
21
+ return error as ErrorWithCause<T>
22
+ }
23
+
24
+ interface AggregateErrorLike extends Error {
25
+ readonly errors: readonly unknown[]
26
+ }
27
+
28
+ interface AggregateErrorConstructorLike {
29
+ new (errors: Iterable<unknown>, message?: string): AggregateErrorLike
30
+ }
31
+
32
+ class Es2020AggregateError extends Error implements AggregateErrorLike {
33
+ readonly errors: readonly unknown[]
34
+
35
+ constructor(errors: readonly unknown[], message: string) {
36
+ super(message)
37
+ this.name = 'AggregateError'
38
+ this.errors = [...errors]
39
+ }
40
+ }
41
+
42
+ /** Создаёт native AggregateError при наличии и точный ES2020 fallback иначе. / Creates a native AggregateError when available and an exact ES2020 fallback otherwise. */
43
+ export function createAggregateError(errors: readonly unknown[], message: string): AggregateErrorLike {
44
+ const candidate: unknown = (globalThis as { AggregateError?: unknown }).AggregateError
45
+ if (typeof candidate === 'function') {
46
+ const AggregateErrorConstructor = candidate as AggregateErrorConstructorLike
47
+ return new AggregateErrorConstructor(errors, message)
48
+ }
49
+ return new Es2020AggregateError(errors, message)
50
+ }
@@ -1,4 +1,5 @@
1
1
  import { validateBoundedInteger, validateNonEmptyString } from './runtimeContract'
2
+ import { hasOwn, lastItem } from './es2020'
2
3
 
3
4
  export type InlineTokenEditorSegmentContract =
4
5
  | { readonly kind: 'text'; readonly text: string }
@@ -44,7 +45,7 @@ export function parseInlineTokenEditorSource(
44
45
  const parsed: InlineTokenEditorSegmentContract[] = []
45
46
  const tokenOccurrences = new Map<string, number>()
46
47
  for (let index = 0; index < segments.length; index += 1) {
47
- if (!Object.hasOwn(segments, index)) {
48
+ if (!hasOwn(segments, index)) {
48
49
  throw new TypeError(`SInlineTokenEditor: segments[${index}] отсутствует в sparse array. / SInlineTokenEditor: segments[${index}] is missing from a sparse array.`)
49
50
  }
50
51
  const record = assertPlainRecord(`SInlineTokenEditor.segments[${index}]`, segments[index])
@@ -54,7 +55,7 @@ export function parseInlineTokenEditorSource(
54
55
  throw new TypeError(`SInlineTokenEditor: segments[${index}].text должен быть непустой строкой. / SInlineTokenEditor: segments[${index}].text must be a non-empty string.`)
55
56
  }
56
57
  const text = record.text
57
- if (parsed.at(-1)?.kind === 'text') {
58
+ if (lastItem(parsed)?.kind === 'text') {
58
59
  throw new TypeError(`SInlineTokenEditor: segments[${index}] является смежным text segment; объедините его в источнике. / SInlineTokenEditor: segments[${index}] is an adjacent text segment; merge it at the source.`)
59
60
  }
60
61
  parsed.push({ kind: 'text', text })
@@ -122,7 +123,7 @@ export function composeInlineTokenSegments(
122
123
  for (const segment of segments) {
123
124
  if (segment.kind === 'text') {
124
125
  if (segment.text.length === 0) continue
125
- const previous = result.at(-1)
126
+ const previous = lastItem(result)
126
127
  if (previous?.kind === 'text') {
127
128
  result[result.length - 1] = { kind: 'text', text: previous.text + segment.text }
128
129
  } else {
@@ -126,6 +126,13 @@ export const ownedAttributeSchemas = Object.freeze({
126
126
  onPointerdown: 'listener',
127
127
  tabindex: 'string-or-number',
128
128
  }),
129
+ SSwitch: defineOwnedAttributeSchema({
130
+ 'aria-label': 'string',
131
+ 'aria-describedby': 'string',
132
+ onClick: 'listener',
133
+ onPointerdown: 'listener',
134
+ tabindex: 'string-or-number',
135
+ }),
129
136
  SCheckboxGroup: defineOwnedAttributeSchema({
130
137
  'aria-describedby': 'string',
131
138
  'data-testid': 'data',
@@ -137,6 +144,10 @@ export const ownedAttributeSchemas = Object.freeze({
137
144
  SColorSelect: defineOwnedAttributeSchema({
138
145
  'aria-label': 'string',
139
146
  }),
147
+ SCombobox: defineOwnedAttributeSchema({
148
+ 'aria-describedby': 'string',
149
+ 'aria-label': 'string',
150
+ }),
140
151
  SDragHandle: defineOwnedAttributeSchema({
141
152
  'data-testid': 'data',
142
153
  }),
@@ -158,7 +169,12 @@ export const ownedAttributeSchemas = Object.freeze({
158
169
  title: 'string',
159
170
  }),
160
171
  SInputText: defineOwnedAttributeSchema({
172
+ 'aria-activedescendant': 'string',
173
+ 'aria-autocomplete': 'string',
174
+ 'aria-controls': 'string',
161
175
  'aria-describedby': 'string',
176
+ 'aria-expanded': 'aria-boolean',
177
+ 'aria-haspopup': 'aria-haspopup',
162
178
  'aria-invalid': 'aria-boolean',
163
179
  'aria-label': 'string',
164
180
  'data-testid': 'data',
@@ -168,10 +184,12 @@ export const ownedAttributeSchemas = Object.freeze({
168
184
  max: 'string-or-number',
169
185
  min: 'string-or-number',
170
186
  onBlur: 'listener',
187
+ onClick: 'listener',
171
188
  onFocus: 'listener',
172
189
  onKeydown: 'listener',
173
190
  onKeyup: 'listener',
174
191
  pattern: 'string',
192
+ role: 'string',
175
193
  step: 'string-or-number',
176
194
  title: 'string',
177
195
  }),
@@ -237,6 +255,10 @@ export const ownedAttributeSchemas = Object.freeze({
237
255
  'data-testid': 'data',
238
256
  title: 'string',
239
257
  }),
258
+ SChip: defineOwnedAttributeSchema({
259
+ 'aria-label': 'string',
260
+ title: 'string',
261
+ }),
240
262
  SCodeBlock: defineOwnedAttributeSchema({
241
263
  'aria-label': 'string',
242
264
  'data-testid': 'data',
@@ -338,7 +360,7 @@ type OwnedAttributeValue<Name extends string, Kind extends OwnedAttributeValueKi
338
360
 
339
361
  export type OwnedAttributeBindings<Component extends OwnedAttributeComponent> = Readonly<Partial<{
340
362
  [Name in OwnedAttributeName<Component>]: OwnedAttributeValue<Name, OwnedAttributeKind<Component, Name>>
341
- }>>
363
+ }> & Partial<Record<`data-${string}`, string | number | boolean>>>
342
364
 
343
365
  export interface OwnedAttrsOptions<Component extends OwnedAttributeComponent> {
344
366
  /** Имя public-компонента для диагностики контракта. / Public component name used in contract diagnostics. */
@@ -420,7 +442,7 @@ export function useOwnedAttrs<Component extends OwnedAttributeComponent>(
420
442
  `raw "${name}" запрещён; атрибут "${name}" не объявлен в exact owned-attrs schema; используйте типизированные props/tokens UI-kit или consumer layout wrapper. Raw "${name}" is forbidden and is not declared by the exact owned-attrs schema; use typed UI-kit props/tokens or a consumer layout wrapper`,
421
443
  )
422
444
  }
423
- const kind = schema[name]
445
+ const kind = schema[name] ?? (name.startsWith('data-') && name.length > 5 ? 'data' : undefined)
424
446
  if (!kind) {
425
447
  throw attributeBoundaryError(
426
448
  options.component,
@@ -19,7 +19,7 @@ let preservedCursor = ''
19
19
  let preservedUserSelect = ''
20
20
 
21
21
  const applyActiveInteraction = (): void => {
22
- const interaction = activeInteractions.at(-1)
22
+ const interaction = lastItem(activeInteractions)
23
23
  if (!lockedBody || !interaction) return
24
24
  lockedBody.style.cursor = interaction.cursor
25
25
  lockedBody.style.userSelect = 'none'
@@ -79,3 +79,4 @@ export function acquirePointerInteractionLease(
79
79
  },
80
80
  }
81
81
  }
82
+ import { lastItem } from './es2020'