@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
|
@@ -255,7 +255,7 @@ export interface STableProps<
|
|
|
255
255
|
</script>
|
|
256
256
|
|
|
257
257
|
<script setup lang="ts" generic="Row extends object = Record<string, unknown>, RowKey extends STableRowKey = STableRowKey, Field extends string = Extract<keyof Row, string>">
|
|
258
|
-
import { computed, useId, useSlots } from 'vue'
|
|
258
|
+
import { computed, useId, useSlots, watch } from 'vue'
|
|
259
259
|
import { ChevronDown, ChevronsUpDown, ChevronUp } from '../../icons/sputnigUiIcons'
|
|
260
260
|
import { hasOwn } from '../../../internal/es2020'
|
|
261
261
|
import { useOwnedAttrs } from '../../../internal/ownedAttrs'
|
|
@@ -548,6 +548,23 @@ const activeSorts = computed<readonly STableSort<Field>[]>(() => {
|
|
|
548
548
|
return singleSort === null ? [] : [singleSort]
|
|
549
549
|
})
|
|
550
550
|
|
|
551
|
+
const NO_PENDING_SORT = Symbol('STable.no-pending-sort')
|
|
552
|
+
let pendingSingleSort: STableSort<Field> | null | typeof NO_PENDING_SORT = NO_PENDING_SORT
|
|
553
|
+
let pendingMultipleSort: readonly STableSort<Field>[] | typeof NO_PENDING_SORT = NO_PENDING_SORT
|
|
554
|
+
|
|
555
|
+
const clearPendingSortIntent = (): void => {
|
|
556
|
+
pendingSingleSort = NO_PENDING_SORT
|
|
557
|
+
pendingMultipleSort = NO_PENDING_SORT
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
watch(validatedSortMode, clearPendingSortIntent, { flush: 'sync' })
|
|
561
|
+
watch(validatedSingleSort, () => {
|
|
562
|
+
pendingSingleSort = NO_PENDING_SORT
|
|
563
|
+
}, { deep: true, flush: 'sync' })
|
|
564
|
+
watch(validatedMultipleSort, () => {
|
|
565
|
+
pendingMultipleSort = NO_PENDING_SORT
|
|
566
|
+
}, { deep: true, flush: 'sync' })
|
|
567
|
+
|
|
551
568
|
function sortIndex(column: STableColumn<Row, Field>): number {
|
|
552
569
|
return activeSorts.value.findIndex((candidate) => candidate.key === column.field)
|
|
553
570
|
}
|
|
@@ -592,8 +609,10 @@ function requestSort(column: STableColumn<Row, Field>): void {
|
|
|
592
609
|
if (!column.sortable) return
|
|
593
610
|
const key = column.field as Field
|
|
594
611
|
if (validatedSortMode.value === 'multiple') {
|
|
595
|
-
const current =
|
|
596
|
-
|
|
612
|
+
const current = pendingMultipleSort === NO_PENDING_SORT
|
|
613
|
+
? activeSorts.value
|
|
614
|
+
: pendingMultipleSort
|
|
615
|
+
const index = current.findIndex((candidate) => candidate.key === key)
|
|
597
616
|
let next: readonly STableSort<Field>[]
|
|
598
617
|
if (index === -1) next = [...current, { key, direction: 'ascending' }]
|
|
599
618
|
else if (current[index]!.direction === 'ascending') {
|
|
@@ -601,15 +620,19 @@ function requestSort(column: STableColumn<Row, Field>): void {
|
|
|
601
620
|
? { key: entry.key, direction: 'descending' }
|
|
602
621
|
: entry)
|
|
603
622
|
} else next = current.filter((_entry, candidateIndex) => candidateIndex !== index)
|
|
623
|
+
pendingMultipleSort = next.map((entry) => ({ ...entry }))
|
|
604
624
|
emit('update:sorts', next)
|
|
605
625
|
emit('multi-sort', next)
|
|
606
626
|
} else {
|
|
607
|
-
const current =
|
|
627
|
+
const current = pendingSingleSort === NO_PENDING_SORT
|
|
628
|
+
? activeSorts.value[0]
|
|
629
|
+
: pendingSingleSort ?? undefined
|
|
608
630
|
const next: STableSort<Field> | null = current?.key !== key
|
|
609
631
|
? { key, direction: 'ascending' }
|
|
610
632
|
: current.direction === 'ascending'
|
|
611
633
|
? { key, direction: 'descending' }
|
|
612
634
|
: null
|
|
635
|
+
pendingSingleSort = next === null ? null : { ...next }
|
|
613
636
|
emit('update:sort', next)
|
|
614
637
|
emit('sort', next)
|
|
615
638
|
}
|
|
@@ -54,19 +54,19 @@
|
|
|
54
54
|
@apply font-mono;
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
.s-tooltip__detail-value[data-tone='info'] {
|
|
57
|
+
.s-tooltip__detail-value[data-s-tone='info'] {
|
|
58
58
|
@apply text-info-700 dark:text-info-300;
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
.s-tooltip__detail-value[data-tone='success'] {
|
|
61
|
+
.s-tooltip__detail-value[data-s-tone='success'] {
|
|
62
62
|
@apply text-success-700 dark:text-success-300;
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
-
.s-tooltip__detail-value[data-tone='warn'] {
|
|
65
|
+
.s-tooltip__detail-value[data-s-tone='warn'] {
|
|
66
66
|
@apply text-warn-700 dark:text-warn-300;
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
.s-tooltip__detail-value[data-tone='danger'] {
|
|
69
|
+
.s-tooltip__detail-value[data-s-tone='danger'] {
|
|
70
70
|
@apply text-danger-700 dark:text-danger-300;
|
|
71
71
|
}
|
|
72
72
|
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<slot
|
|
2
|
+
<slot
|
|
3
|
+
:trigger="triggerBinding"
|
|
4
|
+
:focus-owner="focusOwnerBinding"
|
|
5
|
+
:pointer-target="pointerTargetBinding"
|
|
6
|
+
:is-open="isOpen"
|
|
7
|
+
/>
|
|
3
8
|
<teleport to="body">
|
|
4
9
|
<transition
|
|
5
10
|
enter-active-class="transition-opacity duration-[var(--s-motion-fast)]"
|
|
@@ -27,7 +32,7 @@
|
|
|
27
32
|
<dd
|
|
28
33
|
class="s-tooltip__detail-value"
|
|
29
34
|
:data-presentation="detail.presentation ?? 'text'"
|
|
30
|
-
:data-tone="detail.tone
|
|
35
|
+
:data-s-tone="resolveDetailToneBinding(detail.tone)['data-s-tone']"
|
|
31
36
|
>
|
|
32
37
|
<SBadge
|
|
33
38
|
v-if="detail.presentation === 'badge'"
|
|
@@ -70,6 +75,7 @@ import {
|
|
|
70
75
|
} from 'vue';
|
|
71
76
|
import { mergeIdReferences, useOwnedAttrs } from '../../../internal/ownedAttrs';
|
|
72
77
|
import { hasOwn } from '../../../internal/es2020';
|
|
78
|
+
import { resolveSemanticToneBinding } from '../../../internal/semanticTone';
|
|
73
79
|
import {
|
|
74
80
|
resolveInteractiveElement,
|
|
75
81
|
type ElementRefTarget,
|
|
@@ -127,9 +133,26 @@ export interface STooltipTriggerBinding {
|
|
|
127
133
|
readonly onFocusout: (event: FocusEvent) => void;
|
|
128
134
|
}
|
|
129
135
|
|
|
136
|
+
/** Focus/ARIA owner для split trigger без pointer geometry. / Focus and ARIA owner for a split trigger without pointer geometry. */
|
|
137
|
+
export interface STooltipFocusOwnerBinding {
|
|
138
|
+
readonly ref: VNodeRef;
|
|
139
|
+
readonly 'aria-describedby': string | undefined;
|
|
140
|
+
readonly onFocusin: () => void;
|
|
141
|
+
readonly onFocusout: (event: FocusEvent) => void;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Пассивная pointer/geometry target той же интерактивной композиции. / Passive pointer and geometry target in the same interactive composition. */
|
|
145
|
+
export interface STooltipPointerTargetBinding {
|
|
146
|
+
readonly ref: VNodeRef;
|
|
147
|
+
readonly onPointerenter: (event: PointerEvent) => void;
|
|
148
|
+
readonly onPointerleave: (event: PointerEvent) => void;
|
|
149
|
+
}
|
|
150
|
+
|
|
130
151
|
/** Scoped-slot API явного trigger owner. / Scoped-slot API for the explicit trigger owner. */
|
|
131
152
|
export interface STooltipTriggerSlotProps {
|
|
132
153
|
readonly trigger: STooltipTriggerBinding;
|
|
154
|
+
readonly focusOwner: STooltipFocusOwnerBinding;
|
|
155
|
+
readonly pointerTarget: STooltipPointerTargetBinding;
|
|
133
156
|
readonly isOpen: boolean;
|
|
134
157
|
}
|
|
135
158
|
|
|
@@ -171,8 +194,11 @@ const layerZIndex = ref('var(--s-layer-floating)');
|
|
|
171
194
|
let layerRegistration: LayerRegistration | null = null;
|
|
172
195
|
let hideTimer: number | null = null;
|
|
173
196
|
let showTimer: number | null = null;
|
|
174
|
-
let
|
|
175
|
-
let
|
|
197
|
+
let combinedTarget: ElementRefTarget = null;
|
|
198
|
+
let focusOwnerTarget: ElementRefTarget = null;
|
|
199
|
+
let pointerTarget: ElementRefTarget = null;
|
|
200
|
+
let focusOwnerElement: HTMLElement | null = null;
|
|
201
|
+
let pointerTargetElement: HTMLElement | null = null;
|
|
176
202
|
let pointerInside = false;
|
|
177
203
|
let focusInside = false;
|
|
178
204
|
let touchActive = false;
|
|
@@ -302,9 +328,15 @@ const detailSeverityByTone: Record<TooltipDetailTone, BadgeSeverity> = {
|
|
|
302
328
|
};
|
|
303
329
|
const resolveDetailSeverity = (tone: TooltipDetailTone | undefined): BadgeSeverity =>
|
|
304
330
|
detailSeverityByTone[tone ?? 'neutral'];
|
|
331
|
+
const resolveDetailToneBinding = (tone: TooltipDetailTone | undefined) => resolveSemanticToneBinding(
|
|
332
|
+
'STooltip',
|
|
333
|
+
'detail.tone',
|
|
334
|
+
tone ?? 'neutral',
|
|
335
|
+
['neutral', 'info', 'success', 'warn', 'danger'] as const,
|
|
336
|
+
);
|
|
305
337
|
const placement = computed<FloatingPlacement>(() => `${props.position}-center`);
|
|
306
338
|
const {
|
|
307
|
-
anchorRef:
|
|
339
|
+
anchorRef: positionAnchorRef,
|
|
308
340
|
panelRef: tooltipRef,
|
|
309
341
|
floatingStyle,
|
|
310
342
|
arrowStyle,
|
|
@@ -346,11 +378,11 @@ const closeImmediately = () => {
|
|
|
346
378
|
};
|
|
347
379
|
|
|
348
380
|
const openImmediately = () => {
|
|
349
|
-
if (isOpen.value || !
|
|
381
|
+
if (isOpen.value || !focusOwnerElement || !pointerTargetElement) return;
|
|
350
382
|
layerRegistration = registerLayer({
|
|
351
383
|
kind: 'tooltip',
|
|
352
384
|
root: () => tooltipRef.value,
|
|
353
|
-
anchor: () =>
|
|
385
|
+
anchor: () => positionAnchorRef.value,
|
|
354
386
|
blocksParentFocusTrap: false,
|
|
355
387
|
restoreTarget: null,
|
|
356
388
|
requestDismiss: closeImmediately,
|
|
@@ -360,7 +392,7 @@ const openImmediately = () => {
|
|
|
360
392
|
};
|
|
361
393
|
|
|
362
394
|
const scheduleShow = () => {
|
|
363
|
-
if (props.disabled || !hasContent.value || !
|
|
395
|
+
if (props.disabled || !hasContent.value || !focusOwnerElement || !pointerTargetElement) return;
|
|
364
396
|
clearHideTimer();
|
|
365
397
|
clearShowTimer();
|
|
366
398
|
showTimer = window.setTimeout(() => {
|
|
@@ -412,37 +444,100 @@ const handleFocusIn = () => {
|
|
|
412
444
|
};
|
|
413
445
|
|
|
414
446
|
const handleFocusOut = (event: FocusEvent) => {
|
|
415
|
-
if (event.relatedTarget instanceof Node &&
|
|
447
|
+
if (event.relatedTarget instanceof Node && focusOwnerElement?.contains(event.relatedTarget)) return;
|
|
416
448
|
focusInside = false;
|
|
417
449
|
hideWhenInactive();
|
|
418
450
|
};
|
|
419
451
|
|
|
420
|
-
const
|
|
452
|
+
const validateSplitComposition = (): void => {
|
|
453
|
+
if (!focusOwnerElement || !pointerTargetElement || combinedTarget !== null) return;
|
|
454
|
+
if (focusOwnerElement === pointerTargetElement) {
|
|
455
|
+
throw triggerContractError(
|
|
456
|
+
'focusOwner и pointerTarget должны быть разными владельцами одной интерактивной композиции. '
|
|
457
|
+
+ 'focusOwner and pointerTarget must be distinct owners in one interactive composition.',
|
|
458
|
+
);
|
|
459
|
+
}
|
|
460
|
+
};
|
|
461
|
+
|
|
462
|
+
const commitFocusOwnerElement = (element: HTMLElement | null) => {
|
|
421
463
|
if (element !== null && !isFocusableElement(element)) {
|
|
422
464
|
throw triggerContractError(
|
|
423
|
-
'trigger должен владеть canonical focus owner; используйте интерактивный UI-kit leaf. '
|
|
424
|
-
+ '
|
|
465
|
+
'trigger/focusOwner должен владеть canonical focus owner; используйте интерактивный UI-kit leaf. '
|
|
466
|
+
+ 'trigger/focusOwner must own a canonical focus owner; use an interactive UI-kit leaf.',
|
|
425
467
|
);
|
|
426
468
|
}
|
|
427
|
-
|
|
428
|
-
triggerRef.value = element;
|
|
469
|
+
focusOwnerElement = element;
|
|
429
470
|
pointerInside = false;
|
|
430
471
|
focusInside = false;
|
|
431
472
|
touchActive = false;
|
|
432
473
|
if (element === null) closeImmediately();
|
|
474
|
+
validateSplitComposition();
|
|
433
475
|
};
|
|
434
476
|
|
|
435
|
-
const
|
|
436
|
-
if (
|
|
437
|
-
if (target !== null && triggerTarget !== null) {
|
|
477
|
+
const commitPointerTargetElement = (element: HTMLElement | null) => {
|
|
478
|
+
if (element !== null && combinedTarget === null && isFocusableElement(element)) {
|
|
438
479
|
throw triggerContractError(
|
|
439
|
-
'
|
|
440
|
-
+ '
|
|
480
|
+
'pointerTarget должен быть пассивным и не может создавать дополнительный tab stop. '
|
|
481
|
+
+ 'pointerTarget must be passive and cannot create an additional tab stop.',
|
|
441
482
|
);
|
|
442
483
|
}
|
|
443
|
-
|
|
484
|
+
pointerTargetElement = element;
|
|
485
|
+
positionAnchorRef.value = element;
|
|
486
|
+
pointerInside = false;
|
|
487
|
+
touchActive = false;
|
|
488
|
+
if (element === null) closeImmediately();
|
|
489
|
+
validateSplitComposition();
|
|
490
|
+
};
|
|
491
|
+
|
|
492
|
+
const assertBindingMode = (binding: 'combined' | 'focusOwner' | 'pointerTarget'): void => {
|
|
493
|
+
if (binding === 'combined' && (focusOwnerTarget !== null || pointerTarget !== null)) {
|
|
494
|
+
throw triggerContractError('combined trigger нельзя смешивать со split trigger. combined trigger cannot be mixed with a split trigger.');
|
|
495
|
+
}
|
|
496
|
+
if (binding !== 'combined' && combinedTarget !== null) {
|
|
497
|
+
throw triggerContractError('split trigger нельзя смешивать с combined trigger. A split trigger cannot be mixed with a combined trigger.');
|
|
498
|
+
}
|
|
499
|
+
};
|
|
500
|
+
|
|
501
|
+
const bindCombinedElement: VNodeRef = (target) => {
|
|
502
|
+
if (target === combinedTarget) return;
|
|
503
|
+
if (target !== null) {
|
|
504
|
+
assertBindingMode('combined');
|
|
505
|
+
if (combinedTarget !== null) {
|
|
506
|
+
throw triggerContractError('trigger binding применён более чем к одному leaf. The trigger binding was applied to more than one leaf.');
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
combinedTarget = target;
|
|
444
510
|
if (target === null || target instanceof HTMLElement) {
|
|
445
|
-
|
|
511
|
+
commitFocusOwnerElement(target);
|
|
512
|
+
commitPointerTargetElement(target);
|
|
513
|
+
}
|
|
514
|
+
};
|
|
515
|
+
|
|
516
|
+
const bindFocusOwnerElement: VNodeRef = (target) => {
|
|
517
|
+
if (target === focusOwnerTarget) return;
|
|
518
|
+
if (target !== null) {
|
|
519
|
+
assertBindingMode('focusOwner');
|
|
520
|
+
if (focusOwnerTarget !== null) {
|
|
521
|
+
throw triggerContractError('focusOwner binding применён более чем к одному leaf. focusOwner binding was applied to more than one leaf.');
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
focusOwnerTarget = target;
|
|
525
|
+
if (target === null || target instanceof HTMLElement) {
|
|
526
|
+
commitFocusOwnerElement(target);
|
|
527
|
+
}
|
|
528
|
+
};
|
|
529
|
+
|
|
530
|
+
const bindPointerTargetElement: VNodeRef = (target) => {
|
|
531
|
+
if (target === pointerTarget) return;
|
|
532
|
+
if (target !== null) {
|
|
533
|
+
assertBindingMode('pointerTarget');
|
|
534
|
+
if (pointerTarget !== null) {
|
|
535
|
+
throw triggerContractError('pointerTarget binding применён более чем к одному leaf. pointerTarget binding was applied to more than one leaf.');
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
pointerTarget = target;
|
|
539
|
+
if (target === null || target instanceof HTMLElement) {
|
|
540
|
+
commitPointerTargetElement(target);
|
|
446
541
|
}
|
|
447
542
|
};
|
|
448
543
|
|
|
@@ -467,7 +562,7 @@ const validatedTriggerDescribedBy = computed(() => {
|
|
|
467
562
|
});
|
|
468
563
|
|
|
469
564
|
const triggerBinding = computed<STooltipTriggerBinding>(() => ({
|
|
470
|
-
ref:
|
|
565
|
+
ref: bindCombinedElement,
|
|
471
566
|
'aria-describedby': mergeIdReferences(
|
|
472
567
|
validatedTriggerDescribedBy.value,
|
|
473
568
|
isOpen.value ? tooltipId : undefined,
|
|
@@ -481,6 +576,22 @@ const triggerBinding = computed<STooltipTriggerBinding>(() => ({
|
|
|
481
576
|
onFocusout: handleFocusOut,
|
|
482
577
|
}));
|
|
483
578
|
|
|
579
|
+
const focusOwnerBinding = computed<STooltipFocusOwnerBinding>(() => ({
|
|
580
|
+
ref: bindFocusOwnerElement,
|
|
581
|
+
'aria-describedby': mergeIdReferences(
|
|
582
|
+
validatedTriggerDescribedBy.value,
|
|
583
|
+
isOpen.value ? tooltipId : undefined,
|
|
584
|
+
),
|
|
585
|
+
onFocusin: handleFocusIn,
|
|
586
|
+
onFocusout: handleFocusOut,
|
|
587
|
+
}));
|
|
588
|
+
|
|
589
|
+
const pointerTargetBinding = computed<STooltipPointerTargetBinding>(() => ({
|
|
590
|
+
ref: bindPointerTargetElement,
|
|
591
|
+
onPointerenter: handlePointerEnter,
|
|
592
|
+
onPointerleave: handlePointerLeave,
|
|
593
|
+
}));
|
|
594
|
+
|
|
484
595
|
const handleWindowBlur = () => {
|
|
485
596
|
if (isOpen.value) {
|
|
486
597
|
closeImmediately();
|
|
@@ -530,20 +641,39 @@ watchEffect(() => {
|
|
|
530
641
|
});
|
|
531
642
|
|
|
532
643
|
onMounted(() => {
|
|
533
|
-
if (
|
|
644
|
+
if (combinedTarget !== null && (focusOwnerTarget !== null || pointerTarget !== null)) {
|
|
645
|
+
throw triggerContractError('combined trigger нельзя смешивать со split trigger. combined trigger cannot be mixed with a split trigger.');
|
|
646
|
+
}
|
|
647
|
+
if (combinedTarget === null && (focusOwnerTarget === null || pointerTarget === null)) {
|
|
648
|
+
throw triggerContractError(
|
|
649
|
+
'default slot обязан применить combined trigger либо обе половины split trigger: focusOwner и pointerTarget. '
|
|
650
|
+
+ 'The default slot must apply the combined trigger or both split trigger bindings: focusOwner and pointerTarget.',
|
|
651
|
+
);
|
|
652
|
+
}
|
|
653
|
+
if (combinedTarget !== null) {
|
|
654
|
+
const element = resolveInteractiveElement('STooltip: trigger', combinedTarget);
|
|
655
|
+
commitFocusOwnerElement(element);
|
|
656
|
+
commitPointerTargetElement(element);
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
659
|
+
if (focusOwnerTarget === null || pointerTarget === null) {
|
|
534
660
|
throw triggerContractError(
|
|
535
|
-
'
|
|
536
|
-
+ '
|
|
661
|
+
'split trigger требует focusOwner и pointerTarget. '
|
|
662
|
+
+ 'A split trigger requires focusOwner and pointerTarget.',
|
|
537
663
|
);
|
|
538
664
|
}
|
|
539
|
-
|
|
665
|
+
commitFocusOwnerElement(resolveInteractiveElement('STooltip: focusOwner', focusOwnerTarget));
|
|
666
|
+
commitPointerTargetElement(resolveInteractiveElement('STooltip: pointerTarget', pointerTarget));
|
|
540
667
|
});
|
|
541
668
|
|
|
542
669
|
onBeforeUnmount(() => {
|
|
543
670
|
closeImmediately();
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
671
|
+
combinedTarget = null;
|
|
672
|
+
focusOwnerTarget = null;
|
|
673
|
+
pointerTarget = null;
|
|
674
|
+
focusOwnerElement = null;
|
|
675
|
+
pointerTargetElement = null;
|
|
676
|
+
positionAnchorRef.value = null;
|
|
547
677
|
unbindWindowListeners();
|
|
548
678
|
});
|
|
549
679
|
</script>
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
@reference "../../../styles/reference.css";
|
|
2
|
+
|
|
3
|
+
.s-tooltip-target {
|
|
4
|
+
@apply inline-flex min-w-0 shrink-0 items-center justify-center align-middle;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
.s-tooltip-target[data-appearance='keycap'] {
|
|
8
|
+
@apply border border-surface-300 bg-surface-100 font-mono font-semibold text-surface-700 shadow-sm;
|
|
9
|
+
border-radius: var(--radius-xs);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.s-tooltip-target[data-appearance='keycap'][data-size='xs'] {
|
|
13
|
+
@apply min-h-4 min-w-4 px-1 text-[0.625rem] leading-none;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.s-tooltip-target[data-appearance='keycap'][data-size='sm'] {
|
|
17
|
+
@apply min-h-5 min-w-5 px-1.5 text-xs leading-none;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.s-tooltip-target[data-appearance='keycap'][data-size='md'] {
|
|
21
|
+
@apply min-h-6 min-w-6 px-2 text-sm leading-none;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
:global(.dark .s-tooltip-target[data-appearance='keycap']) {
|
|
25
|
+
@apply border-surface-600 bg-surface-800 text-surface-200;
|
|
26
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<component
|
|
3
|
+
:is="rootTag"
|
|
4
|
+
ref="targetRef"
|
|
5
|
+
v-bind="ownedAttrs.bindings()"
|
|
6
|
+
class="s-tooltip-target"
|
|
7
|
+
:data-appearance="resolvedAppearance"
|
|
8
|
+
:data-size="resolvedSize"
|
|
9
|
+
@pointerenter="emit('pointerenter', $event)"
|
|
10
|
+
@pointerleave="emit('pointerleave', $event)"
|
|
11
|
+
>
|
|
12
|
+
<slot />
|
|
13
|
+
</component>
|
|
14
|
+
</template>
|
|
15
|
+
|
|
16
|
+
<script setup lang="ts">
|
|
17
|
+
import { computed, ref, type VNodeChild } from 'vue'
|
|
18
|
+
import type { ElementApi } from '../../../internal/interactiveElement'
|
|
19
|
+
import { useOwnedAttrs } from '../../../internal/ownedAttrs'
|
|
20
|
+
import { validateExactString } from '../../../internal/runtimeContract'
|
|
21
|
+
|
|
22
|
+
defineOptions({
|
|
23
|
+
name: 'STooltipTarget',
|
|
24
|
+
inheritAttrs: false,
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
export type STooltipTargetTag = 'span' | 'kbd'
|
|
28
|
+
export type STooltipTargetAppearance = 'plain' | 'keycap'
|
|
29
|
+
export type STooltipTargetSize = 'xs' | 'sm' | 'md'
|
|
30
|
+
export type STooltipTargetApi = ElementApi<HTMLElement>
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Пассивная pointer/geometry target для split-контракта STooltip.
|
|
34
|
+
* Passive pointer and geometry target for the STooltip split contract.
|
|
35
|
+
*/
|
|
36
|
+
export interface Props {
|
|
37
|
+
/** Семантический HTML root без собственной интерактивности. / Semantic HTML root without its own interactivity. */
|
|
38
|
+
as?: STooltipTargetTag
|
|
39
|
+
/** Нейтральная или keycap presentation. / Plain or keycap presentation. */
|
|
40
|
+
appearance?: STooltipTargetAppearance
|
|
41
|
+
/** Централизованная visual density target. / Centralized visual density of the target. */
|
|
42
|
+
size?: STooltipTargetSize
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
46
|
+
as: 'span',
|
|
47
|
+
appearance: 'plain',
|
|
48
|
+
size: 'sm',
|
|
49
|
+
})
|
|
50
|
+
const emit = defineEmits<{
|
|
51
|
+
pointerenter: [event: PointerEvent]
|
|
52
|
+
pointerleave: [event: PointerEvent]
|
|
53
|
+
}>()
|
|
54
|
+
const slots = defineSlots<{
|
|
55
|
+
/** Пассивное содержимое target. / Passive target content. */
|
|
56
|
+
default: () => VNodeChild
|
|
57
|
+
}>()
|
|
58
|
+
|
|
59
|
+
if (!slots.default) {
|
|
60
|
+
throw new Error('STooltipTarget: default slot обязателен. / STooltipTarget: the default slot is required.')
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const targetRef = ref<HTMLElement | null>(null)
|
|
64
|
+
const ownedAttrs = useOwnedAttrs({ component: 'STooltipTarget', owner: 'passive tooltip target' })
|
|
65
|
+
const resolvedAs = computed(() => validateExactString(
|
|
66
|
+
'STooltipTarget',
|
|
67
|
+
'as',
|
|
68
|
+
props.as,
|
|
69
|
+
['span', 'kbd'] as const,
|
|
70
|
+
))
|
|
71
|
+
const rootTag = computed(() => resolvedAs.value === 'kbd' ? 'kbd' : 'span')
|
|
72
|
+
const resolvedAppearance = computed(() => validateExactString(
|
|
73
|
+
'STooltipTarget',
|
|
74
|
+
'appearance',
|
|
75
|
+
props.appearance,
|
|
76
|
+
['plain', 'keycap'] as const,
|
|
77
|
+
))
|
|
78
|
+
const resolvedSize = computed(() => validateExactString(
|
|
79
|
+
'STooltipTarget',
|
|
80
|
+
'size',
|
|
81
|
+
props.size,
|
|
82
|
+
['xs', 'sm', 'md'] as const,
|
|
83
|
+
))
|
|
84
|
+
|
|
85
|
+
const getElement = (): HTMLElement => {
|
|
86
|
+
const element = targetRef.value
|
|
87
|
+
if (!element) throw new Error('STooltipTarget: native target element is unavailable')
|
|
88
|
+
return element
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
defineExpose<STooltipTargetApi>({ getElement })
|
|
92
|
+
</script>
|
|
93
|
+
|
|
94
|
+
<style lang="postcss" src="./STooltipTarget.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',
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div
|
|
3
3
|
v-bind="ownedAttrs.bindings()"
|
|
4
|
+
:data-s-tone="toneBinding['data-s-tone']"
|
|
4
5
|
class="s-tabs"
|
|
5
6
|
:data-testid="testId"
|
|
6
7
|
:data-composition="composition"
|
|
@@ -9,7 +10,6 @@
|
|
|
9
10
|
:data-density="tabDensity"
|
|
10
11
|
:data-sizing="tabSizing"
|
|
11
12
|
:data-list-width="tabListWidth"
|
|
12
|
-
:data-tone="tone"
|
|
13
13
|
:data-close-mode="closeButtonMode"
|
|
14
14
|
:data-close-visibility="closeButtonVisibility"
|
|
15
15
|
:data-radius="surfaceRadius"
|
|
@@ -58,6 +58,7 @@ import {
|
|
|
58
58
|
type VNodeChild,
|
|
59
59
|
} from 'vue'
|
|
60
60
|
import { useOwnedAttrs } from '../../../internal/ownedAttrs'
|
|
61
|
+
import { resolveSemanticToneBinding } from '../../../internal/semanticTone'
|
|
61
62
|
import { lastItem } from '../../../internal/es2020'
|
|
62
63
|
import {
|
|
63
64
|
tabsContextKey,
|
|
@@ -80,6 +81,7 @@ export type STabsComposition = 'managed' | 'custom'
|
|
|
80
81
|
export type STabsVariant = 'default' | 'flat' | 'bottom' | 'icon-only' | 'icon-with-label'
|
|
81
82
|
export type STabsActivationMode = 'automatic' | 'manual'
|
|
82
83
|
export type STabsTone = 'primary' | 'success'
|
|
84
|
+
const TABS_TONES = ['primary', 'success'] as const satisfies readonly STabsTone[]
|
|
83
85
|
export type STabsSizing =
|
|
84
86
|
| 'content'
|
|
85
87
|
| 'compact'
|
|
@@ -176,6 +178,12 @@ const slots = defineSlots<{
|
|
|
176
178
|
controls?: () => VNodeChild
|
|
177
179
|
}>()
|
|
178
180
|
const ownedAttrs = useOwnedAttrs({ component: 'STabs', owner: 'tabs state and layout root' })
|
|
181
|
+
const toneBinding = computed(() => resolveSemanticToneBinding(
|
|
182
|
+
'STabs',
|
|
183
|
+
'tone',
|
|
184
|
+
props.tone,
|
|
185
|
+
TABS_TONES,
|
|
186
|
+
))
|
|
179
187
|
|
|
180
188
|
function assertStableKey(value: unknown, coordinate: string): asserts value is string {
|
|
181
189
|
if (typeof value !== 'string' || !value.trim() || value !== value.trim()) {
|
|
@@ -569,12 +577,12 @@ defineExpose({
|
|
|
569
577
|
--s-tab-selected-border-color: var(--color-primary-400);
|
|
570
578
|
--s-tab-disabled-color: var(--color-surface-600);
|
|
571
579
|
}
|
|
572
|
-
.s-tabs[data-tone='success'] {
|
|
580
|
+
.s-tabs[data-s-tone='success'] {
|
|
573
581
|
--s-tab-hover-color: var(--color-success-700);
|
|
574
582
|
--s-tab-selected-color: var(--color-success-700);
|
|
575
583
|
--s-tab-selected-border-color: var(--color-success-600);
|
|
576
584
|
}
|
|
577
|
-
:global(.dark) .s-tabs[data-tone='success'] {
|
|
585
|
+
:global(.dark) .s-tabs[data-s-tone='success'] {
|
|
578
586
|
--s-tab-hover-color: var(--color-success-300);
|
|
579
587
|
--s-tab-selected-color: var(--color-success-300);
|
|
580
588
|
--s-tab-selected-border-color: var(--color-success-400);
|
|
@@ -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
|
+
}
|
|
@@ -294,6 +294,9 @@ export const ownedAttributeSchemas = Object.freeze({
|
|
|
294
294
|
'data-testid': 'data',
|
|
295
295
|
id: 'string',
|
|
296
296
|
}),
|
|
297
|
+
STooltipTarget: defineOwnedAttributeSchema({
|
|
298
|
+
'data-testid': 'data',
|
|
299
|
+
}),
|
|
297
300
|
SVirtualList: defineOwnedAttributeSchema({}),
|
|
298
301
|
SAsyncState: defineOwnedAttributeSchema({}),
|
|
299
302
|
SDataGrid: defineOwnedAttributeSchema({}),
|