@pgcorp/ui-kit 0.1.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 (251) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +157 -0
  3. package/docs/accessibility.md +27 -0
  4. package/docs/getting-started.md +80 -0
  5. package/docs/licensing.md +30 -0
  6. package/docs/public-api.md +39 -0
  7. package/docs/security.md +21 -0
  8. package/docs/theming.md +35 -0
  9. package/docs/versioning.md +23 -0
  10. package/index.ts +3 -0
  11. package/package.json +592 -0
  12. package/src/components/icons/_internal/generatedSputnigUiIconMarkup.ts +971 -0
  13. package/src/components/icons/sputnigUiIcons.ts +530 -0
  14. package/src/components/layout/SAppShell.vue +232 -0
  15. package/src/components/layout/SDockRegion.vue +200 -0
  16. package/src/components/layout/SResizeHandle.vue +251 -0
  17. package/src/components/layout/SSideMenuButton.vue +104 -0
  18. package/src/components/layout/SSideMenuRail.vue +47 -0
  19. package/src/components/layout/SStack.css +25 -0
  20. package/src/components/layout/SStack.vue +57 -0
  21. package/src/components/layout/SStatusBarShell.css +15 -0
  22. package/src/components/layout/SStatusBarShell.vue +26 -0
  23. package/src/components/layout/SWorkbenchLayout.vue +53 -0
  24. package/src/components/layout/SWorkbenchPanelShell.css +161 -0
  25. package/src/components/layout/SWorkbenchPanelShell.vue +286 -0
  26. package/src/components/layout/_internal/SAppBrand.css +45 -0
  27. package/src/components/layout/_internal/SAppBrand.vue +37 -0
  28. package/src/components/layout/_internal/SAppBrandContent.vue +30 -0
  29. package/src/components/layout/types.ts +28 -0
  30. package/src/components/shared/_internal/SDialogSurface.css +127 -0
  31. package/src/components/shared/_internal/SDialogSurface.vue +117 -0
  32. package/src/components/shared/_internal/SDisclosureSummary.css +92 -0
  33. package/src/components/shared/_internal/SDisclosureSummary.vue +121 -0
  34. package/src/components/shared/_internal/SListboxGroup.css +18 -0
  35. package/src/components/shared/_internal/SListboxGroup.vue +24 -0
  36. package/src/components/shared/_internal/SMenuItem.css +112 -0
  37. package/src/components/shared/_internal/SMenuItem.vue +213 -0
  38. package/src/components/shared/_internal/SMenuLayer.css +21 -0
  39. package/src/components/shared/_internal/SMenuLayer.vue +48 -0
  40. package/src/components/shared/_internal/SMenuSeparator.css +5 -0
  41. package/src/components/shared/_internal/SMenuSeparator.vue +13 -0
  42. package/src/components/shared/_internal/SToastItem.css +65 -0
  43. package/src/components/shared/_internal/SToastItem.vue +58 -0
  44. package/src/components/shared/_internal/STreeNode.vue +761 -0
  45. package/src/components/shared/_internal/dialogSurface.ts +105 -0
  46. package/src/components/shared/_internal/focusTrap.ts +40 -0
  47. package/src/components/shared/_internal/listboxContext.ts +16 -0
  48. package/src/components/shared/_internal/markerContract.ts +10 -0
  49. package/src/components/shared/_internal/menuContext.ts +23 -0
  50. package/src/components/shared/_internal/selectionContract.ts +239 -0
  51. package/src/components/shared/_internal/sidebarGroupContext.ts +31 -0
  52. package/src/components/shared/_internal/tabsContext.ts +57 -0
  53. package/src/components/shared/_internal/treeRuntime.ts +203 -0
  54. package/src/components/shared/_internal/useSelectionRoving.ts +162 -0
  55. package/src/components/shared/codeLanguages.ts +168 -0
  56. package/src/components/shared/complex/SKanbanCard.css +8 -0
  57. package/src/components/shared/complex/SKanbanCard.vue +76 -0
  58. package/src/components/shared/complex/SKanbanLane.vue +82 -0
  59. package/src/components/shared/complex/STree.css +24 -0
  60. package/src/components/shared/complex/STree.vue +288 -0
  61. package/src/components/shared/complex/treeAdapter.ts +210 -0
  62. package/src/components/shared/complex/types.ts +102 -0
  63. package/src/components/shared/containers/SCloudChrome.css +103 -0
  64. package/src/components/shared/containers/SCloudChrome.vue +87 -0
  65. package/src/components/shared/containers/SCloudChromeEmpty.css +10 -0
  66. package/src/components/shared/containers/SCloudChromeEmpty.vue +18 -0
  67. package/src/components/shared/containers/SCloudChromeRow.css +78 -0
  68. package/src/components/shared/containers/SCloudChromeRow.vue +108 -0
  69. package/src/components/shared/containers/SCollapsiblePanel.css +57 -0
  70. package/src/components/shared/containers/SCollapsiblePanel.vue +360 -0
  71. package/src/components/shared/containers/SDrawer.css +60 -0
  72. package/src/components/shared/containers/SDrawer.vue +86 -0
  73. package/src/components/shared/containers/SFormGrid.css +26 -0
  74. package/src/components/shared/containers/SFormGrid.vue +46 -0
  75. package/src/components/shared/containers/SFormGridItem.css +18 -0
  76. package/src/components/shared/containers/SFormGridItem.vue +29 -0
  77. package/src/components/shared/containers/SLeftSidebar.vue +74 -0
  78. package/src/components/shared/containers/SModal.css +27 -0
  79. package/src/components/shared/containers/SModal.vue +105 -0
  80. package/src/components/shared/containers/SPageHeader.css +66 -0
  81. package/src/components/shared/containers/SPageHeader.vue +45 -0
  82. package/src/components/shared/containers/SPanel.css +134 -0
  83. package/src/components/shared/containers/SPanel.vue +169 -0
  84. package/src/components/shared/containers/SPopover.css +35 -0
  85. package/src/components/shared/containers/SPopover.vue +551 -0
  86. package/src/components/shared/containers/SSidebarGroup.css +12 -0
  87. package/src/components/shared/containers/SSidebarGroup.vue +443 -0
  88. package/src/components/shared/containers/SSidebarSection.css +41 -0
  89. package/src/components/shared/containers/SSidebarSection.vue +268 -0
  90. package/src/components/shared/containers/disclosureSummary.ts +48 -0
  91. package/src/components/shared/containers/popover.ts +72 -0
  92. package/src/components/shared/containers/sidebar.ts +206 -0
  93. package/src/components/shared/controls/SButton.css +274 -0
  94. package/src/components/shared/controls/SButton.vue +211 -0
  95. package/src/components/shared/controls/SCheckbox.css +65 -0
  96. package/src/components/shared/controls/SCheckbox.vue +130 -0
  97. package/src/components/shared/controls/SCheckboxGroup.vue +134 -0
  98. package/src/components/shared/controls/SChoiceCards.css +29 -0
  99. package/src/components/shared/controls/SChoiceCards.vue +145 -0
  100. package/src/components/shared/controls/SColorSelect.vue +146 -0
  101. package/src/components/shared/controls/SComboboxTrigger.css +35 -0
  102. package/src/components/shared/controls/SComboboxTrigger.vue +122 -0
  103. package/src/components/shared/controls/SContextMenu.css +11 -0
  104. package/src/components/shared/controls/SContextMenu.vue +186 -0
  105. package/src/components/shared/controls/SContextToggleButton.vue +64 -0
  106. package/src/components/shared/controls/SDragHandle.vue +100 -0
  107. package/src/components/shared/controls/SDropdownMenu.css +5 -0
  108. package/src/components/shared/controls/SDropdownMenu.vue +188 -0
  109. package/src/components/shared/controls/SField.css +79 -0
  110. package/src/components/shared/controls/SField.vue +89 -0
  111. package/src/components/shared/controls/SFieldGroup.css +58 -0
  112. package/src/components/shared/controls/SFieldGroup.vue +117 -0
  113. package/src/components/shared/controls/SFieldLabel.css +12 -0
  114. package/src/components/shared/controls/SFieldLabel.vue +51 -0
  115. package/src/components/shared/controls/SInlineTokenEditor.css +61 -0
  116. package/src/components/shared/controls/SInlineTokenEditor.vue +1081 -0
  117. package/src/components/shared/controls/SInputText.css +66 -0
  118. package/src/components/shared/controls/SInputText.vue +321 -0
  119. package/src/components/shared/controls/SInteractiveSurface.css +72 -0
  120. package/src/components/shared/controls/SInteractiveSurface.vue +326 -0
  121. package/src/components/shared/controls/SLink.css +114 -0
  122. package/src/components/shared/controls/SLink.ts +85 -0
  123. package/src/components/shared/controls/SLink.vue +114 -0
  124. package/src/components/shared/controls/SListbox.css +31 -0
  125. package/src/components/shared/controls/SListbox.vue +388 -0
  126. package/src/components/shared/controls/SListboxOption.css +63 -0
  127. package/src/components/shared/controls/SListboxOption.vue +85 -0
  128. package/src/components/shared/controls/SMarker.css +19 -0
  129. package/src/components/shared/controls/SMarker.vue +33 -0
  130. package/src/components/shared/controls/SMenuSurface.css +20 -0
  131. package/src/components/shared/controls/SMenuSurface.vue +333 -0
  132. package/src/components/shared/controls/SSegmentedControl.css +38 -0
  133. package/src/components/shared/controls/SSegmentedControl.vue +134 -0
  134. package/src/components/shared/controls/SSelect.css +12 -0
  135. package/src/components/shared/controls/SSelect.vue +338 -0
  136. package/src/components/shared/controls/STextarea.css +31 -0
  137. package/src/components/shared/controls/STextarea.vue +316 -0
  138. package/src/components/shared/controls/_internal/SInlineTokenSurface.vue +224 -0
  139. package/src/components/shared/controls/menu.ts +178 -0
  140. package/src/components/shared/data-display/SActionCard.css +6 -0
  141. package/src/components/shared/data-display/SActionCard.vue +49 -0
  142. package/src/components/shared/data-display/SActionList.css +15 -0
  143. package/src/components/shared/data-display/SActionList.vue +34 -0
  144. package/src/components/shared/data-display/SActionListItem.css +114 -0
  145. package/src/components/shared/data-display/SActionListItem.vue +230 -0
  146. package/src/components/shared/data-display/SBadge.css +73 -0
  147. package/src/components/shared/data-display/SBadge.vue +52 -0
  148. package/src/components/shared/data-display/SCodeBlock.css +56 -0
  149. package/src/components/shared/data-display/SCodeBlock.vue +188 -0
  150. package/src/components/shared/data-display/SCodeEditor.css +178 -0
  151. package/src/components/shared/data-display/SCodeEditor.vue +274 -0
  152. package/src/components/shared/data-display/SCodeSearchPanel.vue +327 -0
  153. package/src/components/shared/data-display/SCopyField.vue +77 -0
  154. package/src/components/shared/data-display/SDocBlock.css +136 -0
  155. package/src/components/shared/data-display/SDocBlock.vue +358 -0
  156. package/src/components/shared/data-display/SEmptyState.css +42 -0
  157. package/src/components/shared/data-display/SEmptyState.vue +79 -0
  158. package/src/components/shared/data-display/SExpandableText.vue +314 -0
  159. package/src/components/shared/data-display/SJsonTree.css +6 -0
  160. package/src/components/shared/data-display/SJsonTree.vue +111 -0
  161. package/src/components/shared/data-display/SKeyValueGrid.css +37 -0
  162. package/src/components/shared/data-display/SKeyValueGrid.vue +277 -0
  163. package/src/components/shared/data-display/SLinkedSystemsList.vue +270 -0
  164. package/src/components/shared/data-display/SMessage.css +88 -0
  165. package/src/components/shared/data-display/SMessage.vue +152 -0
  166. package/src/components/shared/data-display/SMetricCard.css +32 -0
  167. package/src/components/shared/data-display/SMetricCard.vue +117 -0
  168. package/src/components/shared/data-display/SProgressBar.css +84 -0
  169. package/src/components/shared/data-display/SProgressBar.vue +114 -0
  170. package/src/components/shared/data-display/SProgressIndicator.css +59 -0
  171. package/src/components/shared/data-display/SProgressIndicator.vue +104 -0
  172. package/src/components/shared/data-display/SSectionHeader.css +84 -0
  173. package/src/components/shared/data-display/SSectionHeader.vue +58 -0
  174. package/src/components/shared/data-display/SStatus.css +90 -0
  175. package/src/components/shared/data-display/SStatus.vue +79 -0
  176. package/src/components/shared/data-display/STable.css +83 -0
  177. package/src/components/shared/data-display/STable.vue +339 -0
  178. package/src/components/shared/data-display/SToastContainer.css +53 -0
  179. package/src/components/shared/data-display/SToastContainer.vue +31 -0
  180. package/src/components/shared/data-display/STooltip.css +114 -0
  181. package/src/components/shared/data-display/STooltip.vue +550 -0
  182. package/src/components/shared/data-display/STooltipAnchor.css +21 -0
  183. package/src/components/shared/data-display/STooltipAnchor.vue +119 -0
  184. package/src/components/shared/data-display/SVirtualList.css +24 -0
  185. package/src/components/shared/data-display/SVirtualList.ts +19 -0
  186. package/src/components/shared/data-display/SVirtualList.vue +361 -0
  187. package/src/components/shared/data-display/json.ts +136 -0
  188. package/src/components/shared/data-display/table.ts +76 -0
  189. package/src/components/shared/database/SDataGrid.css +3 -0
  190. package/src/components/shared/database/SDataGrid.vue +464 -0
  191. package/src/components/shared/database/SSqlEditor.vue +363 -0
  192. package/src/components/shared/database/dataGrid.ts +61 -0
  193. package/src/components/shared/feedback/SAsyncState.css +26 -0
  194. package/src/components/shared/feedback/SAsyncState.vue +190 -0
  195. package/src/components/shared/graph/SGraphViewport.ts +1 -0
  196. package/src/components/shared/graph/SGraphViewport.vue +858 -0
  197. package/src/components/shared/graph/_internal/graphViewportContract.ts +362 -0
  198. package/src/components/shared/markdown.ts +169 -0
  199. package/src/components/shared/navigation/SBottomNav.vue +692 -0
  200. package/src/components/shared/navigation/SBreadcrumbs.css +26 -0
  201. package/src/components/shared/navigation/SBreadcrumbs.vue +155 -0
  202. package/src/components/shared/navigation/SCatalogNavigator.css +100 -0
  203. package/src/components/shared/navigation/SCatalogNavigator.vue +308 -0
  204. package/src/components/shared/navigation/STab.vue +363 -0
  205. package/src/components/shared/navigation/STabList.vue +442 -0
  206. package/src/components/shared/navigation/STabPanel.vue +165 -0
  207. package/src/components/shared/navigation/STabPanels.vue +33 -0
  208. package/src/components/shared/navigation/STabs.vue +617 -0
  209. package/src/components/shared/navigation/SWizardSteps.css +69 -0
  210. package/src/components/shared/navigation/SWizardSteps.vue +171 -0
  211. package/src/components/shared/navigation/_internal/SCatalogNavigatorGroup.vue +63 -0
  212. package/src/components/shared/persona/SPersonaProjectPicker.css +36 -0
  213. package/src/components/shared/persona/SPersonaProjectPicker.vue +361 -0
  214. package/src/components/shared/persona/SPersonaRunWorkbench.css +109 -0
  215. package/src/components/shared/persona/SPersonaRunWorkbench.vue +549 -0
  216. package/src/components/shared/persona/_internal/SPersonaRunConsole.css +40 -0
  217. package/src/components/shared/persona/_internal/SPersonaRunConsole.vue +298 -0
  218. package/src/components/shared/persona/_internal/inventory.ts +110 -0
  219. package/src/components/shared/persona/_internal/labels.ts +79 -0
  220. package/src/components/shared/persona/presentation.ts +371 -0
  221. package/src/components/shared/persona/types.ts +256 -0
  222. package/src/composables/uiPreferencesReset.ts +52 -0
  223. package/src/composables/useClipboard.ts +37 -0
  224. package/src/composables/useDialogOverlay.ts +85 -0
  225. package/src/composables/useDisclosure.ts +40 -0
  226. package/src/composables/useFloatingPosition.ts +245 -0
  227. package/src/composables/useNotifier.ts +26 -0
  228. package/src/composables/useSidebarPanelState.ts +610 -0
  229. package/src/internal/codeEditorContract.ts +110 -0
  230. package/src/internal/codeEditorExtensions.ts +27 -0
  231. package/src/internal/feedbackPresentation.ts +136 -0
  232. package/src/internal/fieldSurface.ts +59 -0
  233. package/src/internal/focusNavigation.ts +69 -0
  234. package/src/internal/inlineTokenEditorContract.ts +136 -0
  235. package/src/internal/interactiveElement.ts +33 -0
  236. package/src/internal/layerStack.ts +202 -0
  237. package/src/internal/linkTarget.ts +256 -0
  238. package/src/internal/ownedAttrs.ts +464 -0
  239. package/src/internal/passiveContentContract.ts +129 -0
  240. package/src/internal/pointerInteractionLease.ts +81 -0
  241. package/src/internal/runtimeContract.ts +194 -0
  242. package/src/internal/runtimeTheme.ts +159 -0
  243. package/src/internal/semanticSizing.ts +60 -0
  244. package/src/internal/useMenuLayer.ts +129 -0
  245. package/src/stores/useNotifierStore.ts +69 -0
  246. package/src/styles/reference.css +4 -0
  247. package/src/styles/style.css +151 -0
  248. package/src/styles/tokens.css +836 -0
  249. package/src/theme.ts +449 -0
  250. package/src/types/highlight-js-lib.d.ts +76 -0
  251. package/src/variants.ts +15 -0
@@ -0,0 +1,464 @@
1
+ <template>
2
+ <SPanel
3
+ v-bind="ownedAttrs.bindings()"
4
+ :title="validatedTitle"
5
+ :title-id="validatedTitle ? generatedTitleId : undefined"
6
+ :footer-id="hasStatus ? generatedStatusId : undefined"
7
+ :heading-level="3"
8
+ no-content-padding
9
+ content-layout="block"
10
+ surface-overflow="clip"
11
+ >
12
+ <template v-if="$slots.actions" #actions>
13
+ <slot
14
+ name="actions"
15
+ :pending-count="pendingCount"
16
+ :readonly="validatedReadOnly"
17
+ :disabled="validatedDisabled"
18
+ />
19
+ </template>
20
+
21
+ <STable
22
+ :columns="tableColumns"
23
+ :data="validatedGrid.rows"
24
+ :get-row-key="getRowKey"
25
+ :loading="validatedLoading"
26
+ loading-message="Загрузка данных..."
27
+ :error-message="errorMessage"
28
+ :empty-message="emptyMessage"
29
+ :selected-row-key="selectedRowId"
30
+ :max-height="validatedMaxHeight"
31
+ density="compact"
32
+ :row-state="rowState"
33
+ :cell-state="cellState"
34
+ surface="plain"
35
+ :aria-label="resolvedAccessibleName.ariaLabel"
36
+ :aria-labelledby="resolvedAccessibleName.ariaLabelledby"
37
+ :aria-describedby="hasStatus ? generatedStatusId : undefined"
38
+ >
39
+ <template #column="{ column }">
40
+ <slot name="column" :column="resolveColumn(column.field)">{{ column.header }}</slot>
41
+ </template>
42
+ <template #cell="{ row, column: tableColumn }">
43
+ <slot
44
+ name="cell"
45
+ :row="row"
46
+ :column="resolveColumn(tableColumn.field)"
47
+ :cell="resolveCell(row, tableColumn.field)"
48
+ :editable="isEditable(row, resolveColumn(tableColumn.field))"
49
+ :edit="(value: string) => editCell(row, resolveColumn(tableColumn.field), value)"
50
+ :commit="(input: SDataGridCommitInput) => commitCell(row, resolveColumn(tableColumn.field), input)"
51
+ :cancel="() => cancelCell(row, resolveColumn(tableColumn.field))"
52
+ >
53
+ <SInputText
54
+ v-if="isEditable(row, resolveColumn(tableColumn.field))"
55
+ :model-value="draftValue(row, resolveColumn(tableColumn.field))"
56
+ appearance="grid-cell"
57
+ size="sm"
58
+ :disabled="validatedDisabled"
59
+ :aria-label="`${resolveColumn(tableColumn.field).label}: ${row.id}`"
60
+ @update:model-value="editCell(row, resolveColumn(tableColumn.field), $event)"
61
+ @blur="commitCell(row, resolveColumn(tableColumn.field), { source: 'draft' })"
62
+ @keydown.enter.prevent="commitCell(row, resolveColumn(tableColumn.field), { source: 'draft' })"
63
+ @keydown.escape.prevent="cancelCell(row, resolveColumn(tableColumn.field))"
64
+ />
65
+ <span v-else class="s-data-grid__value" :title="resolveCell(row, tableColumn.field).display">
66
+ {{ resolveCell(row, tableColumn.field).display }}
67
+ </span>
68
+ </slot>
69
+ </template>
70
+ </STable>
71
+
72
+ <template v-if="hasStatus" #footer>
73
+ <slot name="status" :pending-count="pendingCount">
74
+ <SStatus
75
+ :label="`Ожидают применения: ${pendingCount}`"
76
+ tone="warn"
77
+ size="sm"
78
+ live="polite"
79
+ />
80
+ </slot>
81
+ </template>
82
+ </SPanel>
83
+ </template>
84
+
85
+ <script lang="ts">
86
+ export type {
87
+ SDataGridCell,
88
+ SDataGridCellKind,
89
+ SDataGridColumn,
90
+ SDataGridColumnKey,
91
+ SDataGridCommitInput,
92
+ SDataGridEditPayload,
93
+ SDataGridProps,
94
+ SDataGridRow,
95
+ SDataGridRowState,
96
+ } from './dataGrid'
97
+ </script>
98
+
99
+ <script setup lang="ts" generic="ColumnKey extends string = string">
100
+ import { computed, reactive, useId, useSlots, watch } from 'vue'
101
+ import { useOwnedAttrs } from '../../../internal/ownedAttrs'
102
+ import {
103
+ validateScrollableViewportMaxBlockSize,
104
+ validateTableColumnInlineSize,
105
+ validateTableColumnMinInlineSize,
106
+ } from '../../../internal/semanticSizing'
107
+ import {
108
+ validateBoolean,
109
+ validateDomId,
110
+ validateExactString,
111
+ validateNonEmptyString,
112
+ } from '../../../internal/runtimeContract'
113
+ import SInputText from '../controls/SInputText.vue'
114
+ import SPanel from '../containers/SPanel.vue'
115
+ import SStatus from '../data-display/SStatus.vue'
116
+ import STable from '../data-display/STable.vue'
117
+ import type {
118
+ STableCellState,
119
+ STableColumn,
120
+ STableRowState,
121
+ } from '../data-display/table'
122
+ import type {
123
+ SDataGridCell,
124
+ SDataGridColumn,
125
+ SDataGridColumnKey,
126
+ SDataGridCommitInput,
127
+ SDataGridEditPayload,
128
+ SDataGridProps,
129
+ SDataGridRow,
130
+ } from './dataGrid'
131
+
132
+ defineOptions({ inheritAttrs: false })
133
+
134
+ interface ValidatedGrid<Key extends SDataGridColumnKey> {
135
+ columns: readonly SDataGridColumn<Key>[]
136
+ rows: readonly SDataGridRow<Key>[]
137
+ columnByKey: ReadonlyMap<Key, SDataGridColumn<Key>>
138
+ rowById: ReadonlyMap<string, SDataGridRow<Key>>
139
+ }
140
+
141
+ const props = withDefaults(defineProps<SDataGridProps<ColumnKey>>(), {
142
+ title: undefined,
143
+ loading: false,
144
+ disabled: false,
145
+ readOnly: false,
146
+ errorMessage: '',
147
+ emptyMessage: 'Нет строк для отображения',
148
+ selectedRowId: undefined,
149
+ maxHeight: 'none',
150
+ ariaLabel: undefined,
151
+ ariaLabelledby: undefined,
152
+ })
153
+
154
+ const ownedAttrs = useOwnedAttrs({ component: 'SDataGrid', owner: 'data grid adapter root' })
155
+ const slots = useSlots()
156
+
157
+ const emit = defineEmits<{
158
+ 'cell-edit': [payload: SDataGridEditPayload<ColumnKey>]
159
+ 'cell-commit': [payload: SDataGridEditPayload<ColumnKey>]
160
+ 'cell-cancel': [payload: Omit<SDataGridEditPayload<ColumnKey>, 'value'>]
161
+ }>()
162
+
163
+ const CELL_KINDS = ['text', 'number', 'boolean', 'json', 'datetime', 'binary'] as const
164
+ const COLUMN_ALIGNMENTS = ['left', 'center', 'right'] as const
165
+ const ROW_STATES = ['clean', 'inserted', 'updated', 'deleted'] as const
166
+ const COMMIT_SOURCES = ['draft', 'value'] as const
167
+
168
+ function assertReadonlyArray(value: unknown, coordinate: string): asserts value is readonly unknown[] {
169
+ if (Array.isArray(value)) return
170
+ throw new TypeError(
171
+ `SDataGrid: ${coordinate} должен быть array. / SDataGrid: ${coordinate} must be an array.`,
172
+ )
173
+ }
174
+
175
+ function assertRecord(value: unknown, coordinate: string): asserts value is Record<string, unknown> {
176
+ if (typeof value === 'object' && value !== null && !Array.isArray(value)) return
177
+ throw new TypeError(
178
+ `SDataGrid: ${coordinate} должен быть object. / SDataGrid: ${coordinate} must be an object.`,
179
+ )
180
+ }
181
+
182
+ function validateOptionalBoolean(coordinate: string, value: unknown): void {
183
+ if (value !== undefined) validateBoolean('SDataGrid', coordinate, value)
184
+ }
185
+
186
+ function validateCell(
187
+ rowId: string,
188
+ column: SDataGridColumn<ColumnKey>,
189
+ value: unknown,
190
+ ): asserts value is SDataGridCell {
191
+ const coordinate = `rows[${JSON.stringify(rowId)}].cells[${JSON.stringify(column.key)}]`
192
+ assertRecord(value, coordinate)
193
+ if (typeof value.display !== 'string') {
194
+ throw new TypeError(
195
+ `SDataGrid: ${coordinate}.display должен быть string. `
196
+ + `/ SDataGrid: ${coordinate}.display must be a string.`,
197
+ )
198
+ }
199
+ if (value.kind !== undefined) {
200
+ const kind = validateExactString('SDataGrid', `${coordinate}.kind`, value.kind, CELL_KINDS)
201
+ if (column.type !== undefined && kind !== column.type) {
202
+ throw new TypeError(
203
+ `SDataGrid: ${coordinate}.kind ${JSON.stringify(kind)} не совпадает с column.type ${JSON.stringify(column.type)}. `
204
+ + `/ SDataGrid: ${coordinate}.kind ${JSON.stringify(kind)} does not match column.type ${JSON.stringify(column.type)}.`,
205
+ )
206
+ }
207
+ }
208
+ validateOptionalBoolean(`${coordinate}.pending`, value.pending)
209
+ validateOptionalBoolean(`${coordinate}.readonly`, value.readonly)
210
+ }
211
+
212
+ const validatedGrid = computed<ValidatedGrid<ColumnKey>>(() => {
213
+ assertReadonlyArray(props.columns, 'columns')
214
+ assertReadonlyArray(props.rows, 'rows')
215
+
216
+ const columnByKey = new Map<ColumnKey, SDataGridColumn<ColumnKey>>()
217
+ for (const columnValue of props.columns) {
218
+ assertRecord(columnValue, 'columns[]')
219
+ const column = columnValue as unknown as SDataGridColumn<ColumnKey>
220
+ const key = validateNonEmptyString('SDataGrid', 'columns[].key', column.key) as ColumnKey
221
+ validateNonEmptyString('SDataGrid', `columns[${JSON.stringify(key)}].label`, column.label)
222
+ if (columnByKey.has(key)) {
223
+ throw new TypeError(
224
+ `SDataGrid: columns содержит duplicate key ${JSON.stringify(key)}. `
225
+ + `/ SDataGrid: columns contains duplicate key ${JSON.stringify(key)}.`,
226
+ )
227
+ }
228
+ if (column.type !== undefined) {
229
+ validateExactString('SDataGrid', `columns[${JSON.stringify(key)}].type`, column.type, CELL_KINDS)
230
+ }
231
+ if (column.width !== undefined) {
232
+ validateTableColumnInlineSize('SDataGrid', `columns[${JSON.stringify(key)}].width`, column.width)
233
+ }
234
+ if (column.minWidth !== undefined) {
235
+ validateTableColumnMinInlineSize('SDataGrid', `columns[${JSON.stringify(key)}].minWidth`, column.minWidth)
236
+ }
237
+ if (column.align !== undefined) {
238
+ validateExactString('SDataGrid', `columns[${JSON.stringify(key)}].align`, column.align, COLUMN_ALIGNMENTS)
239
+ }
240
+ validateOptionalBoolean(`columns[${JSON.stringify(key)}].editable`, column.editable)
241
+ columnByKey.set(key, column)
242
+ }
243
+
244
+ const rowById = new Map<string, SDataGridRow<ColumnKey>>()
245
+ for (const rowValue of props.rows) {
246
+ assertRecord(rowValue, 'rows[]')
247
+ const row = rowValue as unknown as SDataGridRow<ColumnKey>
248
+ const rowId = validateNonEmptyString('SDataGrid', 'rows[].id', row.id)
249
+ if (rowById.has(rowId)) {
250
+ throw new TypeError(
251
+ `SDataGrid: rows содержит duplicate id ${JSON.stringify(rowId)}. `
252
+ + `/ SDataGrid: rows contains duplicate id ${JSON.stringify(rowId)}.`,
253
+ )
254
+ }
255
+ if (row.state !== undefined) {
256
+ validateExactString('SDataGrid', `rows[${JSON.stringify(rowId)}].state`, row.state, ROW_STATES)
257
+ }
258
+ assertRecord(row.cells, `rows[${JSON.stringify(rowId)}].cells`)
259
+ const actualKeys = Object.keys(row.cells)
260
+ for (const actualKey of actualKeys) {
261
+ if (!columnByKey.has(actualKey as ColumnKey)) {
262
+ throw new TypeError(
263
+ `SDataGrid: row ${JSON.stringify(rowId)} содержит unknown cell ${JSON.stringify(actualKey)}. `
264
+ + `/ SDataGrid: row ${JSON.stringify(rowId)} contains unknown cell ${JSON.stringify(actualKey)}.`,
265
+ )
266
+ }
267
+ }
268
+ for (const [key, column] of columnByKey) {
269
+ if (!Object.hasOwn(row.cells, key)) {
270
+ throw new TypeError(
271
+ `SDataGrid: row ${JSON.stringify(rowId)} не содержит cell ${JSON.stringify(key)}. `
272
+ + `/ SDataGrid: row ${JSON.stringify(rowId)} is missing cell ${JSON.stringify(key)}.`,
273
+ )
274
+ }
275
+ validateCell(rowId, column, row.cells[key])
276
+ }
277
+ rowById.set(rowId, row)
278
+ }
279
+
280
+ return { columns: props.columns, rows: props.rows, columnByKey, rowById }
281
+ })
282
+
283
+ const validatedTitle = computed(() => props.title === undefined
284
+ ? undefined
285
+ : validateNonEmptyString('SDataGrid', 'title', props.title))
286
+ const generatedTitleId = `s-data-grid-title-${useId()}`
287
+ const generatedStatusId = `s-data-grid-status-${useId()}`
288
+ const resolvedAccessibleName = computed(() => {
289
+ if (validatedTitle.value !== undefined) {
290
+ if (props.ariaLabel !== undefined || props.ariaLabelledby !== undefined) {
291
+ throw new TypeError(
292
+ 'SDataGrid: title уже задаёт accessible name; не передавайте ariaLabel/ariaLabelledby. '
293
+ + '/ SDataGrid: title already provides the accessible name; do not provide ariaLabel/ariaLabelledby.',
294
+ )
295
+ }
296
+ return { ariaLabel: undefined, ariaLabelledby: generatedTitleId }
297
+ }
298
+ if (props.ariaLabel !== undefined && props.ariaLabelledby !== undefined) {
299
+ throw new TypeError('SDataGrid: задайте только ariaLabel или ariaLabelledby. / SDataGrid: provide only ariaLabel or ariaLabelledby.')
300
+ }
301
+ if (props.ariaLabelledby !== undefined) {
302
+ return {
303
+ ariaLabel: undefined,
304
+ ariaLabelledby: validateDomId('SDataGrid', 'ariaLabelledby', props.ariaLabelledby),
305
+ }
306
+ }
307
+ return {
308
+ ariaLabel: props.ariaLabel === undefined
309
+ ? 'Таблица данных'
310
+ : validateNonEmptyString('SDataGrid', 'ariaLabel', props.ariaLabel),
311
+ ariaLabelledby: undefined,
312
+ }
313
+ })
314
+ const validatedLoading = computed(() => validateBoolean('SDataGrid', 'loading', props.loading))
315
+ const validatedDisabled = computed(() => validateBoolean('SDataGrid', 'disabled', props.disabled))
316
+ const validatedReadOnly = computed(() => validateBoolean('SDataGrid', 'readOnly', props.readOnly))
317
+ const validatedMaxHeight = computed(() => validateScrollableViewportMaxBlockSize(
318
+ 'SDataGrid',
319
+ 'maxHeight',
320
+ props.maxHeight,
321
+ ))
322
+
323
+ const tableColumns = computed<readonly STableColumn<SDataGridRow<ColumnKey>, string>[]>(() => (
324
+ validatedGrid.value.columns.map((column) => ({
325
+ field: column.key,
326
+ header: column.label,
327
+ width: column.width,
328
+ minWidth: column.minWidth,
329
+ align: column.align,
330
+ value: (row: SDataGridRow<ColumnKey>) => resolveCell(row, column.key),
331
+ }))
332
+ ))
333
+
334
+ const drafts = reactive(new Map<string, Map<ColumnKey, string>>())
335
+ watch(
336
+ () => [props.columns, props.rows] as const,
337
+ () => drafts.clear(),
338
+ { flush: 'sync' },
339
+ )
340
+
341
+ const pendingCount = computed(() => validatedGrid.value.rows.filter((row) => (
342
+ row.state !== undefined && row.state !== 'clean'
343
+ )).length)
344
+ const hasStatus = computed(() => Boolean(slots.status) || pendingCount.value > 0)
345
+
346
+ function getRowKey(row: SDataGridRow<ColumnKey>): string {
347
+ return row.id
348
+ }
349
+
350
+ function resolveColumn(key: string): SDataGridColumn<ColumnKey> {
351
+ const column = validatedGrid.value.columnByKey.get(key as ColumnKey)
352
+ if (column) return column
353
+ throw new TypeError(
354
+ `SDataGrid: table adapter получил unknown column ${JSON.stringify(key)}. `
355
+ + `/ SDataGrid: table adapter received unknown column ${JSON.stringify(key)}.`,
356
+ )
357
+ }
358
+
359
+ function resolveCell(row: SDataGridRow<ColumnKey>, columnKeyValue: string): SDataGridCell {
360
+ const columnKey = resolveColumn(columnKeyValue).key
361
+ if (Object.hasOwn(row.cells, columnKey)) return row.cells[columnKey]
362
+ throw new TypeError(
363
+ `SDataGrid: row ${JSON.stringify(row.id)} не содержит cell ${JSON.stringify(columnKey)}. `
364
+ + `/ SDataGrid: row ${JSON.stringify(row.id)} is missing cell ${JSON.stringify(columnKey)}.`,
365
+ )
366
+ }
367
+
368
+ function draftRow(rowId: string): Map<ColumnKey, string> | undefined {
369
+ return drafts.get(rowId)
370
+ }
371
+
372
+ function draftValue(row: SDataGridRow<ColumnKey>, column: SDataGridColumn<ColumnKey>): string {
373
+ const rowDrafts = draftRow(row.id)
374
+ if (rowDrafts?.has(column.key)) {
375
+ const value = rowDrafts.get(column.key)
376
+ if (value !== undefined) return value
377
+ throw new Error(
378
+ `SDataGrid: draft ${JSON.stringify(row.id)}/${JSON.stringify(column.key)} нарушил string invariant. `
379
+ + `/ SDataGrid: draft ${JSON.stringify(row.id)}/${JSON.stringify(column.key)} violated the string invariant.`,
380
+ )
381
+ }
382
+ return resolveCell(row, column.key).display
383
+ }
384
+
385
+ function isEditable(row: SDataGridRow<ColumnKey>, column: SDataGridColumn<ColumnKey>): boolean {
386
+ return !validatedDisabled.value
387
+ && !validatedReadOnly.value
388
+ && row.state !== 'deleted'
389
+ && column.editable === true
390
+ && resolveCell(row, column.key).readonly !== true
391
+ }
392
+
393
+ function editCell(row: SDataGridRow<ColumnKey>, column: SDataGridColumn<ColumnKey>, value: string): void {
394
+ if (!isEditable(row, column)) return
395
+ let rowDrafts = draftRow(row.id)
396
+ if (!rowDrafts) {
397
+ rowDrafts = new Map<ColumnKey, string>()
398
+ drafts.set(row.id, rowDrafts)
399
+ }
400
+ rowDrafts.set(column.key, value)
401
+ emit('cell-edit', { rowId: row.id, columnKey: column.key, value })
402
+ }
403
+
404
+ function validateCommitInput(input: unknown): SDataGridCommitInput {
405
+ assertRecord(input, 'commit input')
406
+ const source = validateExactString('SDataGrid', 'commit input.source', input.source, COMMIT_SOURCES)
407
+ const hasValue = Object.hasOwn(input, 'value')
408
+ if (source === 'draft' && hasValue) {
409
+ throw new TypeError(
410
+ 'SDataGrid: draft commit не должен содержать value. / SDataGrid: draft commit must not contain value.',
411
+ )
412
+ }
413
+ if (source === 'value' && !hasValue) {
414
+ throw new TypeError(
415
+ 'SDataGrid: value commit должен содержать value. / SDataGrid: value commit must contain value.',
416
+ )
417
+ }
418
+ return source === 'draft' ? { source } : { source, value: input.value }
419
+ }
420
+
421
+ function clearDraft(rowId: string, columnKey: ColumnKey): void {
422
+ const rowDrafts = draftRow(rowId)
423
+ if (!rowDrafts) return
424
+ rowDrafts.delete(columnKey)
425
+ if (rowDrafts.size === 0) drafts.delete(rowId)
426
+ }
427
+
428
+ function commitCell(
429
+ row: SDataGridRow<ColumnKey>,
430
+ column: SDataGridColumn<ColumnKey>,
431
+ inputValue: SDataGridCommitInput,
432
+ ): void {
433
+ if (!isEditable(row, column)) return
434
+ const input = validateCommitInput(inputValue)
435
+ const rowDrafts = draftRow(row.id)
436
+ if (input.source === 'draft' && !rowDrafts?.has(column.key)) return
437
+ const committed = input.source === 'value' ? input.value : rowDrafts?.get(column.key)
438
+ if (input.source === 'draft' && committed === undefined) {
439
+ throw new Error(
440
+ `SDataGrid: draft ${JSON.stringify(row.id)}/${JSON.stringify(column.key)} исчез до commit. `
441
+ + `/ SDataGrid: draft ${JSON.stringify(row.id)}/${JSON.stringify(column.key)} disappeared before commit.`,
442
+ )
443
+ }
444
+ clearDraft(row.id, column.key)
445
+ emit('cell-commit', { rowId: row.id, columnKey: column.key, value: committed })
446
+ }
447
+
448
+ function cancelCell(row: SDataGridRow<ColumnKey>, column: SDataGridColumn<ColumnKey>): void {
449
+ if (!isEditable(row, column)) return
450
+ if (!draftRow(row.id)?.has(column.key)) return
451
+ clearDraft(row.id, column.key)
452
+ emit('cell-cancel', { rowId: row.id, columnKey: column.key })
453
+ }
454
+
455
+ function rowState(row: SDataGridRow<ColumnKey>): STableRowState {
456
+ return row.state === undefined || row.state === 'clean' ? 'default' : row.state
457
+ }
458
+
459
+ function cellState(row: SDataGridRow<ColumnKey>, column: STableColumn<SDataGridRow<ColumnKey>, string>): STableCellState {
460
+ return resolveCell(row, column.field).pending === true ? 'pending' : 'default'
461
+ }
462
+ </script>
463
+
464
+ <style lang="postcss" src="./SDataGrid.css" scoped></style>