@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.
- package/README.md +39 -0
- package/docs/public-api.md +6 -0
- package/package.json +5 -1
- package/src/components/shared/_internal/SHeaderSurface.css +98 -0
- package/src/components/shared/_internal/SHeaderSurface.vue +80 -0
- package/src/components/shared/_internal/SToastItem.css +8 -8
- package/src/components/shared/_internal/SToastItem.vue +8 -1
- package/src/components/shared/_internal/useRadioGroup.ts +106 -0
- package/src/components/shared/containers/SPageHeader.vue +13 -20
- package/src/components/shared/controls/SButton.css +6 -6
- package/src/components/shared/controls/SButton.vue +9 -1
- package/src/components/shared/controls/SCheckbox.vue +14 -43
- package/src/components/shared/controls/SChoiceCards.vue +16 -39
- package/src/components/shared/controls/SField.css +0 -12
- package/src/components/shared/controls/SField.vue +14 -10
- package/src/components/shared/controls/SFieldGroup.css +1 -19
- package/src/components/shared/controls/SFieldGroup.vue +13 -10
- package/src/components/shared/controls/SFieldLabel.css +0 -1
- package/src/components/shared/controls/SFieldLabel.vue +2 -1
- package/src/components/shared/controls/SInteractiveSurface.css +3 -3
- package/src/components/shared/controls/SInteractiveSurface.vue +9 -2
- package/src/components/shared/controls/SMarker.css +16 -16
- package/src/components/shared/controls/SMarker.vue +27 -1
- package/src/components/shared/controls/SSegmentedControl.vue +15 -43
- package/src/components/shared/controls/SSwitch.vue +14 -32
- package/src/components/shared/data-display/SActionListItem.css +2 -2
- package/src/components/shared/data-display/SActionListItem.vue +8 -1
- package/src/components/shared/data-display/SBadge.vue +41 -18
- package/src/components/shared/data-display/SChip.vue +22 -10
- package/src/components/shared/data-display/SMessage.css +8 -8
- package/src/components/shared/data-display/SMessage.vue +8 -1
- package/src/components/shared/data-display/SProgressBar.css +12 -12
- package/src/components/shared/data-display/SProgressBar.vue +12 -6
- package/src/components/shared/data-display/SProgressIndicator.css +5 -5
- package/src/components/shared/data-display/SProgressIndicator.vue +9 -1
- package/src/components/shared/data-display/SSectionHeader.vue +17 -21
- package/src/components/shared/data-display/SStatus.css +10 -10
- package/src/components/shared/data-display/SStatus.vue +9 -1
- package/src/components/shared/data-display/STable.vue +27 -4
- package/src/components/shared/data-display/STooltip.css +4 -4
- package/src/components/shared/data-display/STooltip.vue +159 -29
- package/src/components/shared/data-display/STooltipTarget.css +26 -0
- package/src/components/shared/data-display/STooltipTarget.vue +94 -0
- package/src/components/shared/database/SDataGrid.vue +13 -0
- package/src/components/shared/navigation/STabs.vue +11 -3
- package/src/internal/fieldPresentation.ts +28 -0
- package/src/internal/ownedAttrs.ts +3 -0
- package/src/internal/pillSurface.ts +93 -0
- package/src/internal/semanticTone.ts +23 -0
- package/src/internal/useBinaryInput.ts +76 -0
- package/src/styles/style.css +114 -0
- package/src/styles/tokens.css +31 -0
- package/src/components/shared/containers/SPageHeader.css +0 -82
- package/src/components/shared/data-display/SBadge.css +0 -73
- package/src/components/shared/data-display/SChip.css +0 -59
- 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
|
|
package/docs/public-api.md
CHANGED
|
@@ -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.
|
|
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-
|
|
36
|
+
.s-toast[data-s-tone='info'] {
|
|
37
37
|
box-shadow: var(--s-toast-shadow-info);
|
|
38
38
|
}
|
|
39
|
-
.s-toast[data-
|
|
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-
|
|
44
|
+
.s-toast[data-s-tone='success'] {
|
|
45
45
|
box-shadow: var(--s-toast-shadow-success);
|
|
46
46
|
}
|
|
47
|
-
.s-toast[data-
|
|
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-
|
|
52
|
+
.s-toast[data-s-tone='warn'] {
|
|
53
53
|
box-shadow: var(--s-toast-shadow-warn);
|
|
54
54
|
}
|
|
55
|
-
.s-toast[data-
|
|
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-
|
|
60
|
+
.s-toast[data-s-tone='danger'] {
|
|
61
61
|
box-shadow: var(--s-toast-shadow-danger);
|
|
62
62
|
}
|
|
63
|
-
.s-toast[data-
|
|
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
|
-
<
|
|
30
|
+
<SHeaderSurface
|
|
30
31
|
v-bind="ownedAttrs.bindings()"
|
|
31
|
-
|
|
32
|
-
:
|
|
33
|
-
:
|
|
34
|
-
:
|
|
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
|
-
<
|
|
37
|
-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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,
|