@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
package/README.md CHANGED
@@ -6,7 +6,7 @@
6
6
  **A typed PGCorp design system for Vue 3, spanning foundational controls and
7
7
  complete workbench interfaces.**
8
8
 
9
- `Vue 3` · `TypeScript` · `118 exact exports` · `6 themes` · `MIT`
9
+ `Vue 3` · `TypeScript` · `122 exact exports` · `6 themes` · `MIT`
10
10
 
11
11
  ## Что входит / What is included
12
12
 
@@ -32,7 +32,7 @@ npm install @pgcorp/ui-kit vue pinia vue-router
32
32
  | Требование / Requirement | Версия / Version |
33
33
  | --- | --- |
34
34
  | Node.js | `>=20.19.0` |
35
- | npm | `>=11.10.0 <12` |
35
+ | npm | `>=10.9.2 <12` |
36
36
  | Vue | `^3.5.0` |
37
37
  | Pinia | `^3.0.4` |
38
38
  | Vue Router | `^5.0.0` |
@@ -82,7 +82,7 @@ import SButton from '@pgcorp/ui-kit/shared/controls/SButton.vue'
82
82
  content-layout="stack"
83
83
  content-gap="content"
84
84
  >
85
- <SButton variant="primary">
85
+ <SButton appearance="solid" severity="primary">
86
86
  Сохранить
87
87
  </SButton>
88
88
  </SPanel>
@@ -100,9 +100,9 @@ monorepo alias or copied UI-kit files.
100
100
  | Группа / Group | Назначение / Purpose | Примеры exports / Example exports |
101
101
  | --- | --- | --- |
102
102
  | Layout | App shell, dock regions, stacks, workbench layout | `layout/SAppShell.vue`, `layout/SStack.vue`, `layout/SWorkbenchLayout.vue` |
103
- | Controls | Buttons, fields, inputs, selects, menus, selection | `shared/controls/SButton.vue`, `shared/controls/SInputText.vue`, `shared/controls/SSelect.vue` |
103
+ | Controls | Buttons, fields, inputs, switches, selects, comboboxes, menus | `shared/controls/SButton.vue`, `shared/controls/SSwitch.vue`, `shared/controls/SCombobox.vue` |
104
104
  | Containers | Panels, modal, drawer, popover, sidebars | `shared/containers/SPanel.vue`, `shared/containers/SModal.vue`, `shared/containers/SPopover.vue` |
105
- | Data display | Tables, status, progress, code, JSON, tooltips | `shared/data-display/STable.vue`, `shared/data-display/SCodeBlock.vue`, `shared/data-display/SStatus.vue` |
105
+ | Data display | Tables, chips, status, progress, code, JSON, tooltips | `shared/data-display/STable.vue`, `shared/data-display/SChip.vue`, `shared/data-display/SStatus.vue` |
106
106
  | Navigation | Tabs, breadcrumbs, catalog navigation, wizard | `shared/navigation/STabs.vue`, `shared/navigation/SBreadcrumbs.vue`, `shared/navigation/SWizardSteps.vue` |
107
107
  | Complex surfaces | Tree, Kanban, data grid, SQL editor, graph | `shared/complex/STree.vue`, `shared/database/SDataGrid.vue`, `shared/graph/SGraphViewport.vue` |
108
108
  | Runtime | Themes, variants, icons, composables, stores | `theme`, `variants`, `icons`, `composables/useNotifier`, `stores/useNotifierStore` |
@@ -18,6 +18,10 @@
18
18
 
19
19
  Компонентный contract выражается через typed props, slots, events и exposed
20
20
  methods. Внутренний DOM, CSS-классы и private modules не являются API.
21
+ Публичные source-SFC объявляют runtime props и events локально: consumer Vue
22
+ compiler регистрирует их без межфайлового filesystem type resolver.
23
+ Публикуемый production-source компилируется с `target/lib: ES2020`, совпадающим
24
+ с baseline `@vue/tsconfig/tsconfig.dom.json`, и не требует скрытых полифиллов.
21
25
 
22
26
  ## English
23
27
 
@@ -37,3 +41,7 @@ Primary groups:
37
41
 
38
42
  Component contracts are expressed through typed props, slots, events, and exposed
39
43
  methods. Internal DOM, CSS classes, and private modules are not API.
44
+ Public source SFCs declare runtime props and events locally, so the consumer Vue
45
+ compiler registers them without a cross-file filesystem type resolver.
46
+ Published production source compiles with the `target/lib: ES2020` baseline from
47
+ `@vue/tsconfig/tsconfig.dom.json` and does not require implicit polyfills.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pgcorp/ui-kit",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
4
4
  "description": "Typed Vue 3 design system with accessible components, semantic themes, and workbench patterns.",
5
5
  "type": "module",
6
6
  "types": "./index.ts",
@@ -38,7 +38,7 @@
38
38
  },
39
39
  "engines": {
40
40
  "node": ">=20.19.0",
41
- "npm": ">=11.10.0 <12"
41
+ "npm": ">=10.9.2 <12"
42
42
  },
43
43
  "publishConfig": {
44
44
  "access": "public",
@@ -207,6 +207,9 @@
207
207
  "shared/controls/SColorSelect.vue": [
208
208
  "src/components/shared/controls/SColorSelect.vue"
209
209
  ],
210
+ "shared/controls/SCombobox.vue": [
211
+ "src/components/shared/controls/SCombobox.vue"
212
+ ],
210
213
  "shared/controls/SContextMenu.vue": [
211
214
  "src/components/shared/controls/SContextMenu.vue"
212
215
  ],
@@ -261,6 +264,9 @@
261
264
  "shared/controls/SSelect.vue": [
262
265
  "src/components/shared/controls/SSelect.vue"
263
266
  ],
267
+ "shared/controls/SSwitch.vue": [
268
+ "src/components/shared/controls/SSwitch.vue"
269
+ ],
264
270
  "shared/controls/STextarea.vue": [
265
271
  "src/components/shared/controls/STextarea.vue"
266
272
  ],
@@ -279,6 +285,9 @@
279
285
  "shared/data-display/SBadge.vue": [
280
286
  "src/components/shared/data-display/SBadge.vue"
281
287
  ],
288
+ "shared/data-display/SChip.vue": [
289
+ "src/components/shared/data-display/SChip.vue"
290
+ ],
282
291
  "shared/data-display/SCodeBlock.vue": [
283
292
  "src/components/shared/data-display/SCodeBlock.vue"
284
293
  ],
@@ -468,6 +477,7 @@
468
477
  "./shared/controls/SCheckboxGroup.vue": "./src/components/shared/controls/SCheckboxGroup.vue",
469
478
  "./shared/controls/SChoiceCards.vue": "./src/components/shared/controls/SChoiceCards.vue",
470
479
  "./shared/controls/SColorSelect.vue": "./src/components/shared/controls/SColorSelect.vue",
480
+ "./shared/controls/SCombobox.vue": "./src/components/shared/controls/SCombobox.vue",
471
481
  "./shared/controls/SContextMenu.vue": "./src/components/shared/controls/SContextMenu.vue",
472
482
  "./shared/controls/SContextToggleButton.vue": "./src/components/shared/controls/SContextToggleButton.vue",
473
483
  "./shared/controls/SDragHandle.vue": "./src/components/shared/controls/SDragHandle.vue",
@@ -486,12 +496,14 @@
486
496
  "./shared/controls/SRange.vue": "./src/components/shared/controls/SRange.vue",
487
497
  "./shared/controls/SSegmentedControl.vue": "./src/components/shared/controls/SSegmentedControl.vue",
488
498
  "./shared/controls/SSelect.vue": "./src/components/shared/controls/SSelect.vue",
499
+ "./shared/controls/SSwitch.vue": "./src/components/shared/controls/SSwitch.vue",
489
500
  "./shared/controls/STextarea.vue": "./src/components/shared/controls/STextarea.vue",
490
501
  "./shared/data-display/json": "./src/components/shared/data-display/json.ts",
491
502
  "./shared/data-display/SActionCard.vue": "./src/components/shared/data-display/SActionCard.vue",
492
503
  "./shared/data-display/SActionList.vue": "./src/components/shared/data-display/SActionList.vue",
493
504
  "./shared/data-display/SActionListItem.vue": "./src/components/shared/data-display/SActionListItem.vue",
494
505
  "./shared/data-display/SBadge.vue": "./src/components/shared/data-display/SBadge.vue",
506
+ "./shared/data-display/SChip.vue": "./src/components/shared/data-display/SChip.vue",
495
507
  "./shared/data-display/SCodeBlock.vue": "./src/components/shared/data-display/SCodeBlock.vue",
496
508
  "./shared/data-display/SCodeEditor.vue": "./src/components/shared/data-display/SCodeEditor.vue",
497
509
  "./shared/data-display/SCodeSearchPanel.vue": "./src/components/shared/data-display/SCodeSearchPanel.vue",
@@ -582,7 +594,7 @@
582
594
  "eslint-plugin-vue": "^10.8.0",
583
595
  "jsdom": "^28.1.0",
584
596
  "pinia": "^3.0.4",
585
- "postcss": "8.5.16",
597
+ "postcss": "8.5.19",
586
598
  "postcss-selector-parser": "7.1.4",
587
599
  "storybook": "10.5.3",
588
600
  "typescript": "^5.9.3",
@@ -1,6 +1,7 @@
1
1
  <script setup lang="ts">
2
2
  import { computed, getCurrentInstance, useSlots } from 'vue'
3
3
  import type { RouteLocationRaw } from 'vue-router'
4
+ import { hasOwn } from '../../internal/es2020'
4
5
  import { useOwnedAttrs } from '../../internal/ownedAttrs'
5
6
  import { validateNonEmptyString } from '../../internal/runtimeContract'
6
7
  import SBadge from '../shared/data-display/SBadge.vue'
@@ -98,8 +99,8 @@ function toKebabCase(value: string): string {
98
99
  function hasExplicitProp(propName: string): boolean {
99
100
  const vnodeProps = componentInstance.vnode.props
100
101
  return vnodeProps !== null && (
101
- Object.hasOwn(vnodeProps, propName)
102
- || Object.hasOwn(vnodeProps, toKebabCase(propName))
102
+ hasOwn(vnodeProps, propName)
103
+ || hasOwn(vnodeProps, toKebabCase(propName))
103
104
  )
104
105
  }
105
106
 
@@ -1,8 +1,9 @@
1
1
  @reference "../../styles/reference.css";
2
2
 
3
- .s-stack {
4
- @apply flex min-h-0 min-w-0 flex-col;
5
- }
3
+ .s-stack { @apply flex min-h-0 min-w-0; }
4
+
5
+ .s-stack[data-direction='column'] { @apply flex-col; }
6
+ .s-stack[data-direction='row'] { @apply flex-row; }
6
7
 
7
8
  .s-stack[data-fill='container'] {
8
9
  @apply h-full flex-1;
@@ -13,6 +14,11 @@
13
14
  .s-stack[data-align='end'] { @apply items-end; }
14
15
  .s-stack[data-align='stretch'] { @apply items-stretch; }
15
16
 
17
+ .s-stack[data-justify='start'] { @apply justify-start; }
18
+ .s-stack[data-justify='center'] { @apply justify-center; }
19
+ .s-stack[data-justify='end'] { @apply justify-end; }
20
+ .s-stack[data-justify='between'] { @apply justify-between; }
21
+
16
22
  .s-stack[data-gap='tight'] { gap: 0.25rem; }
17
23
  .s-stack[data-gap='control'] { gap: var(--s-space-control-gap); }
18
24
  .s-stack[data-gap='content'] { gap: var(--s-space-content-gap); }
@@ -4,9 +4,11 @@
4
4
  v-bind="ownedAttrs.bindings()"
5
5
  class="s-stack"
6
6
  :data-gap="gap"
7
+ :data-direction="direction"
7
8
  :data-padding-end="paddingEnd"
8
9
  :data-fill="fill"
9
10
  :data-align="align"
11
+ :data-justify="justify"
10
12
  >
11
13
  <slot />
12
14
  </component>
@@ -22,12 +24,14 @@ export type SStackGap = 'none' | 'tight' | 'control' | 'content' | 'section' | '
22
24
  export type SStackPadding = 'none' | 'control' | 'content' | 'section' | 'panel'
23
25
 
24
26
  /**
25
- * Вертикальный layout-owner с ограниченной семантической шкалой интервалов.
26
- * Vertical layout owner with a constrained semantic spacing scale.
27
+ * Flex layout-owner с ограниченной семантической шкалой интервалов.
28
+ * Flex layout owner with a constrained semantic spacing scale.
27
29
  */
28
30
  export interface Props {
29
31
  /** Семантический HTML-container без интерактивной роли. / Semantic non-interactive HTML container. */
30
32
  as?: 'div' | 'section' | 'article' | 'ul' | 'ol'
33
+ /** Направление основной оси. / Main-axis direction. */
34
+ direction?: 'column' | 'row'
31
35
  /** Интервал между непосредственными участниками. / Gap between direct participants. */
32
36
  gap?: SStackGap
33
37
  /** Нижний внутренний отступ для scroll-safe composition. / End padding for scroll-safe composition. */
@@ -36,14 +40,18 @@ export interface Props {
36
40
  fill?: 'content' | 'container'
37
41
  /** Выравнивание участников по поперечной оси. / Cross-axis participant alignment. */
38
42
  align?: 'stretch' | 'start' | 'center' | 'end'
43
+ /** Распределение участников по основной оси. / Main-axis participant distribution. */
44
+ justify?: 'start' | 'center' | 'end' | 'between'
39
45
  }
40
46
 
41
47
  withDefaults(defineProps<Props>(), {
42
48
  as: 'div',
49
+ direction: 'column',
43
50
  gap: 'content',
44
51
  paddingEnd: 'none',
45
52
  fill: 'content',
46
53
  align: 'stretch',
54
+ justify: 'start',
47
55
  })
48
56
 
49
57
  const ownedAttrs = useOwnedAttrs({ component: 'SStack', owner: 'semantic stack root' })
@@ -32,8 +32,36 @@
32
32
  </ul>
33
33
  </template>
34
34
 
35
+ <script lang="ts">
36
+ import type {
37
+ SelectionKey,
38
+ TreeActionsLayout,
39
+ TreeDensity,
40
+ TreeExpandControl,
41
+ TreeModel,
42
+ TreeRowVariant,
43
+ TreeSelection,
44
+ TreeSelectionRail,
45
+ } from './types'
46
+
47
+ /** Публичный контракт дерева принадлежит source-SFC runtime. / The public tree contract is owned by the source-SFC runtime. */
48
+ export interface STreeProps<T extends object, K extends SelectionKey = SelectionKey> {
49
+ nodes: readonly T[]
50
+ model: TreeModel<T, K>
51
+ label: string
52
+ selection: TreeSelection<K>
53
+ expanded?: ReadonlySet<K>
54
+ density?: TreeDensity
55
+ actionsLayout?: TreeActionsLayout
56
+ expandControl?: TreeExpandControl
57
+ selectionRail?: TreeSelectionRail
58
+ rowVariant?: TreeRowVariant
59
+ }
60
+ </script>
61
+
35
62
  <script setup lang="ts" generic="T extends object, K extends SelectionKey">
36
63
  import { computed, nextTick, provide, ref, watch } from 'vue'
64
+ import { lastItem } from '../../../internal/es2020'
37
65
  import { useOwnedAttrs } from '../../../internal/ownedAttrs'
38
66
  import { assertSelectionKey, selectionKeyToken } from '../_internal/useSelectionRoving'
39
67
  import STreeNode from '../_internal/STreeNode.vue'
@@ -46,12 +74,9 @@ import {
46
74
  import { validateTreeModel } from './treeAdapter'
47
75
  import type {
48
76
  STreeNodeContentScope,
49
- STreeProps,
50
- SelectionKey,
51
77
  TreeActivationOrigin,
52
78
  TreeNodeActivationEvent,
53
79
  TreeNodeEvent,
54
- TreeSelection,
55
80
  } from './types'
56
81
 
57
82
  defineOptions({ inheritAttrs: false })
@@ -172,7 +197,7 @@ function handleTreeItemKeydown(item: RegisteredTreeItem<T, K>, event: KeyboardEv
172
197
  }
173
198
  if (event.key === 'End') {
174
199
  event.preventDefault()
175
- void focusItem(items.at(-1))
200
+ void focusItem(lastItem(items))
176
201
  return
177
202
  }
178
203
  if (event.key === 'ArrowRight' && item.hasChildren()) {
@@ -5,6 +5,7 @@ import {
5
5
  type SelectionIdentity,
6
6
  type SelectionKey,
7
7
  } from '../_internal/useSelectionRoving'
8
+ import { withErrorCause } from '../../../internal/es2020'
8
9
  import type { TreeModel } from './types'
9
10
 
10
11
  export interface FlatTreeAdapter<T extends object, K extends SelectionKey> {
@@ -130,7 +131,7 @@ function readModelValue<R>(
130
131
  try {
131
132
  return reader()
132
133
  } catch (error) {
133
- throw new TypeError(`STree: model.${field} failed at ${coordinate}`, { cause: error })
134
+ throw withErrorCause(new TypeError(`STree: model.${field} failed at ${coordinate}`), error)
134
135
  }
135
136
  }
136
137
 
@@ -51,18 +51,7 @@ export type TreeExpandControl = 'button' | 'row'
51
51
  export type TreeSelectionRail = 'hidden' | 'visible'
52
52
  export type TreeRowVariant = 'default' | 'table'
53
53
 
54
- export interface STreeProps<T extends object, K extends SelectionKey = SelectionKey> {
55
- nodes: readonly T[]
56
- model: TreeModel<T, K>
57
- label: string
58
- selection: TreeSelection<K>
59
- expanded?: ReadonlySet<K>
60
- density?: TreeDensity
61
- actionsLayout?: TreeActionsLayout
62
- expandControl?: TreeExpandControl
63
- selectionRail?: TreeSelectionRail
64
- rowVariant?: TreeRowVariant
65
- }
54
+ export type { STreeProps } from './STree.vue'
66
55
 
67
56
  export interface TreeNodeEvent<T extends object, K extends SelectionKey = SelectionKey> {
68
57
  node: T
@@ -33,7 +33,7 @@ import { type FunctionalComponent, type HTMLAttributes, type VNodeProps } from '
33
33
  import SPanel from './SPanel.vue';
34
34
  import SSidebarGroup from './SSidebarGroup.vue';
35
35
  import type { SAsyncStateContract } from '../feedback/SAsyncState.vue';
36
- import type { SidebarGroupEmits, SidebarSectionsState } from './sidebar';
36
+ import type { SidebarSectionsState } from './sidebar';
37
37
  import { useOwnedAttrs } from '../../../internal/ownedAttrs';
38
38
 
39
39
  defineOptions({ inheritAttrs: false });
@@ -48,6 +48,11 @@ export interface Props {
48
48
  scrollBehavior?: 'auto' | 'body' | 'self' | 'child';
49
49
  }
50
50
 
51
+ interface Emits {
52
+ 'update-sections': [state: SidebarSectionsState];
53
+ 'toggle-section': [key: string];
54
+ }
55
+
51
56
  withDefaults(defineProps<Props>(), {
52
57
  title: undefined,
53
58
  icon: undefined,
@@ -56,7 +61,7 @@ withDefaults(defineProps<Props>(), {
56
61
  scrollBehavior: 'child',
57
62
  });
58
63
 
59
- const emit = defineEmits<SidebarGroupEmits>();
64
+ const emit = defineEmits<Emits>();
60
65
  const ownedAttrs = useOwnedAttrs({ component: 'SLeftSidebar', owner: 'left-sidebar root' });
61
66
  </script>
62
67
 
@@ -31,6 +31,17 @@
31
31
  @apply m-0 text-2xl font-semibold leading-tight text-surface-900 dark:text-surface-50;
32
32
  }
33
33
 
34
+ .s-page-header__title[data-presentation='assistive'] {
35
+ position: absolute;
36
+ width: 1px;
37
+ height: 1px;
38
+ padding: 0;
39
+ margin: -1px;
40
+ clip-path: inset(50%);
41
+ white-space: nowrap;
42
+ border: 0;
43
+ }
44
+
34
45
  .s-page-header[data-size='compact'] .s-page-header__title {
35
46
  @apply text-xl;
36
47
  }
@@ -11,6 +11,7 @@ withDefaults(defineProps<{
11
11
  appearance?: 'plain' | 'panel'
12
12
  size?: 'compact' | 'default'
13
13
  actionsLayout?: 'responsive' | 'inline' | 'stacked'
14
+ titlePresentation?: 'visible' | 'assistive'
14
15
  }>(), {
15
16
  description: undefined,
16
17
  eyebrow: undefined,
@@ -18,6 +19,7 @@ withDefaults(defineProps<{
18
19
  appearance: 'panel',
19
20
  size: 'default',
20
21
  actionsLayout: 'responsive',
22
+ titlePresentation: 'visible',
21
23
  })
22
24
 
23
25
  const ownedAttrs = useOwnedAttrs({ component: 'SPageHeader', owner: 'page header root' })
@@ -33,7 +35,11 @@ const ownedAttrs = useOwnedAttrs({ component: 'SPageHeader', owner: 'page header
33
35
  >
34
36
  <div class="s-page-header__copy">
35
37
  <div v-if="eyebrow" class="s-page-header__eyebrow">{{ eyebrow }}</div>
36
- <component :is="`h${headingLevel}`" class="s-page-header__title">{{ title }}</component>
38
+ <component
39
+ :is="`h${headingLevel}`"
40
+ class="s-page-header__title"
41
+ :data-presentation="titlePresentation"
42
+ >{{ title }}</component>
37
43
  <p v-if="description" class="s-page-header__description">{{ description }}</p>
38
44
  </div>
39
45
  <div v-if="$slots.actions" class="s-page-header__actions">
@@ -93,6 +93,7 @@ import SButton, {
93
93
  } from '../controls/SButton.vue';
94
94
  import { useFloatingPosition, type FloatingPlacement } from '../../../composables/useFloatingPosition';
95
95
  import { findRelativeTabTarget, getFocusableElements } from '../../../internal/focusNavigation';
96
+ import { lastItem } from '../../../internal/es2020';
96
97
  import { resolveInteractiveElement } from '../../../internal/interactiveElement';
97
98
  import { registerLayer, type LayerRegistration } from '../../../internal/layerStack';
98
99
  import { useOwnedAttrs } from '../../../internal/ownedAttrs';
@@ -495,7 +496,7 @@ function handlePanelTab(event: KeyboardEvent): void {
495
496
  const focusable = getFocusableElements(panel);
496
497
  const active = document.activeElement;
497
498
  const first = focusable[0];
498
- const last = focusable.at(-1);
499
+ const last = lastItem(focusable);
499
500
  const leavesPanel = focusable.length === 0
500
501
  || (event.shiftKey ? active === panel || active === first : active === last);
501
502
  if (!leavesPanel) return;
@@ -16,6 +16,7 @@ import {
16
16
  } from 'vue';
17
17
 
18
18
  import { useOwnedAttrs } from '../../../internal/ownedAttrs';
19
+ import { hasOwn, lastItem } from '../../../internal/es2020';
19
20
  import {
20
21
  sidebarGroupContextKey,
21
22
  type RegisteredSidebarSection,
@@ -27,7 +28,6 @@ import {
27
28
  createSidebarSectionsTransition,
28
29
  sidebarSectionsStateEqual,
29
30
  type SectionState,
30
- type SidebarGroupEmits,
31
31
  type SidebarSectionReplacement,
32
32
  type SidebarSectionsState,
33
33
  } from './sidebar';
@@ -38,7 +38,12 @@ const props = defineProps<{
38
38
  sectionsState: SidebarSectionsState;
39
39
  }>();
40
40
 
41
- const emit = defineEmits<SidebarGroupEmits>();
41
+ interface Emits {
42
+ 'update-sections': [state: SidebarSectionsState];
43
+ 'toggle-section': [key: string];
44
+ }
45
+
46
+ const emit = defineEmits<Emits>();
42
47
 
43
48
  const ownedAttrs = useOwnedAttrs({ component: 'SSidebarGroup', owner: 'sidebar group root' });
44
49
  const groupRef = ref<HTMLDivElement | null>(null);
@@ -147,7 +152,7 @@ function fitOpenSectionSizes(
147
152
 
148
153
  let delta = targetSize - [...sizes.values()].reduce((sum, size) => sum + size, 0);
149
154
  if (delta > 0) {
150
- const growTarget = flexible ?? openSections.at(-1);
155
+ const growTarget = flexible ?? lastItem(openSections);
151
156
  if (growTarget) sizes.set(growTarget.key, sizes.get(growTarget.key)! + delta);
152
157
  delta = 0;
153
158
  }
@@ -200,7 +205,7 @@ function registerSection(registration: SidebarSectionRegistration): () => void {
200
205
  if (!(registration.element instanceof HTMLElement)) {
201
206
  throw new TypeError('SSidebarGroup: registered section element must be an HTMLElement');
202
207
  }
203
- if (!Object.hasOwn(currentState(), registration.key)) {
208
+ if (!hasOwn(currentState(), registration.key)) {
204
209
  throw new Error(`SSidebarGroup: registered section '${registration.key}' has no controlled state`);
205
210
  }
206
211
  if (registrations.value.some((current) => current.key === registration.key)) {
@@ -371,7 +376,7 @@ function resizeSections(topKey: string, bottomKey: string, requestedTopSize: num
371
376
  );
372
377
  const delta = requestedTopSize - currentUpperSize;
373
378
  const shrinking = delta > 0 ? [...lower] : [...upper].reverse();
374
- const growing = delta > 0 ? upper.at(-1) : lower[0];
379
+ const growing = delta > 0 ? lastItem(upper) : lower[0];
375
380
  let transferable = Math.abs(delta);
376
381
  transferable = Math.min(
377
382
  transferable,
@@ -163,7 +163,7 @@ export function createSidebarSectionsTransition(
163
163
  if (typeof replacement.key !== 'string' || replacement.key.trim().length === 0) {
164
164
  throw new TypeError(`SSidebarGroup: replacements[${index}].key must be a non-empty string`);
165
165
  }
166
- if (!Object.hasOwn(current, replacement.key)) {
166
+ if (!hasOwn(current, replacement.key)) {
167
167
  throw new Error(`SSidebarGroup: cannot replace unknown section '${replacement.key}'`);
168
168
  }
169
169
  if (replacementsByKey.has(replacement.key)) {
@@ -204,3 +204,4 @@ export function sidebarSectionsStateEqual(
204
204
  && leftSection.minSize === rightSection.minSize;
205
205
  });
206
206
  }
207
+ import { hasOwn } from '../../../internal/es2020';
@@ -146,6 +146,10 @@
146
146
  background: var(--s-button-solid-background-hover);
147
147
  }
148
148
 
149
+ .s-button[aria-disabled='true'] {
150
+ pointer-events: none;
151
+ }
152
+
149
153
  .s-button[data-appearance='solid']:active:not(:disabled) {
150
154
  border-color: var(--s-button-solid-background-active);
151
155
  background: var(--s-button-solid-background-active);
@@ -219,6 +223,14 @@
219
223
  cursor: not-allowed;
220
224
  }
221
225
 
226
+ .s-button[aria-disabled='true'] {
227
+ border-color: transparent;
228
+ background: var(--s-button-disabled-background);
229
+ box-shadow: none;
230
+ color: var(--s-button-disabled-foreground);
231
+ cursor: not-allowed;
232
+ }
233
+
222
234
  .s-button[data-appearance='ghost']:disabled,
223
235
  .s-button[data-appearance='text']:disabled {
224
236
  background: transparent;
@@ -246,6 +258,7 @@
246
258
  .s-button[data-width='fit'] { width: fit-content; }
247
259
  .s-button[data-width='full'] .s-button__content { width: 100%; }
248
260
  .s-button[data-content-align='start'] .s-button__content { justify-content: flex-start; text-align: left; }
261
+ .s-button[data-content-align='end'] .s-button__content { justify-content: flex-end; text-align: right; }
249
262
  .s-button[data-content-align='between'] .s-button__content { justify-content: space-between; text-align: left; }
250
263
 
251
264
  .s-button[data-shape='pill'],