@pgcorp/ui-kit 0.7.2 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/README.md +135 -8
  2. package/docs/accessibility.md +12 -0
  3. package/docs/getting-started.md +32 -2
  4. package/docs/public-api.md +19 -2
  5. package/docs/theming.md +16 -0
  6. package/package.json +18 -3
  7. package/src/components/shared/codeLanguages.ts +2 -107
  8. package/src/components/shared/containers/SPanel.css +32 -1
  9. package/src/components/shared/containers/SPanel.vue +4 -0
  10. package/src/components/shared/controls/SCheckbox.vue +2 -2
  11. package/src/components/shared/controls/SInteractiveSurface.css +4 -0
  12. package/src/components/shared/controls/SInteractiveSurface.vue +1 -1
  13. package/src/components/shared/controls/SListbox.vue +4 -4
  14. package/src/components/shared/controls/SSwitch.vue +2 -2
  15. package/src/components/shared/data-display/SChart.css +149 -0
  16. package/src/components/shared/data-display/SChart.vue +493 -0
  17. package/src/components/shared/data-display/SChip.css +19 -0
  18. package/src/components/shared/data-display/SChip.vue +30 -6
  19. package/src/components/shared/data-display/SCodeBlock.css +68 -0
  20. package/src/components/shared/data-display/SCodeBlock.vue +102 -16
  21. package/src/components/shared/data-display/SMetricCard.css +1 -0
  22. package/src/components/shared/data-display/SMetricCard.vue +1 -0
  23. package/src/components/shared/data-display/STable.vue +120 -145
  24. package/src/components/shared/data-display/chart.ts +54 -0
  25. package/src/components/shared/navigation/STabList.vue +0 -3
  26. package/src/internal/chartGeometry.ts +748 -0
  27. package/src/internal/codeLanguageIdentity.ts +89 -0
  28. package/src/internal/lazyCodeEditor.ts +1 -0
  29. package/src/internal/linkTarget.ts +1 -3
  30. package/src/internal/ownedAttrs.ts +1 -0
  31. package/src/internal/useBinaryInput.ts +9 -2
  32. package/src/styles/tailwind.css +3 -0
  33. package/src/styles/tokens.css +33 -0
@@ -0,0 +1,493 @@
1
+ <script setup lang="ts">
2
+ import { computed, onBeforeUnmount, onMounted, ref, shallowRef, watchEffect } from 'vue'
3
+ import { useOwnedAttrs } from '../../../internal/ownedAttrs'
4
+ import { resolveChartModel } from '../../../internal/chartGeometry'
5
+ import type { ChartDimensions } from '../../../internal/chartGeometry'
6
+ import {
7
+ validateBoundedInteger,
8
+ validateDomId,
9
+ validateExactString,
10
+ validateNonEmptyString,
11
+ } from '../../../internal/runtimeContract'
12
+ import SEmptyState from './SEmptyState.vue'
13
+ import type {
14
+ SChartAccessibility,
15
+ SChartBarMode,
16
+ SChartCategory,
17
+ SChartCurve,
18
+ SChartDataLabels,
19
+ SChartDataTablePresentation,
20
+ SChartDomain,
21
+ SChartGrid,
22
+ SChartLegend,
23
+ SChartMarkers,
24
+ SChartRadialLabel,
25
+ SChartSeries,
26
+ SChartSize,
27
+ SChartType,
28
+ SChartXLabels,
29
+ } from './chart'
30
+
31
+ defineOptions({ inheritAttrs: false })
32
+
33
+ export interface Props {
34
+ type: SChartType
35
+ categories: readonly SChartCategory[]
36
+ series: readonly SChartSeries[]
37
+ accessibility: SChartAccessibility
38
+ size?: SChartSize
39
+ domain?: SChartDomain
40
+ xAxis?: boolean
41
+ yAxis?: boolean
42
+ xAxisLabel?: string
43
+ yAxisLabel?: string
44
+ xLabels?: SChartXLabels
45
+ yTickCount?: number
46
+ grid?: SChartGrid
47
+ legend?: SChartLegend
48
+ dataLabels?: SChartDataLabels
49
+ markers?: SChartMarkers
50
+ curve?: SChartCurve
51
+ barMode?: SChartBarMode
52
+ radialLabel?: SChartRadialLabel
53
+ centerLabel?: string
54
+ centerValue?: string
55
+ dataTablePresentation?: SChartDataTablePresentation
56
+ emptyLabel?: string
57
+ valueFormatter?: (value: number) => string
58
+ }
59
+
60
+ const props = withDefaults(defineProps<Props>(), {
61
+ size: 'md',
62
+ domain: () => ({ mode: 'auto' }),
63
+ xAxis: undefined,
64
+ yAxis: undefined,
65
+ xAxisLabel: undefined,
66
+ yAxisLabel: undefined,
67
+ xLabels: undefined,
68
+ yTickCount: 5,
69
+ grid: undefined,
70
+ legend: undefined,
71
+ dataLabels: 'none',
72
+ markers: undefined,
73
+ curve: 'smooth',
74
+ barMode: 'grouped',
75
+ radialLabel: 'percentage',
76
+ centerLabel: undefined,
77
+ centerValue: undefined,
78
+ dataTablePresentation: 'assistive',
79
+ emptyLabel: 'Нет данных для графика',
80
+ valueFormatter: undefined,
81
+ })
82
+
83
+ const ownedAttrs = useOwnedAttrs({ component: 'SChart', owner: 'chart root' })
84
+ const viewportElement = ref<HTMLElement | null>(null)
85
+ const viewportDimensions = shallowRef<ChartDimensions | undefined>(undefined)
86
+ let resizeObserver: ResizeObserver | undefined
87
+ const numberFormatter = new Intl.NumberFormat(undefined, { maximumFractionDigits: 2 })
88
+ const validatedType = computed(() => validateExactString(
89
+ 'SChart', 'type', props.type, ['line', 'area', 'bar', 'pie', 'donut'] as const,
90
+ ))
91
+ const validatedSize = computed(() => validateExactString(
92
+ 'SChart', 'size', props.size, ['compact', 'sm', 'md', 'lg'] as const,
93
+ ))
94
+ const validatedXLabels = computed(() => validateExactString(
95
+ 'SChart',
96
+ 'xLabels',
97
+ props.xLabels ?? (props.size === 'compact' ? 'none' : 'auto'),
98
+ ['none', 'endpoints', 'auto', 'all'] as const,
99
+ ))
100
+ const validatedGrid = computed(() => validateExactString(
101
+ 'SChart',
102
+ 'grid',
103
+ props.grid ?? (props.size === 'compact' || isRadial.value ? 'none' : 'horizontal'),
104
+ ['none', 'horizontal', 'vertical', 'both'] as const,
105
+ ))
106
+ const validatedLegend = computed(() => validateExactString(
107
+ 'SChart',
108
+ 'legend',
109
+ props.legend ?? (props.size === 'compact' || (!isRadial.value && props.series.length <= 1) ? 'none' : 'bottom'),
110
+ ['none', 'top', 'bottom'] as const,
111
+ ))
112
+ const validatedDataLabels = computed(() => validateExactString(
113
+ 'SChart', 'dataLabels', props.dataLabels, ['none', 'latest', 'all'] as const,
114
+ ))
115
+ const validatedMarkers = computed(() => validateExactString(
116
+ 'SChart',
117
+ 'markers',
118
+ props.markers ?? (props.type === 'line' || props.type === 'area' ? 'latest' : 'none'),
119
+ ['none', 'latest', 'all'] as const,
120
+ ))
121
+ const validatedCurve = computed(() => validateExactString(
122
+ 'SChart', 'curve', props.curve, ['linear', 'smooth'] as const,
123
+ ))
124
+ const validatedBarMode = computed(() => validateExactString(
125
+ 'SChart', 'barMode', props.barMode, ['grouped', 'stacked'] as const,
126
+ ))
127
+ const validatedRadialLabel = computed(() => validateExactString(
128
+ 'SChart', 'radialLabel', props.radialLabel, ['value', 'percentage', 'category'] as const,
129
+ ))
130
+ const validatedTablePresentation = computed(() => validateExactString(
131
+ 'SChart',
132
+ 'dataTablePresentation',
133
+ props.dataTablePresentation,
134
+ ['assistive', 'visible', 'none'] as const,
135
+ ))
136
+ const validatedYTickCount = computed(() => validateBoundedInteger(
137
+ 'SChart', 'yTickCount', props.yTickCount, { minimum: 2, maximum: 8 },
138
+ ))
139
+ const isRadial = computed(() => validatedType.value === 'pie' || validatedType.value === 'donut')
140
+ const resolvedXAxis = computed(() => props.xAxis ?? (!isRadial.value && props.size !== 'compact'))
141
+ const resolvedYAxis = computed(() => props.yAxis ?? (!isRadial.value && props.size !== 'compact'))
142
+ const resolvedValueFormatter = computed(() => {
143
+ if (props.valueFormatter === undefined) return (value: number): string => numberFormatter.format(value)
144
+ if (typeof props.valueFormatter !== 'function') {
145
+ throw new TypeError('SChart: valueFormatter должен быть function')
146
+ }
147
+ return props.valueFormatter
148
+ })
149
+ const validatedEmptyLabel = computed(() => validateNonEmptyString(
150
+ 'SChart', 'emptyLabel', props.emptyLabel,
151
+ ))
152
+ const validatedXAxisLabel = computed(() => props.xAxisLabel === undefined
153
+ ? undefined
154
+ : validateNonEmptyString('SChart', 'xAxisLabel', props.xAxisLabel))
155
+ const validatedYAxisLabel = computed(() => props.yAxisLabel === undefined
156
+ ? undefined
157
+ : validateNonEmptyString('SChart', 'yAxisLabel', props.yAxisLabel))
158
+ const validatedCenterLabel = computed(() => props.centerLabel === undefined
159
+ ? undefined
160
+ : validateNonEmptyString('SChart', 'centerLabel', props.centerLabel))
161
+ const validatedCenterValue = computed(() => props.centerValue === undefined
162
+ ? undefined
163
+ : validateNonEmptyString('SChart', 'centerValue', props.centerValue))
164
+
165
+ const accessibility = computed<SChartAccessibility>(() => {
166
+ const candidate: unknown = props.accessibility
167
+ if (typeof candidate !== 'object' || candidate === null || Array.isArray(candidate)) {
168
+ throw new TypeError('SChart: accessibility должен быть discriminated object')
169
+ }
170
+ const record = candidate as Record<string, unknown>
171
+ const allowed = record.mode === 'label'
172
+ ? ['mode', 'label', 'description']
173
+ : record.mode === 'labelledby'
174
+ ? ['mode', 'id', 'describedby']
175
+ : ['mode']
176
+ const unexpected = Object.keys(record).filter((key) => !allowed.includes(key))
177
+ if (unexpected.length > 0) {
178
+ throw new Error(`SChart: accessibility.mode="${String(record.mode)}" не допускает поля ${unexpected.join(', ')}`)
179
+ }
180
+ if (record.mode === 'decorative') return { mode: 'decorative' }
181
+ if (record.mode === 'label') {
182
+ return {
183
+ mode: 'label',
184
+ label: validateNonEmptyString('SChart', 'accessibility.label', record.label),
185
+ description: record.description === undefined
186
+ ? undefined
187
+ : validateNonEmptyString('SChart', 'accessibility.description', record.description),
188
+ }
189
+ }
190
+ if (record.mode === 'labelledby') {
191
+ return {
192
+ mode: 'labelledby',
193
+ id: validateDomId('SChart', 'accessibility.id', record.id),
194
+ describedby: record.describedby === undefined
195
+ ? undefined
196
+ : validateDomId('SChart', 'accessibility.describedby', record.describedby),
197
+ }
198
+ }
199
+ throw new TypeError(`SChart: неизвестный accessibility.mode "${String(record.mode)}"`)
200
+ })
201
+
202
+ const model = computed(() => resolveChartModel({
203
+ type: validatedType.value,
204
+ size: validatedSize.value,
205
+ categories: props.categories,
206
+ series: props.series,
207
+ domain: props.domain,
208
+ xAxis: resolvedXAxis.value,
209
+ yAxis: resolvedYAxis.value,
210
+ xLabels: validatedXLabels.value,
211
+ yTickCount: validatedYTickCount.value,
212
+ curve: validatedCurve.value,
213
+ barMode: validatedBarMode.value,
214
+ dataLabels: validatedDataLabels.value,
215
+ markers: validatedMarkers.value,
216
+ radialLabel: validatedRadialLabel.value,
217
+ valueFormatter: resolvedValueFormatter.value,
218
+ dimensions: viewportDimensions.value,
219
+ }))
220
+
221
+ const showHorizontalGrid = computed(() => validatedGrid.value === 'horizontal' || validatedGrid.value === 'both')
222
+ const showVerticalGrid = computed(() => validatedGrid.value === 'vertical' || validatedGrid.value === 'both')
223
+ const showTable = computed(() => validatedTablePresentation.value !== 'none' && !model.value.empty)
224
+ const tableClass = computed(() => validatedTablePresentation.value === 'assistive' ? 's-chart__data-table--assistive' : undefined)
225
+ const emptyTitle = computed(() => model.value.emptyReason === 'zero-total'
226
+ ? 'Нет ненулевых значений для графика'
227
+ : validatedEmptyLabel.value)
228
+ const accessibleLabel = computed(() => accessibility.value.mode === 'label' ? accessibility.value.label : undefined)
229
+ const accessibleDescription = computed(() => accessibility.value.mode === 'label' ? accessibility.value.description : undefined)
230
+
231
+ function showPointMarker(index: number, count: number): boolean {
232
+ return validatedMarkers.value === 'all'
233
+ || (validatedMarkers.value === 'latest' && index === count - 1)
234
+ }
235
+
236
+ function showPointLabel(index: number, count: number): boolean {
237
+ return validatedDataLabels.value === 'all'
238
+ || (validatedDataLabels.value === 'latest' && index === count - 1)
239
+ }
240
+
241
+ function showBarLabel(categoryKey: string): boolean {
242
+ const lastCategory = props.categories[props.categories.length - 1]
243
+ return validatedDataLabels.value === 'all'
244
+ || (validatedDataLabels.value === 'latest' && categoryKey === lastCategory?.key)
245
+ }
246
+
247
+ onMounted(() => {
248
+ if (viewportElement.value === null || typeof ResizeObserver === 'undefined') return
249
+ resizeObserver = new ResizeObserver(([entry]) => {
250
+ if (!entry || entry.contentRect.width <= 0 || entry.contentRect.height <= 0) return
251
+ const next = { width: entry.contentRect.width, height: entry.contentRect.height }
252
+ const current = viewportDimensions.value
253
+ if (current && Math.abs(current.width - next.width) < 0.5 && Math.abs(current.height - next.height) < 0.5) return
254
+ viewportDimensions.value = next
255
+ })
256
+ resizeObserver.observe(viewportElement.value)
257
+ })
258
+
259
+ onBeforeUnmount(() => resizeObserver?.disconnect())
260
+
261
+ watchEffect(() => {
262
+ void model.value
263
+ void accessibility.value
264
+ void validatedEmptyLabel.value
265
+ void validatedXAxisLabel.value
266
+ void validatedYAxisLabel.value
267
+ void validatedCenterLabel.value
268
+ void validatedCenterValue.value
269
+ if (accessibility.value.mode === 'decorative' && validatedTablePresentation.value === 'assistive') {
270
+ throw new Error('SChart: decorative chart требует dataTablePresentation="none" или "visible"')
271
+ }
272
+ if (isRadial.value && validatedGrid.value !== 'none') {
273
+ throw new Error(`SChart: ${validatedType.value} не поддерживает grid`)
274
+ }
275
+ if (isRadial.value && (validatedXAxisLabel.value || validatedYAxisLabel.value)) {
276
+ throw new Error(`SChart: ${validatedType.value} не поддерживает axis labels`)
277
+ }
278
+ if (validatedBarMode.value === 'stacked' && validatedType.value !== 'bar') {
279
+ throw new Error('SChart: barMode="stacked" разрешён только для type="bar"')
280
+ }
281
+ if ((validatedCenterLabel.value || validatedCenterValue.value) && validatedType.value !== 'donut') {
282
+ throw new Error('SChart: centerLabel/centerValue разрешены только для type="donut"')
283
+ }
284
+ })
285
+ </script>
286
+
287
+ <template>
288
+ <figure
289
+ v-bind="ownedAttrs.bindings()"
290
+ class="s-chart"
291
+ :data-size="validatedSize"
292
+ :data-type="validatedType"
293
+ :data-empty="model.empty || undefined"
294
+ >
295
+ <figcaption v-if="accessibility.mode === 'label'" class="sr-only">{{ accessibility.label }}</figcaption>
296
+ <div v-if="validatedLegend === 'top' && !model.empty" class="s-chart__legend" data-position="top">
297
+ <span v-for="item in model.legend" :key="item.key" class="s-chart__legend-item">
298
+ <span class="s-chart__legend-swatch" :data-s-chart-tone="item.tone" aria-hidden="true"></span>
299
+ <span>{{ item.label }}</span>
300
+ </span>
301
+ </div>
302
+
303
+ <div ref="viewportElement" class="s-chart__viewport">
304
+ <SEmptyState
305
+ v-if="model.empty"
306
+ :title="emptyTitle"
307
+ :size="validatedSize === 'compact' ? 'sm' : 'md'"
308
+ presentation="panel"
309
+ />
310
+ <svg
311
+ v-else
312
+ class="s-chart__svg"
313
+ :viewBox="`0 0 ${model.width} ${model.height}`"
314
+ preserveAspectRatio="xMidYMid meet"
315
+ :role="accessibility.mode === 'decorative' ? undefined : 'img'"
316
+ :aria-hidden="accessibility.mode === 'decorative' || undefined"
317
+ :aria-label="accessibleLabel"
318
+ :aria-labelledby="accessibility.mode === 'labelledby' ? accessibility.id : undefined"
319
+ :aria-describedby="accessibility.mode === 'labelledby' ? accessibility.describedby : undefined"
320
+ focusable="false"
321
+ >
322
+ <title v-if="accessibleLabel">{{ accessibleLabel }}</title>
323
+ <desc v-if="accessibleDescription">{{ accessibleDescription }}</desc>
324
+
325
+ <g v-if="showHorizontalGrid" class="s-chart__grid" aria-hidden="true">
326
+ <line
327
+ v-for="line in model.horizontalGrid"
328
+ :key="line.key"
329
+ :x1="line.x1"
330
+ :x2="line.x2"
331
+ :y1="line.y1"
332
+ :y2="line.y2"
333
+ />
334
+ </g>
335
+ <g v-if="showVerticalGrid" class="s-chart__grid" aria-hidden="true">
336
+ <line
337
+ v-for="line in model.verticalGrid"
338
+ :key="line.key"
339
+ :x1="line.x1"
340
+ :x2="line.x2"
341
+ :y1="line.y1"
342
+ :y2="line.y2"
343
+ />
344
+ </g>
345
+
346
+ <g v-if="resolvedYAxis" class="s-chart__axis" aria-hidden="true">
347
+ <line :x1="model.plotX" :x2="model.plotX" :y1="model.plotY" :y2="model.plotY + model.plotHeight" />
348
+ <text
349
+ v-for="tick in model.yTicks"
350
+ :key="tick.value"
351
+ :x="model.plotX - 8"
352
+ :y="tick.position"
353
+ text-anchor="end"
354
+ dominant-baseline="middle"
355
+ >{{ tick.label }}</text>
356
+ <text
357
+ v-if="validatedYAxisLabel"
358
+ class="s-chart__axis-title"
359
+ :x="12"
360
+ :y="model.plotY + model.plotHeight / 2"
361
+ text-anchor="middle"
362
+ :transform="`rotate(-90 12 ${model.plotY + model.plotHeight / 2})`"
363
+ >{{ validatedYAxisLabel }}</text>
364
+ </g>
365
+ <g v-if="resolvedXAxis" class="s-chart__axis" aria-hidden="true">
366
+ <line :x1="model.plotX" :x2="model.plotX + model.plotWidth" :y1="model.baselineY" :y2="model.baselineY" />
367
+ <text
368
+ v-for="category in model.xLabels"
369
+ :key="category.key"
370
+ :x="category.x"
371
+ :y="model.plotY + model.plotHeight + 18"
372
+ text-anchor="middle"
373
+ >
374
+ <tspan
375
+ v-for="(line, lineIndex) in category.lines"
376
+ :key="lineIndex"
377
+ :x="category.x"
378
+ :dy="lineIndex === 0 ? 0 : 13"
379
+ >{{ line }}</tspan>
380
+ </text>
381
+ <text
382
+ v-if="validatedXAxisLabel"
383
+ class="s-chart__axis-title"
384
+ :x="model.plotX + model.plotWidth / 2"
385
+ :y="model.height - 2"
386
+ text-anchor="middle"
387
+ >{{ validatedXAxisLabel }}</text>
388
+ </g>
389
+
390
+ <g v-if="validatedType === 'area'" class="s-chart__areas" aria-hidden="true">
391
+ <path
392
+ v-for="line in model.lines"
393
+ :key="line.key"
394
+ class="s-chart__area"
395
+ :data-s-chart-tone="line.tone"
396
+ :d="line.areaPath"
397
+ />
398
+ </g>
399
+ <g v-if="validatedType === 'line' || validatedType === 'area'" class="s-chart__lines">
400
+ <g v-for="line in model.lines" :key="line.key" :data-s-chart-tone="line.tone">
401
+ <path class="s-chart__line" :d="line.path" aria-hidden="true" />
402
+ <template v-for="(point, pointIndex) in line.points" :key="point.categoryKey">
403
+ <circle
404
+ v-if="showPointMarker(pointIndex, line.points.length)"
405
+ class="s-chart__marker"
406
+ :cx="point.x"
407
+ :cy="point.y"
408
+ r="4"
409
+ ><title>{{ point.title }}</title></circle>
410
+ <text
411
+ v-if="showPointLabel(pointIndex, line.points.length)"
412
+ class="s-chart__data-label"
413
+ :x="point.x"
414
+ :y="point.y - 9"
415
+ text-anchor="middle"
416
+ aria-hidden="true"
417
+ >{{ point.valueLabel }}</text>
418
+ </template>
419
+ </g>
420
+ </g>
421
+ <g v-if="validatedType === 'bar'" class="s-chart__bars">
422
+ <g v-for="bar in model.bars" :key="bar.key" :data-s-chart-tone="bar.tone">
423
+ <rect
424
+ class="s-chart__bar"
425
+ :x="bar.x"
426
+ :y="bar.y"
427
+ :width="bar.width"
428
+ :height="bar.height"
429
+ rx="2"
430
+ ><title>{{ bar.title }}</title></rect>
431
+ <text
432
+ v-if="showBarLabel(bar.categoryKey)"
433
+ class="s-chart__data-label"
434
+ :x="bar.x + bar.width / 2"
435
+ :y="bar.value >= 0 ? bar.y - 7 : bar.y + bar.height + 13"
436
+ text-anchor="middle"
437
+ aria-hidden="true"
438
+ >{{ bar.valueLabel }}</text>
439
+ </g>
440
+ </g>
441
+
442
+ <g v-if="isRadial" class="s-chart__slices">
443
+ <g v-for="slice in model.slices" :key="slice.key" :data-s-chart-tone="slice.tone">
444
+ <path class="s-chart__slice" :d="slice.path"><title>{{ slice.title }}</title></path>
445
+ <text
446
+ v-if="validatedDataLabels === 'all' && slice.value > 0"
447
+ class="s-chart__radial-label"
448
+ :x="slice.labelX"
449
+ :y="slice.labelY"
450
+ text-anchor="middle"
451
+ dominant-baseline="middle"
452
+ aria-hidden="true"
453
+ >{{ slice.dataLabel }}</text>
454
+ </g>
455
+ <g v-if="validatedType === 'donut' && (validatedCenterLabel || validatedCenterValue)" class="s-chart__center" aria-hidden="true">
456
+ <text v-if="validatedCenterValue" :x="model.centerX" :y="model.centerY - (validatedCenterLabel ? 4 : 0)" text-anchor="middle">{{ validatedCenterValue }}</text>
457
+ <text v-if="validatedCenterLabel" class="s-chart__center-label" :x="model.centerX" :y="model.centerY + (validatedCenterValue ? 14 : 0)" text-anchor="middle">{{ validatedCenterLabel }}</text>
458
+ </g>
459
+ </g>
460
+ </svg>
461
+ </div>
462
+
463
+ <div v-if="validatedLegend === 'bottom' && !model.empty" class="s-chart__legend" data-position="bottom">
464
+ <span v-for="item in model.legend" :key="item.key" class="s-chart__legend-item">
465
+ <span class="s-chart__legend-swatch" :data-s-chart-tone="item.tone" aria-hidden="true"></span>
466
+ <span>{{ item.label }}</span>
467
+ </span>
468
+ </div>
469
+
470
+ <table
471
+ v-if="showTable"
472
+ class="s-chart__data-table"
473
+ :class="tableClass"
474
+ :aria-label="accessibility.mode === 'label' ? `${accessibility.label}: данные` : undefined"
475
+ :aria-labelledby="accessibility.mode === 'labelledby' ? accessibility.id : undefined"
476
+ >
477
+ <thead>
478
+ <tr>
479
+ <th scope="col">Категория</th>
480
+ <th v-for="item in series" :key="item.key" scope="col">{{ item.label }}</th>
481
+ </tr>
482
+ </thead>
483
+ <tbody>
484
+ <tr v-for="row in model.tableRows" :key="row.key">
485
+ <th scope="row">{{ row.label }}</th>
486
+ <td v-for="(value, valueIndex) in row.values" :key="series[valueIndex]?.key">{{ value }}</td>
487
+ </tr>
488
+ </tbody>
489
+ </table>
490
+ </figure>
491
+ </template>
492
+
493
+ <style lang="postcss" src="./SChart.css" scoped></style>
@@ -0,0 +1,19 @@
1
+ @reference "../../../styles/reference.css";
2
+
3
+ .s-chip__content {
4
+ @apply inline-flex min-w-0 items-center;
5
+ gap: inherit;
6
+ }
7
+
8
+ .s-chip[data-mode='action']:not([data-s-pill-disabled='true']),
9
+ .s-chip[data-mode='link']:not([data-s-pill-disabled='true']) {
10
+ @apply cursor-pointer;
11
+ transition:
12
+ filter var(--s-motion-standard) ease,
13
+ box-shadow var(--s-motion-standard) ease;
14
+ }
15
+
16
+ .s-chip[data-mode='action']:not([data-s-pill-disabled='true']):hover,
17
+ .s-chip[data-mode='link']:not([data-s-pill-disabled='true']):hover {
18
+ filter: brightness(0.96);
19
+ }
@@ -1,12 +1,24 @@
1
1
  <template>
2
- <span
2
+ <div
3
3
  v-bind="{ ...ownedAttrs.bindings(), ...surfaceBindings }"
4
4
  class="s-chip"
5
+ :data-mode="resolvedSurface.mode"
5
6
  >
6
- <span v-if="leadingIcon" :class="PILL_ICON_CLASS" aria-hidden="true">
7
- <component :is="leadingIcon" :class="PILL_ICON_GRAPHIC_CLASS" />
8
- </span>
9
- <span :class="[PILL_LABEL_CLASS, PILL_LABEL_TRUNCATE_CLASS]">{{ validatedLabel }}</span>
7
+ <SInteractiveSurface
8
+ :surface="resolvedSurface"
9
+ appearance="bare"
10
+ density="compact"
11
+ padding="none"
12
+ radius="xl"
13
+ @activate="emit('activate', $event)"
14
+ >
15
+ <span class="s-chip__content">
16
+ <span v-if="leadingIcon" :class="PILL_ICON_CLASS" aria-hidden="true">
17
+ <component :is="leadingIcon" :class="PILL_ICON_GRAPHIC_CLASS" />
18
+ </span>
19
+ <span :class="[PILL_LABEL_CLASS, PILL_LABEL_TRUNCATE_CLASS]">{{ validatedLabel }}</span>
20
+ </span>
21
+ </SInteractiveSurface>
10
22
  <SButton
11
23
  v-if="closable"
12
24
  label=""
@@ -23,7 +35,7 @@
23
35
  :title="resolvedCloseLabel"
24
36
  @click.stop="emit('close', $event)"
25
37
  />
26
- </span>
38
+ </div>
27
39
  </template>
28
40
 
29
41
  <script setup lang="ts">
@@ -43,6 +55,7 @@ import {
43
55
  validateNonEmptyString,
44
56
  } from '../../../internal/runtimeContract'
45
57
  import SButton from '../controls/SButton.vue'
58
+ import SInteractiveSurface, { type InteractiveSurfaceContract } from '../controls/SInteractiveSurface.vue'
46
59
  import type { BadgeSeverity } from './SBadge.vue'
47
60
 
48
61
  defineOptions({ inheritAttrs: false })
@@ -58,6 +71,8 @@ export interface Props {
58
71
  closable?: boolean
59
72
  closeLabel?: string
60
73
  disabled?: boolean
74
+ /** Семантика основной поверхности chip; close остаётся отдельным sibling-действием. / Primary chip surface semantics; close remains a separate sibling action. */
75
+ surface?: InteractiveSurfaceContract
61
76
  }
62
77
 
63
78
  const props = withDefaults(defineProps<Props>(), {
@@ -67,9 +82,11 @@ const props = withDefaults(defineProps<Props>(), {
67
82
  closable: false,
68
83
  closeLabel: undefined,
69
84
  disabled: false,
85
+ surface: () => ({ mode: 'static' }),
70
86
  })
71
87
  const emit = defineEmits<{
72
88
  close: [event: MouseEvent]
89
+ activate: [event: MouseEvent]
73
90
  }>()
74
91
  const ownedAttrs = useOwnedAttrs({ component: 'SChip', owner: 'chip root' })
75
92
 
@@ -94,6 +111,11 @@ const surfaceBindings = computed(() => resolvePillSurfaceBindings('SChip', {
94
111
  maxWidth: 'none',
95
112
  iconMotion: 'static',
96
113
  }))
114
+ const resolvedSurface = computed<InteractiveSurfaceContract>(() => {
115
+ const surface = props.surface
116
+ if (surface.mode === 'static') return surface
117
+ return { ...surface, disabled: props.disabled || surface.disabled }
118
+ })
97
119
  const resolvedCloseLabel = computed(() => {
98
120
  validateBoolean('SChip', 'closable', props.closable)
99
121
  validateBoolean('SChip', 'disabled', props.disabled)
@@ -102,3 +124,5 @@ const resolvedCloseLabel = computed(() => {
102
124
  : validateNonEmptyString('SChip', 'closeLabel', props.closeLabel)
103
125
  })
104
126
  </script>
127
+
128
+ <style lang="postcss" src="./SChip.css" scoped></style>
@@ -37,6 +37,7 @@
37
37
 
38
38
  .s-code-block-editor-region {
39
39
  @apply flex min-h-0 min-w-0;
40
+ width: 100%;
40
41
  }
41
42
 
42
43
  .s-code-block--viewport .s-code-block-editor-region {
@@ -54,3 +55,70 @@
54
55
  .s-code-block-language {
55
56
  @apply font-mono;
56
57
  }
58
+
59
+ .s-code-block-plain {
60
+ width: 100%;
61
+ min-width: 0;
62
+ max-width: 100%;
63
+ margin: 0;
64
+ overflow: auto;
65
+ padding: 0.75rem;
66
+ background: transparent;
67
+ color: var(--color-text);
68
+ font-family: var(--font-mono);
69
+ font-size: 0.875rem;
70
+ line-height: 1.5;
71
+ white-space: pre;
72
+ tab-size: inherit;
73
+ }
74
+
75
+ .s-code-block-plain:focus-visible {
76
+ outline: var(--s-focus-ring-width) solid var(--s-focus-ring-color);
77
+ outline-offset: var(--s-focus-ring-inset-offset);
78
+ }
79
+
80
+ .s-code-block-plain[data-viewport='content'] {
81
+ max-height: var(--s-code-editor-viewport-content-max);
82
+ }
83
+
84
+ .s-code-block-plain[data-viewport='document'] {
85
+ overflow: visible;
86
+ }
87
+
88
+ .s-code-block-plain[data-viewport='compact'] {
89
+ height: var(--s-code-editor-viewport-compact);
90
+ }
91
+
92
+ .s-code-block-plain[data-viewport='standard'] {
93
+ height: var(--s-code-editor-viewport-standard);
94
+ }
95
+
96
+ .s-code-block-plain[data-viewport='expanded'] {
97
+ height: var(--s-code-editor-viewport-expanded);
98
+ }
99
+
100
+ .s-code-block-plain[data-viewport='fill'] {
101
+ height: 100%;
102
+ }
103
+
104
+ .s-code-block-plain__lines {
105
+ display: block;
106
+ min-width: max-content;
107
+ }
108
+
109
+ .s-code-block-plain__line {
110
+ display: grid;
111
+ grid-template-columns: max-content minmax(0, 1fr);
112
+ }
113
+
114
+ .s-code-block-plain__line-number {
115
+ min-width: 2.5rem;
116
+ padding-inline-end: 0.75rem;
117
+ color: var(--color-text-muted);
118
+ text-align: end;
119
+ user-select: none;
120
+ }
121
+
122
+ .s-code-block-plain__line-content {
123
+ min-width: 0;
124
+ }