@pgcorp/ui-kit 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/README.md +39 -0
  2. package/docs/public-api.md +6 -0
  3. package/package.json +5 -1
  4. package/src/components/shared/_internal/SHeaderSurface.css +98 -0
  5. package/src/components/shared/_internal/SHeaderSurface.vue +80 -0
  6. package/src/components/shared/_internal/SToastItem.css +8 -8
  7. package/src/components/shared/_internal/SToastItem.vue +8 -1
  8. package/src/components/shared/_internal/useRadioGroup.ts +106 -0
  9. package/src/components/shared/containers/SPageHeader.vue +13 -20
  10. package/src/components/shared/controls/SButton.css +6 -6
  11. package/src/components/shared/controls/SButton.vue +9 -1
  12. package/src/components/shared/controls/SCheckbox.vue +14 -43
  13. package/src/components/shared/controls/SChoiceCards.vue +16 -39
  14. package/src/components/shared/controls/SField.css +0 -12
  15. package/src/components/shared/controls/SField.vue +14 -10
  16. package/src/components/shared/controls/SFieldGroup.css +1 -19
  17. package/src/components/shared/controls/SFieldGroup.vue +13 -10
  18. package/src/components/shared/controls/SFieldLabel.css +0 -1
  19. package/src/components/shared/controls/SFieldLabel.vue +2 -1
  20. package/src/components/shared/controls/SInteractiveSurface.css +3 -3
  21. package/src/components/shared/controls/SInteractiveSurface.vue +9 -2
  22. package/src/components/shared/controls/SMarker.css +16 -16
  23. package/src/components/shared/controls/SMarker.vue +27 -1
  24. package/src/components/shared/controls/SSegmentedControl.vue +15 -43
  25. package/src/components/shared/controls/SSwitch.vue +14 -32
  26. package/src/components/shared/data-display/SActionListItem.css +2 -2
  27. package/src/components/shared/data-display/SActionListItem.vue +8 -1
  28. package/src/components/shared/data-display/SBadge.vue +41 -18
  29. package/src/components/shared/data-display/SChip.vue +22 -10
  30. package/src/components/shared/data-display/SMessage.css +8 -8
  31. package/src/components/shared/data-display/SMessage.vue +8 -1
  32. package/src/components/shared/data-display/SProgressBar.css +12 -12
  33. package/src/components/shared/data-display/SProgressBar.vue +12 -6
  34. package/src/components/shared/data-display/SProgressIndicator.css +5 -5
  35. package/src/components/shared/data-display/SProgressIndicator.vue +9 -1
  36. package/src/components/shared/data-display/SSectionHeader.vue +17 -21
  37. package/src/components/shared/data-display/SStatus.css +10 -10
  38. package/src/components/shared/data-display/SStatus.vue +9 -1
  39. package/src/components/shared/data-display/STable.vue +27 -4
  40. package/src/components/shared/data-display/STooltip.css +4 -4
  41. package/src/components/shared/data-display/STooltip.vue +159 -29
  42. package/src/components/shared/data-display/STooltipTarget.css +26 -0
  43. package/src/components/shared/data-display/STooltipTarget.vue +94 -0
  44. package/src/components/shared/database/SDataGrid.vue +13 -0
  45. package/src/components/shared/navigation/STabs.vue +11 -3
  46. package/src/internal/fieldPresentation.ts +28 -0
  47. package/src/internal/ownedAttrs.ts +3 -0
  48. package/src/internal/pillSurface.ts +93 -0
  49. package/src/internal/semanticTone.ts +23 -0
  50. package/src/internal/useBinaryInput.ts +76 -0
  51. package/src/styles/style.css +114 -0
  52. package/src/styles/tokens.css +31 -0
  53. package/src/components/shared/containers/SPageHeader.css +0 -82
  54. package/src/components/shared/data-display/SBadge.css +0 -73
  55. package/src/components/shared/data-display/SChip.css +0 -59
  56. package/src/components/shared/data-display/SSectionHeader.css +0 -84
@@ -43,11 +43,10 @@
43
43
  </template>
44
44
 
45
45
  <script setup lang="ts">
46
- import { computed, onMounted, ref, useId, watch } from 'vue'
47
46
  import { Check, Minus } from '../../icons/sputnigUiIcons'
48
47
  import { mergeIdReferences, useOwnedAttrs } from '../../../internal/ownedAttrs'
49
48
  import { useInteractiveLeafRegistration } from '../../../internal/passiveContentContract'
50
- import { resolveOptionalDomId } from '../../../internal/runtimeContract'
49
+ import { useBinaryInput } from '../../../internal/useBinaryInput'
51
50
  import SField from './SField.vue'
52
51
 
53
52
  defineOptions({ inheritAttrs: false })
@@ -83,48 +82,20 @@ const emit = defineEmits<{
83
82
  }>()
84
83
  const ownedAttrs = useOwnedAttrs({ component: 'SCheckbox', owner: 'native checkbox input' })
85
84
  useInteractiveLeafRegistration({ owner: 'SCheckbox' })
86
-
87
- const generatedInputId = useId()
88
- const inputId = computed(() => resolveOptionalDomId(
89
- 'SCheckbox',
90
- 'id',
91
- props.id,
92
- `s-checkbox-${generatedInputId}`,
93
- ))
94
- const inputEl = ref<HTMLInputElement | null>(null)
95
-
96
- const checkedValue = computed(() => {
97
- if (typeof props.modelValue !== 'boolean') {
98
- throw new TypeError('SCheckbox: modelValue must be boolean')
99
- }
100
- return props.modelValue
85
+ const {
86
+ checkedValue,
87
+ inputEl,
88
+ inputId,
89
+ onChange,
90
+ onClick,
91
+ } = useBinaryInput({
92
+ owner: 'SCheckbox',
93
+ inputName: 'checkbox',
94
+ props,
95
+ indeterminate: () => props.indeterminate,
96
+ emitUpdate: (value) => emit('update:modelValue', value),
97
+ emitClick: (event) => emit('click', event),
101
98
  })
102
-
103
- function applyIndeterminate(): void {
104
- if (inputEl.value) {
105
- inputEl.value.indeterminate = props.indeterminate && !checkedValue.value
106
- }
107
- }
108
-
109
- onMounted(applyIndeterminate)
110
- watch([() => props.indeterminate, checkedValue], applyIndeterminate)
111
-
112
- function onChange(event: Event): void {
113
- if (props.readonly) return
114
- const target = event.currentTarget
115
- if (!(target instanceof HTMLInputElement)) {
116
- throw new TypeError('SCheckbox: change event owner must be the native checkbox input')
117
- }
118
- emit('update:modelValue', target.checked)
119
- }
120
-
121
- function onClick(event: MouseEvent): void {
122
- if (props.readonly) {
123
- event.preventDefault()
124
- return
125
- }
126
- emit('click', event)
127
- }
128
99
  </script>
129
100
 
130
101
  <style lang="postcss" src="./SCheckbox.css" scoped></style>
@@ -16,26 +16,19 @@
16
16
  :data-columns="columns"
17
17
  :data-density="density"
18
18
  :data-selection-indicator="selectionIndicator"
19
- role="radiogroup"
20
- :aria-labelledby="labelId"
21
- :aria-describedby="describedBy"
22
- :aria-required="required || undefined"
23
- :aria-invalid="invalid || undefined"
24
- :aria-disabled="disabled || undefined"
25
- :aria-readonly="readonly || undefined"
19
+ v-bind="radioGroup.groupBindings({
20
+ labelledBy: labelId,
21
+ describedBy,
22
+ required,
23
+ invalid,
24
+ })"
26
25
  >
27
26
  <button
28
27
  v-for="option in options"
29
- :key="optionToken(option)"
30
- :ref="(element) => roving.register(option.value, resolveNativeElement(element))"
28
+ :key="radioGroup.optionToken(option.value)"
29
+ :ref="radioGroup.optionRef(option.value)"
31
30
  class="s-choice-card"
32
- type="button"
33
- :disabled="disabled || option.disabled"
34
- :aria-checked="option.value === modelValue"
35
- :tabindex="option.value === focusableValue ? 0 : -1"
36
- role="radio"
37
- @click="select(option.value)"
38
- @keydown="roving.handleKeydown($event, option.value)"
31
+ v-bind="radioGroup.optionBindings(option)"
39
32
  >
40
33
  <span v-if="option.kicker" class="s-choice-card__kicker">{{ option.kicker }}</span>
41
34
  <span class="s-choice-card__title">{{ option.title }}</span>
@@ -83,10 +76,10 @@ export interface Props<T extends PublicSelectionKey = string> {
83
76
 
84
77
  <script setup lang="ts" generic="T extends SelectionKey = string">
85
78
  import { computed } from 'vue'
86
- import type { ComponentPublicInstance, VNodeChild } from 'vue'
79
+ import type { VNodeChild } from 'vue'
87
80
  import { useOwnedAttrs } from '../../../internal/ownedAttrs'
88
81
  import { useInteractiveLeafRegistration } from '../../../internal/passiveContentContract'
89
- import { selectionKeyToken, useSelectionRoving } from '../_internal/useSelectionRoving'
82
+ import { useRadioGroup } from '../_internal/useRadioGroup'
90
83
  import type { SelectionKey } from '../_internal/useSelectionRoving'
91
84
  import SFieldGroup from './SFieldGroup.vue'
92
85
 
@@ -116,30 +109,14 @@ defineSlots<{
116
109
  }>()
117
110
  const ownedAttrs = useOwnedAttrs({ component: 'SChoiceCards', owner: 'field group' })
118
111
  useInteractiveLeafRegistration({ owner: 'SChoiceCards' })
119
- const select = (value: T): void => {
120
- if (props.disabled || props.readonly) return
121
- emit('update:modelValue', value)
122
- }
123
- const rovingOptions = computed<readonly ChoiceCardOption<T>[]>(() => (
124
- props.disabled
125
- ? props.options.map((option) => ({ ...option, disabled: true }))
126
- : props.options
127
- ))
128
- const roving = useSelectionRoving({
129
- options: rovingOptions,
112
+ const radioGroup = useRadioGroup({
113
+ options: computed(() => props.options),
130
114
  selectedValue: computed(() => props.modelValue),
131
- select,
115
+ disabled: computed(() => props.disabled),
116
+ readonly: computed(() => props.readonly),
117
+ update: (value) => emit('update:modelValue', value),
132
118
  component: 'SChoiceCards',
133
119
  })
134
- const focusableValue = roving.focusableValue
135
- const optionToken = (option: ChoiceCardOption<T>): string => selectionKeyToken(
136
- 'SChoiceCards',
137
- option.value,
138
- 'option.value',
139
- )
140
- const resolveNativeElement = (element: Element | ComponentPublicInstance | null): HTMLElement | null => (
141
- element instanceof HTMLElement ? element : null
142
- )
143
120
  </script>
144
121
 
145
122
  <style lang="postcss" src="./SChoiceCards.css" scoped></style>
@@ -53,24 +53,12 @@
53
53
  @apply flex items-center;
54
54
  }
55
55
 
56
- .s-field__required {
57
- @apply ml-0.5 text-danger-600 dark:text-danger-400;
58
- }
59
-
60
- .s-field__help,
61
- .s-field__error {
62
- @apply mt-1 min-w-0 whitespace-normal text-xs;
63
- overflow-wrap: anywhere;
64
- }
65
-
66
56
  .s-field__help {
67
57
  grid-area: help;
68
- @apply text-surface-500 dark:text-surface-400;
69
58
  }
70
59
 
71
60
  .s-field__error {
72
61
  grid-area: error;
73
- @apply text-danger-600 dark:text-danger-400;
74
62
  }
75
63
 
76
64
  .s-field[data-disabled='true'] .s-field__label,
@@ -10,7 +10,7 @@
10
10
  >
11
11
  <label v-if="hasLabel" :id="labelId" class="s-field__label" :for="controlId">
12
12
  <slot name="label">{{ label }}</slot>
13
- <span v-if="required" class="s-field__required" aria-hidden="true">*</span>
13
+ <span v-if="required" :class="['s-field__required', FIELD_REQUIRED_CLASS]" aria-hidden="true">*</span>
14
14
  </label>
15
15
 
16
16
  <div class="s-field__control">
@@ -25,10 +25,10 @@
25
25
  />
26
26
  </div>
27
27
 
28
- <p v-if="helpText || $slots.help" :id="helpId" class="s-field__help">
28
+ <p v-if="helpText || $slots.help" :id="helpId" :class="['s-field__help', FIELD_HELP_CLASS]">
29
29
  <slot name="help">{{ helpText }}</slot>
30
30
  </p>
31
- <p v-if="isInvalid && (errorMessage || $slots.error)" :id="errorId" class="s-field__error">
31
+ <p v-if="isInvalid && (errorMessage || $slots.error)" :id="errorId" :class="['s-field__error', FIELD_ERROR_CLASS]">
32
32
  <slot name="error">{{ errorMessage }}</slot>
33
33
  </p>
34
34
  </div>
@@ -36,6 +36,12 @@
36
36
 
37
37
  <script setup lang="ts">
38
38
  import { computed, useSlots } from 'vue'
39
+ import {
40
+ FIELD_ERROR_CLASS,
41
+ FIELD_HELP_CLASS,
42
+ FIELD_REQUIRED_CLASS,
43
+ useFieldSupportingText,
44
+ } from '../../../internal/fieldPresentation'
39
45
  import { useOwnedAttrs } from '../../../internal/ownedAttrs'
40
46
 
41
47
  defineOptions({ inheritAttrs: false })
@@ -76,13 +82,11 @@ const ownedAttrs = useOwnedAttrs({ component: 'SField', owner: 'field root' })
76
82
  const isInvalid = computed(() => props.invalid)
77
83
  const hasLabel = computed(() => Boolean(props.label || slots.label))
78
84
  const labelId = computed(() => `${props.controlId}-label`)
79
- const helpId = computed(() => `${props.controlId}-help`)
80
- const errorId = computed(() => `${props.controlId}-error`)
81
- const describedBy = computed(() => {
82
- const ids: string[] = []
83
- if (props.helpText || slots.help) ids.push(helpId.value)
84
- if (isInvalid.value && (props.errorMessage || slots.error)) ids.push(errorId.value)
85
- return ids.length > 0 ? ids.join(' ') : undefined
85
+ const fieldId = computed(() => props.controlId)
86
+ const { describedBy, errorId, helpId } = useFieldSupportingText({
87
+ baseId: fieldId,
88
+ hasHelp: () => Boolean(props.helpText || slots.help),
89
+ hasError: () => Boolean(isInvalid.value && (props.errorMessage || slots.error)),
86
90
  })
87
91
  </script>
88
92
 
@@ -8,10 +8,6 @@
8
8
  @apply mb-2 min-w-0 text-sm font-medium text-surface-700 dark:text-surface-300;
9
9
  }
10
10
 
11
- .s-field-group__required {
12
- @apply ml-0.5 text-danger-600 dark:text-danger-400;
13
- }
14
-
15
11
  .s-field-group__controls {
16
12
  @apply grid min-w-0 gap-2;
17
13
  }
@@ -32,23 +28,9 @@
32
28
  @apply grid-cols-4;
33
29
  }
34
30
 
35
- .s-field-group__help,
36
- .s-field-group__error {
37
- @apply mt-1 min-w-0 whitespace-normal text-xs;
38
- overflow-wrap: anywhere;
39
- }
40
-
41
- .s-field-group__help {
42
- @apply text-surface-500 dark:text-surface-400;
43
- }
44
-
45
- .s-field-group__error {
46
- @apply text-danger-600 dark:text-danger-400;
47
- }
48
-
49
31
  .s-field-group[data-disabled='true'] .s-field-group__legend,
50
32
  .s-field-group[data-readonly='true'] .s-field-group__legend {
51
- @apply text-surface-400 dark:text-surface-500;
33
+ color: var(--s-field-disabled-foreground);
52
34
  }
53
35
 
54
36
  @media (max-width: 40rem) {
@@ -14,7 +14,7 @@
14
14
  >
15
15
  <legend :id="labelId" class="s-field-group__legend">
16
16
  <slot name="label">{{ validatedLabel }}</slot>
17
- <span v-if="required" class="s-field-group__required" aria-hidden="true">*</span>
17
+ <span v-if="required" :class="['s-field-group__required', FIELD_REQUIRED_CLASS]" aria-hidden="true">*</span>
18
18
  </legend>
19
19
 
20
20
  <div class="s-field-group__controls">
@@ -29,10 +29,10 @@
29
29
  />
30
30
  </div>
31
31
 
32
- <p v-if="helpText || $slots.help" :id="helpId" class="s-field-group__help">
32
+ <p v-if="helpText || $slots.help" :id="helpId" :class="['s-field-group__help', FIELD_HELP_CLASS]">
33
33
  <slot name="help">{{ helpText }}</slot>
34
34
  </p>
35
- <p v-if="invalid && (errorMessage || $slots.error)" :id="errorId" class="s-field-group__error">
35
+ <p v-if="invalid && (errorMessage || $slots.error)" :id="errorId" :class="['s-field-group__error', FIELD_ERROR_CLASS]">
36
36
  <slot name="error">{{ errorMessage }}</slot>
37
37
  </p>
38
38
  </fieldset>
@@ -40,6 +40,12 @@
40
40
 
41
41
  <script setup lang="ts">
42
42
  import { computed, useId, useSlots } from 'vue'
43
+ import {
44
+ FIELD_ERROR_CLASS,
45
+ FIELD_HELP_CLASS,
46
+ FIELD_REQUIRED_CLASS,
47
+ useFieldSupportingText,
48
+ } from '../../../internal/fieldPresentation'
43
49
  import { mergeIdReferences, useOwnedAttrs } from '../../../internal/ownedAttrs'
44
50
  import {
45
51
  validateBoundedInteger,
@@ -92,13 +98,10 @@ const groupId = computed(() => validateDomId(
92
98
  ))
93
99
  const labelId = computed(() => `${groupId.value}-legend`)
94
100
  const validatedLabel = computed(() => validateNonEmptyString('SFieldGroup', 'label', props.label))
95
- const helpId = computed(() => `${groupId.value}-help`)
96
- const errorId = computed(() => `${groupId.value}-error`)
97
- const describedBy = computed(() => {
98
- const ids: string[] = []
99
- if (props.helpText || slots.help) ids.push(helpId.value)
100
- if (props.invalid && (props.errorMessage || slots.error)) ids.push(errorId.value)
101
- return ids.length > 0 ? ids.join(' ') : undefined
101
+ const { describedBy, errorId, helpId } = useFieldSupportingText({
102
+ baseId: groupId,
103
+ hasHelp: () => Boolean(props.helpText || slots.help),
104
+ hasError: () => Boolean(props.invalid && (props.errorMessage || slots.error)),
102
105
  })
103
106
  const validatedLayout = computed<SFieldGroupLayout>(() => validateExactString(
104
107
  'SFieldGroup',
@@ -9,4 +9,3 @@
9
9
  .s-field-label[data-size='md'] { @apply text-sm; }
10
10
  .s-field-label[data-align='end'] { @apply text-right; }
11
11
  .s-field-label[data-disabled='true'] { @apply text-surface-400 dark:text-surface-500; }
12
- .s-field-label__required { @apply ml-0.5 text-danger-600 dark:text-danger-400; }
@@ -8,12 +8,13 @@
8
8
  :data-disabled="disabled || undefined"
9
9
  >
10
10
  <slot />
11
- <span v-if="required" class="s-field-label__required" aria-hidden="true">*</span>
11
+ <span v-if="required" :class="['s-field-label__required', FIELD_REQUIRED_CLASS]" aria-hidden="true">*</span>
12
12
  </label>
13
13
  </template>
14
14
 
15
15
  <script setup lang="ts">
16
16
  import type { VNodeChild } from 'vue'
17
+ import { FIELD_REQUIRED_CLASS } from '../../../internal/fieldPresentation'
17
18
  import { useOwnedAttrs } from '../../../internal/ownedAttrs'
18
19
 
19
20
  defineOptions({ inheritAttrs: false })
@@ -51,9 +51,9 @@
51
51
  .s-interactive-surface[data-radius='md'] { @apply rounded-md; }
52
52
  .s-interactive-surface[data-radius='lg'] { @apply rounded-lg; }
53
53
  .s-interactive-surface[data-radius='xl'] { @apply rounded-xl; }
54
- .s-interactive-surface[data-tone='primary'] { @apply border-primary-200 bg-primary-500/5 dark:border-primary-500/30 dark:bg-primary-500/10; }
55
- .s-interactive-surface[data-tone='success'] { @apply border-success-200 bg-success-50 dark:border-success-800 dark:bg-success-950; }
56
- .s-interactive-surface[data-tone='danger'] { @apply border-danger-200 bg-danger-50 dark:border-danger-800 dark:bg-danger-950; }
54
+ .s-interactive-surface[data-s-tone='primary'] { @apply border-primary-200 bg-primary-500/5 dark:border-primary-500/30 dark:bg-primary-500/10; }
55
+ .s-interactive-surface[data-s-tone='success'] { @apply border-success-200 bg-success-50 dark:border-success-800 dark:bg-success-950; }
56
+ .s-interactive-surface[data-s-tone='danger'] { @apply border-danger-200 bg-danger-50 dark:border-danger-800 dark:bg-danger-950; }
57
57
  .s-interactive-surface[data-selected='true'] { @apply border-primary-400 bg-primary-50 dark:border-primary-600 dark:bg-primary-950; }
58
58
  .s-interactive-surface[data-pressed='true'] { @apply border-primary-500 bg-primary-100 dark:border-primary-500 dark:bg-primary-950/70; }
59
59
  .s-interactive-surface[data-disabled='true'] { @apply cursor-not-allowed opacity-60; }
@@ -5,6 +5,7 @@ import type { STooltipTriggerBinding } from '../data-display/STooltip.vue'
5
5
  import { hasOwn } from '../../../internal/es2020'
6
6
  import { resolveLinkTarget } from '../../../internal/linkTarget'
7
7
  import { useOwnedAttrs } from '../../../internal/ownedAttrs'
8
+ import { resolveSemanticToneBinding } from '../../../internal/semanticTone'
8
9
  import {
9
10
  PassiveContentBoundary,
10
11
  useInteractiveLeafRegistration,
@@ -14,6 +15,7 @@ import {
14
15
  defineOptions({ inheritAttrs: false })
15
16
 
16
17
  export type InteractiveSurfaceTone = 'default' | 'primary' | 'success' | 'danger'
18
+ const INTERACTIVE_SURFACE_TONES = ['default', 'primary', 'success', 'danger'] as const satisfies readonly InteractiveSurfaceTone[]
17
19
  export type InteractiveSurfaceDensity = 'compact' | 'comfortable'
18
20
  export type InteractiveSurfacePadding = 'none' | 'sm' | 'md'
19
21
  export type InteractiveSurfaceRadius = 'md' | 'lg' | 'xl'
@@ -84,6 +86,12 @@ defineSlots<{
84
86
  }>()
85
87
 
86
88
  const ownedAttrs = useOwnedAttrs({ component: 'SInteractiveSurface', owner: 'interactive surface owner' })
89
+ const toneBinding = computed(() => resolveSemanticToneBinding(
90
+ 'SInteractiveSurface',
91
+ 'tone',
92
+ props.tone,
93
+ INTERACTIVE_SURFACE_TONES,
94
+ ))
87
95
 
88
96
  const assertOptionalBoolean = (surface: Record<string, unknown>, field: 'disabled' | 'pressed'): void => {
89
97
  if (surface[field] !== undefined && typeof surface[field] !== 'boolean') {
@@ -213,7 +221,6 @@ const validatedTooltipTrigger = computed<STooltipTriggerBinding | undefined>(()
213
221
  }
214
222
  return candidate as STooltipTriggerBinding
215
223
  })
216
-
217
224
  const activationBindings = (): Readonly<Record<string, unknown>> => ({
218
225
  ...ownedAttrs.bindings(),
219
226
  ...(validatedTooltipTrigger.value ?? {}),
@@ -282,9 +289,9 @@ function activateAction(event: MouseEvent): void {
282
289
  <template>
283
290
  <div
284
291
  v-bind="surfaceMode === 'static' ? ownedAttrs.bindings() : undefined"
292
+ :data-s-tone="toneBinding['data-s-tone']"
285
293
  class="s-interactive-surface"
286
294
  :data-mode="surfaceMode"
287
- :data-tone="tone"
288
295
  :data-density="density"
289
296
  :data-padding="padding"
290
297
  :data-radius="radius"
@@ -1,19 +1,19 @@
1
1
  @reference "../../../styles/reference.css";
2
2
  .s-marker { @apply inline-block h-2 w-2 shrink-0 rounded-full bg-surface-400 ring-1 ring-inset ring-surface-500/50; }
3
3
  .s-marker[data-size='lg'] { @apply h-7 w-7 ring-2 ring-white/80 shadow-sm dark:ring-surface-950/80; }
4
- .s-marker[data-tone='primary'] { @apply bg-primary-500 ring-primary-600/50; }
5
- .s-marker[data-tone='success'] { @apply bg-success-500 ring-success-600/50; }
6
- .s-marker[data-tone='blue'] { @apply bg-blue-400 ring-blue-500/50; }
7
- .s-marker[data-tone='sky'] { @apply bg-sky-400 ring-sky-500/50; }
8
- .s-marker[data-tone='indigo'] { @apply bg-indigo-400 ring-indigo-500/50; }
9
- .s-marker[data-tone='emerald'] { @apply bg-emerald-400 ring-emerald-500/50; }
10
- .s-marker[data-tone='green'] { @apply bg-green-400 ring-green-500/50; }
11
- .s-marker[data-tone='teal'] { @apply bg-teal-400 ring-teal-500/50; }
12
- .s-marker[data-tone='amber'] { @apply bg-amber-400 ring-amber-500/50; }
13
- .s-marker[data-tone='orange'] { @apply bg-orange-400 ring-orange-500/50; }
14
- .s-marker[data-tone='rose'] { @apply bg-rose-400 ring-rose-500/50; }
15
- .s-marker[data-tone='pink'] { @apply bg-pink-400 ring-pink-500/50; }
16
- .s-marker[data-tone='fuchsia'] { @apply bg-fuchsia-400 ring-fuchsia-500/50; }
17
- .s-marker[data-tone='cyan'] { @apply bg-cyan-400 ring-cyan-500/50; }
18
- .s-marker[data-tone='lime'] { @apply bg-lime-400 ring-lime-500/50; }
19
- .s-marker[data-tone='custom'] { background-color: var(--s-marker-custom-color); --tw-ring-color: var(--s-marker-custom-color); }
4
+ .s-marker[data-s-tone='primary'] { @apply bg-primary-500 ring-primary-600/50; }
5
+ .s-marker[data-s-tone='success'] { @apply bg-success-500 ring-success-600/50; }
6
+ .s-marker[data-s-tone='blue'] { @apply bg-blue-400 ring-blue-500/50; }
7
+ .s-marker[data-s-tone='sky'] { @apply bg-sky-400 ring-sky-500/50; }
8
+ .s-marker[data-s-tone='indigo'] { @apply bg-indigo-400 ring-indigo-500/50; }
9
+ .s-marker[data-s-tone='emerald'] { @apply bg-emerald-400 ring-emerald-500/50; }
10
+ .s-marker[data-s-tone='green'] { @apply bg-green-400 ring-green-500/50; }
11
+ .s-marker[data-s-tone='teal'] { @apply bg-teal-400 ring-teal-500/50; }
12
+ .s-marker[data-s-tone='amber'] { @apply bg-amber-400 ring-amber-500/50; }
13
+ .s-marker[data-s-tone='orange'] { @apply bg-orange-400 ring-orange-500/50; }
14
+ .s-marker[data-s-tone='rose'] { @apply bg-rose-400 ring-rose-500/50; }
15
+ .s-marker[data-s-tone='pink'] { @apply bg-pink-400 ring-pink-500/50; }
16
+ .s-marker[data-s-tone='fuchsia'] { @apply bg-fuchsia-400 ring-fuchsia-500/50; }
17
+ .s-marker[data-s-tone='cyan'] { @apply bg-cyan-400 ring-cyan-500/50; }
18
+ .s-marker[data-s-tone='lime'] { @apply bg-lime-400 ring-lime-500/50; }
19
+ .s-marker[data-s-tone='custom'] { background-color: var(--s-marker-custom-color); --tw-ring-color: var(--s-marker-custom-color); }
@@ -1,8 +1,8 @@
1
1
  <template>
2
2
  <span
3
3
  v-bind="ownedAttrs.bindings()"
4
+ :data-s-tone="toneBinding['data-s-tone']"
4
5
  class="s-marker"
5
- :data-tone="marker.tone"
6
6
  :data-size="size"
7
7
  :style="customProperties"
8
8
  aria-hidden="true"
@@ -17,12 +17,38 @@ export type { SCustomMarker, SMarker, SMarkerTone } from '../_internal/markerCon
17
17
  <script setup lang="ts">
18
18
  import { computed } from 'vue'
19
19
  import { useOwnedAttrs } from '../../../internal/ownedAttrs'
20
+ import { resolveSemanticToneBinding } from '../../../internal/semanticTone'
20
21
  import { createCustomMarker, type SMarker } from '../_internal/markerContract'
21
22
 
22
23
  defineOptions({ inheritAttrs: false })
23
24
 
24
25
  const props = withDefaults(defineProps<{ marker: SMarker; size?: 'sm' | 'lg' }>(), { size: 'sm' })
25
26
  const ownedAttrs = useOwnedAttrs({ component: 'SMarker', owner: 'marker root' })
27
+ const MARKER_TONES = [
28
+ 'surface',
29
+ 'primary',
30
+ 'success',
31
+ 'blue',
32
+ 'sky',
33
+ 'indigo',
34
+ 'emerald',
35
+ 'green',
36
+ 'teal',
37
+ 'amber',
38
+ 'orange',
39
+ 'rose',
40
+ 'pink',
41
+ 'fuchsia',
42
+ 'cyan',
43
+ 'lime',
44
+ 'custom',
45
+ ] as const
46
+ const toneBinding = computed(() => resolveSemanticToneBinding(
47
+ 'SMarker',
48
+ 'marker.tone',
49
+ props.marker.tone,
50
+ MARKER_TONES,
51
+ ))
26
52
  const customProperties = computed(() => {
27
53
  if (props.marker.tone !== 'custom') return undefined
28
54
  createCustomMarker(props.marker.color)
@@ -17,27 +17,21 @@
17
17
  :data-option-wrap="optionWrap"
18
18
  :data-invalid="group.invalid || undefined"
19
19
  :data-readonly="group.readonly || undefined"
20
- role="radiogroup"
21
- :aria-labelledby="group.labelId"
22
- :aria-describedby="group.describedBy"
23
- :aria-invalid="group.invalid || undefined"
24
- :aria-required="group.required || undefined"
25
- :aria-readonly="group.readonly || undefined"
20
+ v-bind="radioGroup.groupBindings({
21
+ labelledBy: group.labelId,
22
+ describedBy: group.describedBy,
23
+ required: group.required,
24
+ invalid: group.invalid,
25
+ })"
26
26
  >
27
27
  <button
28
28
  v-for="option in options"
29
- :key="optionToken(option)"
30
- :ref="(element) => roving.register(option.value, resolveNativeElement(element))"
29
+ :key="radioGroup.optionToken(option.value)"
30
+ :ref="radioGroup.optionRef(option.value)"
31
31
  class="s-segmented-control__item"
32
32
  :data-size="size"
33
- type="button"
34
- :aria-checked="option.value === modelValue"
35
- :disabled="group.disabled || option.disabled"
36
- role="radio"
37
- :tabindex="option.value === focusableValue ? 0 : -1"
38
33
  :title="option.title"
39
- @click="selectOption(option)"
40
- @keydown="roving.handleKeydown($event, option.value)"
34
+ v-bind="radioGroup.optionBindings(option)"
41
35
  >{{ option.label }}</button>
42
36
  </div>
43
37
  </template>
@@ -74,10 +68,9 @@ export interface Props<T extends PublicSelectionKey = string> {
74
68
 
75
69
  <script setup lang="ts" generic="T extends SelectionKey = string">
76
70
  import { computed } from 'vue'
77
- import type { ComponentPublicInstance } from 'vue'
78
71
  import { useOwnedAttrs } from '../../../internal/ownedAttrs'
79
72
  import { useInteractiveLeafRegistration } from '../../../internal/passiveContentContract'
80
- import { selectionKeyToken, useSelectionRoving } from '../_internal/useSelectionRoving'
73
+ import { useRadioGroup } from '../_internal/useRadioGroup'
81
74
  import type { SelectionKey } from '../_internal/useSelectionRoving'
82
75
  import SFieldGroup from './SFieldGroup.vue'
83
76
 
@@ -100,35 +93,14 @@ const emit = defineEmits<{
100
93
  }>()
101
94
  const ownedAttrs = useOwnedAttrs({ component: 'SSegmentedControl', owner: 'field group root' })
102
95
  useInteractiveLeafRegistration({ owner: 'SSegmentedControl' })
103
-
104
- function selectOption(option: SegmentedControlOption<T>): void {
105
- if (props.disabled || props.readonly || option.disabled) {
106
- return
107
- }
108
- emit('update:modelValue', option.value)
109
- }
110
-
111
- const rovingOptions = computed(() => props.options.map((option) => ({
112
- value: option.value,
113
- disabled: props.disabled || option.disabled,
114
- })))
115
- const roving = useSelectionRoving({
116
- options: rovingOptions,
96
+ const radioGroup = useRadioGroup({
97
+ options: computed(() => props.options),
117
98
  selectedValue: computed(() => props.modelValue),
118
- select: (value) => {
119
- if (!props.disabled && !props.readonly) emit('update:modelValue', value)
120
- },
99
+ disabled: computed(() => props.disabled),
100
+ readonly: computed(() => props.readonly),
101
+ update: (value) => emit('update:modelValue', value),
121
102
  component: 'SSegmentedControl',
122
103
  })
123
- const focusableValue = roving.focusableValue
124
- const optionToken = (option: SegmentedControlOption<T>): string => selectionKeyToken(
125
- 'SSegmentedControl',
126
- option.value,
127
- 'option.value',
128
- )
129
- const resolveNativeElement = (element: Element | ComponentPublicInstance | null): HTMLElement | null => (
130
- element instanceof HTMLElement ? element : null
131
- )
132
104
  </script>
133
105
 
134
106
  <style lang="postcss" src="./SSegmentedControl.css" scoped></style>