@pgcorp/ui-kit 0.2.0 → 0.3.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.
Files changed (50) hide show
  1. package/README.md +5 -5
  2. package/docs/public-api.md +8 -0
  3. package/package.json +15 -3
  4. package/src/components/layout/SAppShell.vue +3 -2
  5. package/src/components/layout/SStack.css +9 -3
  6. package/src/components/layout/SStack.vue +10 -2
  7. package/src/components/shared/complex/STree.vue +29 -4
  8. package/src/components/shared/complex/treeAdapter.ts +2 -1
  9. package/src/components/shared/complex/types.ts +1 -12
  10. package/src/components/shared/containers/SLeftSidebar.vue +7 -2
  11. package/src/components/shared/containers/SPageHeader.css +11 -0
  12. package/src/components/shared/containers/SPageHeader.vue +7 -1
  13. package/src/components/shared/containers/SPopover.vue +2 -1
  14. package/src/components/shared/containers/SSidebarGroup.vue +10 -5
  15. package/src/components/shared/containers/sidebar.ts +2 -1
  16. package/src/components/shared/controls/SButton.css +13 -0
  17. package/src/components/shared/controls/SButton.vue +135 -29
  18. package/src/components/shared/controls/SCombobox.css +9 -0
  19. package/src/components/shared/controls/SCombobox.vue +322 -0
  20. package/src/components/shared/controls/SInputText.vue +15 -0
  21. package/src/components/shared/controls/SInteractiveSurface.vue +2 -1
  22. package/src/components/shared/controls/SLink.ts +1 -17
  23. package/src/components/shared/controls/SLink.vue +26 -2
  24. package/src/components/shared/controls/SListbox.vue +99 -1
  25. package/src/components/shared/controls/SListboxOption.vue +15 -2
  26. package/src/components/shared/controls/SSwitch.css +48 -0
  27. package/src/components/shared/controls/SSwitch.vue +109 -0
  28. package/src/components/shared/controls/_internal/SInlineTokenSurface.vue +6 -3
  29. package/src/components/shared/data-display/SChip.css +59 -0
  30. package/src/components/shared/data-display/SChip.vue +92 -0
  31. package/src/components/shared/data-display/SDocBlock.vue +10 -10
  32. package/src/components/shared/data-display/SLinkedSystemsList.vue +3 -3
  33. package/src/components/shared/data-display/STable.vue +150 -14
  34. package/src/components/shared/data-display/STooltip.vue +2 -1
  35. package/src/components/shared/data-display/SVirtualList.ts +1 -16
  36. package/src/components/shared/data-display/SVirtualList.vue +18 -2
  37. package/src/components/shared/data-display/json.ts +1 -1
  38. package/src/components/shared/data-display/table.ts +9 -28
  39. package/src/components/shared/database/SDataGrid.vue +29 -8
  40. package/src/components/shared/database/SSqlEditor.vue +3 -2
  41. package/src/components/shared/database/dataGrid.ts +1 -17
  42. package/src/components/shared/navigation/SBottomNav.vue +3 -0
  43. package/src/components/shared/navigation/STabs.vue +2 -1
  44. package/src/composables/uiPreferencesReset.ts +2 -1
  45. package/src/composables/useClipboard.ts +2 -1
  46. package/src/composables/useSidebarPanelState.ts +3 -2
  47. package/src/internal/es2020.ts +50 -0
  48. package/src/internal/inlineTokenEditorContract.ts +4 -3
  49. package/src/internal/ownedAttrs.ts +24 -2
  50. package/src/internal/pointerInteractionLease.ts +2 -1
@@ -1,4 +1,3 @@
1
- import type { Component } from 'vue'
2
1
  import type {
3
2
  HistoryState,
4
3
  LocationQueryRaw,
@@ -67,19 +66,4 @@ export type RouterLinkDestination = LinkDestinationOptions & {
67
66
  /** Строгий XOR-контракт обычного destination. / Strict XOR contract for an ordinary destination. */
68
67
  export type LinkDestination = HrefLinkDestination | RouterLinkDestination
69
68
 
70
- /** Canonical presentation/state API owned by SLink. */
71
- export interface LinkPresentationProps {
72
- readonly variant?: 'default' | 'subtle' | 'brand' | 'navigation' | 'bottom-navigation' | 'document'
73
- readonly external?: boolean
74
- readonly active?: boolean
75
- readonly disabled?: boolean
76
- readonly width?: 'fit' | 'full'
77
- readonly size?: 'inherit' | 'sm' | 'md'
78
- readonly underline?: 'hover' | 'always' | 'none'
79
- readonly wrap?: 'normal' | 'anywhere'
80
- readonly contentKind?: 'text' | 'identifier'
81
- readonly leadingIcon?: Component
82
- }
83
-
84
- /** Полный discriminated public props contract SLink. / Complete discriminated public SLink props contract. */
85
- export type SLinkProps = LinkDestination & LinkPresentationProps
69
+ export type { LinkPresentationProps, Props as SLinkProps } from './SLink.vue'
@@ -1,5 +1,6 @@
1
1
  <script setup lang="ts">
2
2
  import {
3
+ type Component,
3
4
  computed,
4
5
  type VNodeChild,
5
6
  } from 'vue'
@@ -12,13 +13,36 @@ import {
12
13
  type PassiveContentContract,
13
14
  } from '../../../internal/passiveContentContract'
14
15
  import type {
15
- SLinkProps,
16
+ LinkBrowsingTarget,
17
+ LinkRouterTarget,
16
18
  } from './SLink'
17
19
 
18
20
  defineOptions({ inheritAttrs: false })
19
21
 
22
+ /** Canonical presentation/state API owned by SLink. */
23
+ export interface LinkPresentationProps {
24
+ readonly variant?: 'default' | 'subtle' | 'brand' | 'navigation' | 'bottom-navigation' | 'document'
25
+ readonly external?: boolean
26
+ readonly active?: boolean
27
+ readonly disabled?: boolean
28
+ readonly width?: 'fit' | 'full'
29
+ readonly size?: 'inherit' | 'sm' | 'md'
30
+ readonly underline?: 'hover' | 'always' | 'none'
31
+ readonly wrap?: 'normal' | 'anywhere'
32
+ readonly contentKind?: 'text' | 'identifier'
33
+ readonly leadingIcon?: Component
34
+ }
35
+
36
+ interface LinkDestinationOptions {
37
+ readonly target?: LinkBrowsingTarget
38
+ readonly rel?: string
39
+ }
40
+
20
41
  /** Exact href/to XOR plus the canonical link presentation API. */
21
- export type Props = SLinkProps
42
+ export type Props = LinkPresentationProps & LinkDestinationOptions & (
43
+ | { readonly href: string; readonly to?: never }
44
+ | { readonly href?: never; readonly to: LinkRouterTarget }
45
+ )
22
46
 
23
47
  const props = withDefaults(defineProps<Props>(), {
24
48
  href: undefined,
@@ -6,6 +6,7 @@
6
6
  :data-variant="resolvedVariant"
7
7
  :data-invalid="resolvedInvalid || undefined"
8
8
  :data-readonly="resolvedReadonly || undefined"
9
+ :data-focus-mode="resolvedFocusMode"
9
10
  role="listbox"
10
11
  :aria-label="resolvedLabel"
11
12
  :aria-multiselectable="resolvedSelectionMode === 'multiple' || undefined"
@@ -17,23 +18,31 @@
17
18
  <SListboxOption
18
19
  v-if="resolvedClearOptionLabel"
19
20
  :registration-token="CLEAR_OPTION_TOKEN"
21
+ :id="optionId(CLEAR_OPTION_TOKEN)"
20
22
  :label="resolvedClearOptionLabel"
21
23
  :title="resolvedClearOptionLabel"
22
24
  :selected="modelValue === null"
23
25
  :disabled="resolvedDisabled"
24
26
  :appearance="optionAppearance"
27
+ :active="activeToken === CLEAR_OPTION_TOKEN"
28
+ :focus-on-select="resolvedFocusMode === 'roving'"
29
+ @hover="setActiveToken(CLEAR_OPTION_TOKEN)"
25
30
  @select="clearSelection"
26
31
  />
27
32
  <template v-for="entry in inventory.entries" :key="entry.token">
28
33
  <SListboxOption
29
34
  v-if="entry.kind === 'option'"
30
35
  :registration-token="entry.record.token"
36
+ :id="optionId(entry.record.token)"
31
37
  :label="entry.record.option.label"
32
38
  :title="entry.record.option.title ?? entry.record.option.label"
33
39
  :selected="selectedTokens.has(entry.record.token)"
34
40
  :disabled="resolvedDisabled || entry.record.option.disabled"
35
41
  :marker="entry.record.option.marker"
36
42
  :appearance="optionAppearance"
43
+ :active="activeToken === entry.record.token"
44
+ :focus-on-select="resolvedFocusMode === 'roving'"
45
+ @hover="setActiveToken(entry.record.token)"
37
46
  @select="selectOption(entry.record.token)"
38
47
  />
39
48
  <SListboxGroup
@@ -46,12 +55,16 @@
46
55
  v-for="record in entry.records"
47
56
  :key="record.token"
48
57
  :registration-token="record.token"
58
+ :id="optionId(record.token)"
49
59
  :label="record.option.label"
50
60
  :title="record.option.title ?? record.option.label"
51
61
  :selected="selectedTokens.has(record.token)"
52
62
  :disabled="resolvedDisabled || record.option.disabled"
53
63
  :marker="record.option.marker"
54
64
  :appearance="optionAppearance"
65
+ :active="activeToken === record.token"
66
+ :focus-on-select="resolvedFocusMode === 'roving'"
67
+ @hover="setActiveToken(record.token)"
55
68
  @select="selectOption(record.token)"
56
69
  />
57
70
  </SListboxGroup>
@@ -73,6 +86,8 @@ import type {
73
86
  } from '../_internal/selectionContract'
74
87
 
75
88
  export type SListboxVariant = 'select' | 'color'
89
+ export type SListboxFocusMode = 'roving' | 'activedescendant'
90
+ export type SListboxMoveIntent = 'next' | 'previous' | 'first' | 'last'
76
91
  export type SListboxOption<TKey extends PublicSelectionKey = PublicSelectionKey> = PublicSelectionOption<TKey>
77
92
  export type SListboxOptionGroup<TKey extends PublicSelectionKey = PublicSelectionKey> = PublicSelectionOptionGroup<TKey>
78
93
  export type SListboxEntry<TKey extends PublicSelectionKey = PublicSelectionKey> = PublicSelectionEntry<TKey>
@@ -106,6 +121,8 @@ interface SharedProps<TKey extends PublicSelectionKey> {
106
121
  emptyLabel?: string
107
122
  clearOptionLabel?: string
108
123
  id?: string
124
+ focusMode?: SListboxFocusMode
125
+ activeValue?: TKey | null
109
126
  }
110
127
 
111
128
  export type SListboxProps<
@@ -154,9 +171,12 @@ const props = withDefaults(defineProps<ComponentProps<TKey>>(), {
154
171
  emptyLabel: 'Нет доступных вариантов',
155
172
  clearOptionLabel: undefined,
156
173
  id: undefined,
174
+ focusMode: 'roving',
175
+ activeValue: undefined,
157
176
  })
158
177
  const emit = defineEmits<{
159
178
  'update:modelValue': [value: TKey | null] | [value: readonly TKey[]]
179
+ 'update:activeValue': [value: TKey | null]
160
180
  'option-keydown': [payload: SListboxOptionKeydownPayload<TKey>]
161
181
  escape: []
162
182
  tab: [event: KeyboardEvent]
@@ -187,6 +207,9 @@ const contract = computed(() => {
187
207
  disabled: validateBoolean('SListbox', 'disabled', props.disabled),
188
208
  readonly: validateBoolean('SListbox', 'readonly', props.readonly),
189
209
  invalid: validateBoolean('SListbox', 'invalid', props.invalid),
210
+ focusMode: validateExactString(
211
+ 'SListbox', 'focusMode', props.focusMode, ['roving', 'activedescendant'] as const,
212
+ ),
190
213
  selectionMode,
191
214
  clearOptionLabel,
192
215
  }
@@ -199,6 +222,7 @@ const resolvedDisabled = computed(() => contract.value.disabled)
199
222
  const resolvedReadonly = computed(() => contract.value.readonly)
200
223
  const resolvedInvalid = computed(() => contract.value.invalid)
201
224
  const resolvedSelectionMode = computed(() => contract.value.selectionMode)
225
+ const resolvedFocusMode = computed(() => contract.value.focusMode)
202
226
  const resolvedClearOptionLabel = computed(() => contract.value.clearOptionLabel)
203
227
  const selectionState = computed(() => {
204
228
  const resolvedInventory = resolveSelectionInventory<TKey>('SListbox', props.options)
@@ -213,6 +237,25 @@ const inventory = computed(() => selectionState.value.inventory)
213
237
  const selectedTokens = computed(() => selectionState.value.selectedTokens)
214
238
  const optionAppearance = computed(() => resolvedVariant.value === 'color' ? 'color' : 'select')
215
239
  const CLEAR_OPTION_TOKEN = 'clear-option'
240
+ const activeToken = computed(() => {
241
+ if (resolvedFocusMode.value === 'roving') return undefined
242
+ if (resolvedSelectionMode.value !== 'single') {
243
+ throw new Error('SListbox: activedescendant focus mode supports single selection only')
244
+ }
245
+ if (props.activeValue === undefined) {
246
+ throw new TypeError('SListbox: activeValue is required in activedescendant focus mode')
247
+ }
248
+ if (props.activeValue === null) return null
249
+ const token = selectionKeyToken('SListbox', props.activeValue, 'activeValue')
250
+ if (!inventory.value.optionsByToken.has(token)) {
251
+ throw new Error('SListbox: activeValue does not match any option')
252
+ }
253
+ return token
254
+ })
255
+
256
+ function optionId(token: string): string {
257
+ return `${resolvedId.value}-option-${encodeURIComponent(token)}`
258
+ }
216
259
 
217
260
  function emitModelValue(value: TKey | null | readonly TKey[]): void {
218
261
  if (contract.value.selectionMode === 'multiple') {
@@ -281,6 +324,7 @@ function enabledRegistrations(): ListboxOptionRegistration[] {
281
324
  function initializeTabstop(): void {
282
325
  const options = enabledRegistrations()
283
326
  for (const option of registrations) option.element.tabIndex = -1
327
+ if (resolvedFocusMode.value === 'activedescendant') return
284
328
  const selected = options.find((option) => option.selected())
285
329
  const target = selected ?? options[0]
286
330
  if (target) target.element.tabIndex = 0
@@ -300,6 +344,56 @@ function focusOption(target: ListboxOptionRegistration, options = enabledRegistr
300
344
  target.element.focus()
301
345
  }
302
346
 
347
+ function setActiveToken(token: string): void {
348
+ if (resolvedFocusMode.value !== 'activedescendant') return
349
+ const target = enabledRegistrations().find((option) => option.token === token)
350
+ if (!target) return
351
+ emit('update:activeValue', optionValue(target))
352
+ }
353
+
354
+ function activeRegistration(options: ListboxOptionRegistration[]): ListboxOptionRegistration | undefined {
355
+ const token = activeToken.value
356
+ return token === undefined || token === null
357
+ ? undefined
358
+ : options.find((option) => option.token === token)
359
+ }
360
+
361
+ function initializeActive(): void {
362
+ if (resolvedFocusMode.value !== 'activedescendant') {
363
+ throw new Error('SListbox: initializeActive is available only in activedescendant focus mode')
364
+ }
365
+ const options = enabledRegistrations()
366
+ const target = activeRegistration(options) ?? options.find((option) => option.selected()) ?? options[0]
367
+ if (target) emit('update:activeValue', optionValue(target))
368
+ }
369
+
370
+ function moveActive(intent: SListboxMoveIntent): void {
371
+ if (resolvedFocusMode.value !== 'activedescendant') {
372
+ throw new Error('SListbox: moveActive is available only in activedescendant focus mode')
373
+ }
374
+ const options = enabledRegistrations()
375
+ if (options.length === 0) return
376
+ const current = activeRegistration(options)
377
+ const currentIndex = current === undefined ? -1 : options.indexOf(current)
378
+ const nextIndex = intent === 'first'
379
+ ? 0
380
+ : intent === 'last'
381
+ ? options.length - 1
382
+ : intent === 'next'
383
+ ? (Math.max(currentIndex, -1) + 1) % options.length
384
+ : (currentIndex <= 0 ? options.length : currentIndex) - 1
385
+ const target = options[nextIndex]
386
+ if (target) emit('update:activeValue', optionValue(target))
387
+ }
388
+
389
+ function selectActive(): void {
390
+ if (resolvedFocusMode.value !== 'activedescendant') {
391
+ throw new Error('SListbox: selectActive is available only in activedescendant focus mode')
392
+ }
393
+ if (resolvedReadonly.value || resolvedDisabled.value) return
394
+ activeRegistration(enabledRegistrations())?.activate()
395
+ }
396
+
303
397
  function currentOption(
304
398
  options: ListboxOptionRegistration[],
305
399
  event: KeyboardEvent,
@@ -319,6 +413,7 @@ function optionValue(registration: ListboxOptionRegistration): TKey | null {
319
413
  }
320
414
 
321
415
  function handleKeydown(event: KeyboardEvent): void {
416
+ if (resolvedFocusMode.value === 'activedescendant') return
322
417
  if (event.key === 'Tab') {
323
418
  emit('tab', event)
324
419
  return
@@ -376,13 +471,16 @@ function handleKeydown(event: KeyboardEvent): void {
376
471
  }
377
472
 
378
473
  function focusSelectedOrFirst(): void {
474
+ if (resolvedFocusMode.value !== 'roving') {
475
+ throw new Error('SListbox: focusSelectedOrFirst is available only in roving focus mode')
476
+ }
379
477
  const options = enabledRegistrations()
380
478
  const target = options.find((option) => option.selected()) ?? options[0]
381
479
  if (target) focusOption(target, options)
382
480
  }
383
481
 
384
482
  onBeforeUnmount(() => { if (typeaheadTimer) clearTimeout(typeaheadTimer) })
385
- defineExpose({ focusSelectedOrFirst })
483
+ defineExpose({ focusSelectedOrFirst, initializeActive, moveActive, selectActive })
386
484
  </script>
387
485
 
388
486
  <style lang="postcss" src="./SListbox.css" scoped></style>
@@ -1,9 +1,10 @@
1
1
  <template>
2
2
  <li
3
3
  ref="optionRef"
4
+ :id="id"
4
5
  class="s-listbox-option__control"
5
6
  :data-appearance="appearance"
6
- :data-focused="focused || undefined"
7
+ :data-focused="focused || active || undefined"
7
8
  :title="title ?? label"
8
9
  :aria-label="appearance === 'color' ? label : undefined"
9
10
  role="option"
@@ -12,6 +13,8 @@
12
13
  :tabindex="-1"
13
14
  @focus="focused = true"
14
15
  @blur="focused = false"
16
+ @mousedown="onMousedown"
17
+ @mouseenter="emit('hover')"
15
18
  @click.stop="select"
16
19
  >
17
20
  <span class="s-listbox-option__main" :data-appearance="appearance">
@@ -42,6 +45,9 @@ export interface Props {
42
45
  appearance?: 'select' | 'color'
43
46
  marker?: MarkerContract
44
47
  title?: string
48
+ id?: string
49
+ active?: boolean
50
+ focusOnSelect?: boolean
45
51
  }
46
52
 
47
53
  defineOptions({ inheritAttrs: false })
@@ -51,10 +57,14 @@ const props = withDefaults(defineProps<Props>(), {
51
57
  appearance: 'select',
52
58
  marker: undefined,
53
59
  title: undefined,
60
+ id: undefined,
61
+ active: false,
62
+ focusOnSelect: true,
54
63
  })
55
64
 
56
65
  const emit = defineEmits<{
57
66
  select: []
67
+ hover: []
58
68
  }>()
59
69
  useInteractiveLeafRegistration({ owner: 'SListboxOption' })
60
70
  const context = inject(listboxContextKey)
@@ -64,9 +74,12 @@ const optionRef = ref<HTMLLIElement | null>(null)
64
74
  let unregister: (() => void) | undefined
65
75
  const select = (): void => {
66
76
  if (props.disabled) return
67
- optionRef.value?.focus()
77
+ if (props.focusOnSelect) optionRef.value?.focus()
68
78
  emit('select')
69
79
  }
80
+ const onMousedown = (event: MouseEvent): void => {
81
+ if (!props.focusOnSelect) event.preventDefault()
82
+ }
70
83
  onMounted(() => {
71
84
  const element = optionRef.value
72
85
  if (!(element instanceof HTMLLIElement)) throw new Error('SListboxOption: option element is unavailable')
@@ -0,0 +1,48 @@
1
+ @reference "../../../styles/reference.css";
2
+
3
+ .s-switch-control {
4
+ @apply relative inline-flex shrink-0 items-center;
5
+ width: 2.5rem;
6
+ height: 1.5rem;
7
+ }
8
+
9
+ .s-switch-input {
10
+ @apply absolute inset-0 z-10 m-0 cursor-pointer opacity-0;
11
+ }
12
+
13
+ .s-switch-track {
14
+ @apply flex h-5 w-9 items-center rounded-full border border-surface-300 bg-surface-200 p-0.5 transition-colors;
15
+ @apply dark:border-surface-600 dark:bg-surface-700;
16
+ transition-duration: var(--s-motion-standard);
17
+ }
18
+
19
+ .s-switch-thumb {
20
+ @apply block h-3.5 w-3.5 rounded-full bg-surface-0 shadow-sm transition-transform;
21
+ @apply dark:bg-surface-100;
22
+ transition-duration: var(--s-motion-standard);
23
+ }
24
+
25
+ .s-switch-input:checked + .s-switch-track {
26
+ @apply border-primary-500 bg-primary-500 dark:border-primary-400 dark:bg-primary-400;
27
+ }
28
+
29
+ .s-switch-input:checked + .s-switch-track > .s-switch-thumb {
30
+ transform: translateX(1rem);
31
+ }
32
+
33
+ .s-switch-input:not(:disabled):hover + .s-switch-track {
34
+ @apply border-primary-500 dark:border-primary-400;
35
+ }
36
+
37
+ .s-switch-input:focus-visible + .s-switch-track {
38
+ outline: var(--s-focus-ring-width) solid var(--s-focus-ring-strong-color);
39
+ outline-offset: var(--s-focus-ring-offset);
40
+ }
41
+
42
+ .s-switch-input:disabled {
43
+ @apply cursor-not-allowed;
44
+ }
45
+
46
+ .s-switch-input:disabled + .s-switch-track {
47
+ @apply border-surface-200 bg-surface-100 opacity-70 dark:border-surface-700 dark:bg-surface-800;
48
+ }
@@ -0,0 +1,109 @@
1
+ <template>
2
+ <SField
3
+ :control-id="inputId"
4
+ :label="label"
5
+ :required="required"
6
+ :help-text="helpText"
7
+ :error-message="errorMessage"
8
+ :invalid="invalid"
9
+ :disabled="disabled"
10
+ :readonly="readonly"
11
+ layout="inline"
12
+ >
13
+ <template #default="field">
14
+ <span class="s-switch-control">
15
+ <input
16
+ v-bind="ownedAttrs.bindings()"
17
+ :id="field.controlId"
18
+ type="checkbox"
19
+ role="switch"
20
+ class="s-switch-input"
21
+ :checked="checkedValue"
22
+ :disabled="field.disabled"
23
+ :required="field.required"
24
+ :aria-checked="checkedValue"
25
+ :aria-invalid="field.invalid || undefined"
26
+ :aria-readonly="field.readonly || undefined"
27
+ :aria-describedby="mergeIdReferences(ownedAttrs.string('aria-describedby'), field.describedBy)"
28
+ @click.stop="onClick"
29
+ @change="onChange"
30
+ />
31
+ <span class="s-switch-track" aria-hidden="true">
32
+ <span class="s-switch-thumb" />
33
+ </span>
34
+ </span>
35
+ </template>
36
+ </SField>
37
+ </template>
38
+
39
+ <script setup lang="ts">
40
+ import { computed, useId } from 'vue'
41
+ import { mergeIdReferences, useOwnedAttrs } from '../../../internal/ownedAttrs'
42
+ import { useInteractiveLeafRegistration } from '../../../internal/passiveContentContract'
43
+ import { resolveOptionalDomId } from '../../../internal/runtimeContract'
44
+ import SField from './SField.vue'
45
+
46
+ defineOptions({ inheritAttrs: false })
47
+
48
+ export interface Props {
49
+ modelValue: boolean
50
+ label?: string
51
+ disabled?: boolean
52
+ readonly?: boolean
53
+ required?: boolean
54
+ invalid?: boolean
55
+ helpText?: string
56
+ errorMessage?: string
57
+ id?: string
58
+ }
59
+
60
+ const props = withDefaults(defineProps<Props>(), {
61
+ label: undefined,
62
+ disabled: false,
63
+ readonly: false,
64
+ required: false,
65
+ invalid: false,
66
+ helpText: undefined,
67
+ errorMessage: undefined,
68
+ id: undefined,
69
+ })
70
+ const emit = defineEmits<{
71
+ 'update:modelValue': [value: boolean]
72
+ click: [event: MouseEvent]
73
+ }>()
74
+ const ownedAttrs = useOwnedAttrs({ component: 'SSwitch', owner: 'native switch input' })
75
+ useInteractiveLeafRegistration({ owner: 'SSwitch' })
76
+
77
+ const generatedInputId = useId()
78
+ const inputId = computed(() => resolveOptionalDomId(
79
+ 'SSwitch',
80
+ 'id',
81
+ props.id,
82
+ `s-switch-${generatedInputId}`,
83
+ ))
84
+ const checkedValue = computed(() => {
85
+ if (typeof props.modelValue !== 'boolean') {
86
+ throw new TypeError('SSwitch: modelValue must be boolean')
87
+ }
88
+ return props.modelValue
89
+ })
90
+
91
+ function onChange(event: Event): void {
92
+ if (props.readonly) return
93
+ const target = event.currentTarget
94
+ if (!(target instanceof HTMLInputElement)) {
95
+ throw new TypeError('SSwitch: change event owner must be the native switch input')
96
+ }
97
+ emit('update:modelValue', target.checked)
98
+ }
99
+
100
+ function onClick(event: MouseEvent): void {
101
+ if (props.readonly) {
102
+ event.preventDefault()
103
+ return
104
+ }
105
+ emit('click', event)
106
+ }
107
+ </script>
108
+
109
+ <style lang="postcss" src="./SSwitch.css" scoped></style>
@@ -4,6 +4,7 @@ import {
4
4
  createTextVNode,
5
5
  defineComponent,
6
6
  h,
7
+ type Component,
7
8
  type ComponentPublicInstance,
8
9
  type PropType,
9
10
  } from 'vue'
@@ -14,7 +15,9 @@ import type { InlineTokenEditorSegmentContract } from '../../../../internal/inli
14
15
  import type { OwnedAttributeBindings } from '../../../../internal/ownedAttrs'
15
16
  import { useInteractiveLeafRegistration } from '../../../../internal/passiveContentContract'
16
17
  import { validateBoundedInteger } from '../../../../internal/runtimeContract'
17
- import SButton, { type Props as SButtonProps } from '../SButton.vue'
18
+ import SButton, { type SButtonActionProps } from '../SButton.vue'
19
+
20
+ const actionButtonComponent: Component = SButton
18
21
 
19
22
  /** Текст визуального caret helper. / Visual caret-helper text. */
20
23
  export const INLINE_TOKEN_EDITOR_VISUAL_CARET_TEXT = '\u200B'
@@ -35,7 +38,7 @@ export type InlineTokenElementRef = (
35
38
  refs: Record<string, unknown>,
36
39
  ) => void
37
40
 
38
- interface InlineTokenActionButtonBindings extends SButtonProps {
41
+ interface InlineTokenActionButtonBindings extends SButtonActionProps {
39
42
  readonly 'aria-label': string
40
43
  readonly title?: string
41
44
  readonly onClick: (event: MouseEvent) => void
@@ -161,7 +164,7 @@ export default defineComponent({
161
164
  onClick: (event) => emitTokenAction(event, action, segment),
162
165
  onKeydown: stopKeyPropagation,
163
166
  }
164
- return h(SButton, { ...bindings, key: action })
167
+ return h(actionButtonComponent, { ...bindings, key: action })
165
168
  }
166
169
 
167
170
  return () => h('div', {
@@ -0,0 +1,59 @@
1
+ @reference "../../../styles/reference.css";
2
+
3
+ .s-chip {
4
+ @apply inline-flex min-w-0 items-center rounded-full font-medium;
5
+ }
6
+
7
+ .s-chip[data-size='md'] {
8
+ @apply min-h-7 gap-1.5 px-2.5 text-xs;
9
+ }
10
+
11
+ .s-chip[data-size='md'][data-closable='true'] {
12
+ @apply pr-1;
13
+ }
14
+
15
+ .s-chip[data-size='sm'] {
16
+ @apply min-h-6 gap-1 px-2 text-[11px] leading-4;
17
+ }
18
+
19
+ .s-chip[data-size='sm'][data-closable='true'] {
20
+ @apply pr-0.5;
21
+ }
22
+
23
+ .s-chip__icon {
24
+ @apply inline-flex h-3.5 w-3.5 shrink-0 items-center justify-center;
25
+ }
26
+
27
+ .s-chip__icon-graphic {
28
+ @apply h-full w-full;
29
+ }
30
+
31
+ .s-chip__label {
32
+ @apply min-w-0 overflow-hidden text-ellipsis whitespace-nowrap;
33
+ }
34
+
35
+ .s-chip[data-disabled='true'] {
36
+ @apply opacity-60;
37
+ }
38
+
39
+ .s-chip[data-severity='primary'] {
40
+ @apply bg-primary-100 text-primary-800 dark:bg-primary-950 dark:text-primary-50;
41
+ }
42
+ .s-chip[data-severity='secondary'] {
43
+ @apply bg-surface-100 text-surface-800 dark:bg-surface-800 dark:text-surface-50;
44
+ }
45
+ .s-chip[data-severity='success'] {
46
+ @apply bg-success-100 text-success-800 dark:bg-success-950 dark:text-success-50;
47
+ }
48
+ .s-chip[data-severity='info'] {
49
+ @apply bg-info-100 text-info-800 dark:bg-info-950 dark:text-info-50;
50
+ }
51
+ .s-chip[data-severity='warn'] {
52
+ @apply bg-warn-100 text-warn-800 dark:bg-warn-950 dark:text-warn-50;
53
+ }
54
+ .s-chip[data-severity='danger'] {
55
+ @apply bg-danger-100 text-danger-800 dark:bg-danger-950 dark:text-danger-50;
56
+ }
57
+ .s-chip[data-severity='contrast'] {
58
+ @apply bg-surface-800 text-surface-100 dark:bg-surface-200 dark:text-surface-800;
59
+ }