@pgcorp/ui-kit 0.8.0 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -0
- package/docs/public-api.md +8 -0
- package/package.json +1 -1
- package/src/components/shared/controls/SInteractiveSurface.css +50 -4
- package/src/components/shared/controls/SInteractiveSurface.vue +44 -30
- package/src/components/shared/data-display/SActionCard.css +6 -2
- package/src/components/shared/data-display/SActionCard.vue +13 -3
- package/src/components/shared/data-display/SActionListItem.css +7 -8
- package/src/components/shared/data-display/SActionListItem.vue +21 -18
- package/src/components/shared/data-display/SChart.vue +2 -2
- package/src/components/shared/data-display/SChip.css +7 -2
- package/src/internal/chartGeometry.ts +126 -22
package/README.md
CHANGED
|
@@ -219,6 +219,18 @@ axes, grid, legend, markers, labels, auto/fixed domains, and the data table. The
|
|
|
219
219
|
component recomputes SVG geometry for its actual container, preserves readable
|
|
220
220
|
typography, and wraps long category labels at narrow widths.
|
|
221
221
|
|
|
222
|
+
Форматированные подписи шкалы Y участвуют в расчёте поля графика. Крайние
|
|
223
|
+
подписи X привязаны к границам plot-area, поэтому `x-labels="auto"` сохраняет
|
|
224
|
+
полные начальное и конечное значения и не обрезает их краем SVG. Если ширины
|
|
225
|
+
контейнера физически недостаточно, визуальная подпись сокращается, а полное
|
|
226
|
+
значение остаётся в tooltip и доступной таблице данных.
|
|
227
|
+
|
|
228
|
+
Formatted Y-axis labels participate in plot-gutter calculation. Endpoint
|
|
229
|
+
X labels are anchored to the plot boundaries, so `x-labels="auto"` preserves
|
|
230
|
+
the full first and last values instead of clipping them at the SVG edge. When
|
|
231
|
+
the container is physically too narrow, the visual label is shortened while
|
|
232
|
+
the full value remains available through the tooltip and accessible data table.
|
|
233
|
+
|
|
222
234
|
```ts
|
|
223
235
|
import type {
|
|
224
236
|
SChartCategory,
|
|
@@ -266,6 +278,24 @@ nested button. `SPanel` uses `header-layout="auto"` to move actions below the
|
|
|
266
278
|
title based on container width; `inline` and `stacked` remain explicit layout
|
|
267
279
|
options.
|
|
268
280
|
|
|
281
|
+
Action/link chip всегда сохраняет видимый интерактивный контур, включая
|
|
282
|
+
`severity="secondary"` на subtle-поверхности; static chip не получает ложного
|
|
283
|
+
affordance. Для строк и карточек с дополнительными controls используйте
|
|
284
|
+
`SActionCard#actions` или `SActionListItem#actions`. `actions-layout`/
|
|
285
|
+
`trailing-layout` задаёт `inline`, `stacked` или container-responsive
|
|
286
|
+
композицию. Основное действие и trailing controls остаются sibling-элементами
|
|
287
|
+
внутри одной визуальной поверхности, без вложенных интерактивных элементов и
|
|
288
|
+
дублирования рамки.
|
|
289
|
+
|
|
290
|
+
Action and link chips always retain a visible interactive outline, including a
|
|
291
|
+
`secondary` chip on a subtle surface; static chips do not gain a false action
|
|
292
|
+
affordance. For rows and cards with additional controls, use
|
|
293
|
+
`SActionCard#actions` or `SActionListItem#actions`. The `actions-layout` and
|
|
294
|
+
`trailing-layout` props select `inline`, `stacked`, or container-responsive
|
|
295
|
+
composition. The primary action and trailing controls remain siblings inside a
|
|
296
|
+
single visual surface, with no nested interactive elements or duplicated
|
|
297
|
+
chrome.
|
|
298
|
+
|
|
269
299
|
### Лёгкий и редактируемый код / Lightweight and editable code
|
|
270
300
|
|
|
271
301
|
`SCodeBlock` загружает CodeMirror отдельным async chunk только для
|
package/docs/public-api.md
CHANGED
|
@@ -33,6 +33,10 @@ responsive geometry, semantic palette и доступную таблицу да
|
|
|
33
33
|
async boundary; `presentation="plain"` является lightweight read-only контрактом
|
|
34
34
|
без загрузки editor runtime. `SChip.surface` владеет action/link семантикой, а
|
|
35
35
|
`SPanel.headerLayout` — inline/stacked/auto компоновкой заголовка и действий.
|
|
36
|
+
`SInteractiveSurface#actions` и использующие его `SActionCard`/
|
|
37
|
+
`SActionListItem` держат primary activation и дополнительные controls
|
|
38
|
+
sibling-элементами внутри одного chrome; `actionsLayout`/`trailingLayout`
|
|
39
|
+
управляют inline, stacked и container-responsive раскладкой.
|
|
36
40
|
|
|
37
41
|
## English
|
|
38
42
|
|
|
@@ -68,3 +72,7 @@ an accessible data table. `SMetricCard` accepts a compact chart through
|
|
|
68
72
|
`presentation="plain"` is the lightweight read-only contract that does not load
|
|
69
73
|
the editor runtime. `SChip.surface` owns action/link semantics, while
|
|
70
74
|
`SPanel.headerLayout` owns inline/stacked/auto title and action composition.
|
|
75
|
+
`SInteractiveSurface#actions` and its `SActionCard`/`SActionListItem`
|
|
76
|
+
compositions keep primary activation and additional controls as siblings inside
|
|
77
|
+
one chrome; `actionsLayout`/`trailingLayout` control inline, stacked, and
|
|
78
|
+
container-responsive layout.
|
package/package.json
CHANGED
|
@@ -14,6 +14,28 @@
|
|
|
14
14
|
color var(--s-motion-standard) ease;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
.s-interactive-surface:has(.s-interactive-surface__actions > *) {
|
|
18
|
+
container-type: inline-size;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.s-interactive-surface__layout {
|
|
22
|
+
@apply grid min-h-0 min-w-0 w-full items-center;
|
|
23
|
+
grid-template-columns: minmax(0, 1fr);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.s-interactive-surface:has(.s-interactive-surface__actions > *) .s-interactive-surface__layout {
|
|
27
|
+
grid-template-columns: minmax(0, 1fr) auto;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.s-interactive-surface__primary {
|
|
31
|
+
@apply relative flex min-h-0 min-w-0;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.s-interactive-surface__actions {
|
|
35
|
+
@apply flex min-w-0 shrink-0 items-center;
|
|
36
|
+
gap: var(--s-space-control-gap);
|
|
37
|
+
}
|
|
38
|
+
|
|
17
39
|
.s-interactive-surface[data-appearance='plain'] {
|
|
18
40
|
@apply border-transparent bg-transparent dark:border-transparent dark:bg-transparent;
|
|
19
41
|
}
|
|
@@ -46,7 +68,9 @@
|
|
|
46
68
|
pointer-events: none;
|
|
47
69
|
}
|
|
48
70
|
|
|
49
|
-
.s-interactive-surface[data-fill='container']
|
|
71
|
+
.s-interactive-surface[data-fill='container'],
|
|
72
|
+
.s-interactive-surface[data-fill='container'] .s-interactive-surface__layout,
|
|
73
|
+
.s-interactive-surface[data-fill='container'] .s-interactive-surface__primary { @apply h-full w-full; }
|
|
50
74
|
.s-interactive-surface[data-density='compact'] { @apply text-sm; }
|
|
51
75
|
.s-interactive-surface[data-density='comfortable'] { @apply text-base; }
|
|
52
76
|
.s-interactive-surface[data-padding='none'] { @apply p-0; }
|
|
@@ -60,9 +84,31 @@
|
|
|
60
84
|
.s-interactive-surface[data-s-tone='danger'] { @apply border-danger-200 bg-danger-50 dark:border-danger-800 dark:bg-danger-950; }
|
|
61
85
|
.s-interactive-surface[data-selected='true'] { @apply border-primary-400 bg-primary-50 dark:border-primary-600 dark:bg-primary-950; }
|
|
62
86
|
.s-interactive-surface[data-pressed='true'] { @apply border-primary-500 bg-primary-100 dark:border-primary-500 dark:bg-primary-950/70; }
|
|
63
|
-
.s-interactive-surface[data-disabled='true'] { @apply cursor-not-allowed
|
|
64
|
-
.s-interactive-surface[data-
|
|
65
|
-
.s-interactive-surface[data-
|
|
87
|
+
.s-interactive-surface[data-disabled='true'] { @apply cursor-not-allowed; }
|
|
88
|
+
.s-interactive-surface[data-disabled='true']:not(:has(.s-interactive-surface__actions > *)),
|
|
89
|
+
.s-interactive-surface[data-disabled='true']:has(.s-interactive-surface__actions > *) .s-interactive-surface__primary {
|
|
90
|
+
@apply opacity-60;
|
|
91
|
+
}
|
|
92
|
+
.s-interactive-surface[data-content-align='start'] .s-interactive-surface__content { @apply justify-start text-left; }
|
|
93
|
+
.s-interactive-surface[data-content-align='center'] .s-interactive-surface__content { @apply justify-center text-center; }
|
|
94
|
+
|
|
95
|
+
.s-interactive-surface[data-actions-layout='stacked']:has(.s-interactive-surface__actions > *) .s-interactive-surface__layout {
|
|
96
|
+
grid-template-columns: minmax(0, 1fr);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.s-interactive-surface[data-actions-layout='stacked'] .s-interactive-surface__actions {
|
|
100
|
+
@apply w-full;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
@container (max-width: 32rem) {
|
|
104
|
+
.s-interactive-surface[data-actions-layout='responsive'] .s-interactive-surface__layout {
|
|
105
|
+
grid-template-columns: minmax(0, 1fr);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.s-interactive-surface[data-actions-layout='responsive'] .s-interactive-surface__actions {
|
|
109
|
+
@apply w-full;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
66
112
|
|
|
67
113
|
.s-interactive-surface[data-mode='action']:not([data-disabled='true']),
|
|
68
114
|
.s-interactive-surface[data-mode='link']:not([data-disabled='true']) {
|
|
@@ -21,6 +21,7 @@ export type InteractiveSurfacePadding = 'none' | 'sm' | 'md'
|
|
|
21
21
|
export type InteractiveSurfaceRadius = 'md' | 'lg' | 'xl'
|
|
22
22
|
export type InteractiveSurfaceFill = 'content' | 'container'
|
|
23
23
|
export type InteractiveSurfaceAppearance = 'card' | 'plain' | 'bare'
|
|
24
|
+
export type InteractiveSurfaceActionsLayout = 'responsive' | 'inline' | 'stacked'
|
|
24
25
|
|
|
25
26
|
/**
|
|
26
27
|
* Единственный semantic-контракт поверхности: passive, action или ровно одна link-target.
|
|
@@ -60,6 +61,7 @@ export interface Props {
|
|
|
60
61
|
radius?: InteractiveSurfaceRadius
|
|
61
62
|
fill?: InteractiveSurfaceFill
|
|
62
63
|
appearance?: InteractiveSurfaceAppearance
|
|
64
|
+
actionsLayout?: InteractiveSurfaceActionsLayout
|
|
63
65
|
contentAlign?: 'start' | 'center'
|
|
64
66
|
/** Полный binding явного STooltip trigger, направляемый на activation owner. / Complete explicit STooltip trigger binding routed to the activation owner. */
|
|
65
67
|
tooltipTrigger?: STooltipTriggerBinding
|
|
@@ -73,6 +75,7 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
73
75
|
radius: 'lg',
|
|
74
76
|
fill: 'content',
|
|
75
77
|
appearance: 'card',
|
|
78
|
+
actionsLayout: 'inline',
|
|
76
79
|
contentAlign: 'start',
|
|
77
80
|
tooltipTrigger: undefined,
|
|
78
81
|
})
|
|
@@ -80,9 +83,11 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
80
83
|
const emit = defineEmits<{
|
|
81
84
|
activate: [event: MouseEvent]
|
|
82
85
|
}>()
|
|
83
|
-
defineSlots<{
|
|
86
|
+
const slots = defineSlots<{
|
|
84
87
|
/** Пассивное flow-содержимое поверхности. / Passive flow content of the surface. */
|
|
85
88
|
default?: () => VNodeChild
|
|
89
|
+
/** Sibling controls inside the same visual chrome and outside the primary activation owner. */
|
|
90
|
+
actions?: () => VNodeChild
|
|
86
91
|
}>()
|
|
87
92
|
|
|
88
93
|
const ownedAttrs = useOwnedAttrs({ component: 'SInteractiveSurface', owner: 'interactive surface owner' })
|
|
@@ -297,40 +302,49 @@ function activateAction(event: MouseEvent): void {
|
|
|
297
302
|
:data-radius="radius"
|
|
298
303
|
:data-fill="fill"
|
|
299
304
|
:data-appearance="appearance"
|
|
305
|
+
:data-actions-layout="actionsLayout"
|
|
306
|
+
:data-has-actions="Boolean(slots.actions) || undefined"
|
|
300
307
|
:data-selected="selected || undefined"
|
|
301
308
|
:data-pressed="surfacePressed"
|
|
302
309
|
:data-disabled="surfaceDisabled || undefined"
|
|
303
310
|
:data-content-align="contentAlign"
|
|
304
311
|
>
|
|
305
|
-
<
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
<
|
|
333
|
-
|
|
312
|
+
<div class="s-interactive-surface__layout">
|
|
313
|
+
<div class="s-interactive-surface__primary">
|
|
314
|
+
<button
|
|
315
|
+
v-if="surfaceMode === 'action'"
|
|
316
|
+
v-bind="activationBindings()"
|
|
317
|
+
class="s-interactive-surface__activation"
|
|
318
|
+
type="button"
|
|
319
|
+
:disabled="surfaceDisabled"
|
|
320
|
+
:aria-label="resolvedAccessibleLabel"
|
|
321
|
+
:aria-pressed="surfacePressed"
|
|
322
|
+
@click="activateAction"
|
|
323
|
+
>
|
|
324
|
+
<span class="s-interactive-surface__accessible-label">{{ resolvedAccessibleLabel }}</span>
|
|
325
|
+
</button>
|
|
326
|
+
<component
|
|
327
|
+
v-else-if="surfaceMode === 'link'"
|
|
328
|
+
:is="linkComponent"
|
|
329
|
+
v-bind="linkBindings()"
|
|
330
|
+
class="s-interactive-surface__activation"
|
|
331
|
+
:aria-label="resolvedAccessibleLabel"
|
|
332
|
+
:aria-current="selected ? 'page' : undefined"
|
|
333
|
+
:aria-disabled="surfaceDisabled || undefined"
|
|
334
|
+
:tabindex="surfaceDisabled ? -1 : undefined"
|
|
335
|
+
@click="activateLink"
|
|
336
|
+
>
|
|
337
|
+
<span class="s-interactive-surface__accessible-label">{{ resolvedAccessibleLabel }}</span>
|
|
338
|
+
</component>
|
|
339
|
+
<div class="s-interactive-surface__content" data-passive-content="">
|
|
340
|
+
<PassiveContentBoundary :contract="passiveContentContract">
|
|
341
|
+
<slot />
|
|
342
|
+
</PassiveContentBoundary>
|
|
343
|
+
</div>
|
|
344
|
+
</div>
|
|
345
|
+
<div v-if="$slots.actions" class="s-interactive-surface__actions">
|
|
346
|
+
<slot name="actions" />
|
|
347
|
+
</div>
|
|
334
348
|
</div>
|
|
335
349
|
</div>
|
|
336
350
|
</template>
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
@reference "../../../styles/reference.css";
|
|
2
2
|
|
|
3
|
-
.s-action-card { @apply
|
|
3
|
+
.s-action-card { @apply min-h-0 min-w-0; }
|
|
4
4
|
.s-action-card[data-fill='container'] { @apply h-full w-full; }
|
|
5
5
|
.s-action-card__content { @apply flex min-h-0 min-w-0 w-full flex-1 flex-col; }
|
|
6
|
-
.s-action-card__actions { @apply flex shrink-0 items-center gap-1; }
|
|
6
|
+
.s-action-card__actions { @apply flex min-w-0 max-w-full shrink-0 items-center gap-1; }
|
|
7
|
+
.s-action-card[data-actions-layout='responsive'] .s-action-card__actions,
|
|
8
|
+
.s-action-card[data-actions-layout='stacked'] .s-action-card__actions {
|
|
9
|
+
@apply w-full;
|
|
10
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import type {
|
|
2
|
+
import type { VNodeChild } from 'vue'
|
|
3
|
+
import type { InteractiveSurfaceActionsLayout, InteractiveSurfaceContract, InteractiveSurfaceDensity, InteractiveSurfaceFill, InteractiveSurfacePadding, InteractiveSurfaceRadius, InteractiveSurfaceTone } from '../controls/SInteractiveSurface.vue'
|
|
3
4
|
import { useOwnedAttrs } from '../../../internal/ownedAttrs'
|
|
4
5
|
import SInteractiveSurface from '../controls/SInteractiveSurface.vue'
|
|
5
6
|
|
|
@@ -13,6 +14,7 @@ export interface Props {
|
|
|
13
14
|
padding?: InteractiveSurfacePadding
|
|
14
15
|
radius?: InteractiveSurfaceRadius
|
|
15
16
|
fill?: InteractiveSurfaceFill
|
|
17
|
+
actionsLayout?: InteractiveSurfaceActionsLayout
|
|
16
18
|
}
|
|
17
19
|
|
|
18
20
|
withDefaults(defineProps<Props>(), {
|
|
@@ -22,14 +24,19 @@ withDefaults(defineProps<Props>(), {
|
|
|
22
24
|
padding: 'md',
|
|
23
25
|
radius: 'lg',
|
|
24
26
|
fill: 'content',
|
|
27
|
+
actionsLayout: 'inline',
|
|
25
28
|
})
|
|
26
29
|
|
|
27
30
|
const emit = defineEmits<{ activate: [event: MouseEvent] }>()
|
|
31
|
+
defineSlots<{
|
|
32
|
+
default?: () => VNodeChild
|
|
33
|
+
actions?: () => VNodeChild
|
|
34
|
+
}>()
|
|
28
35
|
const ownedAttrs = useOwnedAttrs({ component: 'SActionCard', owner: 'action card root' })
|
|
29
36
|
</script>
|
|
30
37
|
|
|
31
38
|
<template>
|
|
32
|
-
<div v-bind="ownedAttrs.bindings()" class="s-action-card" :data-has-actions="Boolean($slots.actions) || undefined" :data-fill="fill" :data-mode="surface.mode">
|
|
39
|
+
<div v-bind="ownedAttrs.bindings()" class="s-action-card" :data-has-actions="Boolean($slots.actions) || undefined" :data-fill="fill" :data-mode="surface.mode" :data-actions-layout="actionsLayout">
|
|
33
40
|
<SInteractiveSurface
|
|
34
41
|
:surface="surface"
|
|
35
42
|
:selected="selected"
|
|
@@ -38,11 +45,14 @@ const ownedAttrs = useOwnedAttrs({ component: 'SActionCard', owner: 'action card
|
|
|
38
45
|
:padding="padding"
|
|
39
46
|
:radius="radius"
|
|
40
47
|
:fill="fill"
|
|
48
|
+
:actions-layout="actionsLayout"
|
|
41
49
|
@activate="emit('activate', $event)"
|
|
42
50
|
>
|
|
43
51
|
<div class="s-action-card__content"><slot /></div>
|
|
52
|
+
<template v-if="$slots.actions" #actions>
|
|
53
|
+
<div class="s-action-card__actions"><slot name="actions" /></div>
|
|
54
|
+
</template>
|
|
44
55
|
</SInteractiveSurface>
|
|
45
|
-
<div v-if="$slots.actions" class="s-action-card__actions"><slot name="actions" /></div>
|
|
46
56
|
</div>
|
|
47
57
|
</template>
|
|
48
58
|
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
@reference "../../../styles/reference.css";
|
|
2
2
|
|
|
3
3
|
.s-action-list-item {
|
|
4
|
-
@apply
|
|
4
|
+
@apply min-h-12 min-w-0;
|
|
5
5
|
height: max-content;
|
|
6
|
-
grid-template-columns: minmax(0, 1fr) auto;
|
|
7
6
|
}
|
|
8
7
|
|
|
9
8
|
.s-action-list-item[data-height-mode='fill'] {
|
|
@@ -94,7 +93,7 @@
|
|
|
94
93
|
.s-action-list-item[data-density='compact'] .s-action-list-item__icon { @apply h-7 w-7; }
|
|
95
94
|
|
|
96
95
|
.s-action-list-item[data-trailing-layout='stacked'] {
|
|
97
|
-
|
|
96
|
+
@apply w-full;
|
|
98
97
|
}
|
|
99
98
|
|
|
100
99
|
.s-action-list-item[data-trailing-layout='stacked'] .s-action-list-item__trailing {
|
|
@@ -103,12 +102,12 @@
|
|
|
103
102
|
}
|
|
104
103
|
|
|
105
104
|
@container (max-width: 32rem) {
|
|
106
|
-
.s-action-list-item[data-trailing-layout='responsive'] {
|
|
107
|
-
grid-template-columns: minmax(0, 1fr);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
105
|
.s-action-list-item[data-trailing-layout='responsive'] .s-action-list-item__trailing {
|
|
111
|
-
@apply w-full justify-end pb-2 pr-2;
|
|
106
|
+
@apply w-full flex-wrap justify-end gap-y-1 pb-2 pr-2;
|
|
112
107
|
padding-left: calc(3.5rem + var(--s-action-list-item-indent));
|
|
113
108
|
}
|
|
109
|
+
|
|
110
|
+
.s-action-list-item[data-trailing-layout='responsive'] .s-action-list-item__trailing > * {
|
|
111
|
+
max-width: 100%;
|
|
112
|
+
}
|
|
114
113
|
}
|
|
@@ -194,6 +194,7 @@ function emitAction(event: MouseEvent): void {
|
|
|
194
194
|
:padding="surfacePadding"
|
|
195
195
|
:radius="surfaceRadius"
|
|
196
196
|
:fill="heightMode === 'fill' ? 'container' : 'content'"
|
|
197
|
+
:actions-layout="trailingLayout"
|
|
197
198
|
@activate="emit('activate', $event)"
|
|
198
199
|
>
|
|
199
200
|
<div class="s-action-list-item__main" :class="{ 's-action-list-item__main--static': resolvedMode === 'static' }">
|
|
@@ -212,25 +213,27 @@ function emitAction(event: MouseEvent): void {
|
|
|
212
213
|
</span>
|
|
213
214
|
</span>
|
|
214
215
|
</div>
|
|
216
|
+
<template v-if="hasTrailing" #actions>
|
|
217
|
+
<div class="s-action-list-item__trailing">
|
|
218
|
+
<slot name="status" />
|
|
219
|
+
<SBadge v-if="statusLabel" :severity="statusSeverity">{{ statusLabel }}</SBadge>
|
|
220
|
+
<SBadge v-for="badge in validatedBadges" :key="badge.id" :severity="badge.severity">
|
|
221
|
+
{{ badge.label }}
|
|
222
|
+
</SBadge>
|
|
223
|
+
<SButton
|
|
224
|
+
v-if="actionLabel"
|
|
225
|
+
size="sm"
|
|
226
|
+
:label="actionLabel"
|
|
227
|
+
:leading-icon="actionIcon"
|
|
228
|
+
:severity="actionSeverity"
|
|
229
|
+
:disabled="surfaceDisabled || actionDisabled"
|
|
230
|
+
@click="emitAction"
|
|
231
|
+
/>
|
|
232
|
+
<slot name="actions" :disabled="surfaceDisabled || actionDisabled" :emit-action="emitAction" />
|
|
233
|
+
<component :is="trailingIcon" v-if="trailingIcon" class="s-action-list-item__trailing-icon" aria-hidden="true" />
|
|
234
|
+
</div>
|
|
235
|
+
</template>
|
|
215
236
|
</SActionCard>
|
|
216
|
-
<div v-if="hasTrailing" class="s-action-list-item__trailing">
|
|
217
|
-
<slot name="status" />
|
|
218
|
-
<SBadge v-if="statusLabel" :severity="statusSeverity">{{ statusLabel }}</SBadge>
|
|
219
|
-
<SBadge v-for="badge in validatedBadges" :key="badge.id" :severity="badge.severity">
|
|
220
|
-
{{ badge.label }}
|
|
221
|
-
</SBadge>
|
|
222
|
-
<SButton
|
|
223
|
-
v-if="actionLabel"
|
|
224
|
-
size="sm"
|
|
225
|
-
:label="actionLabel"
|
|
226
|
-
:leading-icon="actionIcon"
|
|
227
|
-
:severity="actionSeverity"
|
|
228
|
-
:disabled="surfaceDisabled || actionDisabled"
|
|
229
|
-
@click="emitAction"
|
|
230
|
-
/>
|
|
231
|
-
<slot name="actions" :disabled="surfaceDisabled || actionDisabled" :emit-action="emitAction" />
|
|
232
|
-
<component :is="trailingIcon" v-if="trailingIcon" class="s-action-list-item__trailing-icon" aria-hidden="true" />
|
|
233
|
-
</div>
|
|
234
237
|
</div>
|
|
235
238
|
</template>
|
|
236
239
|
|
|
@@ -352,7 +352,7 @@ watchEffect(() => {
|
|
|
352
352
|
:y="tick.position"
|
|
353
353
|
text-anchor="end"
|
|
354
354
|
dominant-baseline="middle"
|
|
355
|
-
>{{ tick.label }}</text>
|
|
355
|
+
>{{ tick.displayLabel }}<title v-if="tick.displayLabel !== tick.label">{{ tick.label }}</title></text>
|
|
356
356
|
<text
|
|
357
357
|
v-if="validatedYAxisLabel"
|
|
358
358
|
class="s-chart__axis-title"
|
|
@@ -369,7 +369,7 @@ watchEffect(() => {
|
|
|
369
369
|
:key="category.key"
|
|
370
370
|
:x="category.x"
|
|
371
371
|
:y="model.plotY + model.plotHeight + 18"
|
|
372
|
-
text-anchor="
|
|
372
|
+
:text-anchor="category.textAnchor"
|
|
373
373
|
>
|
|
374
374
|
<tspan
|
|
375
375
|
v-for="(line, lineIndex) in category.lines"
|
|
@@ -8,12 +8,17 @@
|
|
|
8
8
|
.s-chip[data-mode='action']:not([data-s-pill-disabled='true']),
|
|
9
9
|
.s-chip[data-mode='link']:not([data-s-pill-disabled='true']) {
|
|
10
10
|
@apply cursor-pointer;
|
|
11
|
+
box-shadow: inset 0 0 0 1px color-mix(in srgb, currentColor 42%, transparent);
|
|
11
12
|
transition:
|
|
12
|
-
filter var(--s-motion-standard) ease,
|
|
13
13
|
box-shadow var(--s-motion-standard) ease;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
.s-chip[data-mode='action']:not([data-s-pill-disabled='true']):hover,
|
|
17
17
|
.s-chip[data-mode='link']:not([data-s-pill-disabled='true']):hover {
|
|
18
|
-
|
|
18
|
+
box-shadow: inset 0 0 0 1px color-mix(in srgb, currentColor 68%, transparent);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.s-chip[data-mode='action']:active,
|
|
22
|
+
.s-chip[data-mode='link']:active {
|
|
23
|
+
box-shadow: inset 0 0 0 1px currentColor;
|
|
19
24
|
}
|
|
@@ -79,6 +79,7 @@ export interface ChartLegendItem {
|
|
|
79
79
|
export interface ChartTick {
|
|
80
80
|
readonly value: number
|
|
81
81
|
readonly label: string
|
|
82
|
+
readonly displayLabel: string
|
|
82
83
|
readonly position: number
|
|
83
84
|
}
|
|
84
85
|
|
|
@@ -87,6 +88,7 @@ export interface ChartCategoryLabel {
|
|
|
87
88
|
readonly label: string
|
|
88
89
|
readonly lines: readonly string[]
|
|
89
90
|
readonly x: number
|
|
91
|
+
readonly textAnchor: 'start' | 'middle' | 'end'
|
|
90
92
|
}
|
|
91
93
|
|
|
92
94
|
export interface ChartGridLine {
|
|
@@ -158,6 +160,12 @@ interface PlotRect {
|
|
|
158
160
|
readonly height: number
|
|
159
161
|
}
|
|
160
162
|
|
|
163
|
+
interface ResolvedDomain {
|
|
164
|
+
readonly minimum: number
|
|
165
|
+
readonly maximum: number
|
|
166
|
+
readonly ticks: readonly number[]
|
|
167
|
+
}
|
|
168
|
+
|
|
161
169
|
const SIZE_DIMENSIONS: Readonly<Record<SChartSize, ChartDimensions>> = {
|
|
162
170
|
compact: { width: 360, height: 96 },
|
|
163
171
|
sm: { width: 640, height: 240 },
|
|
@@ -393,6 +401,7 @@ function resolvePlot(
|
|
|
393
401
|
xAxis: boolean,
|
|
394
402
|
yAxis: boolean,
|
|
395
403
|
dataLabels: SChartDataLabels,
|
|
404
|
+
yTickLabels: readonly string[],
|
|
396
405
|
): PlotRect {
|
|
397
406
|
if (type === 'pie' || type === 'donut') {
|
|
398
407
|
const inset = size === 'compact' ? 4 : 12
|
|
@@ -403,7 +412,15 @@ function resolvePlot(
|
|
|
403
412
|
: (size === 'compact' ? 14 : 24)
|
|
404
413
|
const right = size === 'compact' ? 6 : 16
|
|
405
414
|
const bottom = xAxis ? (size === 'compact' ? 32 : 48) : (size === 'compact' ? 5 : 12)
|
|
406
|
-
const
|
|
415
|
+
const defaultLeft = yAxis ? (size === 'compact' ? 34 : 54) : (size === 'compact' ? 6 : 14)
|
|
416
|
+
const tickFontSize = size === 'compact' ? 9 : 11
|
|
417
|
+
const tickWidth = yAxis
|
|
418
|
+
? Math.max(0, ...yTickLabels.map((label) => estimateAxisTextWidth(label, tickFontSize)))
|
|
419
|
+
: 0
|
|
420
|
+
const desiredLeft = yAxis ? Math.max(defaultLeft, Math.ceil(tickWidth + 16)) : defaultLeft
|
|
421
|
+
const minimumPlotWidth = Math.min(size === 'compact' ? 72 : 120, dimensions.width * 0.4)
|
|
422
|
+
const maximumLeft = Math.max(0, dimensions.width - right - minimumPlotWidth)
|
|
423
|
+
const left = Math.min(desiredLeft, maximumLeft)
|
|
407
424
|
return {
|
|
408
425
|
x: left,
|
|
409
426
|
y: top,
|
|
@@ -412,6 +429,28 @@ function resolvePlot(
|
|
|
412
429
|
}
|
|
413
430
|
}
|
|
414
431
|
|
|
432
|
+
function estimateAxisTextWidth(label: string, fontSize: number): number {
|
|
433
|
+
const units = Array.from(label).reduce((total, character) => {
|
|
434
|
+
if (/\s/u.test(character)) return total + 0.36
|
|
435
|
+
if (/[.,:;!|ilI1'`]/u.test(character)) return total + 0.34
|
|
436
|
+
if (/[MW@%ШЩЖЮ]/u.test(character)) return total + 0.92
|
|
437
|
+
return total + 0.62
|
|
438
|
+
}, 0)
|
|
439
|
+
return units * fontSize
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function truncateAxisLabelToWidth(label: string, availableWidth: number, fontSize: number): string {
|
|
443
|
+
if (estimateAxisTextWidth(label, fontSize) <= availableWidth) return label
|
|
444
|
+
const ellipsis = '…'
|
|
445
|
+
const characters = Array.from(label)
|
|
446
|
+
while (characters.length > 0) {
|
|
447
|
+
const candidate = `${characters.join('')}${ellipsis}`
|
|
448
|
+
if (estimateAxisTextWidth(candidate, fontSize) <= availableWidth) return candidate
|
|
449
|
+
characters.pop()
|
|
450
|
+
}
|
|
451
|
+
return ellipsis
|
|
452
|
+
}
|
|
453
|
+
|
|
415
454
|
function truncateAxisWord(word: string, maximumCharacters: number): string {
|
|
416
455
|
if (word.length <= maximumCharacters) return word
|
|
417
456
|
return `${word.slice(0, Math.max(1, maximumCharacters - 1))}…`
|
|
@@ -461,16 +500,9 @@ function cartesianModel(
|
|
|
461
500
|
series: readonly SChartSeries[],
|
|
462
501
|
dimensions: ChartDimensions,
|
|
463
502
|
plot: PlotRect,
|
|
503
|
+
resolvedDomain: ResolvedDomain,
|
|
504
|
+
yTickLabels: readonly string[],
|
|
464
505
|
): Omit<ChartModel, 'empty' | 'emptyReason' | 'slices' | 'centerX' | 'centerY'> {
|
|
465
|
-
const domainValues = input.type === 'bar' && input.barMode === 'stacked'
|
|
466
|
-
? resolveStackedDomainValues(series, categories.length)
|
|
467
|
-
: series.flatMap((item) => [...item.values])
|
|
468
|
-
const resolvedDomain = resolveDomain(
|
|
469
|
-
input.domain,
|
|
470
|
-
domainValues,
|
|
471
|
-
input.type === 'bar' || input.type === 'area',
|
|
472
|
-
input.yTickCount,
|
|
473
|
-
)
|
|
474
506
|
const range = resolvedDomain.maximum - resolvedDomain.minimum
|
|
475
507
|
const yAt = (value: number): number => plot.y + plot.height - ((value - resolvedDomain.minimum) / range) * plot.height
|
|
476
508
|
const baselineY = Math.max(plot.y, Math.min(plot.y + plot.height, yAt(0)))
|
|
@@ -552,17 +584,57 @@ function cartesianModel(
|
|
|
552
584
|
if (input.xLabels === 'endpoints') return index === 0 || index === categories.length - 1
|
|
553
585
|
return categories.length <= 6 || index === 0 || index === categories.length - 1
|
|
554
586
|
}
|
|
555
|
-
const
|
|
556
|
-
|
|
587
|
+
const visibleCategories = categories
|
|
588
|
+
.map((category, index) => ({ category, index, x: xAt(index) }))
|
|
589
|
+
.filter(({ index }) => showXLabel(index))
|
|
590
|
+
const xLabels = visibleCategories.map(({ category, x }, visibleIndex): ChartCategoryLabel => {
|
|
591
|
+
const previous = visibleCategories[visibleIndex - 1]
|
|
592
|
+
const next = visibleCategories[visibleIndex + 1]
|
|
593
|
+
if (visibleCategories.length === 1) {
|
|
594
|
+
return {
|
|
595
|
+
key: category.key,
|
|
596
|
+
label: category.label,
|
|
597
|
+
lines: wrapAxisLabel(category.label, plot.width),
|
|
598
|
+
x: plot.x + plot.width / 2,
|
|
599
|
+
textAnchor: 'middle',
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
if (!previous) {
|
|
603
|
+
const rightBoundary = (x + next!.x) / 2
|
|
604
|
+
return {
|
|
605
|
+
key: category.key,
|
|
606
|
+
label: category.label,
|
|
607
|
+
lines: wrapAxisLabel(category.label, rightBoundary - plot.x),
|
|
608
|
+
x: plot.x,
|
|
609
|
+
textAnchor: 'start',
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
if (!next) {
|
|
613
|
+
const leftBoundary = (previous.x + x) / 2
|
|
614
|
+
return {
|
|
557
615
|
key: category.key,
|
|
558
616
|
label: category.label,
|
|
559
|
-
lines: wrapAxisLabel(category.label,
|
|
560
|
-
x:
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
617
|
+
lines: wrapAxisLabel(category.label, plot.x + plot.width - leftBoundary),
|
|
618
|
+
x: plot.x + plot.width,
|
|
619
|
+
textAnchor: 'end',
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
const leftBoundary = (previous.x + x) / 2
|
|
623
|
+
const rightBoundary = (x + next.x) / 2
|
|
624
|
+
return {
|
|
625
|
+
key: category.key,
|
|
626
|
+
label: category.label,
|
|
627
|
+
lines: wrapAxisLabel(category.label, 2 * Math.min(x - leftBoundary, rightBoundary - x)),
|
|
628
|
+
x,
|
|
629
|
+
textAnchor: 'middle',
|
|
630
|
+
}
|
|
631
|
+
})
|
|
632
|
+
const tickFontSize = input.size === 'compact' ? 9 : 11
|
|
633
|
+
const tickLabelWidth = Math.max(1, plot.x - 16)
|
|
634
|
+
const yTicks = resolvedDomain.ticks.map((value, index) => ({
|
|
564
635
|
value,
|
|
565
|
-
label:
|
|
636
|
+
label: yTickLabels[index]!,
|
|
637
|
+
displayLabel: truncateAxisLabelToWidth(yTickLabels[index]!, tickLabelWidth, tickFontSize),
|
|
566
638
|
position: yAt(value),
|
|
567
639
|
}))
|
|
568
640
|
const horizontalGrid = yTicks.map((tick) => ({
|
|
@@ -725,6 +797,32 @@ export function resolveChartModel(input: ResolveChartModelInput): ChartModel {
|
|
|
725
797
|
centerY: dimensions.height / 2,
|
|
726
798
|
}
|
|
727
799
|
}
|
|
800
|
+
if (input.type === 'pie' || input.type === 'donut') {
|
|
801
|
+
const plot = resolvePlot(
|
|
802
|
+
dimensions,
|
|
803
|
+
input.type,
|
|
804
|
+
input.size,
|
|
805
|
+
input.xAxis,
|
|
806
|
+
input.yAxis,
|
|
807
|
+
input.dataLabels,
|
|
808
|
+
[],
|
|
809
|
+
)
|
|
810
|
+
return radialModel(input, categories, series, dimensions, plot)
|
|
811
|
+
}
|
|
812
|
+
const domainValues = input.type === 'bar' && input.barMode === 'stacked'
|
|
813
|
+
? resolveStackedDomainValues(series, categories.length)
|
|
814
|
+
: series.flatMap((item) => [...item.values])
|
|
815
|
+
const resolvedDomain = resolveDomain(
|
|
816
|
+
input.domain,
|
|
817
|
+
domainValues,
|
|
818
|
+
input.type === 'bar' || input.type === 'area',
|
|
819
|
+
input.yTickCount,
|
|
820
|
+
)
|
|
821
|
+
const yTickLabels = resolvedDomain.ticks.map((value) => formatValue(
|
|
822
|
+
input.valueFormatter,
|
|
823
|
+
value,
|
|
824
|
+
`yTick.${value}`,
|
|
825
|
+
))
|
|
728
826
|
const plot = resolvePlot(
|
|
729
827
|
dimensions,
|
|
730
828
|
input.type,
|
|
@@ -732,11 +830,17 @@ export function resolveChartModel(input: ResolveChartModelInput): ChartModel {
|
|
|
732
830
|
input.xAxis,
|
|
733
831
|
input.yAxis,
|
|
734
832
|
input.dataLabels,
|
|
833
|
+
yTickLabels,
|
|
834
|
+
)
|
|
835
|
+
const model = cartesianModel(
|
|
836
|
+
input,
|
|
837
|
+
categories,
|
|
838
|
+
series,
|
|
839
|
+
dimensions,
|
|
840
|
+
plot,
|
|
841
|
+
resolvedDomain,
|
|
842
|
+
yTickLabels,
|
|
735
843
|
)
|
|
736
|
-
if (input.type === 'pie' || input.type === 'donut') {
|
|
737
|
-
return radialModel(input, categories, series, dimensions, plot)
|
|
738
|
-
}
|
|
739
|
-
const model = cartesianModel(input, categories, series, dimensions, plot)
|
|
740
844
|
return {
|
|
741
845
|
...model,
|
|
742
846
|
empty: false,
|