@pgcorp/ui-kit 0.8.1 → 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.
Files changed (113) hide show
  1. package/README.md +332 -9
  2. package/docs/getting-started.md +135 -0
  3. package/docs/public-api.md +53 -4
  4. package/docs/rich-document.md +216 -0
  5. package/docs/scroll-viewport.md +167 -0
  6. package/docs/theming.md +17 -0
  7. package/package.json +108 -50
  8. package/src/components/layout/SAppShell.vue +3 -1
  9. package/src/components/layout/SDockRegion.vue +5 -3
  10. package/src/components/layout/SResizeHandle.vue +45 -5
  11. package/src/components/layout/SWorkbenchPanelShell.vue +4 -2
  12. package/src/components/shared/_internal/SDialogSurface.css +23 -0
  13. package/src/components/shared/_internal/SDialogSurface.vue +9 -2
  14. package/src/components/shared/_internal/SToastItem.css +4 -0
  15. package/src/components/shared/_internal/SToastItem.vue +44 -8
  16. package/src/components/shared/_internal/STreeNode.vue +24 -11
  17. package/src/components/shared/_internal/dialogSurface.ts +7 -1
  18. package/src/components/shared/complex/SKanbanLane.vue +4 -2
  19. package/src/components/shared/containers/SCloudChromeRow.vue +3 -2
  20. package/src/components/shared/containers/SDrawer.css +11 -2
  21. package/src/components/shared/containers/SDrawer.vue +48 -9
  22. package/src/components/shared/containers/SModal.vue +5 -2
  23. package/src/components/shared/containers/SPanel.vue +4 -2
  24. package/src/components/shared/containers/SPopover.vue +7 -3
  25. package/src/components/shared/containers/SUiKitLocaleProvider.vue +57 -0
  26. package/src/components/shared/controls/SButton.css +14 -10
  27. package/src/components/shared/controls/SCombobox.vue +8 -6
  28. package/src/components/shared/controls/SComboboxTrigger.css +7 -4
  29. package/src/components/shared/controls/SComboboxTrigger.vue +5 -2
  30. package/src/components/shared/controls/SContextToggleButton.vue +6 -4
  31. package/src/components/shared/controls/SDragHandle.vue +8 -4
  32. package/src/components/shared/controls/SDropdownMenu.vue +7 -4
  33. package/src/components/shared/controls/SInputText.css +5 -4
  34. package/src/components/shared/controls/SListbox.vue +4 -2
  35. package/src/components/shared/controls/SSelect.vue +4 -2
  36. package/src/components/shared/controls/STextarea.ts +13 -0
  37. package/src/components/shared/controls/STextarea.vue +391 -37
  38. package/src/components/shared/controls/_internal/SInlineTokenSurface.vue +4 -2
  39. package/src/components/shared/data-display/SChart.vue +7 -5
  40. package/src/components/shared/data-display/SChip.vue +3 -1
  41. package/src/components/shared/data-display/SCodeBlock.css +4 -0
  42. package/src/components/shared/data-display/SCodeBlock.vue +46 -22
  43. package/src/components/shared/data-display/SCodeEditor.css +17 -0
  44. package/src/components/shared/data-display/SCodeEditor.vue +173 -8
  45. package/src/components/shared/data-display/SCodeSearchPanel.vue +48 -33
  46. package/src/components/shared/data-display/SCodeWorkspaceEditor.css +13 -0
  47. package/src/components/shared/data-display/SCodeWorkspaceEditor.vue +355 -0
  48. package/src/components/shared/data-display/SControlledToastViewport.css +89 -0
  49. package/src/components/shared/data-display/SControlledToastViewport.vue +205 -0
  50. package/src/components/shared/data-display/SCopyField.vue +12 -10
  51. package/src/components/shared/data-display/SDiffViewer.css +122 -0
  52. package/src/components/shared/data-display/SDiffViewer.vue +499 -0
  53. package/src/components/shared/data-display/SDocBlock.vue +99 -71
  54. package/src/components/shared/data-display/SExpandableText.vue +10 -8
  55. package/src/components/shared/data-display/SJsonTree.vue +4 -2
  56. package/src/components/shared/data-display/SLinkedSystemsList.vue +4 -2
  57. package/src/components/shared/data-display/SMessage.vue +6 -6
  58. package/src/components/shared/data-display/SRichDocument.css +8 -0
  59. package/src/components/shared/data-display/SRichDocument.vue +65 -0
  60. package/src/components/shared/data-display/SScrollViewport.css +53 -0
  61. package/src/components/shared/data-display/SScrollViewport.vue +470 -0
  62. package/src/components/shared/data-display/STable.vue +38 -19
  63. package/src/components/shared/data-display/SToastContainer.vue +22 -17
  64. package/src/components/shared/data-display/SVirtualList.ts +33 -0
  65. package/src/components/shared/data-display/SVirtualList.vue +321 -36
  66. package/src/components/shared/data-display/_internal/SCodeTokenLine.css +72 -0
  67. package/src/components/shared/data-display/_internal/SCodeTokenLine.vue +79 -0
  68. package/src/components/shared/data-display/_internal/SRichBlockNode.css +85 -0
  69. package/src/components/shared/data-display/_internal/SRichBlockNode.vue +282 -0
  70. package/src/components/shared/data-display/_internal/SRichInlineNode.css +6 -0
  71. package/src/components/shared/data-display/_internal/SRichInlineNode.vue +76 -0
  72. package/src/components/shared/data-display/_internal/SRichMath.css +42 -0
  73. package/src/components/shared/data-display/_internal/SRichMath.vue +102 -0
  74. package/src/components/shared/data-display/codeEditor.ts +200 -0
  75. package/src/components/shared/data-display/codeWorkspace.ts +233 -0
  76. package/src/components/shared/data-display/diff.ts +541 -0
  77. package/src/components/shared/data-display/richDocument.ts +876 -0
  78. package/src/components/shared/data-display/scrollViewport.ts +47 -0
  79. package/src/components/shared/data-display/toast.ts +194 -0
  80. package/src/components/shared/database/SDataGrid.vue +7 -4
  81. package/src/components/shared/database/SSqlEditor.vue +39 -39
  82. package/src/components/shared/feedback/SAsyncState.vue +3 -1
  83. package/src/components/shared/graph/SGraphViewport.vue +11 -9
  84. package/src/components/shared/navigation/SBreadcrumbs.vue +8 -5
  85. package/src/components/shared/navigation/SCatalogNavigator.vue +26 -20
  86. package/src/components/shared/navigation/STabList.vue +10 -6
  87. package/src/components/shared/navigation/STabs.vue +15 -11
  88. package/src/components/shared/navigation/SWizardSteps.vue +4 -2
  89. package/src/components/shared/persona/SPersonaProjectPicker.vue +31 -27
  90. package/src/components/shared/persona/SPersonaRunWorkbench.vue +39 -33
  91. package/src/components/shared/persona/_internal/SPersonaRunConsole.vue +8 -6
  92. package/src/components/shared/persona/_internal/labels.ts +66 -57
  93. package/src/components/shared/persona/presentation.ts +140 -110
  94. package/src/composables/useFloatingPosition.ts +39 -2
  95. package/src/composables/useUiKitLocale.ts +17 -0
  96. package/src/internal/codeHighlight.ts +110 -0
  97. package/src/internal/diffEngine.ts +464 -0
  98. package/src/internal/diffRuntime.ts +189 -0
  99. package/src/internal/feedbackPresentation.ts +33 -14
  100. package/src/internal/localeContext.ts +22 -0
  101. package/src/internal/ownedAttrs.ts +8 -0
  102. package/src/internal/runtimeContract.ts +11 -9
  103. package/src/internal/semanticSizing.ts +13 -0
  104. package/src/locales/catalog.ts +634 -0
  105. package/src/locales/locale.ts +472 -0
  106. package/src/locales/messages.generated.ts +1826 -0
  107. package/src/locales/messages.ts +150 -0
  108. package/src/stores/useNotifierStore.ts +61 -44
  109. package/src/styles/tokens.css +23 -2
  110. package/src/theme.ts +59 -32
  111. package/src/workers/diff.worker.ts +73 -0
  112. package/src/workers/diffProtocol.ts +155 -0
  113. package/src/components/shared/data-display/SToastContainer.css +0 -53
@@ -5,6 +5,10 @@
5
5
  :role="liveAttributes.role"
6
6
  :aria-live="liveAttributes.ariaLive"
7
7
  :aria-atomic="liveAttributes.ariaAtomic"
8
+ @mouseenter="emit('lifetime-pause', notification.id, 'pointer')"
9
+ @mouseleave="emit('lifetime-resume', notification.id, 'pointer')"
10
+ @focusin="emit('lifetime-pause', notification.id, 'focus')"
11
+ @focusout="onFocusOut"
8
12
  >
9
13
  <div class="s-toast-icon">
10
14
  <component
@@ -20,40 +24,61 @@
20
24
  <p v-if="notification.message" class="s-toast-body">
21
25
  {{ notification.message }}
22
26
  </p>
27
+ <div v-if="notification.actions?.length" class="s-toast-actions">
28
+ <SButton
29
+ v-for="action in notification.actions"
30
+ :key="action.id"
31
+ size="xs"
32
+ severity="secondary"
33
+ :appearance="action.appearance ?? 'soft'"
34
+ :label="action.label"
35
+ @click="emit('action', { notificationId: notification.id, actionId: action.id })"
36
+ />
37
+ </div>
23
38
  </div>
24
39
 
25
- <div class="s-toast-close">
40
+ <div v-if="notification.dismissible !== false" class="s-toast-close">
26
41
  <SButton
27
- size="sm"
42
+ size="xs"
28
43
  appearance="ghost"
29
44
  icon-only
30
45
  :leading-icon="X"
31
- aria-label="Закрыть"
46
+ :aria-label="t('feedback.closeNotification')"
32
47
  @click="emit('dismiss', notification.id)"
33
48
  />
34
49
  </div>
35
50
  </div>
36
51
  </template>
37
52
 
38
- <script setup lang="ts">
53
+ <script setup lang="ts" generic="Id extends SToastId = string, ActionId extends SToastActionId = string">
39
54
  import { computed } from 'vue'
55
+ import { useUiKitLocale } from '../../../composables/useUiKitLocale'
40
56
 
41
57
  import { X } from '../../icons/sputnigUiIcons'
42
58
  import { resolveFeedbackLive, resolveFeedbackPresentation } from '../../../internal/feedbackPresentation'
43
59
  import { resolveSemanticToneBinding } from '../../../internal/semanticTone'
44
- import type { Notification } from '../../../stores/useNotifierStore';
60
+ import type {
61
+ SToastActionId,
62
+ SToastActionIntent,
63
+ SToastId,
64
+ SToastPresentation,
65
+ } from '../data-display/toast'
45
66
  import SButton from '../controls/SButton.vue'
46
67
 
47
68
  defineOptions({ inheritAttrs: false })
48
69
 
49
70
  const props = defineProps<{
50
- notification: Notification
71
+ notification: SToastPresentation<Id, ActionId>
51
72
  }>()
52
73
 
53
74
  const emit = defineEmits<{
54
- dismiss: [id: string]
75
+ dismiss: [id: Id]
76
+ action: [intent: SToastActionIntent<Id, ActionId>]
77
+ 'lifetime-pause': [id: Id, reason: 'pointer' | 'focus']
78
+ 'lifetime-resume': [id: Id, reason: 'pointer' | 'focus']
55
79
  }>()
56
- const presentation = computed(() => resolveFeedbackPresentation(props.notification.severity))
80
+ const { t } = useUiKitLocale()
81
+ const presentation = computed(() => resolveFeedbackPresentation(props.notification.severity, t))
57
82
  const toneBinding = computed(() => resolveSemanticToneBinding(
58
83
  'SToastItem',
59
84
  'notification.severity',
@@ -61,5 +86,16 @@ const toneBinding = computed(() => resolveSemanticToneBinding(
61
86
  ['info', 'success', 'warn', 'danger'] as const,
62
87
  ))
63
88
  const liveAttributes = computed(() => resolveFeedbackLive(props.notification.severity, 'auto'))
89
+
90
+ function onFocusOut(event: FocusEvent): void {
91
+ const currentTarget = event.currentTarget
92
+ const relatedTarget = event.relatedTarget
93
+ if (
94
+ currentTarget instanceof HTMLElement
95
+ && relatedTarget instanceof Node
96
+ && currentTarget.contains(relatedTarget)
97
+ ) return
98
+ emit('lifetime-resume', props.notification.id, 'focus')
99
+ }
64
100
  </script>
65
101
  <style lang="postcss" src="./SToastItem.css" scoped></style>
@@ -217,6 +217,7 @@
217
217
 
218
218
  <script setup lang="ts" generic="T extends object, K extends SelectionKey">
219
219
  import { computed, defineComponent, h, inject, onBeforeUnmount, onMounted, provide, ref } from 'vue'
220
+ import { useUiKitLocale } from '../../../composables/useUiKitLocale'
220
221
  import type { PropType } from 'vue'
221
222
  import { Check, ChevronDown, ChevronRight, Minus } from '../../icons/sputnigUiIcons'
222
223
  import {
@@ -247,6 +248,7 @@ const props = defineProps<{
247
248
  position: number
248
249
  setSize: number
249
250
  }>()
251
+ const { t } = useUiKitLocale()
250
252
 
251
253
  const slots = defineSlots<{
252
254
  'col-left': (props: STreeNodeContentScope<T, K>) => unknown
@@ -336,12 +338,14 @@ const nodeIndent = computed(() => {
336
338
  if (runtime.density.value === 'compact') return `${props.level * 0.85 + 0.25}rem`
337
339
  return `${props.level === 0 ? 1 : props.level * 1.5}rem`
338
340
  })
339
- const toggleLabel = computed(() =>
340
- `${isExpanded.value ? 'Свернуть' : 'Развернуть'} узел ${nodeLabel.value}`,
341
- )
342
- const selectionLabel = computed(() =>
343
- `${isSelected.value || isPartiallySelected.value ? 'Снять выбор' : 'Выбрать'}: ${nodeLabel.value}`,
344
- )
341
+ const toggleLabel = computed(() => t(
342
+ isExpanded.value ? 'tree.collapseNode' : 'tree.expandNode',
343
+ { label: nodeLabel.value },
344
+ ))
345
+ const selectionLabel = computed(() => t(
346
+ isSelected.value || isPartiallySelected.value ? 'tree.deselectNode' : 'tree.selectNode',
347
+ { label: nodeLabel.value },
348
+ ))
345
349
  const selectionPressedState = computed(() => isPartiallySelected.value ? 'mixed' : isSelected.value)
346
350
  const selectionCheckedState = computed(() => isPartiallySelected.value ? 'mixed' : isSelected.value)
347
351
 
@@ -689,7 +693,7 @@ onBeforeUnmount(() => {
689
693
  }
690
694
 
691
695
  .s-tree-node-wrapper--compact .s-tree-node {
692
- min-height: var(--s-control-hitbox-xs);
696
+ min-height: var(--s-control-hitbox-2xs);
693
697
  gap: 0.25rem;
694
698
  padding: 0 0.25rem;
695
699
  }
@@ -709,6 +713,10 @@ onBeforeUnmount(() => {
709
713
  padding-right: 0.78rem;
710
714
  }
711
715
 
716
+ .s-tree-node-wrapper--overlay-actions > .s-tree-node-hitbox .s-tree-node-content-shell {
717
+ position: relative;
718
+ }
719
+
712
720
  .s-tree-node-wrapper--overlay-actions .s-tree-node-right {
713
721
  position: relative;
714
722
  z-index: var(--s-layer-sticky);
@@ -716,7 +724,7 @@ onBeforeUnmount(() => {
716
724
  justify-content: flex-end;
717
725
  }
718
726
 
719
- .s-tree-node-wrapper--overlay-actions .s-tree-node-actions {
727
+ .s-tree-node-wrapper--overlay-actions > .s-tree-node-hitbox .s-tree-node-actions {
720
728
  position: absolute;
721
729
  top: 50%;
722
730
  right: 0.42rem;
@@ -727,17 +735,22 @@ onBeforeUnmount(() => {
727
735
  background: color-mix(in srgb, var(--color-surface-0) 98%, transparent);
728
736
  box-shadow: -18px 0 24px color-mix(in srgb, var(--color-surface-0) 86%, transparent);
729
737
  opacity: 0;
738
+ pointer-events: none;
730
739
  transform: translateY(-50%);
731
740
  transition: opacity var(--s-motion-fast) ease, transform var(--s-motion-fast) ease;
732
741
  }
733
742
 
734
- .s-tree-node-wrapper--overlay-actions .s-tree-node-actions--has-persistent {
743
+ .s-tree-node-wrapper--overlay-actions > .s-tree-node-hitbox .s-tree-node-actions--has-persistent {
735
744
  opacity: 1;
745
+ pointer-events: auto;
736
746
  }
737
747
 
738
- .s-tree-node-wrapper--overlay-actions .s-tree-node:hover .s-tree-node-actions,
739
- .s-tree-node-wrapper--overlay-actions .s-tree-node:focus-within .s-tree-node-actions {
748
+ .s-tree-node-wrapper--overlay-actions > .s-tree-node-hitbox:hover > .s-tree-node-row > .s-tree-node-content-shell > .s-tree-node-actions,
749
+ .s-tree-node-wrapper--overlay-actions > .s-tree-node-hitbox:focus-within > .s-tree-node-row > .s-tree-node-content-shell > .s-tree-node-actions,
750
+ .s-tree-node-wrapper--overlay-actions > .s-tree-node-hitbox:hover .s-tree-node > .s-tree-node-right > .s-tree-node-actions,
751
+ .s-tree-node-wrapper--overlay-actions > .s-tree-node-hitbox:focus-within .s-tree-node > .s-tree-node-right > .s-tree-node-actions {
740
752
  opacity: 1;
753
+ pointer-events: auto;
741
754
  transform: translate(-0.04rem, -50%);
742
755
  }
743
756
 
@@ -1,5 +1,6 @@
1
1
  export type DialogSurfaceModalSize = 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl' | 'wide' | 'full'
2
2
  export type DialogSurfaceDrawerSize = 'sm' | 'md' | 'lg'
3
+ export type DialogSurfaceDrawerPlacement = 'right' | 'bottom'
3
4
  export type DialogSurfaceTone = 'default' | 'subtle'
4
5
  export type DialogSurfaceScrollBehavior = 'page' | 'body'
5
6
 
@@ -14,6 +15,7 @@ export interface ModalDialogSurfacePresentation {
14
15
  export interface DrawerDialogSurfacePresentation {
15
16
  kind: 'drawer'
16
17
  size: DialogSurfaceDrawerSize
18
+ placement: DialogSurfaceDrawerPlacement
17
19
  mobileFullScreen: boolean
18
20
  }
19
21
 
@@ -38,9 +40,10 @@ export class DialogSurfaceContractError extends Error {
38
40
  }
39
41
 
40
42
  const modalKeys = new Set(['kind', 'size', 'fullscreen', 'surface', 'scrollBehavior'])
41
- const drawerKeys = new Set(['kind', 'size', 'mobileFullScreen'])
43
+ const drawerKeys = new Set(['kind', 'size', 'placement', 'mobileFullScreen'])
42
44
  const modalSizes = new Set<DialogSurfaceModalSize>(['sm', 'md', 'lg', 'xl', '2xl', '3xl', 'wide', 'full'])
43
45
  const drawerSizes = new Set<DialogSurfaceDrawerSize>(['sm', 'md', 'lg'])
46
+ const drawerPlacements = new Set<DialogSurfaceDrawerPlacement>(['right', 'bottom'])
44
47
  const surfaceTones = new Set<DialogSurfaceTone>(['default', 'subtle'])
45
48
  const scrollBehaviors = new Set<DialogSurfaceScrollBehavior>(['page', 'body'])
46
49
 
@@ -92,6 +95,9 @@ export function assertDialogSurfacePresentation(
92
95
  if (!drawerSizes.has(candidate.size as DialogSurfaceDrawerSize)) {
93
96
  throw new DialogSurfaceContractError('invalid-size', `Недопустимый размер SDrawer: ${String(candidate.size)}`)
94
97
  }
98
+ if (!drawerPlacements.has(candidate.placement as DialogSurfaceDrawerPlacement)) {
99
+ throw new DialogSurfaceContractError('invalid-presentation', `Недопустимое размещение SDrawer: ${String(candidate.placement)}`)
100
+ }
95
101
  if (typeof candidate.mobileFullScreen !== 'boolean') {
96
102
  throw new DialogSurfaceContractError('invalid-presentation', 'SDrawer presentation требует boolean mobileFullScreen')
97
103
  }
@@ -25,6 +25,7 @@
25
25
 
26
26
  <script setup lang="ts">
27
27
  import { computed, type VNodeChild } from 'vue'
28
+ import { useUiKitLocale } from '../../../composables/useUiKitLocale'
28
29
  import { useOwnedAttrs } from '../../../internal/ownedAttrs'
29
30
  import { validateBoundedInteger, validateNonEmptyString } from '../../../internal/runtimeContract'
30
31
  import SStack from '../../layout/SStack.vue'
@@ -50,11 +51,12 @@ export interface Props {
50
51
  }
51
52
 
52
53
  const props = withDefaults(defineProps<Props>(), {
53
- emptyText: 'Нет элементов',
54
+ emptyText: undefined,
54
55
  defaultOpen: true,
55
56
  open: undefined,
56
57
  density: 'compact',
57
58
  })
59
+ const { t } = useUiKitLocale()
58
60
 
59
61
  defineEmits<{
60
62
  (e: 'update:open', value: boolean): void
@@ -75,7 +77,7 @@ const validatedCount = computed(() => validateBoundedInteger(
75
77
  const validatedEmptyText = computed(() => validateNonEmptyString(
76
78
  'SKanbanLane',
77
79
  'emptyText',
78
- props.emptyText,
80
+ props.emptyText ?? t('kanban.empty'),
79
81
  ))
80
82
 
81
83
  const ownedAttrs = useOwnedAttrs({ component: 'SKanbanLane', owner: 'collapsible lane composition' })
@@ -11,6 +11,7 @@
11
11
  >
12
12
  <div v-if="editing && draggable" class="s-cloud-chrome-row__drag-handle">
13
13
  <SDragHandle
14
+ size="xs"
14
15
  :label="label"
15
16
  :can-move-previous="canMoveUp"
16
17
  :can-move-next="canMoveDown"
@@ -27,7 +28,7 @@
27
28
  <slot name="actions" />
28
29
  <SButton
29
30
  v-if="editing && canHide && !hidden"
30
- size="sm"
31
+ size="xs"
31
32
  severity="secondary"
32
33
  appearance="ghost"
33
34
  icon-only
@@ -38,7 +39,7 @@
38
39
  />
39
40
  <SButton
40
41
  v-if="editing && canRestore"
41
- size="sm"
42
+ size="xs"
42
43
  severity="secondary"
43
44
  appearance="ghost"
44
45
  icon-only
@@ -15,6 +15,10 @@
15
15
  @apply absolute inset-0 flex items-stretch justify-end;
16
16
  }
17
17
 
18
+ .s-drawer-positioner[data-placement='bottom'] {
19
+ @apply items-end justify-stretch;
20
+ }
21
+
18
22
  .s-drawer-backdrop-enter-active,
19
23
  .s-drawer-backdrop-leave-active {
20
24
  transition: opacity var(--s-motion-standard) ease;
@@ -38,9 +42,14 @@
38
42
  transform: translateX(1.5rem);
39
43
  }
40
44
 
45
+ .s-drawer-shell[data-placement='bottom'].s-drawer-panel-enter-from,
46
+ .s-drawer-shell[data-placement='bottom'].s-drawer-panel-leave-to {
47
+ transform: translateY(1.5rem);
48
+ }
49
+
41
50
  @media (max-width: 767px) {
42
- .s-drawer-panel-enter-from,
43
- .s-drawer-panel-leave-to {
51
+ .s-drawer-shell[data-placement='right'].s-drawer-panel-enter-from,
52
+ .s-drawer-shell[data-placement='right'].s-drawer-panel-leave-to {
44
53
  transform: translateY(1.5rem);
45
54
  }
46
55
  }
@@ -5,13 +5,13 @@
5
5
  </transition>
6
6
 
7
7
  <transition name="s-drawer-panel">
8
- <div v-if="open" class="s-drawer-shell" :style="surfaceStyle">
9
- <div class="s-drawer-positioner">
8
+ <div v-if="open" class="s-drawer-shell" :data-placement="validatedPlacement" :style="surfaceStyle">
9
+ <div class="s-drawer-positioner" :data-placement="validatedPlacement">
10
10
  <SDialogSurface
11
11
  v-bind="ownedAttrs.bindings()"
12
12
  :presentation="dialogPresentation"
13
13
  :title="title"
14
- :accessible-name="ariaLabel"
14
+ :accessible-name="resolvedAriaLabel"
15
15
  :closable="closable"
16
16
  :set-surface-element="setPanelElement"
17
17
  @request-close="closeDrawer"
@@ -31,11 +31,14 @@
31
31
  </template>
32
32
 
33
33
  <script setup lang="ts">
34
- import { computed, toRef } from 'vue'
34
+ import { computed } from 'vue'
35
+ import { useUiKitLocale } from '../../../composables/useUiKitLocale'
35
36
  import { useOwnedAttrs } from '../../../internal/ownedAttrs'
36
37
  import { useDialogOverlay } from '../../../composables/useDialogOverlay'
38
+ import { validateBoolean, validateExactString } from '../../../internal/runtimeContract'
37
39
  import SDialogSurface from '../_internal/SDialogSurface.vue'
38
40
  import type {
41
+ DialogSurfaceDrawerPlacement,
39
42
  DialogSurfaceDrawerSize,
40
43
  DrawerDialogSurfacePresentation,
41
44
  } from '../_internal/dialogSurface'
@@ -43,12 +46,14 @@ import type {
43
46
  defineOptions({ inheritAttrs: false })
44
47
 
45
48
  export type DrawerSize = DialogSurfaceDrawerSize
49
+ export type DrawerPlacement = DialogSurfaceDrawerPlacement
46
50
 
47
51
  export interface Props {
48
52
  open: boolean
49
53
  title?: string
50
54
  closable?: boolean
51
55
  size?: DrawerSize
56
+ placement?: DrawerPlacement
52
57
  mobileFullScreen?: boolean
53
58
  ariaLabel?: string
54
59
  }
@@ -57,28 +62,62 @@ const props = withDefaults(defineProps<Props>(), {
57
62
  title: undefined,
58
63
  closable: true,
59
64
  size: 'md',
65
+ placement: 'right',
60
66
  mobileFullScreen: true,
61
- ariaLabel: 'Боковая панель',
67
+ ariaLabel: undefined,
62
68
  })
69
+ const { t } = useUiKitLocale()
63
70
 
64
71
  const emit = defineEmits<{
65
72
  'update:open': [value: boolean]
66
73
  }>()
67
74
 
68
75
  const ownedAttrs = useOwnedAttrs({ component: 'SDrawer', owner: 'drawer dialog surface' })
76
+ const resolvedAriaLabel = computed(() => props.ariaLabel ?? t('dialog.drawer'))
77
+ const validatedPlacement = computed<DrawerPlacement>(() => validateExactString(
78
+ 'SDrawer',
79
+ 'placement',
80
+ props.placement,
81
+ ['right', 'bottom'] as const,
82
+ ))
83
+ const validatedSize = computed<DrawerSize>(() => validateExactString(
84
+ 'SDrawer',
85
+ 'size',
86
+ props.size,
87
+ ['sm', 'md', 'lg'] as const,
88
+ ))
89
+ const validatedOpen = computed(() => validateBoolean('SDrawer', 'open', props.open))
90
+ const validatedClosable = computed(() => validateBoolean('SDrawer', 'closable', props.closable))
91
+ const validatedMobileFullScreen = computed(() => validateBoolean(
92
+ 'SDrawer',
93
+ 'mobileFullScreen',
94
+ props.mobileFullScreen,
95
+ ))
69
96
  const dialogPresentation = computed<DrawerDialogSurfacePresentation>(() => ({
70
97
  kind: 'drawer',
71
- size: props.size,
72
- mobileFullScreen: props.mobileFullScreen,
98
+ size: validatedSize.value,
99
+ placement: validatedPlacement.value,
100
+ mobileFullScreen: validatedMobileFullScreen.value,
73
101
  }))
74
102
 
103
+ function assertInitialDrawerContract(): void {
104
+ void validatedPlacement.value
105
+ void validatedSize.value
106
+ void validatedOpen.value
107
+ void validatedClosable.value
108
+ void validatedMobileFullScreen.value
109
+ }
110
+
111
+ // Validate before the overlay owner acquires global layer and focus resources.
112
+ assertInitialDrawerContract()
113
+
75
114
  function closeDrawer() {
76
115
  emit('update:open', false)
77
116
  }
78
117
 
79
118
  const { setPanelElement, backdropStyle, surfaceStyle } = useDialogOverlay({
80
- open: toRef(props, 'open'),
81
- closable: toRef(props, 'closable'),
119
+ open: validatedOpen,
120
+ closable: validatedClosable,
82
121
  requestClose: closeDrawer,
83
122
  })
84
123
  </script>
@@ -11,7 +11,7 @@
11
11
  v-bind="ownedAttrs.bindings()"
12
12
  :presentation="dialogPresentation"
13
13
  :title="title"
14
- :accessible-name="ariaLabel"
14
+ :accessible-name="resolvedAriaLabel"
15
15
  :closable="closable"
16
16
  :set-surface-element="setPanelElement"
17
17
  @request-close="closeModal"
@@ -38,6 +38,7 @@
38
38
 
39
39
  <script setup lang="ts">
40
40
  import { computed, toRef } from 'vue'
41
+ import { useUiKitLocale } from '../../../composables/useUiKitLocale'
41
42
  import { useOwnedAttrs } from '../../../internal/ownedAttrs'
42
43
  import { useDialogOverlay } from '../../../composables/useDialogOverlay'
43
44
  import SDialogSurface from '../_internal/SDialogSurface.vue'
@@ -74,8 +75,9 @@ const props = withDefaults(defineProps<Props>(), {
74
75
  layout: 'desktop',
75
76
  surface: 'default',
76
77
  scrollBehavior: 'page',
77
- ariaLabel: 'Модальное окно',
78
+ ariaLabel: undefined,
78
79
  })
80
+ const { t } = useUiKitLocale()
79
81
 
80
82
  const emit = defineEmits<{
81
83
  'update:open': [value: boolean]
@@ -88,6 +90,7 @@ function closeModal() {
88
90
  const isFullscreen = computed(() => props.fullScreen || props.layout === 'mobile-fullscreen')
89
91
 
90
92
  const ownedAttrs = useOwnedAttrs({ component: 'SModal', owner: 'modal dialog surface' })
93
+ const resolvedAriaLabel = computed(() => props.ariaLabel ?? t('dialog.modal'))
91
94
  const dialogPresentation = computed<ModalDialogSurfacePresentation>(() => ({
92
95
  kind: 'modal',
93
96
  size: props.size,
@@ -49,11 +49,11 @@
49
49
  >
50
50
  <SAsyncState :state="state">
51
51
  <template v-if="state.status === 'error' && state.endpoint" #error>
52
- <SMessage severity="danger" title="Ошибка ответа сервера">
52
+ <SMessage severity="danger" :title="t('feedback.serverResponseError')">
53
53
  <div class="space-y-1">
54
54
  <div>{{ state.message }}</div>
55
55
  <div class="text-xs text-danger-700 dark:text-danger-300">
56
- Запрос: {{ state.endpoint }}
56
+ {{ t('feedback.request') }} {{ state.endpoint }}
57
57
  </div>
58
58
  </div>
59
59
  </SMessage>
@@ -82,6 +82,7 @@
82
82
  import { computed, useSlots, watchEffect, type FunctionalComponent, type HTMLAttributes, type VNodeProps } from 'vue'
83
83
  import { useOwnedAttrs } from '../../../internal/ownedAttrs'
84
84
  import { validateDomId } from '../../../internal/runtimeContract'
85
+ import { useUiKitLocale } from '../../../composables/useUiKitLocale'
85
86
  import SAsyncState, { type SAsyncStateContract } from '../feedback/SAsyncState.vue'
86
87
  import SMessage from '../data-display/SMessage.vue'
87
88
  import SProgressBar from '../data-display/SProgressBar.vue'
@@ -148,6 +149,7 @@ const props = withDefaults(defineProps<Props>(), {
148
149
  })
149
150
 
150
151
  const slots = useSlots()
152
+ const { t } = useUiKitLocale()
151
153
  const ownedAttrs = useOwnedAttrs({ component: 'SPanel', owner: 'panel surface root' })
152
154
  const validatedTitleId = computed(() => {
153
155
  if (props.titleId === undefined) return undefined
@@ -56,7 +56,7 @@
56
56
  tabindex="-1"
57
57
  role="dialog"
58
58
  :id="panelId"
59
- :aria-label="ariaLabel"
59
+ :aria-label="resolvedAriaLabel"
60
60
  @keydown.tab="handlePanelTab"
61
61
  @focusout="handlePanelFocusout"
62
62
  >
@@ -92,6 +92,7 @@ import SButton, {
92
92
  type SButtonWidth,
93
93
  } from '../controls/SButton.vue';
94
94
  import { useFloatingPosition, type FloatingPlacement } from '../../../composables/useFloatingPosition';
95
+ import { useUiKitLocale } from '../../../composables/useUiKitLocale';
95
96
  import { findRelativeTabTarget, getFocusableElements } from '../../../internal/focusNavigation';
96
97
  import { lastItem } from '../../../internal/es2020';
97
98
  import { resolveInteractiveElement } from '../../../internal/interactiveElement';
@@ -141,7 +142,7 @@ const props = withDefaults(defineProps<Props>(), {
141
142
  align: 'right',
142
143
  surface: 'default',
143
144
  width: 'trigger',
144
- ariaLabel: 'Дополнительные действия',
145
+ ariaLabel: undefined,
145
146
  triggerAppearance: 'ghost',
146
147
  triggerSize: 'md',
147
148
  triggerWidth: 'full',
@@ -159,6 +160,8 @@ const props = withDefaults(defineProps<Props>(), {
159
160
  disabled: false,
160
161
  open: undefined,
161
162
  });
163
+ const { t } = useUiKitLocale();
164
+ const resolvedAriaLabel = computed(() => props.ariaLabel ?? t('dialog.moreActions'));
162
165
  const emit = defineEmits<{
163
166
  (e: 'update:open', value: boolean): void;
164
167
  }>();
@@ -192,7 +195,7 @@ const layerZIndex = ref('var(--s-layer-root-floating)');
192
195
  const resolvedTriggerAriaLabel = computed(() =>
193
196
  props.triggerAriaLabel ?? (
194
197
  props.triggerIconOnly || (!props.triggerLabel && !slots.default)
195
- ? props.ariaLabel
198
+ ? resolvedAriaLabel.value
196
199
  : undefined
197
200
  ),
198
201
  );
@@ -216,6 +219,7 @@ const {
216
219
  boundaryPadding: () => 8,
217
220
  flip: () => true,
218
221
  shift: () => true,
222
+ constrainMainAxis: () => true,
219
223
  widthMatch: () => props.width === 'trigger' ? 'min' : 'none',
220
224
  zIndex: () => layerZIndex.value,
221
225
  scrollStrategy: 'reposition',
@@ -0,0 +1,57 @@
1
+ <template>
2
+ <slot v-if="acceptsNoOwnedAttributes" />
3
+ </template>
4
+
5
+ <script setup lang="ts">
6
+ import { computed, provide } from 'vue'
7
+
8
+ import { useOwnedAttrs } from '../../../internal/ownedAttrs'
9
+ import {
10
+ UI_KIT_LOCALE_CONTEXT,
11
+ resolveUiKitLocaleContext,
12
+ type UiKitLocaleContext,
13
+ } from '../../../internal/localeContext'
14
+ import { UI_KIT_LOCALES, type UiKitLocale, type UiKitMessagesOverride } from '../../../locales/locale'
15
+ import { createUiKitTranslate, mergeUiKitMessages, UI_KIT_MESSAGES } from '../../../locales/messages'
16
+
17
+ defineOptions({ inheritAttrs: false })
18
+
19
+ const props = defineProps<{
20
+ /** Locale for this subtree; omission inherits the nearest provider and ultimately defaults to ru. */
21
+ locale?: UiKitLocale
22
+ /** Exact deep partial override for this subtree. Unknown fields fail at runtime. */
23
+ messages?: UiKitMessagesOverride
24
+ }>()
25
+
26
+ defineSlots<{
27
+ default: () => unknown
28
+ }>()
29
+
30
+ const parent = resolveUiKitLocaleContext()
31
+ const ownedAttrs = useOwnedAttrs({ component: 'SUiKitLocaleProvider', owner: 'DOM-less locale boundary' })
32
+ const locale = computed<UiKitLocale>(() => {
33
+ const value = props.locale ?? parent.locale.value
34
+ if (!UI_KIT_LOCALES.includes(value)) {
35
+ throw new TypeError(`SUiKitLocaleProvider: неизвестная locale ${JSON.stringify(value)}.`)
36
+ }
37
+ return value
38
+ })
39
+ const messages = computed(() => {
40
+ const base = props.locale === undefined || props.locale === parent.locale.value
41
+ ? parent.messages.value
42
+ : UI_KIT_MESSAGES[locale.value]
43
+ return mergeUiKitMessages(base, props.messages)
44
+ })
45
+ const context: UiKitLocaleContext = Object.freeze({
46
+ locale,
47
+ messages,
48
+ t: createUiKitTranslate(() => messages.value),
49
+ })
50
+
51
+ provide(UI_KIT_LOCALE_CONTEXT, context)
52
+
53
+ const acceptsNoOwnedAttributes = computed(() => {
54
+ ownedAttrs.reject('locale provider не создаёт DOM owner / locale provider does not render a DOM owner')
55
+ return true
56
+ })
57
+ </script>
@@ -28,20 +28,22 @@
28
28
  }
29
29
 
30
30
  .s-button[data-size='xs']:not([data-icon-only='true']) {
31
- min-height: var(--s-control-hitbox-xs);
32
- padding: 0.25rem 0.5rem;
31
+ block-size: var(--s-control-hitbox-xs);
32
+ min-block-size: var(--s-control-hitbox-xs);
33
+ padding: 0 0.5rem;
33
34
  font-size: 0.75rem;
34
35
  }
35
36
 
36
37
  .s-button[data-density='compact'][data-size='xs']:not([data-icon-only='true']) {
37
- height: var(--s-control-hitbox-xs);
38
- min-height: var(--s-control-hitbox-xs);
38
+ block-size: var(--s-control-hitbox-2xs);
39
+ min-block-size: var(--s-control-hitbox-2xs);
39
40
  padding-block: 0;
40
41
  }
41
42
 
42
43
  .s-button[data-size='sm']:not([data-icon-only='true']) {
43
- min-height: var(--s-control-hitbox-sm);
44
- padding: 0.375rem 0.625rem;
44
+ block-size: var(--s-control-hitbox-sm);
45
+ min-block-size: var(--s-control-hitbox-sm);
46
+ padding: 0 0.625rem;
45
47
  font-size: 0.75rem;
46
48
  }
47
49
 
@@ -57,14 +59,16 @@
57
59
  }
58
60
 
59
61
  .s-button[data-size='md']:not([data-icon-only='true']) {
60
- min-height: var(--s-control-hitbox-md);
61
- padding: 0.625rem 1.25rem;
62
+ block-size: var(--s-control-hitbox-md);
63
+ min-block-size: var(--s-control-hitbox-md);
64
+ padding: 0 1.25rem;
62
65
  font-size: 0.875rem;
63
66
  }
64
67
 
65
68
  .s-button[data-size='lg']:not([data-icon-only='true']) {
66
- min-height: var(--s-control-hitbox-lg);
67
- padding: 0.875rem 1.75rem;
69
+ block-size: var(--s-control-hitbox-lg);
70
+ min-block-size: var(--s-control-hitbox-lg);
71
+ padding: 0 1.75rem;
68
72
  font-size: 1.125rem;
69
73
  }
70
74