@pgcorp/ui-kit 0.5.0 → 0.6.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/package.json +1 -1
- package/src/components/shared/_internal/SHeaderSurface.css +98 -0
- package/src/components/shared/_internal/SHeaderSurface.vue +80 -0
- package/src/components/shared/_internal/useRadioGroup.ts +106 -0
- package/src/components/shared/containers/SPageHeader.vue +13 -20
- 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/SSegmentedControl.vue +15 -43
- package/src/components/shared/controls/SSwitch.vue +14 -32
- 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/SSectionHeader.vue +17 -21
- package/src/components/shared/database/SDataGrid.vue +13 -0
- package/src/internal/fieldPresentation.ts +28 -0
- package/src/internal/pillSurface.ts +92 -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
|
@@ -9,4 +9,3 @@
|
|
|
9
9
|
.s-field-label[data-size='md'] { @apply text-sm; }
|
|
10
10
|
.s-field-label[data-align='end'] { @apply text-right; }
|
|
11
11
|
.s-field-label[data-disabled='true'] { @apply text-surface-400 dark:text-surface-500; }
|
|
12
|
-
.s-field-label__required { @apply ml-0.5 text-danger-600 dark:text-danger-400; }
|
|
@@ -8,12 +8,13 @@
|
|
|
8
8
|
:data-disabled="disabled || undefined"
|
|
9
9
|
>
|
|
10
10
|
<slot />
|
|
11
|
-
<span v-if="required" class="s-field-label__required" aria-hidden="true">*</span>
|
|
11
|
+
<span v-if="required" :class="['s-field-label__required', FIELD_REQUIRED_CLASS]" aria-hidden="true">*</span>
|
|
12
12
|
</label>
|
|
13
13
|
</template>
|
|
14
14
|
|
|
15
15
|
<script setup lang="ts">
|
|
16
16
|
import type { VNodeChild } from 'vue'
|
|
17
|
+
import { FIELD_REQUIRED_CLASS } from '../../../internal/fieldPresentation'
|
|
17
18
|
import { useOwnedAttrs } from '../../../internal/ownedAttrs'
|
|
18
19
|
|
|
19
20
|
defineOptions({ inheritAttrs: false })
|
|
@@ -17,27 +17,21 @@
|
|
|
17
17
|
:data-option-wrap="optionWrap"
|
|
18
18
|
:data-invalid="group.invalid || undefined"
|
|
19
19
|
:data-readonly="group.readonly || undefined"
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
20
|
+
v-bind="radioGroup.groupBindings({
|
|
21
|
+
labelledBy: group.labelId,
|
|
22
|
+
describedBy: group.describedBy,
|
|
23
|
+
required: group.required,
|
|
24
|
+
invalid: group.invalid,
|
|
25
|
+
})"
|
|
26
26
|
>
|
|
27
27
|
<button
|
|
28
28
|
v-for="option in options"
|
|
29
|
-
:key="optionToken(option)"
|
|
30
|
-
:ref="
|
|
29
|
+
:key="radioGroup.optionToken(option.value)"
|
|
30
|
+
:ref="radioGroup.optionRef(option.value)"
|
|
31
31
|
class="s-segmented-control__item"
|
|
32
32
|
:data-size="size"
|
|
33
|
-
type="button"
|
|
34
|
-
:aria-checked="option.value === modelValue"
|
|
35
|
-
:disabled="group.disabled || option.disabled"
|
|
36
|
-
role="radio"
|
|
37
|
-
:tabindex="option.value === focusableValue ? 0 : -1"
|
|
38
33
|
:title="option.title"
|
|
39
|
-
|
|
40
|
-
@keydown="roving.handleKeydown($event, option.value)"
|
|
34
|
+
v-bind="radioGroup.optionBindings(option)"
|
|
41
35
|
>{{ option.label }}</button>
|
|
42
36
|
</div>
|
|
43
37
|
</template>
|
|
@@ -74,10 +68,9 @@ export interface Props<T extends PublicSelectionKey = string> {
|
|
|
74
68
|
|
|
75
69
|
<script setup lang="ts" generic="T extends SelectionKey = string">
|
|
76
70
|
import { computed } from 'vue'
|
|
77
|
-
import type { ComponentPublicInstance } from 'vue'
|
|
78
71
|
import { useOwnedAttrs } from '../../../internal/ownedAttrs'
|
|
79
72
|
import { useInteractiveLeafRegistration } from '../../../internal/passiveContentContract'
|
|
80
|
-
import {
|
|
73
|
+
import { useRadioGroup } from '../_internal/useRadioGroup'
|
|
81
74
|
import type { SelectionKey } from '../_internal/useSelectionRoving'
|
|
82
75
|
import SFieldGroup from './SFieldGroup.vue'
|
|
83
76
|
|
|
@@ -100,35 +93,14 @@ const emit = defineEmits<{
|
|
|
100
93
|
}>()
|
|
101
94
|
const ownedAttrs = useOwnedAttrs({ component: 'SSegmentedControl', owner: 'field group root' })
|
|
102
95
|
useInteractiveLeafRegistration({ owner: 'SSegmentedControl' })
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
if (props.disabled || props.readonly || option.disabled) {
|
|
106
|
-
return
|
|
107
|
-
}
|
|
108
|
-
emit('update:modelValue', option.value)
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
const rovingOptions = computed(() => props.options.map((option) => ({
|
|
112
|
-
value: option.value,
|
|
113
|
-
disabled: props.disabled || option.disabled,
|
|
114
|
-
})))
|
|
115
|
-
const roving = useSelectionRoving({
|
|
116
|
-
options: rovingOptions,
|
|
96
|
+
const radioGroup = useRadioGroup({
|
|
97
|
+
options: computed(() => props.options),
|
|
117
98
|
selectedValue: computed(() => props.modelValue),
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
99
|
+
disabled: computed(() => props.disabled),
|
|
100
|
+
readonly: computed(() => props.readonly),
|
|
101
|
+
update: (value) => emit('update:modelValue', value),
|
|
121
102
|
component: 'SSegmentedControl',
|
|
122
103
|
})
|
|
123
|
-
const focusableValue = roving.focusableValue
|
|
124
|
-
const optionToken = (option: SegmentedControlOption<T>): string => selectionKeyToken(
|
|
125
|
-
'SSegmentedControl',
|
|
126
|
-
option.value,
|
|
127
|
-
'option.value',
|
|
128
|
-
)
|
|
129
|
-
const resolveNativeElement = (element: Element | ComponentPublicInstance | null): HTMLElement | null => (
|
|
130
|
-
element instanceof HTMLElement ? element : null
|
|
131
|
-
)
|
|
132
104
|
</script>
|
|
133
105
|
|
|
134
106
|
<style lang="postcss" src="./SSegmentedControl.css" scoped></style>
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
<template #default="field">
|
|
14
14
|
<span class="s-switch-control">
|
|
15
15
|
<input
|
|
16
|
+
ref="inputEl"
|
|
16
17
|
v-bind="ownedAttrs.bindings()"
|
|
17
18
|
:id="field.controlId"
|
|
18
19
|
type="checkbox"
|
|
@@ -37,10 +38,9 @@
|
|
|
37
38
|
</template>
|
|
38
39
|
|
|
39
40
|
<script setup lang="ts">
|
|
40
|
-
import { computed, useId } from 'vue'
|
|
41
41
|
import { mergeIdReferences, useOwnedAttrs } from '../../../internal/ownedAttrs'
|
|
42
42
|
import { useInteractiveLeafRegistration } from '../../../internal/passiveContentContract'
|
|
43
|
-
import {
|
|
43
|
+
import { useBinaryInput } from '../../../internal/useBinaryInput'
|
|
44
44
|
import SField from './SField.vue'
|
|
45
45
|
|
|
46
46
|
defineOptions({ inheritAttrs: false })
|
|
@@ -73,37 +73,19 @@ const emit = defineEmits<{
|
|
|
73
73
|
}>()
|
|
74
74
|
const ownedAttrs = useOwnedAttrs({ component: 'SSwitch', owner: 'native switch input' })
|
|
75
75
|
useInteractiveLeafRegistration({ owner: 'SSwitch' })
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
return props.modelValue
|
|
76
|
+
const {
|
|
77
|
+
checkedValue,
|
|
78
|
+
inputEl,
|
|
79
|
+
inputId,
|
|
80
|
+
onChange,
|
|
81
|
+
onClick,
|
|
82
|
+
} = useBinaryInput({
|
|
83
|
+
owner: 'SSwitch',
|
|
84
|
+
inputName: 'switch',
|
|
85
|
+
props,
|
|
86
|
+
emitUpdate: (value) => emit('update:modelValue', value),
|
|
87
|
+
emitClick: (event) => emit('click', event),
|
|
89
88
|
})
|
|
90
|
-
|
|
91
|
-
function onChange(event: Event): void {
|
|
92
|
-
if (props.readonly) return
|
|
93
|
-
const target = event.currentTarget
|
|
94
|
-
if (!(target instanceof HTMLInputElement)) {
|
|
95
|
-
throw new TypeError('SSwitch: change event owner must be the native switch input')
|
|
96
|
-
}
|
|
97
|
-
emit('update:modelValue', target.checked)
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function onClick(event: MouseEvent): void {
|
|
101
|
-
if (props.readonly) {
|
|
102
|
-
event.preventDefault()
|
|
103
|
-
return
|
|
104
|
-
}
|
|
105
|
-
emit('click', event)
|
|
106
|
-
}
|
|
107
89
|
</script>
|
|
108
90
|
|
|
109
91
|
<style lang="postcss" src="./SSwitch.css" scoped></style>
|
|
@@ -1,42 +1,56 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<span
|
|
3
|
-
v-bind="ownedAttrs.bindings()"
|
|
3
|
+
v-bind="{ ...ownedAttrs.bindings(), ...surfaceBindings }"
|
|
4
4
|
class="s-badge"
|
|
5
|
-
:class="{ 's-badge--dot': dot }"
|
|
6
|
-
:data-severity="severity"
|
|
7
|
-
:data-size="size"
|
|
8
|
-
:data-max-width="maxWidth"
|
|
9
|
-
:data-truncate="truncate || undefined"
|
|
10
|
-
:data-icon-motion="iconMotion"
|
|
11
5
|
>
|
|
12
6
|
<template v-if="!dot">
|
|
13
|
-
<span v-if="leadingIcon" class="
|
|
14
|
-
<component
|
|
7
|
+
<span v-if="leadingIcon" :class="PILL_ICON_CLASS" aria-hidden="true">
|
|
8
|
+
<component
|
|
9
|
+
:is="leadingIcon"
|
|
10
|
+
:class="[
|
|
11
|
+
PILL_ICON_GRAPHIC_CLASS,
|
|
12
|
+
{ [PILL_ICON_SPIN_CLASS]: iconMotion === 'spin' },
|
|
13
|
+
]"
|
|
14
|
+
/>
|
|
15
|
+
</span>
|
|
16
|
+
<span :class="[PILL_LABEL_CLASS, { [PILL_LABEL_TRUNCATE_CLASS]: truncate }]">
|
|
17
|
+
<slot />
|
|
15
18
|
</span>
|
|
16
|
-
<span class="s-badge__label"><slot /></span>
|
|
17
19
|
</template>
|
|
18
20
|
</span>
|
|
19
21
|
</template>
|
|
20
22
|
|
|
21
23
|
<script setup lang="ts">
|
|
22
|
-
import type
|
|
24
|
+
import { computed, type Component } from 'vue';
|
|
23
25
|
import { useOwnedAttrs } from '../../../internal/ownedAttrs';
|
|
26
|
+
import {
|
|
27
|
+
PILL_ICON_CLASS,
|
|
28
|
+
PILL_ICON_GRAPHIC_CLASS,
|
|
29
|
+
PILL_ICON_SPIN_CLASS,
|
|
30
|
+
PILL_LABEL_CLASS,
|
|
31
|
+
PILL_LABEL_TRUNCATE_CLASS,
|
|
32
|
+
resolvePillSurfaceBindings,
|
|
33
|
+
type PillSurfaceIconMotion,
|
|
34
|
+
type PillSurfaceMaxWidth,
|
|
35
|
+
type PillSurfaceSeverity,
|
|
36
|
+
type PillSurfaceSize,
|
|
37
|
+
} from '../../../internal/pillSurface';
|
|
24
38
|
|
|
25
39
|
defineOptions({ inheritAttrs: false });
|
|
26
40
|
|
|
27
|
-
export type BadgeSeverity =
|
|
41
|
+
export type BadgeSeverity = PillSurfaceSeverity;
|
|
28
42
|
|
|
29
43
|
export interface Props {
|
|
30
44
|
severity?: BadgeSeverity;
|
|
31
45
|
dot?: boolean;
|
|
32
|
-
size?:
|
|
46
|
+
size?: PillSurfaceSize;
|
|
33
47
|
leadingIcon?: Component;
|
|
34
|
-
iconMotion?:
|
|
48
|
+
iconMotion?: PillSurfaceIconMotion;
|
|
35
49
|
truncate?: boolean;
|
|
36
|
-
maxWidth?:
|
|
50
|
+
maxWidth?: PillSurfaceMaxWidth;
|
|
37
51
|
}
|
|
38
52
|
|
|
39
|
-
withDefaults(defineProps<Props>(), {
|
|
53
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
40
54
|
severity: 'secondary',
|
|
41
55
|
dot: false,
|
|
42
56
|
size: 'md',
|
|
@@ -47,6 +61,15 @@ withDefaults(defineProps<Props>(), {
|
|
|
47
61
|
});
|
|
48
62
|
|
|
49
63
|
const ownedAttrs = useOwnedAttrs({ component: 'SBadge', owner: 'badge root' });
|
|
64
|
+
const surfaceBindings = computed(() => resolvePillSurfaceBindings('SBadge', {
|
|
65
|
+
kind: 'badge',
|
|
66
|
+
severity: props.severity,
|
|
67
|
+
size: props.size,
|
|
68
|
+
dot: props.dot,
|
|
69
|
+
closable: false,
|
|
70
|
+
disabled: false,
|
|
71
|
+
truncate: props.truncate,
|
|
72
|
+
maxWidth: props.maxWidth,
|
|
73
|
+
iconMotion: props.iconMotion,
|
|
74
|
+
}));
|
|
50
75
|
</script>
|
|
51
|
-
|
|
52
|
-
<style lang="postcss" src="./SBadge.css" scoped></style>
|
|
@@ -1,16 +1,12 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<span
|
|
3
|
-
v-bind="ownedAttrs.bindings()"
|
|
3
|
+
v-bind="{ ...ownedAttrs.bindings(), ...surfaceBindings }"
|
|
4
4
|
class="s-chip"
|
|
5
|
-
:data-severity="validatedSeverity"
|
|
6
|
-
:data-size="validatedSize"
|
|
7
|
-
:data-closable="closable || undefined"
|
|
8
|
-
:data-disabled="disabled || undefined"
|
|
9
5
|
>
|
|
10
|
-
<span v-if="leadingIcon" class="
|
|
11
|
-
<component :is="leadingIcon" class="
|
|
6
|
+
<span v-if="leadingIcon" :class="PILL_ICON_CLASS" aria-hidden="true">
|
|
7
|
+
<component :is="leadingIcon" :class="PILL_ICON_GRAPHIC_CLASS" />
|
|
12
8
|
</span>
|
|
13
|
-
<span class="
|
|
9
|
+
<span :class="[PILL_LABEL_CLASS, PILL_LABEL_TRUNCATE_CLASS]">{{ validatedLabel }}</span>
|
|
14
10
|
<SButton
|
|
15
11
|
v-if="closable"
|
|
16
12
|
label=""
|
|
@@ -34,6 +30,13 @@
|
|
|
34
30
|
import { computed, type Component } from 'vue'
|
|
35
31
|
import { X } from '../../icons/sputnigUiIcons'
|
|
36
32
|
import { useOwnedAttrs } from '../../../internal/ownedAttrs'
|
|
33
|
+
import {
|
|
34
|
+
PILL_ICON_CLASS,
|
|
35
|
+
PILL_ICON_GRAPHIC_CLASS,
|
|
36
|
+
PILL_LABEL_CLASS,
|
|
37
|
+
PILL_LABEL_TRUNCATE_CLASS,
|
|
38
|
+
resolvePillSurfaceBindings,
|
|
39
|
+
} from '../../../internal/pillSurface'
|
|
37
40
|
import {
|
|
38
41
|
validateBoolean,
|
|
39
42
|
validateExactString,
|
|
@@ -80,6 +83,17 @@ const validatedSeverity = computed(() => validateExactString(
|
|
|
80
83
|
const validatedSize = computed(() => validateExactString(
|
|
81
84
|
'SChip', 'size', props.size, ['sm', 'md'] as const,
|
|
82
85
|
))
|
|
86
|
+
const surfaceBindings = computed(() => resolvePillSurfaceBindings('SChip', {
|
|
87
|
+
kind: 'chip',
|
|
88
|
+
severity: validatedSeverity.value,
|
|
89
|
+
size: validatedSize.value,
|
|
90
|
+
dot: false,
|
|
91
|
+
closable: props.closable,
|
|
92
|
+
disabled: props.disabled,
|
|
93
|
+
truncate: true,
|
|
94
|
+
maxWidth: 'none',
|
|
95
|
+
iconMotion: 'static',
|
|
96
|
+
}))
|
|
83
97
|
const resolvedCloseLabel = computed(() => {
|
|
84
98
|
validateBoolean('SChip', 'closable', props.closable)
|
|
85
99
|
validateBoolean('SChip', 'disabled', props.disabled)
|
|
@@ -88,5 +102,3 @@ const resolvedCloseLabel = computed(() => {
|
|
|
88
102
|
: validateNonEmptyString('SChip', 'closeLabel', props.closeLabel)
|
|
89
103
|
})
|
|
90
104
|
</script>
|
|
91
|
-
|
|
92
|
-
<style lang="postcss" src="./SChip.css" scoped></style>
|
|
@@ -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
|
|
|
@@ -32,27 +33,22 @@ const ownedAttrs = useOwnedAttrs({ component: 'SSectionHeader', owner: 'section
|
|
|
32
33
|
</script>
|
|
33
34
|
|
|
34
35
|
<template>
|
|
35
|
-
<
|
|
36
|
+
<SHeaderSurface
|
|
36
37
|
v-bind="ownedAttrs.bindings()"
|
|
37
|
-
|
|
38
|
-
:
|
|
39
|
-
:
|
|
40
|
-
:
|
|
41
|
-
:
|
|
42
|
-
:
|
|
43
|
-
:
|
|
38
|
+
kind="section"
|
|
39
|
+
:title="title"
|
|
40
|
+
:eyebrow="eyebrow"
|
|
41
|
+
:description="description"
|
|
42
|
+
:meta="meta"
|
|
43
|
+
:heading-level="headingLevel"
|
|
44
|
+
:size="size"
|
|
45
|
+
:variant="variant"
|
|
46
|
+
:divider="divider"
|
|
47
|
+
:padding="padding"
|
|
48
|
+
:spacing-after="spacingAfter"
|
|
49
|
+
:trailing-layout="trailingLayout"
|
|
44
50
|
>
|
|
45
|
-
<
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
<p v-if="description" class="s-section-header__description">{{ description }}</p>
|
|
49
|
-
<p v-if="meta" class="s-section-header__meta">{{ meta }}</p>
|
|
50
|
-
</div>
|
|
51
|
-
<div v-if="$slots.badges || $slots.actions" class="s-section-header__trailing">
|
|
52
|
-
<div v-if="$slots.badges" class="s-section-header__badges"><slot name="badges" /></div>
|
|
53
|
-
<div v-if="$slots.actions" class="s-section-header__actions"><slot name="actions" /></div>
|
|
54
|
-
</div>
|
|
55
|
-
</header>
|
|
51
|
+
<template v-if="$slots.badges" #badges><slot name="badges" /></template>
|
|
52
|
+
<template v-if="$slots.actions" #actions><slot name="actions" /></template>
|
|
53
|
+
</SHeaderSurface>
|
|
56
54
|
</template>
|
|
57
|
-
|
|
58
|
-
<style lang="postcss" src="./SSectionHeader.css" scoped></style>
|
|
@@ -8,6 +8,9 @@
|
|
|
8
8
|
no-content-padding
|
|
9
9
|
content-layout="block"
|
|
10
10
|
surface-overflow="clip"
|
|
11
|
+
:surface-tone="validatedPresentation === 'embedded' ? 'transparent' : 'default'"
|
|
12
|
+
:surface-border="validatedPresentation === 'embedded' ? 'none' : 'default'"
|
|
13
|
+
:surface-radius="validatedPresentation === 'embedded' ? 'none' : 'default'"
|
|
11
14
|
>
|
|
12
15
|
<template v-if="$slots.actions" #actions>
|
|
13
16
|
<slot
|
|
@@ -113,6 +116,8 @@ export interface SDataGridProps<Key extends SDataGridColumnKey = SDataGridColumn
|
|
|
113
116
|
emptyMessage?: string
|
|
114
117
|
selectedRowId?: string
|
|
115
118
|
maxHeight?: ScrollableViewportMaxBlockSize
|
|
119
|
+
/** Surface chrome для самостоятельного или вложенного grid. / Surface chrome for a standalone or embedded grid. */
|
|
120
|
+
presentation?: 'panel' | 'embedded'
|
|
116
121
|
/** Accessible name без visible title. / Accessible name when there is no visible title. */
|
|
117
122
|
ariaLabel?: string
|
|
118
123
|
/** DOM id external visible title. / DOM id of an external visible title. */
|
|
@@ -168,6 +173,7 @@ const props = withDefaults(defineProps<SDataGridProps<ColumnKey>>(), {
|
|
|
168
173
|
emptyMessage: 'Нет строк для отображения',
|
|
169
174
|
selectedRowId: undefined,
|
|
170
175
|
maxHeight: 'none',
|
|
176
|
+
presentation: 'panel',
|
|
171
177
|
ariaLabel: undefined,
|
|
172
178
|
ariaLabelledby: undefined,
|
|
173
179
|
})
|
|
@@ -185,6 +191,7 @@ const CELL_KINDS = ['text', 'number', 'boolean', 'json', 'datetime', 'binary'] a
|
|
|
185
191
|
const COLUMN_ALIGNMENTS = ['left', 'center', 'right'] as const
|
|
186
192
|
const ROW_STATES = ['clean', 'inserted', 'updated', 'deleted'] as const
|
|
187
193
|
const COMMIT_SOURCES = ['draft', 'value'] as const
|
|
194
|
+
const PRESENTATIONS = ['panel', 'embedded'] as const
|
|
188
195
|
|
|
189
196
|
function assertReadonlyArray(value: unknown, coordinate: string): asserts value is readonly unknown[] {
|
|
190
197
|
if (Array.isArray(value)) return
|
|
@@ -335,6 +342,12 @@ const resolvedAccessibleName = computed(() => {
|
|
|
335
342
|
const validatedLoading = computed(() => validateBoolean('SDataGrid', 'loading', props.loading))
|
|
336
343
|
const validatedDisabled = computed(() => validateBoolean('SDataGrid', 'disabled', props.disabled))
|
|
337
344
|
const validatedReadOnly = computed(() => validateBoolean('SDataGrid', 'readOnly', props.readOnly))
|
|
345
|
+
const validatedPresentation = computed(() => validateExactString(
|
|
346
|
+
'SDataGrid',
|
|
347
|
+
'presentation',
|
|
348
|
+
props.presentation,
|
|
349
|
+
PRESENTATIONS,
|
|
350
|
+
))
|
|
338
351
|
const validatedMaxHeight = computed(() => validateScrollableViewportMaxBlockSize(
|
|
339
352
|
'SDataGrid',
|
|
340
353
|
'maxHeight',
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { computed, type ComputedRef } from 'vue'
|
|
2
|
+
|
|
3
|
+
export const FIELD_REQUIRED_CLASS = 'ml-0.5 text-danger-600 dark:text-danger-400'
|
|
4
|
+
export const FIELD_SUPPORT_CLASS = 'mt-1 min-w-0 whitespace-normal text-xs [overflow-wrap:anywhere]'
|
|
5
|
+
export const FIELD_HELP_CLASS = `${FIELD_SUPPORT_CLASS} text-surface-500 dark:text-surface-400`
|
|
6
|
+
export const FIELD_ERROR_CLASS = `${FIELD_SUPPORT_CLASS} text-danger-600 dark:text-danger-400`
|
|
7
|
+
|
|
8
|
+
export interface FieldSupportingTextOptions {
|
|
9
|
+
readonly baseId: ComputedRef<string>
|
|
10
|
+
readonly hasHelp: () => boolean
|
|
11
|
+
readonly hasError: () => boolean
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Владеет exact ID и aria-describedby contract для help/error у field и fieldset.
|
|
16
|
+
* Owns the exact help/error ID and aria-describedby contract for fields and fieldsets.
|
|
17
|
+
*/
|
|
18
|
+
export function useFieldSupportingText(options: FieldSupportingTextOptions) {
|
|
19
|
+
const helpId = computed(() => `${options.baseId.value}-help`)
|
|
20
|
+
const errorId = computed(() => `${options.baseId.value}-error`)
|
|
21
|
+
const describedBy = computed(() => {
|
|
22
|
+
const ids: string[] = []
|
|
23
|
+
if (options.hasHelp()) ids.push(helpId.value)
|
|
24
|
+
if (options.hasError()) ids.push(errorId.value)
|
|
25
|
+
return ids.length > 0 ? ids.join(' ') : undefined
|
|
26
|
+
})
|
|
27
|
+
return { describedBy, errorId, helpId } as const
|
|
28
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import {
|
|
2
|
+
validateBoolean,
|
|
3
|
+
validateExactString,
|
|
4
|
+
} from './runtimeContract'
|
|
5
|
+
|
|
6
|
+
const PILL_KINDS = ['badge', 'chip'] as const
|
|
7
|
+
const PILL_SEVERITIES = [
|
|
8
|
+
'primary',
|
|
9
|
+
'secondary',
|
|
10
|
+
'success',
|
|
11
|
+
'info',
|
|
12
|
+
'warn',
|
|
13
|
+
'danger',
|
|
14
|
+
'contrast',
|
|
15
|
+
] as const
|
|
16
|
+
const PILL_SIZES = ['xs', 'sm', 'md'] as const
|
|
17
|
+
const PILL_MAX_WIDTHS = ['none', 'sm', 'md'] as const
|
|
18
|
+
const PILL_ICON_MOTIONS = ['static', 'spin'] as const
|
|
19
|
+
|
|
20
|
+
export type PillSurfaceKind = typeof PILL_KINDS[number]
|
|
21
|
+
export type PillSurfaceSeverity = typeof PILL_SEVERITIES[number]
|
|
22
|
+
export type PillSurfaceSize = typeof PILL_SIZES[number]
|
|
23
|
+
export type PillSurfaceMaxWidth = typeof PILL_MAX_WIDTHS[number]
|
|
24
|
+
export type PillSurfaceIconMotion = typeof PILL_ICON_MOTIONS[number]
|
|
25
|
+
|
|
26
|
+
export const PILL_ICON_CLASS = 's-pill-icon inline-flex h-[var(--s-pill-icon-size)] w-[var(--s-pill-icon-size)] shrink-0 items-center justify-center'
|
|
27
|
+
export const PILL_ICON_GRAPHIC_CLASS = 's-pill-icon-graphic h-full w-full'
|
|
28
|
+
export const PILL_ICON_SPIN_CLASS = 'animate-spin'
|
|
29
|
+
export const PILL_LABEL_CLASS = 's-pill-label min-w-0'
|
|
30
|
+
export const PILL_LABEL_TRUNCATE_CLASS = 'overflow-hidden text-ellipsis whitespace-nowrap'
|
|
31
|
+
|
|
32
|
+
export interface PillSurfaceContract {
|
|
33
|
+
kind: PillSurfaceKind
|
|
34
|
+
severity: PillSurfaceSeverity
|
|
35
|
+
size: PillSurfaceSize
|
|
36
|
+
dot: boolean
|
|
37
|
+
closable: boolean
|
|
38
|
+
disabled: boolean
|
|
39
|
+
truncate: boolean
|
|
40
|
+
maxWidth: PillSurfaceMaxWidth
|
|
41
|
+
iconMotion: PillSurfaceIconMotion
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface PillSurfaceBindings {
|
|
45
|
+
'data-s-pill-surface': PillSurfaceKind
|
|
46
|
+
'data-s-feedback-tone': PillSurfaceSeverity
|
|
47
|
+
'data-s-pill-size': PillSurfaceSize
|
|
48
|
+
'data-s-pill-dot'?: 'true'
|
|
49
|
+
'data-s-pill-closable'?: 'true'
|
|
50
|
+
'data-s-pill-disabled'?: 'true'
|
|
51
|
+
'data-s-pill-truncate'?: 'true'
|
|
52
|
+
'data-s-pill-max-width'?: Exclude<PillSurfaceMaxWidth, 'none'>
|
|
53
|
+
'data-s-pill-icon-motion'?: Exclude<PillSurfaceIconMotion, 'static'>
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Создаёт exact bindings единого pill/feedback presentation owner.
|
|
58
|
+
* Creates exact bindings for the canonical pill/feedback presentation owner.
|
|
59
|
+
*/
|
|
60
|
+
export function resolvePillSurfaceBindings(
|
|
61
|
+
owner: 'SBadge' | 'SChip',
|
|
62
|
+
contract: PillSurfaceContract,
|
|
63
|
+
): PillSurfaceBindings {
|
|
64
|
+
const kind = validateExactString(owner, 'pill kind', contract.kind, PILL_KINDS)
|
|
65
|
+
const severity = validateExactString(owner, 'severity', contract.severity, PILL_SEVERITIES)
|
|
66
|
+
const size = validateExactString(owner, 'size', contract.size, PILL_SIZES)
|
|
67
|
+
const maxWidth = validateExactString(owner, 'maxWidth', contract.maxWidth, PILL_MAX_WIDTHS)
|
|
68
|
+
const iconMotion = validateExactString(owner, 'iconMotion', contract.iconMotion, PILL_ICON_MOTIONS)
|
|
69
|
+
const dot = validateBoolean(owner, 'dot', contract.dot)
|
|
70
|
+
const closable = validateBoolean(owner, 'closable', contract.closable)
|
|
71
|
+
const disabled = validateBoolean(owner, 'disabled', contract.disabled)
|
|
72
|
+
const truncate = validateBoolean(owner, 'truncate', contract.truncate)
|
|
73
|
+
|
|
74
|
+
if (kind === 'chip' && dot) {
|
|
75
|
+
throw new TypeError('SChip: dot presentation не поддерживается. / SChip: dot presentation is not supported.')
|
|
76
|
+
}
|
|
77
|
+
if (kind === 'badge' && closable) {
|
|
78
|
+
throw new TypeError('SBadge: closable presentation не поддерживается. / SBadge: closable presentation is not supported.')
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
'data-s-pill-surface': kind,
|
|
83
|
+
'data-s-feedback-tone': severity,
|
|
84
|
+
'data-s-pill-size': size,
|
|
85
|
+
...(dot ? { 'data-s-pill-dot': 'true' as const } : {}),
|
|
86
|
+
...(closable ? { 'data-s-pill-closable': 'true' as const } : {}),
|
|
87
|
+
...(disabled ? { 'data-s-pill-disabled': 'true' as const } : {}),
|
|
88
|
+
...(truncate ? { 'data-s-pill-truncate': 'true' as const } : {}),
|
|
89
|
+
...(maxWidth === 'none' ? {} : { 'data-s-pill-max-width': maxWidth }),
|
|
90
|
+
...(iconMotion === 'static' ? {} : { 'data-s-pill-icon-motion': iconMotion }),
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { computed, onMounted, ref, useId, watch } from 'vue'
|
|
2
|
+
|
|
3
|
+
import { resolveOptionalDomId } from './runtimeContract'
|
|
4
|
+
|
|
5
|
+
export type BinaryInputOwner = 'SCheckbox' | 'SSwitch'
|
|
6
|
+
|
|
7
|
+
export interface BinaryInputModelContract {
|
|
8
|
+
readonly modelValue: boolean
|
|
9
|
+
readonly readonly: boolean
|
|
10
|
+
readonly id?: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface BinaryInputOptions {
|
|
14
|
+
readonly owner: BinaryInputOwner
|
|
15
|
+
readonly inputName: 'checkbox' | 'switch'
|
|
16
|
+
readonly props: BinaryInputModelContract
|
|
17
|
+
readonly indeterminate?: () => boolean
|
|
18
|
+
readonly emitUpdate: (value: boolean) => void
|
|
19
|
+
readonly emitClick: (event: MouseEvent) => void
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Единый runtime owner controlled binary input: identity, value, readonly и native events.
|
|
24
|
+
* Canonical runtime owner for controlled binary input identity, value, readonly, and native events.
|
|
25
|
+
*/
|
|
26
|
+
export function useBinaryInput(options: BinaryInputOptions) {
|
|
27
|
+
const generatedInputId = useId()
|
|
28
|
+
const inputId = computed(() => resolveOptionalDomId(
|
|
29
|
+
options.owner,
|
|
30
|
+
'id',
|
|
31
|
+
options.props.id,
|
|
32
|
+
`s-${options.inputName}-${generatedInputId}`,
|
|
33
|
+
))
|
|
34
|
+
const inputEl = ref<HTMLInputElement | null>(null)
|
|
35
|
+
const checkedValue = computed(() => {
|
|
36
|
+
if (typeof options.props.modelValue !== 'boolean') {
|
|
37
|
+
throw new TypeError(`${options.owner}: modelValue must be boolean`)
|
|
38
|
+
}
|
|
39
|
+
return options.props.modelValue
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
const applyIndeterminate = (): void => {
|
|
43
|
+
if (inputEl.value) {
|
|
44
|
+
inputEl.value.indeterminate = (options.indeterminate?.() ?? false) && !checkedValue.value
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
onMounted(applyIndeterminate)
|
|
48
|
+
watch([options.indeterminate ?? (() => false), checkedValue], applyIndeterminate)
|
|
49
|
+
|
|
50
|
+
const onChange = (event: Event): void => {
|
|
51
|
+
if (options.props.readonly) return
|
|
52
|
+
const target = event.currentTarget
|
|
53
|
+
if (!(target instanceof HTMLInputElement)) {
|
|
54
|
+
throw new TypeError(
|
|
55
|
+
`${options.owner}: change event owner must be the native ${options.inputName} input`,
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
options.emitUpdate(target.checked)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const onClick = (event: MouseEvent): void => {
|
|
62
|
+
if (options.props.readonly) {
|
|
63
|
+
event.preventDefault()
|
|
64
|
+
return
|
|
65
|
+
}
|
|
66
|
+
options.emitClick(event)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
checkedValue,
|
|
71
|
+
inputEl,
|
|
72
|
+
inputId,
|
|
73
|
+
onChange,
|
|
74
|
+
onClick,
|
|
75
|
+
} as const
|
|
76
|
+
}
|