@pgcorp/ui-kit 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -0
- package/package.json +1 -1
- package/src/components/shared/_internal/SHeaderSurface.css +98 -0
- package/src/components/shared/_internal/SHeaderSurface.vue +80 -0
- package/src/components/shared/_internal/useRadioGroup.ts +106 -0
- package/src/components/shared/containers/SPageHeader.vue +13 -20
- package/src/components/shared/controls/SCheckbox.vue +14 -43
- package/src/components/shared/controls/SChoiceCards.vue +16 -39
- package/src/components/shared/controls/SField.css +0 -12
- package/src/components/shared/controls/SField.vue +14 -10
- package/src/components/shared/controls/SFieldGroup.css +1 -19
- package/src/components/shared/controls/SFieldGroup.vue +13 -10
- package/src/components/shared/controls/SFieldLabel.css +0 -1
- package/src/components/shared/controls/SFieldLabel.vue +2 -1
- package/src/components/shared/controls/SSegmentedControl.vue +15 -43
- package/src/components/shared/controls/SSwitch.vue +14 -32
- package/src/components/shared/data-display/SBadge.vue +41 -18
- package/src/components/shared/data-display/SChip.vue +22 -10
- package/src/components/shared/data-display/SSectionHeader.vue +17 -21
- package/src/components/shared/data-display/STable.vue +131 -18
- package/src/components/shared/data-display/table.ts +1 -0
- package/src/components/shared/database/SDataGrid.vue +13 -0
- package/src/internal/fieldPresentation.ts +28 -0
- package/src/internal/pillSurface.ts +92 -0
- package/src/internal/useBinaryInput.ts +76 -0
- package/src/styles/style.css +114 -0
- package/src/styles/tokens.css +31 -0
- package/src/components/shared/containers/SPageHeader.css +0 -82
- package/src/components/shared/data-display/SBadge.css +0 -73
- package/src/components/shared/data-display/SChip.css +0 -59
- package/src/components/shared/data-display/SSectionHeader.css +0 -84
|
@@ -58,6 +58,8 @@
|
|
|
58
58
|
width="full"
|
|
59
59
|
:content-align="column.align === 'right' ? 'end' : column.align === 'center' ? 'center' : 'start'"
|
|
60
60
|
:trailing-icon="sortIcon(column)"
|
|
61
|
+
:badge="sortPriority(column) ?? undefined"
|
|
62
|
+
badge-severity="primary"
|
|
61
63
|
@click="requestSort(column)"
|
|
62
64
|
>
|
|
63
65
|
<span
|
|
@@ -193,6 +195,7 @@ import type {
|
|
|
193
195
|
STableRowState,
|
|
194
196
|
STableSelectionMode,
|
|
195
197
|
STableSort,
|
|
198
|
+
STableSortMode,
|
|
196
199
|
STableSurface,
|
|
197
200
|
} from './table'
|
|
198
201
|
|
|
@@ -209,6 +212,7 @@ export type {
|
|
|
209
212
|
STableSelectionMode,
|
|
210
213
|
STableSort,
|
|
211
214
|
STableSortDirection,
|
|
215
|
+
STableSortMode,
|
|
212
216
|
STableSurface,
|
|
213
217
|
} from './table'
|
|
214
218
|
|
|
@@ -232,7 +236,9 @@ export interface STableProps<
|
|
|
232
236
|
expandedRowKeys?: readonly RowKey[]
|
|
233
237
|
isRowExpandable?: (row: Row, index: number) => boolean
|
|
234
238
|
getRowLabel?: (row: Row, index: number) => string
|
|
239
|
+
sortMode?: STableSortMode
|
|
235
240
|
sort?: STableSort<Field> | null
|
|
241
|
+
sorts?: readonly STableSort<Field>[]
|
|
236
242
|
maxHeight?: ScrollableViewportMaxBlockSize
|
|
237
243
|
density?: STableDensity
|
|
238
244
|
surface?: STableSurface
|
|
@@ -282,7 +288,9 @@ const props = withDefaults(defineProps<STableProps<Row, RowKey, Field>>(), {
|
|
|
282
288
|
expandedRowKeys: () => [],
|
|
283
289
|
isRowExpandable: undefined,
|
|
284
290
|
getRowLabel: undefined,
|
|
291
|
+
sortMode: 'single',
|
|
285
292
|
sort: null,
|
|
293
|
+
sorts: () => [],
|
|
286
294
|
maxHeight: 'none',
|
|
287
295
|
density: 'default',
|
|
288
296
|
surface: 'bordered',
|
|
@@ -301,6 +309,8 @@ const emit = defineEmits<{
|
|
|
301
309
|
'row-expand': [row: Row, index: number, expanded: boolean]
|
|
302
310
|
'update:sort': [value: STableSort<Field> | null]
|
|
303
311
|
sort: [value: STableSort<Field> | null]
|
|
312
|
+
'update:sorts': [value: readonly STableSort<Field>[]]
|
|
313
|
+
'multi-sort': [value: readonly STableSort<Field>[]]
|
|
304
314
|
}>()
|
|
305
315
|
const slots = useSlots()
|
|
306
316
|
|
|
@@ -313,6 +323,8 @@ const TABLE_CELL_STATES = ['default', 'pending'] as const
|
|
|
313
323
|
const TABLE_COLUMN_CONTENT = ['field', 'value', 'slot'] as const
|
|
314
324
|
const TABLE_HEADER_PRESENTATIONS = ['visible', 'assistive'] as const
|
|
315
325
|
const TABLE_SELECTION_MODES = ['none', 'multiple'] as const
|
|
326
|
+
const TABLE_SORT_MODES = ['single', 'multiple'] as const
|
|
327
|
+
const TABLE_SORT_DIRECTIONS = ['ascending', 'descending'] as const
|
|
316
328
|
|
|
317
329
|
const ownedAttrs = useOwnedAttrs({ component: 'STable', owner: 'table scroll surface' })
|
|
318
330
|
useInteractiveLeafRegistration({ owner: 'STable' })
|
|
@@ -389,6 +401,12 @@ const validatedSelectionMode = computed(() => validateExactString(
|
|
|
389
401
|
props.selectionMode,
|
|
390
402
|
TABLE_SELECTION_MODES,
|
|
391
403
|
))
|
|
404
|
+
const validatedSortMode = computed(() => validateExactString(
|
|
405
|
+
'STable',
|
|
406
|
+
'sortMode',
|
|
407
|
+
props.sortMode,
|
|
408
|
+
TABLE_SORT_MODES,
|
|
409
|
+
))
|
|
392
410
|
|
|
393
411
|
const validatedColumns = computed<readonly STableColumn<Row, Field>[]>(() => {
|
|
394
412
|
const fields = new Set<string>()
|
|
@@ -454,9 +472,15 @@ const validatedColumns = computed<readonly STableColumn<Row, Field>[]>(() => {
|
|
|
454
472
|
return props.columns
|
|
455
473
|
})
|
|
456
474
|
|
|
457
|
-
|
|
458
|
-
if (
|
|
459
|
-
|
|
475
|
+
function validateSortEntry(value: unknown, coordinate: string): STableSort<Field> {
|
|
476
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
477
|
+
throw new TypeError(
|
|
478
|
+
`STable: ${coordinate} должен быть STableSort object. `
|
|
479
|
+
+ `/ STable: ${coordinate} must be an STableSort object.`,
|
|
480
|
+
)
|
|
481
|
+
}
|
|
482
|
+
const entry = value as Record<string, unknown>
|
|
483
|
+
const key = validateNonEmptyString('STable', `${coordinate}.key`, entry.key) as Field
|
|
460
484
|
const column = validatedColumns.value.find((candidate) => candidate.field === key)
|
|
461
485
|
if (!column?.sortable) {
|
|
462
486
|
throw new TypeError(`STable: sort key ${JSON.stringify(key)} должен ссылаться на sortable column. / STable: sort key ${JSON.stringify(key)} must reference a sortable column.`)
|
|
@@ -465,25 +489,100 @@ const validatedSort = computed<STableSort<Field> | null>(() => {
|
|
|
465
489
|
key,
|
|
466
490
|
direction: validateExactString(
|
|
467
491
|
'STable',
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
492
|
+
`${coordinate}.direction`,
|
|
493
|
+
entry.direction,
|
|
494
|
+
TABLE_SORT_DIRECTIONS,
|
|
471
495
|
),
|
|
472
496
|
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
const validatedSingleSort = computed<STableSort<Field> | null>(() => {
|
|
500
|
+
if (validatedSortMode.value === 'multiple') {
|
|
501
|
+
if (props.sort !== null && props.sort !== undefined) {
|
|
502
|
+
throw new TypeError(
|
|
503
|
+
'STable: sortMode="multiple" несовместим с sort; используйте v-model:sorts. '
|
|
504
|
+
+ '/ STable: sortMode="multiple" is incompatible with sort; use v-model:sorts.',
|
|
505
|
+
)
|
|
506
|
+
}
|
|
507
|
+
return null
|
|
508
|
+
}
|
|
509
|
+
if (props.sort === null || props.sort === undefined) return null
|
|
510
|
+
return validateSortEntry(props.sort, 'sort')
|
|
511
|
+
})
|
|
512
|
+
|
|
513
|
+
const validatedMultipleSort = computed<readonly STableSort<Field>[]>(() => {
|
|
514
|
+
const sorts: unknown = props.sorts
|
|
515
|
+
if (!Array.isArray(sorts)) {
|
|
516
|
+
throw new TypeError(
|
|
517
|
+
'STable: sorts должен быть ordered STableSort array. '
|
|
518
|
+
+ '/ STable: sorts must be an ordered STableSort array.',
|
|
519
|
+
)
|
|
520
|
+
}
|
|
521
|
+
if (validatedSortMode.value === 'single') {
|
|
522
|
+
if (sorts.length > 0) {
|
|
523
|
+
throw new TypeError(
|
|
524
|
+
'STable: sortMode="single" несовместим с непустым sorts; используйте v-model:sort. '
|
|
525
|
+
+ '/ STable: sortMode="single" is incompatible with non-empty sorts; use v-model:sort.',
|
|
526
|
+
)
|
|
527
|
+
}
|
|
528
|
+
return []
|
|
529
|
+
}
|
|
530
|
+
const keys = new Set<Field>()
|
|
531
|
+
return sorts.map((entry, index) => {
|
|
532
|
+
const validated = validateSortEntry(entry, `sorts[${index}]`)
|
|
533
|
+
if (keys.has(validated.key)) {
|
|
534
|
+
throw new TypeError(
|
|
535
|
+
`STable: sorts содержит duplicate key ${JSON.stringify(validated.key)}. `
|
|
536
|
+
+ `/ STable: sorts contains duplicate key ${JSON.stringify(validated.key)}.`,
|
|
537
|
+
)
|
|
538
|
+
}
|
|
539
|
+
keys.add(validated.key)
|
|
540
|
+
return validated
|
|
541
|
+
})
|
|
473
542
|
})
|
|
474
543
|
|
|
544
|
+
const activeSorts = computed<readonly STableSort<Field>[]>(() => {
|
|
545
|
+
const singleSort = validatedSingleSort.value
|
|
546
|
+
const multipleSort = validatedMultipleSort.value
|
|
547
|
+
if (validatedSortMode.value === 'multiple') return multipleSort
|
|
548
|
+
return singleSort === null ? [] : [singleSort]
|
|
549
|
+
})
|
|
550
|
+
|
|
551
|
+
function sortIndex(column: STableColumn<Row, Field>): number {
|
|
552
|
+
return activeSorts.value.findIndex((candidate) => candidate.key === column.field)
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
function sortPriority(column: STableColumn<Row, Field>): number | null {
|
|
556
|
+
if (validatedSortMode.value !== 'multiple') return null
|
|
557
|
+
const index = sortIndex(column)
|
|
558
|
+
return index === -1 ? null : index + 1
|
|
559
|
+
}
|
|
560
|
+
|
|
475
561
|
const columnAriaSort = (column: STableColumn<Row, Field>): 'none' | 'ascending' | 'descending' | undefined => {
|
|
476
562
|
if (!column.sortable) return undefined
|
|
477
|
-
|
|
563
|
+
if (validatedSortMode.value === 'multiple') {
|
|
564
|
+
const primary = activeSorts.value[0]
|
|
565
|
+
return primary?.key === column.field ? primary.direction : undefined
|
|
566
|
+
}
|
|
567
|
+
const current = activeSorts.value[0]
|
|
568
|
+
return current?.key === column.field ? current.direction : 'none'
|
|
478
569
|
}
|
|
479
570
|
|
|
480
571
|
const sortIcon = (column: STableColumn<Row, Field>) => {
|
|
481
|
-
const direction =
|
|
572
|
+
const direction = activeSorts.value.find((candidate) => candidate.key === column.field)?.direction
|
|
482
573
|
return direction === 'ascending' ? ChevronUp : direction === 'descending' ? ChevronDown : ChevronsUpDown
|
|
483
574
|
}
|
|
484
575
|
|
|
485
576
|
const sortActionLabel = (column: STableColumn<Row, Field>): string => {
|
|
486
|
-
const
|
|
577
|
+
const index = sortIndex(column)
|
|
578
|
+
const current = index === -1 ? undefined : activeSorts.value[index]
|
|
579
|
+
if (validatedSortMode.value === 'multiple') {
|
|
580
|
+
if (!current) return `${column.header}: добавить в сортировку по возрастанию`
|
|
581
|
+
const state = current.direction === 'ascending' ? 'по возрастанию' : 'по убыванию'
|
|
582
|
+
const action = current.direction === 'ascending' ? 'сортировать по убыванию' : 'убрать из сортировки'
|
|
583
|
+
return `${column.header}: приоритет ${index + 1} из ${activeSorts.value.length}, ${state}; ${action}`
|
|
584
|
+
}
|
|
585
|
+
const direction = current?.direction
|
|
487
586
|
if (direction === 'ascending') return `${column.header}: сортировать по убыванию`
|
|
488
587
|
if (direction === 'descending') return `${column.header}: сбросить сортировку`
|
|
489
588
|
return `${column.header}: сортировать по возрастанию`
|
|
@@ -491,15 +590,29 @@ const sortActionLabel = (column: STableColumn<Row, Field>): string => {
|
|
|
491
590
|
|
|
492
591
|
function requestSort(column: STableColumn<Row, Field>): void {
|
|
493
592
|
if (!column.sortable) return
|
|
494
|
-
const current = validatedSort.value
|
|
495
593
|
const key = column.field as Field
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
594
|
+
if (validatedSortMode.value === 'multiple') {
|
|
595
|
+
const current = activeSorts.value
|
|
596
|
+
const index = sortIndex(column)
|
|
597
|
+
let next: readonly STableSort<Field>[]
|
|
598
|
+
if (index === -1) next = [...current, { key, direction: 'ascending' }]
|
|
599
|
+
else if (current[index]!.direction === 'ascending') {
|
|
600
|
+
next = current.map((entry, candidateIndex) => candidateIndex === index
|
|
601
|
+
? { key: entry.key, direction: 'descending' }
|
|
602
|
+
: entry)
|
|
603
|
+
} else next = current.filter((_entry, candidateIndex) => candidateIndex !== index)
|
|
604
|
+
emit('update:sorts', next)
|
|
605
|
+
emit('multi-sort', next)
|
|
606
|
+
} else {
|
|
607
|
+
const current = activeSorts.value[0]
|
|
608
|
+
const next: STableSort<Field> | null = current?.key !== key
|
|
609
|
+
? { key, direction: 'ascending' }
|
|
610
|
+
: current.direction === 'ascending'
|
|
611
|
+
? { key, direction: 'descending' }
|
|
612
|
+
: null
|
|
613
|
+
emit('update:sort', next)
|
|
614
|
+
emit('sort', next)
|
|
615
|
+
}
|
|
503
616
|
}
|
|
504
617
|
|
|
505
618
|
function describeRowKey(key: STableRowKey): string {
|
|
@@ -671,7 +784,7 @@ const rowExpandableByRow = computed<ReadonlyMap<Row, boolean>>(() => {
|
|
|
671
784
|
|
|
672
785
|
const validatedRows = computed<readonly Row[]>(() => {
|
|
673
786
|
const columns = validatedColumns.value
|
|
674
|
-
void
|
|
787
|
+
void activeSorts.value
|
|
675
788
|
void resolvedRowKeys.value
|
|
676
789
|
void validatedSelectedRowKey.value
|
|
677
790
|
void validatedSelectedRowKeys.value
|
|
@@ -14,6 +14,7 @@ export type STableColumnContent = 'field' | 'value' | 'slot'
|
|
|
14
14
|
export type STableColumnHeaderPresentation = 'visible' | 'assistive'
|
|
15
15
|
export type STableSelectionMode = 'none' | 'multiple'
|
|
16
16
|
export type STableSortDirection = 'ascending' | 'descending'
|
|
17
|
+
export type STableSortMode = 'single' | 'multiple'
|
|
17
18
|
|
|
18
19
|
/** Controlled sort state; consumer owns data ordering. / Управляемое состояние сортировки; порядок данных принадлежит consumer. */
|
|
19
20
|
export interface STableSort<Field extends string = string> {
|
|
@@ -8,6 +8,9 @@
|
|
|
8
8
|
no-content-padding
|
|
9
9
|
content-layout="block"
|
|
10
10
|
surface-overflow="clip"
|
|
11
|
+
:surface-tone="validatedPresentation === 'embedded' ? 'transparent' : 'default'"
|
|
12
|
+
:surface-border="validatedPresentation === 'embedded' ? 'none' : 'default'"
|
|
13
|
+
:surface-radius="validatedPresentation === 'embedded' ? 'none' : 'default'"
|
|
11
14
|
>
|
|
12
15
|
<template v-if="$slots.actions" #actions>
|
|
13
16
|
<slot
|
|
@@ -113,6 +116,8 @@ export interface SDataGridProps<Key extends SDataGridColumnKey = SDataGridColumn
|
|
|
113
116
|
emptyMessage?: string
|
|
114
117
|
selectedRowId?: string
|
|
115
118
|
maxHeight?: ScrollableViewportMaxBlockSize
|
|
119
|
+
/** Surface chrome для самостоятельного или вложенного grid. / Surface chrome for a standalone or embedded grid. */
|
|
120
|
+
presentation?: 'panel' | 'embedded'
|
|
116
121
|
/** Accessible name без visible title. / Accessible name when there is no visible title. */
|
|
117
122
|
ariaLabel?: string
|
|
118
123
|
/** DOM id external visible title. / DOM id of an external visible title. */
|
|
@@ -168,6 +173,7 @@ const props = withDefaults(defineProps<SDataGridProps<ColumnKey>>(), {
|
|
|
168
173
|
emptyMessage: 'Нет строк для отображения',
|
|
169
174
|
selectedRowId: undefined,
|
|
170
175
|
maxHeight: 'none',
|
|
176
|
+
presentation: 'panel',
|
|
171
177
|
ariaLabel: undefined,
|
|
172
178
|
ariaLabelledby: undefined,
|
|
173
179
|
})
|
|
@@ -185,6 +191,7 @@ const CELL_KINDS = ['text', 'number', 'boolean', 'json', 'datetime', 'binary'] a
|
|
|
185
191
|
const COLUMN_ALIGNMENTS = ['left', 'center', 'right'] as const
|
|
186
192
|
const ROW_STATES = ['clean', 'inserted', 'updated', 'deleted'] as const
|
|
187
193
|
const COMMIT_SOURCES = ['draft', 'value'] as const
|
|
194
|
+
const PRESENTATIONS = ['panel', 'embedded'] as const
|
|
188
195
|
|
|
189
196
|
function assertReadonlyArray(value: unknown, coordinate: string): asserts value is readonly unknown[] {
|
|
190
197
|
if (Array.isArray(value)) return
|
|
@@ -335,6 +342,12 @@ const resolvedAccessibleName = computed(() => {
|
|
|
335
342
|
const validatedLoading = computed(() => validateBoolean('SDataGrid', 'loading', props.loading))
|
|
336
343
|
const validatedDisabled = computed(() => validateBoolean('SDataGrid', 'disabled', props.disabled))
|
|
337
344
|
const validatedReadOnly = computed(() => validateBoolean('SDataGrid', 'readOnly', props.readOnly))
|
|
345
|
+
const validatedPresentation = computed(() => validateExactString(
|
|
346
|
+
'SDataGrid',
|
|
347
|
+
'presentation',
|
|
348
|
+
props.presentation,
|
|
349
|
+
PRESENTATIONS,
|
|
350
|
+
))
|
|
338
351
|
const validatedMaxHeight = computed(() => validateScrollableViewportMaxBlockSize(
|
|
339
352
|
'SDataGrid',
|
|
340
353
|
'maxHeight',
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { computed, type ComputedRef } from 'vue'
|
|
2
|
+
|
|
3
|
+
export const FIELD_REQUIRED_CLASS = 'ml-0.5 text-danger-600 dark:text-danger-400'
|
|
4
|
+
export const FIELD_SUPPORT_CLASS = 'mt-1 min-w-0 whitespace-normal text-xs [overflow-wrap:anywhere]'
|
|
5
|
+
export const FIELD_HELP_CLASS = `${FIELD_SUPPORT_CLASS} text-surface-500 dark:text-surface-400`
|
|
6
|
+
export const FIELD_ERROR_CLASS = `${FIELD_SUPPORT_CLASS} text-danger-600 dark:text-danger-400`
|
|
7
|
+
|
|
8
|
+
export interface FieldSupportingTextOptions {
|
|
9
|
+
readonly baseId: ComputedRef<string>
|
|
10
|
+
readonly hasHelp: () => boolean
|
|
11
|
+
readonly hasError: () => boolean
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Владеет exact ID и aria-describedby contract для help/error у field и fieldset.
|
|
16
|
+
* Owns the exact help/error ID and aria-describedby contract for fields and fieldsets.
|
|
17
|
+
*/
|
|
18
|
+
export function useFieldSupportingText(options: FieldSupportingTextOptions) {
|
|
19
|
+
const helpId = computed(() => `${options.baseId.value}-help`)
|
|
20
|
+
const errorId = computed(() => `${options.baseId.value}-error`)
|
|
21
|
+
const describedBy = computed(() => {
|
|
22
|
+
const ids: string[] = []
|
|
23
|
+
if (options.hasHelp()) ids.push(helpId.value)
|
|
24
|
+
if (options.hasError()) ids.push(errorId.value)
|
|
25
|
+
return ids.length > 0 ? ids.join(' ') : undefined
|
|
26
|
+
})
|
|
27
|
+
return { describedBy, errorId, helpId } as const
|
|
28
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import {
|
|
2
|
+
validateBoolean,
|
|
3
|
+
validateExactString,
|
|
4
|
+
} from './runtimeContract'
|
|
5
|
+
|
|
6
|
+
const PILL_KINDS = ['badge', 'chip'] as const
|
|
7
|
+
const PILL_SEVERITIES = [
|
|
8
|
+
'primary',
|
|
9
|
+
'secondary',
|
|
10
|
+
'success',
|
|
11
|
+
'info',
|
|
12
|
+
'warn',
|
|
13
|
+
'danger',
|
|
14
|
+
'contrast',
|
|
15
|
+
] as const
|
|
16
|
+
const PILL_SIZES = ['xs', 'sm', 'md'] as const
|
|
17
|
+
const PILL_MAX_WIDTHS = ['none', 'sm', 'md'] as const
|
|
18
|
+
const PILL_ICON_MOTIONS = ['static', 'spin'] as const
|
|
19
|
+
|
|
20
|
+
export type PillSurfaceKind = typeof PILL_KINDS[number]
|
|
21
|
+
export type PillSurfaceSeverity = typeof PILL_SEVERITIES[number]
|
|
22
|
+
export type PillSurfaceSize = typeof PILL_SIZES[number]
|
|
23
|
+
export type PillSurfaceMaxWidth = typeof PILL_MAX_WIDTHS[number]
|
|
24
|
+
export type PillSurfaceIconMotion = typeof PILL_ICON_MOTIONS[number]
|
|
25
|
+
|
|
26
|
+
export const PILL_ICON_CLASS = 's-pill-icon inline-flex h-[var(--s-pill-icon-size)] w-[var(--s-pill-icon-size)] shrink-0 items-center justify-center'
|
|
27
|
+
export const PILL_ICON_GRAPHIC_CLASS = 's-pill-icon-graphic h-full w-full'
|
|
28
|
+
export const PILL_ICON_SPIN_CLASS = 'animate-spin'
|
|
29
|
+
export const PILL_LABEL_CLASS = 's-pill-label min-w-0'
|
|
30
|
+
export const PILL_LABEL_TRUNCATE_CLASS = 'overflow-hidden text-ellipsis whitespace-nowrap'
|
|
31
|
+
|
|
32
|
+
export interface PillSurfaceContract {
|
|
33
|
+
kind: PillSurfaceKind
|
|
34
|
+
severity: PillSurfaceSeverity
|
|
35
|
+
size: PillSurfaceSize
|
|
36
|
+
dot: boolean
|
|
37
|
+
closable: boolean
|
|
38
|
+
disabled: boolean
|
|
39
|
+
truncate: boolean
|
|
40
|
+
maxWidth: PillSurfaceMaxWidth
|
|
41
|
+
iconMotion: PillSurfaceIconMotion
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface PillSurfaceBindings {
|
|
45
|
+
'data-s-pill-surface': PillSurfaceKind
|
|
46
|
+
'data-s-feedback-tone': PillSurfaceSeverity
|
|
47
|
+
'data-s-pill-size': PillSurfaceSize
|
|
48
|
+
'data-s-pill-dot'?: 'true'
|
|
49
|
+
'data-s-pill-closable'?: 'true'
|
|
50
|
+
'data-s-pill-disabled'?: 'true'
|
|
51
|
+
'data-s-pill-truncate'?: 'true'
|
|
52
|
+
'data-s-pill-max-width'?: Exclude<PillSurfaceMaxWidth, 'none'>
|
|
53
|
+
'data-s-pill-icon-motion'?: Exclude<PillSurfaceIconMotion, 'static'>
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Создаёт exact bindings единого pill/feedback presentation owner.
|
|
58
|
+
* Creates exact bindings for the canonical pill/feedback presentation owner.
|
|
59
|
+
*/
|
|
60
|
+
export function resolvePillSurfaceBindings(
|
|
61
|
+
owner: 'SBadge' | 'SChip',
|
|
62
|
+
contract: PillSurfaceContract,
|
|
63
|
+
): PillSurfaceBindings {
|
|
64
|
+
const kind = validateExactString(owner, 'pill kind', contract.kind, PILL_KINDS)
|
|
65
|
+
const severity = validateExactString(owner, 'severity', contract.severity, PILL_SEVERITIES)
|
|
66
|
+
const size = validateExactString(owner, 'size', contract.size, PILL_SIZES)
|
|
67
|
+
const maxWidth = validateExactString(owner, 'maxWidth', contract.maxWidth, PILL_MAX_WIDTHS)
|
|
68
|
+
const iconMotion = validateExactString(owner, 'iconMotion', contract.iconMotion, PILL_ICON_MOTIONS)
|
|
69
|
+
const dot = validateBoolean(owner, 'dot', contract.dot)
|
|
70
|
+
const closable = validateBoolean(owner, 'closable', contract.closable)
|
|
71
|
+
const disabled = validateBoolean(owner, 'disabled', contract.disabled)
|
|
72
|
+
const truncate = validateBoolean(owner, 'truncate', contract.truncate)
|
|
73
|
+
|
|
74
|
+
if (kind === 'chip' && dot) {
|
|
75
|
+
throw new TypeError('SChip: dot presentation не поддерживается. / SChip: dot presentation is not supported.')
|
|
76
|
+
}
|
|
77
|
+
if (kind === 'badge' && closable) {
|
|
78
|
+
throw new TypeError('SBadge: closable presentation не поддерживается. / SBadge: closable presentation is not supported.')
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
'data-s-pill-surface': kind,
|
|
83
|
+
'data-s-feedback-tone': severity,
|
|
84
|
+
'data-s-pill-size': size,
|
|
85
|
+
...(dot ? { 'data-s-pill-dot': 'true' as const } : {}),
|
|
86
|
+
...(closable ? { 'data-s-pill-closable': 'true' as const } : {}),
|
|
87
|
+
...(disabled ? { 'data-s-pill-disabled': 'true' as const } : {}),
|
|
88
|
+
...(truncate ? { 'data-s-pill-truncate': 'true' as const } : {}),
|
|
89
|
+
...(maxWidth === 'none' ? {} : { 'data-s-pill-max-width': maxWidth }),
|
|
90
|
+
...(iconMotion === 'static' ? {} : { 'data-s-pill-icon-motion': iconMotion }),
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { computed, onMounted, ref, useId, watch } from 'vue'
|
|
2
|
+
|
|
3
|
+
import { resolveOptionalDomId } from './runtimeContract'
|
|
4
|
+
|
|
5
|
+
export type BinaryInputOwner = 'SCheckbox' | 'SSwitch'
|
|
6
|
+
|
|
7
|
+
export interface BinaryInputModelContract {
|
|
8
|
+
readonly modelValue: boolean
|
|
9
|
+
readonly readonly: boolean
|
|
10
|
+
readonly id?: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface BinaryInputOptions {
|
|
14
|
+
readonly owner: BinaryInputOwner
|
|
15
|
+
readonly inputName: 'checkbox' | 'switch'
|
|
16
|
+
readonly props: BinaryInputModelContract
|
|
17
|
+
readonly indeterminate?: () => boolean
|
|
18
|
+
readonly emitUpdate: (value: boolean) => void
|
|
19
|
+
readonly emitClick: (event: MouseEvent) => void
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Единый runtime owner controlled binary input: identity, value, readonly и native events.
|
|
24
|
+
* Canonical runtime owner for controlled binary input identity, value, readonly, and native events.
|
|
25
|
+
*/
|
|
26
|
+
export function useBinaryInput(options: BinaryInputOptions) {
|
|
27
|
+
const generatedInputId = useId()
|
|
28
|
+
const inputId = computed(() => resolveOptionalDomId(
|
|
29
|
+
options.owner,
|
|
30
|
+
'id',
|
|
31
|
+
options.props.id,
|
|
32
|
+
`s-${options.inputName}-${generatedInputId}`,
|
|
33
|
+
))
|
|
34
|
+
const inputEl = ref<HTMLInputElement | null>(null)
|
|
35
|
+
const checkedValue = computed(() => {
|
|
36
|
+
if (typeof options.props.modelValue !== 'boolean') {
|
|
37
|
+
throw new TypeError(`${options.owner}: modelValue must be boolean`)
|
|
38
|
+
}
|
|
39
|
+
return options.props.modelValue
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
const applyIndeterminate = (): void => {
|
|
43
|
+
if (inputEl.value) {
|
|
44
|
+
inputEl.value.indeterminate = (options.indeterminate?.() ?? false) && !checkedValue.value
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
onMounted(applyIndeterminate)
|
|
48
|
+
watch([options.indeterminate ?? (() => false), checkedValue], applyIndeterminate)
|
|
49
|
+
|
|
50
|
+
const onChange = (event: Event): void => {
|
|
51
|
+
if (options.props.readonly) return
|
|
52
|
+
const target = event.currentTarget
|
|
53
|
+
if (!(target instanceof HTMLInputElement)) {
|
|
54
|
+
throw new TypeError(
|
|
55
|
+
`${options.owner}: change event owner must be the native ${options.inputName} input`,
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
options.emitUpdate(target.checked)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const onClick = (event: MouseEvent): void => {
|
|
62
|
+
if (options.props.readonly) {
|
|
63
|
+
event.preventDefault()
|
|
64
|
+
return
|
|
65
|
+
}
|
|
66
|
+
options.emitClick(event)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
checkedValue,
|
|
71
|
+
inputEl,
|
|
72
|
+
inputId,
|
|
73
|
+
onChange,
|
|
74
|
+
onClick,
|
|
75
|
+
} as const
|
|
76
|
+
}
|
package/src/styles/style.css
CHANGED
|
@@ -26,6 +26,120 @@ body {
|
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
@layer components {
|
|
29
|
+
:where([data-s-feedback-tone="primary"]) {
|
|
30
|
+
--s-feedback-tone-background: var(--s-feedback-primary-background);
|
|
31
|
+
--s-feedback-tone-foreground: var(--s-feedback-primary-foreground);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
:where([data-s-feedback-tone="secondary"]) {
|
|
35
|
+
--s-feedback-tone-background: var(--s-feedback-secondary-background);
|
|
36
|
+
--s-feedback-tone-foreground: var(--s-feedback-secondary-foreground);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
:where([data-s-feedback-tone="success"]) {
|
|
40
|
+
--s-feedback-tone-background: var(--s-feedback-success-background);
|
|
41
|
+
--s-feedback-tone-foreground: var(--s-feedback-success-foreground);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
:where([data-s-feedback-tone="info"]) {
|
|
45
|
+
--s-feedback-tone-background: var(--s-feedback-info-background);
|
|
46
|
+
--s-feedback-tone-foreground: var(--s-feedback-info-foreground);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
:where([data-s-feedback-tone="warn"]) {
|
|
50
|
+
--s-feedback-tone-background: var(--s-feedback-warn-background);
|
|
51
|
+
--s-feedback-tone-foreground: var(--s-feedback-warn-foreground);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
:where([data-s-feedback-tone="danger"]) {
|
|
55
|
+
--s-feedback-tone-background: var(--s-feedback-danger-background);
|
|
56
|
+
--s-feedback-tone-foreground: var(--s-feedback-danger-foreground);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
:where([data-s-feedback-tone="contrast"]) {
|
|
60
|
+
--s-feedback-tone-background: var(--s-feedback-contrast-background);
|
|
61
|
+
--s-feedback-tone-foreground: var(--s-feedback-contrast-foreground);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
:where([data-s-pill-surface]) {
|
|
65
|
+
display: inline-flex;
|
|
66
|
+
min-width: 0;
|
|
67
|
+
align-items: center;
|
|
68
|
+
border-radius: var(--s-feedback-pill-radius);
|
|
69
|
+
background-color: var(--s-feedback-tone-background);
|
|
70
|
+
color: var(--s-feedback-tone-foreground);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
:where([data-s-pill-surface="badge"]) {
|
|
74
|
+
--s-pill-icon-size: 0.75rem;
|
|
75
|
+
gap: 0.25rem;
|
|
76
|
+
font-weight: 600;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
:where([data-s-pill-surface="chip"]) {
|
|
80
|
+
--s-pill-icon-size: 0.875rem;
|
|
81
|
+
font-weight: 500;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
:where([data-s-pill-surface="badge"][data-s-pill-size="md"]) {
|
|
85
|
+
padding: 0.125rem 0.625rem;
|
|
86
|
+
font-size: 0.75rem;
|
|
87
|
+
line-height: 1rem;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
:where([data-s-pill-surface="badge"][data-s-pill-size="sm"]) {
|
|
91
|
+
padding: 0.125rem 0.5rem;
|
|
92
|
+
font-size: 0.6875rem;
|
|
93
|
+
line-height: 1rem;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
:where([data-s-pill-surface="badge"][data-s-pill-size="xs"]) {
|
|
97
|
+
padding: 0 0.375rem;
|
|
98
|
+
font-size: 0.625rem;
|
|
99
|
+
line-height: 1rem;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
:where([data-s-pill-surface="chip"][data-s-pill-size="md"]) {
|
|
103
|
+
min-height: 1.75rem;
|
|
104
|
+
gap: 0.375rem;
|
|
105
|
+
padding-inline: 0.625rem;
|
|
106
|
+
font-size: 0.75rem;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
:where([data-s-pill-surface="chip"][data-s-pill-size="sm"]) {
|
|
110
|
+
min-height: 1.5rem;
|
|
111
|
+
gap: 0.25rem;
|
|
112
|
+
padding-inline: 0.5rem;
|
|
113
|
+
font-size: 0.6875rem;
|
|
114
|
+
line-height: 1rem;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
:where([data-s-pill-surface="chip"][data-s-pill-size="md"][data-s-pill-closable="true"]) {
|
|
118
|
+
padding-right: 0.25rem;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
:where([data-s-pill-surface="chip"][data-s-pill-size="sm"][data-s-pill-closable="true"]) {
|
|
122
|
+
padding-right: 0.125rem;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
:where([data-s-pill-surface][data-s-pill-disabled="true"]) {
|
|
126
|
+
opacity: 0.6;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
:where([data-s-pill-surface][data-s-pill-dot="true"]) {
|
|
130
|
+
width: 0.625rem;
|
|
131
|
+
height: 0.625rem;
|
|
132
|
+
padding: 0;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
:where([data-s-pill-surface][data-s-pill-max-width="sm"]) {
|
|
136
|
+
max-width: 9.5rem;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
:where([data-s-pill-surface][data-s-pill-max-width="md"]) {
|
|
140
|
+
max-width: 14rem;
|
|
141
|
+
}
|
|
142
|
+
|
|
29
143
|
:where([data-s-field-surface]) {
|
|
30
144
|
transition-property: color, background-color, border-color, box-shadow;
|
|
31
145
|
transition-duration: var(--s-motion-standard);
|
package/src/styles/tokens.css
CHANGED
|
@@ -231,6 +231,7 @@
|
|
|
231
231
|
/* Semantic feedback surfaces */
|
|
232
232
|
--s-feedback-surface: color-mix(in srgb, var(--color-surface) 88%, transparent);
|
|
233
233
|
--s-feedback-backdrop-blur: 1.5rem;
|
|
234
|
+
--s-feedback-pill-radius: var(--s-radius-pill);
|
|
234
235
|
--s-floating-surface: var(--color-surface);
|
|
235
236
|
--s-floating-foreground: var(--color-text);
|
|
236
237
|
--s-floating-outline: color-mix(in srgb, var(--color-border) 44%, transparent);
|
|
@@ -607,6 +608,21 @@
|
|
|
607
608
|
--s-progress-indicator-foreground-warn: var(--color-warn-600);
|
|
608
609
|
--s-progress-indicator-foreground-danger: var(--color-danger-600);
|
|
609
610
|
|
|
611
|
+
--s-feedback-primary-background: var(--color-primary-100);
|
|
612
|
+
--s-feedback-primary-foreground: var(--color-primary-800);
|
|
613
|
+
--s-feedback-secondary-background: var(--color-surface-100);
|
|
614
|
+
--s-feedback-secondary-foreground: var(--color-surface-800);
|
|
615
|
+
--s-feedback-success-background: var(--color-success-100);
|
|
616
|
+
--s-feedback-success-foreground: var(--color-success-800);
|
|
617
|
+
--s-feedback-info-background: var(--color-info-100);
|
|
618
|
+
--s-feedback-info-foreground: var(--color-info-800);
|
|
619
|
+
--s-feedback-warn-background: var(--color-warn-100);
|
|
620
|
+
--s-feedback-warn-foreground: var(--color-warn-800);
|
|
621
|
+
--s-feedback-danger-background: var(--color-danger-100);
|
|
622
|
+
--s-feedback-danger-foreground: var(--color-danger-800);
|
|
623
|
+
--s-feedback-contrast-background: var(--color-surface-800);
|
|
624
|
+
--s-feedback-contrast-foreground: var(--color-surface-100);
|
|
625
|
+
|
|
610
626
|
--s-graph-node-tone-neutral-fill: var(--color-surface-50);
|
|
611
627
|
--s-graph-node-tone-neutral-border: var(--color-surface-500);
|
|
612
628
|
--s-graph-node-tone-neutral-label: var(--color-surface-900);
|
|
@@ -742,6 +758,21 @@
|
|
|
742
758
|
--s-progress-indicator-foreground-warn: var(--color-warn-300);
|
|
743
759
|
--s-progress-indicator-foreground-danger: var(--color-danger-300);
|
|
744
760
|
|
|
761
|
+
--s-feedback-primary-background: var(--color-primary-950);
|
|
762
|
+
--s-feedback-primary-foreground: var(--color-primary-50);
|
|
763
|
+
--s-feedback-secondary-background: var(--color-surface-800);
|
|
764
|
+
--s-feedback-secondary-foreground: var(--color-surface-50);
|
|
765
|
+
--s-feedback-success-background: var(--color-success-950);
|
|
766
|
+
--s-feedback-success-foreground: var(--color-success-50);
|
|
767
|
+
--s-feedback-info-background: var(--color-info-950);
|
|
768
|
+
--s-feedback-info-foreground: var(--color-info-50);
|
|
769
|
+
--s-feedback-warn-background: var(--color-warn-950);
|
|
770
|
+
--s-feedback-warn-foreground: var(--color-warn-50);
|
|
771
|
+
--s-feedback-danger-background: var(--color-danger-950);
|
|
772
|
+
--s-feedback-danger-foreground: var(--color-danger-50);
|
|
773
|
+
--s-feedback-contrast-background: var(--color-surface-200);
|
|
774
|
+
--s-feedback-contrast-foreground: var(--color-surface-800);
|
|
775
|
+
|
|
745
776
|
--s-graph-node-tone-neutral-fill: var(--color-surface-700);
|
|
746
777
|
--s-graph-node-tone-neutral-border: var(--color-surface-300);
|
|
747
778
|
--s-graph-node-tone-neutral-label: var(--color-surface-50);
|