@pgcorp/ui-kit 0.5.0 → 0.7.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 (56) hide show
  1. package/README.md +39 -0
  2. package/docs/public-api.md +6 -0
  3. package/package.json +5 -1
  4. package/src/components/shared/_internal/SHeaderSurface.css +98 -0
  5. package/src/components/shared/_internal/SHeaderSurface.vue +80 -0
  6. package/src/components/shared/_internal/SToastItem.css +8 -8
  7. package/src/components/shared/_internal/SToastItem.vue +8 -1
  8. package/src/components/shared/_internal/useRadioGroup.ts +106 -0
  9. package/src/components/shared/containers/SPageHeader.vue +13 -20
  10. package/src/components/shared/controls/SButton.css +6 -6
  11. package/src/components/shared/controls/SButton.vue +9 -1
  12. package/src/components/shared/controls/SCheckbox.vue +14 -43
  13. package/src/components/shared/controls/SChoiceCards.vue +16 -39
  14. package/src/components/shared/controls/SField.css +0 -12
  15. package/src/components/shared/controls/SField.vue +14 -10
  16. package/src/components/shared/controls/SFieldGroup.css +1 -19
  17. package/src/components/shared/controls/SFieldGroup.vue +13 -10
  18. package/src/components/shared/controls/SFieldLabel.css +0 -1
  19. package/src/components/shared/controls/SFieldLabel.vue +2 -1
  20. package/src/components/shared/controls/SInteractiveSurface.css +3 -3
  21. package/src/components/shared/controls/SInteractiveSurface.vue +9 -2
  22. package/src/components/shared/controls/SMarker.css +16 -16
  23. package/src/components/shared/controls/SMarker.vue +27 -1
  24. package/src/components/shared/controls/SSegmentedControl.vue +15 -43
  25. package/src/components/shared/controls/SSwitch.vue +14 -32
  26. package/src/components/shared/data-display/SActionListItem.css +2 -2
  27. package/src/components/shared/data-display/SActionListItem.vue +8 -1
  28. package/src/components/shared/data-display/SBadge.vue +41 -18
  29. package/src/components/shared/data-display/SChip.vue +22 -10
  30. package/src/components/shared/data-display/SMessage.css +8 -8
  31. package/src/components/shared/data-display/SMessage.vue +8 -1
  32. package/src/components/shared/data-display/SProgressBar.css +12 -12
  33. package/src/components/shared/data-display/SProgressBar.vue +12 -6
  34. package/src/components/shared/data-display/SProgressIndicator.css +5 -5
  35. package/src/components/shared/data-display/SProgressIndicator.vue +9 -1
  36. package/src/components/shared/data-display/SSectionHeader.vue +17 -21
  37. package/src/components/shared/data-display/SStatus.css +10 -10
  38. package/src/components/shared/data-display/SStatus.vue +9 -1
  39. package/src/components/shared/data-display/STable.vue +27 -4
  40. package/src/components/shared/data-display/STooltip.css +4 -4
  41. package/src/components/shared/data-display/STooltip.vue +159 -29
  42. package/src/components/shared/data-display/STooltipTarget.css +26 -0
  43. package/src/components/shared/data-display/STooltipTarget.vue +94 -0
  44. package/src/components/shared/database/SDataGrid.vue +13 -0
  45. package/src/components/shared/navigation/STabs.vue +11 -3
  46. package/src/internal/fieldPresentation.ts +28 -0
  47. package/src/internal/ownedAttrs.ts +3 -0
  48. package/src/internal/pillSurface.ts +93 -0
  49. package/src/internal/semanticTone.ts +23 -0
  50. package/src/internal/useBinaryInput.ts +76 -0
  51. package/src/styles/style.css +114 -0
  52. package/src/styles/tokens.css +31 -0
  53. package/src/components/shared/containers/SPageHeader.css +0 -82
  54. package/src/components/shared/data-display/SBadge.css +0 -73
  55. package/src/components/shared/data-display/SChip.css +0 -59
  56. package/src/components/shared/data-display/SSectionHeader.css +0 -84
package/README.md CHANGED
@@ -159,6 +159,16 @@ Multi-column sorting uses controlled `v-model:sorts`: the consumer sets
159
159
  adds a descriptor, changes its direction, or removes it; the visible number is
160
160
  the priority. The table never reorders rows on its own.
161
161
 
162
+ Последовательные действия заголовков формируют внутренний intent draft до
163
+ подтверждения controlled prop. Consumer может применить `sort`/`sorts` через
164
+ store или асинхронный render-cycle: второй критерий не теряет первый. Любое
165
+ authoritative обновление prop становится новой базой следующих действий.
166
+
167
+ Sequential header actions update an internal intent draft until the controlled
168
+ prop is acknowledged. Consumers may apply `sort` or `sorts` through a store or
169
+ an asynchronous render cycle without losing an earlier criterion. Any
170
+ authoritative prop update becomes the base for subsequent actions.
171
+
162
172
  ```vue
163
173
  <STable
164
174
  v-model:sorts="sorts"
@@ -170,6 +180,31 @@ the priority. The table never reorders rows on its own.
170
180
  />
171
181
  ```
172
182
 
183
+ ### Подсказка на части действия / Tooltip for part of an action
184
+
185
+ Обычный tooltip использует combined `trigger`. Для пассивного фрагмента внутри
186
+ уже интерактивного leaf используйте split bindings: `focusOwner` остаётся на
187
+ кнопке и владеет ARIA/focus, а `pointerTarget` получает пассивный
188
+ `STooltipTarget`. Компонент target не создаёт `role` или `tabindex`.
189
+
190
+ A regular tooltip uses the combined `trigger`. For a passive fragment inside an
191
+ existing interactive leaf, use split bindings: `focusOwner` remains on the
192
+ button and owns ARIA/focus, while `pointerTarget` is applied to a passive
193
+ `STooltipTarget`. The target component adds neither a role nor a tab stop.
194
+
195
+ ```vue
196
+ <STooltip text="Открывает палитру команд" v-slot="{ focusOwner, pointerTarget }">
197
+ <SButton v-bind="focusOwner" width="full" content-align="between">
198
+ Открыть палитру команд
199
+ <STooltipTarget
200
+ v-bind="pointerTarget"
201
+ as="kbd"
202
+ appearance="keycap"
203
+ >⌘K</STooltipTarget>
204
+ </SButton>
205
+ </STooltip>
206
+ ```
207
+
173
208
  ## Темы и стили / Themes and styles
174
209
 
175
210
  Для обычного приложения подключайте полный style entrypoint один раз:
@@ -244,6 +279,10 @@ import { themeVariants } from '@pgcorp/ui-kit/theme'
244
279
  Do not style internal DOM through `:deep` or private selectors.
245
280
  - Выражайте состояние через documented props, slots, variants и tokens.
246
281
  Express state through documented props, slots, variants, and tokens.
282
+ - Используйте собственные `data-*` как test/tour hooks; UI-kit-owned `data-s-*`
283
+ являются внутренней presentation boundary и не являются selector API.
284
+ Use consumer-owned `data-*` for test and tour hooks; UI-kit-owned `data-s-*`
285
+ are an internal presentation boundary, not a selector API.
247
286
  - Передавайте человекочитаемые labels интерактивным компонентам.
248
287
  Provide human-readable labels to interactive components.
249
288
 
@@ -18,6 +18,9 @@
18
18
 
19
19
  Компонентный contract выражается через typed props, slots, events и exposed
20
20
  methods. Внутренний DOM, CSS-классы и private modules не являются API.
21
+ UI-kit-owned атрибуты `data-s-*` относятся к внутренней presentation boundary и
22
+ не являются стабильными consumer selectors. Произвольные consumer-owned
23
+ `data-*` остаются поддерживаемыми test/tour hooks canonical owner.
21
24
  Публичные source-SFC объявляют runtime props и events локально: consumer Vue
22
25
  compiler регистрирует их без межфайлового filesystem type resolver.
23
26
  Публикуемый production-source компилируется с `target/lib: ES2020`, совпадающим
@@ -41,6 +44,9 @@ Primary groups:
41
44
 
42
45
  Component contracts are expressed through typed props, slots, events, and exposed
43
46
  methods. Internal DOM, CSS classes, and private modules are not API.
47
+ UI-kit-owned `data-s-*` attributes belong to the internal presentation boundary
48
+ and are not stable consumer selectors. Arbitrary consumer-owned `data-*`
49
+ attributes remain supported test and tour hooks on the canonical owner.
44
50
  Public source SFCs declare runtime props and events locally, so the consumer Vue
45
51
  compiler registers them without a cross-file filesystem type resolver.
46
52
  Published production source compiles with the `target/lib: ES2020` baseline from
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pgcorp/ui-kit",
3
- "version": "0.5.0",
3
+ "version": "0.7.0",
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",
@@ -348,6 +348,9 @@
348
348
  "shared/data-display/STooltipAnchor.vue": [
349
349
  "src/components/shared/data-display/STooltipAnchor.vue"
350
350
  ],
351
+ "shared/data-display/STooltipTarget.vue": [
352
+ "src/components/shared/data-display/STooltipTarget.vue"
353
+ ],
351
354
  "shared/data-display/SVirtualList": [
352
355
  "src/components/shared/data-display/SVirtualList.ts"
353
356
  ],
@@ -524,6 +527,7 @@
524
527
  "./shared/data-display/SToastContainer.vue": "./src/components/shared/data-display/SToastContainer.vue",
525
528
  "./shared/data-display/STooltip.vue": "./src/components/shared/data-display/STooltip.vue",
526
529
  "./shared/data-display/STooltipAnchor.vue": "./src/components/shared/data-display/STooltipAnchor.vue",
530
+ "./shared/data-display/STooltipTarget.vue": "./src/components/shared/data-display/STooltipTarget.vue",
527
531
  "./shared/data-display/SVirtualList": "./src/components/shared/data-display/SVirtualList.ts",
528
532
  "./shared/data-display/SVirtualList.vue": "./src/components/shared/data-display/SVirtualList.vue",
529
533
  "./shared/database/SDataGrid.vue": "./src/components/shared/database/SDataGrid.vue",
@@ -0,0 +1,98 @@
1
+ @reference "../../../styles/reference.css";
2
+
3
+ .s-page-header,
4
+ .s-section-header {
5
+ @apply flex min-w-0 items-start justify-between;
6
+ }
7
+
8
+ .s-page-header {
9
+ @apply gap-4 border border-surface-200 bg-surface-0 p-5 shadow-sm;
10
+ @apply dark:border-surface-700 dark:bg-surface-900;
11
+ border-radius: var(--radius-sm);
12
+ }
13
+
14
+ .s-section-header { @apply flex-wrap gap-3; }
15
+
16
+ .s-page-header[data-appearance='plain'] {
17
+ @apply border-x-0 border-t-0 bg-transparent px-0 pt-0 shadow-none dark:bg-transparent;
18
+ border-radius: 0;
19
+ }
20
+
21
+ .s-page-header[data-size='compact'] { @apply p-4; }
22
+ .s-page-header[data-appearance='plain'][data-size='compact'] { @apply px-0 pt-0; }
23
+
24
+ .s-page-header[data-appearance='bare'] {
25
+ @apply border-0 bg-transparent p-0 shadow-none dark:bg-transparent;
26
+ border-radius: 0;
27
+ }
28
+
29
+ .s-section-header[data-spacing-after='sm'] { @apply mb-2; }
30
+ .s-section-header[data-spacing-after='md'] { @apply mb-3; }
31
+ .s-section-header[data-divider='true'] { @apply border-b border-surface-200 pb-3 dark:border-surface-700; }
32
+ .s-section-header[data-padding='sm'] { @apply p-2; }
33
+ .s-section-header[data-padding='md'] { @apply p-4; }
34
+ .s-section-header[data-divider='true'][data-padding='sm'] { @apply pb-2; }
35
+ .s-section-header[data-divider='true'][data-padding='md'] { @apply pb-4; }
36
+
37
+ .s-page-header__copy,
38
+ .s-section-header__copy { @apply min-w-0; }
39
+ .s-section-header__copy { @apply flex-1; }
40
+
41
+ .s-page-header__eyebrow,
42
+ .s-section-header__eyebrow { @apply text-xs uppercase text-surface-500 dark:text-surface-400; }
43
+ .s-page-header__eyebrow { @apply font-medium; }
44
+ .s-section-header__eyebrow { @apply font-semibold; }
45
+
46
+ .s-page-header__title,
47
+ .s-section-header__title { @apply m-0 font-semibold text-surface-900 dark:text-surface-50; }
48
+ .s-page-header__title { @apply text-2xl leading-tight; }
49
+
50
+ .s-page-header__title[data-presentation='assistive'] {
51
+ position: absolute;
52
+ width: 1px;
53
+ height: 1px;
54
+ padding: 0;
55
+ margin: -1px;
56
+ clip-path: inset(50%);
57
+ white-space: nowrap;
58
+ border: 0;
59
+ }
60
+
61
+ .s-page-header[data-size='compact'] .s-page-header__title { @apply text-xl; }
62
+ .s-section-header__title { @apply break-words text-base; }
63
+ .s-section-header[data-size='sm'] .s-section-header__title { @apply text-sm; }
64
+ .s-section-header[data-variant='display'] .s-section-header__title {
65
+ @apply text-xl font-semibold uppercase tracking-[0.22em] text-surface-700 dark:text-surface-200;
66
+ }
67
+
68
+ .s-page-header__eyebrow + .s-page-header__title,
69
+ .s-section-header__eyebrow + .s-section-header__title { @apply mt-1; }
70
+
71
+ .s-page-header__description,
72
+ .s-section-header__description,
73
+ .s-section-header__meta { @apply text-surface-500 dark:text-surface-400; }
74
+ .s-page-header__description { @apply mt-2 max-w-4xl text-sm; }
75
+ .s-section-header__description,
76
+ .s-section-header__meta {
77
+ @apply mt-1 min-w-0 break-words text-xs;
78
+ overflow-wrap: anywhere;
79
+ }
80
+
81
+ .s-page-header__actions,
82
+ .s-section-header__trailing,
83
+ .s-section-header__badges,
84
+ .s-section-header__actions { @apply flex min-w-0 flex-wrap items-center justify-end gap-2; }
85
+ .s-page-header__actions { @apply shrink-0; }
86
+ .s-page-header[data-actions-layout='stacked'] { @apply flex-col; }
87
+
88
+ .s-page-header[data-actions-layout='stacked'] .s-page-header__actions,
89
+ .s-section-header[data-trailing-layout='stacked'] .s-section-header__trailing,
90
+ .s-section-header[data-trailing-layout='stacked'] .s-section-header__actions { @apply w-full justify-start; }
91
+
92
+ @media (max-width: 639px) {
93
+ .s-page-header[data-actions-layout='responsive'] { @apply flex-col; }
94
+
95
+ .s-page-header[data-actions-layout='responsive'] .s-page-header__actions,
96
+ .s-section-header[data-trailing-layout='responsive'] .s-section-header__trailing,
97
+ .s-section-header[data-trailing-layout='responsive'] .s-section-header__actions { @apply w-full justify-start; }
98
+ }
@@ -0,0 +1,80 @@
1
+ <template>
2
+ <header
3
+ :class="kind === 'page' ? 's-page-header' : 's-section-header'"
4
+ :data-kind="kind"
5
+ :data-actions-layout="kind === 'page' ? trailingLayout : undefined"
6
+ :data-trailing-layout="kind === 'section' ? trailingLayout : undefined"
7
+ :data-appearance="kind === 'page' ? appearance : undefined"
8
+ :data-size="size"
9
+ :data-variant="kind === 'section' ? variant : undefined"
10
+ :data-divider="kind === 'section' && divider ? 'true' : undefined"
11
+ :data-padding="kind === 'section' ? padding : undefined"
12
+ :data-spacing-after="kind === 'section' ? spacingAfter : undefined"
13
+ >
14
+ <div :class="kind === 'page' ? 's-page-header__copy' : 's-section-header__copy'">
15
+ <div
16
+ v-if="eyebrow"
17
+ :class="kind === 'page' ? 's-page-header__eyebrow' : 's-section-header__eyebrow'"
18
+ >{{ eyebrow }}</div>
19
+ <component
20
+ :is="`h${headingLevel}`"
21
+ :class="kind === 'page' ? 's-page-header__title' : 's-section-header__title'"
22
+ :data-presentation="titlePresentation"
23
+ >{{ title }}</component>
24
+ <p
25
+ v-if="description"
26
+ :class="kind === 'page' ? 's-page-header__description' : 's-section-header__description'"
27
+ >{{ description }}</p>
28
+ <p v-if="kind === 'section' && meta" class="s-section-header__meta">{{ meta }}</p>
29
+ </div>
30
+
31
+ <div v-if="hasTrailing" :class="kind === 'page' ? 's-page-header__actions' : 's-section-header__trailing'">
32
+ <template v-if="kind === 'page'">
33
+ <slot name="actions" />
34
+ </template>
35
+ <template v-else>
36
+ <div v-if="$slots.badges" class="s-section-header__badges"><slot name="badges" /></div>
37
+ <div v-if="$slots.actions" class="s-section-header__actions"><slot name="actions" /></div>
38
+ </template>
39
+ </div>
40
+ </header>
41
+ </template>
42
+
43
+ <script setup lang="ts">
44
+ import { computed, useSlots } from 'vue'
45
+
46
+ export interface Props {
47
+ kind: 'page' | 'section'
48
+ title: string
49
+ eyebrow?: string | null
50
+ description?: string | null
51
+ meta?: string | null
52
+ headingLevel: 1 | 2 | 3 | 4
53
+ titlePresentation?: 'visible' | 'assistive'
54
+ appearance?: 'bare' | 'plain' | 'panel'
55
+ size: 'compact' | 'default' | 'sm' | 'md'
56
+ variant?: 'default' | 'display'
57
+ divider?: boolean
58
+ padding?: 'none' | 'sm' | 'md'
59
+ spacingAfter?: 'none' | 'sm' | 'md'
60
+ trailingLayout: 'responsive' | 'inline' | 'stacked'
61
+ }
62
+
63
+ const props = withDefaults(defineProps<Props>(), {
64
+ eyebrow: undefined,
65
+ description: undefined,
66
+ meta: undefined,
67
+ titlePresentation: 'visible',
68
+ appearance: 'panel',
69
+ variant: 'default',
70
+ divider: false,
71
+ padding: 'none',
72
+ spacingAfter: 'none',
73
+ })
74
+ const slots = useSlots()
75
+ const hasTrailing = computed(() => props.kind === 'page'
76
+ ? Boolean(slots.actions)
77
+ : Boolean(slots.badges || slots.actions))
78
+ </script>
79
+
80
+ <style lang="postcss" src="./SHeaderSurface.css" scoped></style>
@@ -33,33 +33,33 @@
33
33
  }
34
34
 
35
35
  /* Severity: info */
36
- .s-toast[data-severity='info'] {
36
+ .s-toast[data-s-tone='info'] {
37
37
  box-shadow: var(--s-toast-shadow-info);
38
38
  }
39
- .s-toast[data-severity='info'] .s-toast-icon {
39
+ .s-toast[data-s-tone='info'] .s-toast-icon {
40
40
  @apply text-info-500;
41
41
  }
42
42
 
43
43
  /* Severity: success */
44
- .s-toast[data-severity='success'] {
44
+ .s-toast[data-s-tone='success'] {
45
45
  box-shadow: var(--s-toast-shadow-success);
46
46
  }
47
- .s-toast[data-severity='success'] .s-toast-icon {
47
+ .s-toast[data-s-tone='success'] .s-toast-icon {
48
48
  @apply text-success-500;
49
49
  }
50
50
 
51
51
  /* Severity: warn */
52
- .s-toast[data-severity='warn'] {
52
+ .s-toast[data-s-tone='warn'] {
53
53
  box-shadow: var(--s-toast-shadow-warn);
54
54
  }
55
- .s-toast[data-severity='warn'] .s-toast-icon {
55
+ .s-toast[data-s-tone='warn'] .s-toast-icon {
56
56
  @apply text-warn-500;
57
57
  }
58
58
 
59
59
  /* Severity: danger */
60
- .s-toast[data-severity='danger'] {
60
+ .s-toast[data-s-tone='danger'] {
61
61
  box-shadow: var(--s-toast-shadow-danger);
62
62
  }
63
- .s-toast[data-severity='danger'] .s-toast-icon {
63
+ .s-toast[data-s-tone='danger'] .s-toast-icon {
64
64
  @apply text-danger-500;
65
65
  }
@@ -1,7 +1,7 @@
1
1
  <template>
2
2
  <div
3
+ :data-s-tone="toneBinding['data-s-tone']"
3
4
  class="s-toast"
4
- :data-severity="notification.severity"
5
5
  :role="liveAttributes.role"
6
6
  :aria-live="liveAttributes.ariaLive"
7
7
  :aria-atomic="liveAttributes.ariaAtomic"
@@ -40,6 +40,7 @@ import { computed } from 'vue'
40
40
 
41
41
  import { X } from '../../icons/sputnigUiIcons'
42
42
  import { resolveFeedbackLive, resolveFeedbackPresentation } from '../../../internal/feedbackPresentation'
43
+ import { resolveSemanticToneBinding } from '../../../internal/semanticTone'
43
44
  import type { Notification } from '../../../stores/useNotifierStore';
44
45
  import SButton from '../controls/SButton.vue'
45
46
 
@@ -53,6 +54,12 @@ const emit = defineEmits<{
53
54
  dismiss: [id: string]
54
55
  }>()
55
56
  const presentation = computed(() => resolveFeedbackPresentation(props.notification.severity))
57
+ const toneBinding = computed(() => resolveSemanticToneBinding(
58
+ 'SToastItem',
59
+ 'notification.severity',
60
+ props.notification.severity,
61
+ ['info', 'success', 'warn', 'danger'] as const,
62
+ ))
56
63
  const liveAttributes = computed(() => resolveFeedbackLive(props.notification.severity, 'auto'))
57
64
  </script>
58
65
  <style lang="postcss" src="./SToastItem.css" scoped></style>
@@ -0,0 +1,106 @@
1
+ import { computed } from 'vue'
2
+ import type { ComponentPublicInstance, ComputedRef, Ref } from 'vue'
3
+
4
+ import {
5
+ selectionKeyToken,
6
+ useSelectionRoving,
7
+ } from './useSelectionRoving'
8
+ import type {
9
+ SelectionKey,
10
+ SelectionRovingOption,
11
+ } from './useSelectionRoving'
12
+
13
+ /** Опция общего radio-group runtime. / An option accepted by the shared radio-group runtime. */
14
+ export type RadioGroupOption<T extends SelectionKey> = SelectionRovingOption<T>
15
+
16
+ /** ARIA-контракт контейнера radio-group. / ARIA contract for a radio-group container. */
17
+ export interface RadioGroupAriaContract {
18
+ labelledBy: string
19
+ describedBy?: string
20
+ required?: boolean
21
+ invalid?: boolean
22
+ }
23
+
24
+ /** Общий runtime-контракт radio-group без визуального представления. / Shared radio-group runtime without presentation ownership. */
25
+ export interface RadioGroupRuntime<T extends SelectionKey> {
26
+ focusableValue: ComputedRef<T | undefined>
27
+ groupBindings: (contract: RadioGroupAriaContract) => Record<string, unknown>
28
+ optionBindings: (option: RadioGroupOption<T>) => Record<string, unknown>
29
+ optionRef: (value: T) => (element: Element | ComponentPublicInstance | null) => void
30
+ optionToken: (value: T) => string
31
+ }
32
+
33
+ function resolveNativeElement(
34
+ element: Element | ComponentPublicInstance | null,
35
+ ): HTMLElement | null {
36
+ return element instanceof HTMLElement ? element : null
37
+ }
38
+
39
+ /**
40
+ * Централизует identity, ARIA, readonly/disabled и roving-focus radio-group.
41
+ * Centralizes identity, ARIA, readonly/disabled, and roving focus for a radio group.
42
+ */
43
+ export function useRadioGroup<T extends SelectionKey>(config: {
44
+ component: string
45
+ options: ComputedRef<readonly RadioGroupOption<T>[]>
46
+ selectedValue: Ref<T | null | undefined>
47
+ disabled: ComputedRef<boolean>
48
+ readonly: ComputedRef<boolean>
49
+ update: (value: T) => void
50
+ orientation?: 'horizontal' | 'vertical' | 'both'
51
+ }): RadioGroupRuntime<T> {
52
+ const rovingOptions = computed<readonly RadioGroupOption<T>[]>(() => (
53
+ config.disabled.value
54
+ ? config.options.value.map((option) => ({ ...option, disabled: true }))
55
+ : config.options.value
56
+ ))
57
+ const select = (value: T): void => {
58
+ const option = config.options.value.find((candidate) => candidate.value === value)
59
+ if (config.disabled.value || config.readonly.value || option?.disabled) return
60
+ config.update(value)
61
+ }
62
+ const roving = useSelectionRoving({
63
+ options: rovingOptions,
64
+ selectedValue: config.selectedValue,
65
+ orientation: config.orientation,
66
+ select,
67
+ component: config.component,
68
+ })
69
+
70
+ const groupBindings = (contract: RadioGroupAriaContract): Record<string, unknown> => ({
71
+ role: 'radiogroup',
72
+ 'aria-labelledby': contract.labelledBy,
73
+ 'aria-describedby': contract.describedBy,
74
+ 'aria-required': contract.required || undefined,
75
+ 'aria-invalid': contract.invalid || undefined,
76
+ 'aria-disabled': config.disabled.value || undefined,
77
+ 'aria-readonly': config.readonly.value || undefined,
78
+ })
79
+ const optionBindings = (option: RadioGroupOption<T>): Record<string, unknown> => ({
80
+ type: 'button',
81
+ role: 'radio',
82
+ disabled: config.disabled.value || option.disabled,
83
+ 'aria-checked': option.value === config.selectedValue.value,
84
+ tabindex: option.value === roving.focusableValue.value ? 0 : -1,
85
+ onClick: () => select(option.value),
86
+ onKeydown: (event: KeyboardEvent) => roving.handleKeydown(event, option.value),
87
+ })
88
+ const optionRef = (value: T) => (
89
+ element: Element | ComponentPublicInstance | null,
90
+ ): void => {
91
+ roving.register(value, resolveNativeElement(element))
92
+ }
93
+ const optionToken = (value: T): string => selectionKeyToken(
94
+ config.component,
95
+ value,
96
+ 'option.value',
97
+ )
98
+
99
+ return {
100
+ focusableValue: roving.focusableValue,
101
+ groupBindings,
102
+ optionBindings,
103
+ optionRef,
104
+ optionToken,
105
+ }
106
+ }
@@ -1,5 +1,6 @@
1
1
  <script setup lang="ts">
2
2
  import { useOwnedAttrs } from '../../../internal/ownedAttrs'
3
+ import SHeaderSurface from '../_internal/SHeaderSurface.vue'
3
4
 
4
5
  defineOptions({ inheritAttrs: false })
5
6
 
@@ -26,26 +27,18 @@ const ownedAttrs = useOwnedAttrs({ component: 'SPageHeader', owner: 'page header
26
27
  </script>
27
28
 
28
29
  <template>
29
- <header
30
+ <SHeaderSurface
30
31
  v-bind="ownedAttrs.bindings()"
31
- class="s-page-header"
32
- :data-actions-layout="actionsLayout"
33
- :data-appearance="appearance"
34
- :data-size="size"
32
+ kind="page"
33
+ :title="title"
34
+ :description="description"
35
+ :eyebrow="eyebrow"
36
+ :heading-level="headingLevel"
37
+ :appearance="appearance"
38
+ :size="size"
39
+ :trailing-layout="actionsLayout"
40
+ :title-presentation="titlePresentation"
35
41
  >
36
- <div class="s-page-header__copy">
37
- <div v-if="eyebrow" class="s-page-header__eyebrow">{{ eyebrow }}</div>
38
- <component
39
- :is="`h${headingLevel}`"
40
- class="s-page-header__title"
41
- :data-presentation="titlePresentation"
42
- >{{ title }}</component>
43
- <p v-if="description" class="s-page-header__description">{{ description }}</p>
44
- </div>
45
- <div v-if="$slots.actions" class="s-page-header__actions">
46
- <slot name="actions" />
47
- </div>
48
- </header>
42
+ <template v-if="$slots.actions" #actions><slot name="actions" /></template>
43
+ </SHeaderSurface>
49
44
  </template>
50
-
51
- <style lang="postcss" src="./SPageHeader.css" scoped></style>
@@ -78,7 +78,7 @@
78
78
  .s-button[data-icon-only='true'][data-size='md'] { width: var(--s-control-hitbox-md); height: var(--s-control-hitbox-md); }
79
79
  .s-button[data-icon-only='true'][data-size='lg'] { width: var(--s-control-hitbox-lg); height: var(--s-control-hitbox-lg); }
80
80
 
81
- .s-button[data-severity='secondary'] {
81
+ .s-button[data-s-tone='secondary'] {
82
82
  --s-button-accent: var(--s-button-secondary-accent);
83
83
  --s-button-solid-background: var(--s-button-secondary-solid-background);
84
84
  --s-button-solid-background-hover: var(--s-button-secondary-solid-background-hover);
@@ -88,7 +88,7 @@
88
88
  --s-button-outline: var(--color-surface-300);
89
89
  }
90
90
 
91
- .s-button[data-severity='success'] {
91
+ .s-button[data-s-tone='success'] {
92
92
  --s-button-accent: var(--s-button-success-accent);
93
93
  --s-button-solid-background: var(--s-button-success-solid-background);
94
94
  --s-button-solid-background-hover: var(--s-button-success-solid-background-hover);
@@ -97,7 +97,7 @@
97
97
  --s-button-foreground: var(--s-button-success-foreground);
98
98
  }
99
99
 
100
- .s-button[data-severity='info'] {
100
+ .s-button[data-s-tone='info'] {
101
101
  --s-button-accent: var(--s-button-info-accent);
102
102
  --s-button-solid-background: var(--s-button-info-solid-background);
103
103
  --s-button-solid-background-hover: var(--s-button-info-solid-background-hover);
@@ -106,7 +106,7 @@
106
106
  --s-button-foreground: var(--s-button-info-foreground);
107
107
  }
108
108
 
109
- .s-button[data-severity='warn'] {
109
+ .s-button[data-s-tone='warn'] {
110
110
  --s-button-accent: var(--s-button-warn-accent);
111
111
  --s-button-solid-background: var(--s-button-warn-solid-background);
112
112
  --s-button-solid-background-hover: var(--s-button-warn-solid-background-hover);
@@ -115,7 +115,7 @@
115
115
  --s-button-foreground: var(--s-button-warn-foreground);
116
116
  }
117
117
 
118
- .s-button[data-severity='danger'] {
118
+ .s-button[data-s-tone='danger'] {
119
119
  --s-button-accent: var(--s-button-danger-accent);
120
120
  --s-button-solid-background: var(--s-button-danger-solid-background);
121
121
  --s-button-solid-background-hover: var(--s-button-danger-solid-background-hover);
@@ -124,7 +124,7 @@
124
124
  --s-button-foreground: var(--s-button-danger-foreground);
125
125
  }
126
126
 
127
- .s-button[data-severity='contrast'] {
127
+ .s-button[data-s-tone='contrast'] {
128
128
  --s-button-accent: var(--s-button-contrast-accent);
129
129
  --s-button-solid-background: var(--s-button-contrast-solid-background);
130
130
  --s-button-solid-background-hover: var(--s-button-contrast-solid-background-hover);
@@ -3,8 +3,8 @@
3
3
  :is="rootComponent"
4
4
  :ref="setRootRef"
5
5
  v-bind="rootBindings"
6
+ :data-s-tone="toneBinding['data-s-tone']"
6
7
  class="s-button"
7
- :data-severity="severity"
8
8
  :data-size="size"
9
9
  :data-width="width"
10
10
  :data-content-align="contentAlign"
@@ -63,6 +63,7 @@ import { ChevronRight } from '../../icons/sputnigUiIcons'
63
63
  import type { InteractiveElementApi } from '../../../internal/interactiveElement'
64
64
  import { resolveLinkTarget } from '../../../internal/linkTarget'
65
65
  import { useOwnedAttrs } from '../../../internal/ownedAttrs'
66
+ import { resolveSemanticToneBinding } from '../../../internal/semanticTone'
66
67
  import {
67
68
  PassiveContentBoundary,
68
69
  useInteractiveLeafRegistration,
@@ -79,6 +80,7 @@ import type {
79
80
  defineOptions({ inheritAttrs: false })
80
81
 
81
82
  export type SButtonSeverity = 'primary' | 'secondary' | 'success' | 'info' | 'warn' | 'danger' | 'contrast'
83
+ const BUTTON_SEVERITIES = ['primary', 'secondary', 'success', 'info', 'warn', 'danger', 'contrast'] as const satisfies readonly SButtonSeverity[]
82
84
  export type SButtonSize = 'xs' | 'sm' | 'md' | 'lg'
83
85
  export type SButtonIconSize = 'default' | 'large'
84
86
  export type SButtonDensity = 'default' | 'compact'
@@ -186,6 +188,12 @@ const defaultContentComponent = (): ReturnType<NonNullable<typeof buttonSlots.de
186
188
  )
187
189
  const rootRef = ref<HTMLButtonElement | HTMLAnchorElement | null>(null)
188
190
  const ownedAttrs = useOwnedAttrs({ component: 'SButton', owner: 'native button' })
191
+ const toneBinding = computed(() => resolveSemanticToneBinding(
192
+ 'SButton',
193
+ 'severity',
194
+ props.severity,
195
+ BUTTON_SEVERITIES,
196
+ ))
189
197
  const isRegisteredTreeAction = useTreeActionRegistration({
190
198
  owner: 'SButton',
191
199
  element: rootRef,