@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.
Files changed (31) hide show
  1. package/README.md +21 -0
  2. package/package.json +1 -1
  3. package/src/components/shared/_internal/SHeaderSurface.css +98 -0
  4. package/src/components/shared/_internal/SHeaderSurface.vue +80 -0
  5. package/src/components/shared/_internal/useRadioGroup.ts +106 -0
  6. package/src/components/shared/containers/SPageHeader.vue +13 -20
  7. package/src/components/shared/controls/SCheckbox.vue +14 -43
  8. package/src/components/shared/controls/SChoiceCards.vue +16 -39
  9. package/src/components/shared/controls/SField.css +0 -12
  10. package/src/components/shared/controls/SField.vue +14 -10
  11. package/src/components/shared/controls/SFieldGroup.css +1 -19
  12. package/src/components/shared/controls/SFieldGroup.vue +13 -10
  13. package/src/components/shared/controls/SFieldLabel.css +0 -1
  14. package/src/components/shared/controls/SFieldLabel.vue +2 -1
  15. package/src/components/shared/controls/SSegmentedControl.vue +15 -43
  16. package/src/components/shared/controls/SSwitch.vue +14 -32
  17. package/src/components/shared/data-display/SBadge.vue +41 -18
  18. package/src/components/shared/data-display/SChip.vue +22 -10
  19. package/src/components/shared/data-display/SSectionHeader.vue +17 -21
  20. package/src/components/shared/data-display/STable.vue +131 -18
  21. package/src/components/shared/data-display/table.ts +1 -0
  22. package/src/components/shared/database/SDataGrid.vue +13 -0
  23. package/src/internal/fieldPresentation.ts +28 -0
  24. package/src/internal/pillSurface.ts +92 -0
  25. package/src/internal/useBinaryInput.ts +76 -0
  26. package/src/styles/style.css +114 -0
  27. package/src/styles/tokens.css +31 -0
  28. package/src/components/shared/containers/SPageHeader.css +0 -82
  29. package/src/components/shared/data-display/SBadge.css +0 -73
  30. package/src/components/shared/data-display/SChip.css +0 -59
  31. package/src/components/shared/data-display/SSectionHeader.css +0 -84
@@ -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 })
@@ -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>
@@ -13,6 +13,7 @@
13
13
  <template #default="field">
14
14
  <span class="s-switch-control">
15
15
  <input
16
+ ref="inputEl"
16
17
  v-bind="ownedAttrs.bindings()"
17
18
  :id="field.controlId"
18
19
  type="checkbox"
@@ -37,10 +38,9 @@
37
38
  </template>
38
39
 
39
40
  <script setup lang="ts">
40
- import { computed, useId } from 'vue'
41
41
  import { mergeIdReferences, useOwnedAttrs } from '../../../internal/ownedAttrs'
42
42
  import { useInteractiveLeafRegistration } from '../../../internal/passiveContentContract'
43
- import { resolveOptionalDomId } from '../../../internal/runtimeContract'
43
+ import { useBinaryInput } from '../../../internal/useBinaryInput'
44
44
  import SField from './SField.vue'
45
45
 
46
46
  defineOptions({ inheritAttrs: false })
@@ -73,37 +73,19 @@ const emit = defineEmits<{
73
73
  }>()
74
74
  const ownedAttrs = useOwnedAttrs({ component: 'SSwitch', owner: 'native switch input' })
75
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
76
+ const {
77
+ checkedValue,
78
+ inputEl,
79
+ inputId,
80
+ onChange,
81
+ onClick,
82
+ } = useBinaryInput({
83
+ owner: 'SSwitch',
84
+ inputName: 'switch',
85
+ props,
86
+ emitUpdate: (value) => emit('update:modelValue', value),
87
+ emitClick: (event) => emit('click', event),
89
88
  })
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
89
  </script>
108
90
 
109
91
  <style lang="postcss" src="./SSwitch.css" scoped></style>
@@ -1,42 +1,56 @@
1
1
  <template>
2
2
  <span
3
- v-bind="ownedAttrs.bindings()"
3
+ v-bind="{ ...ownedAttrs.bindings(), ...surfaceBindings }"
4
4
  class="s-badge"
5
- :class="{ 's-badge--dot': dot }"
6
- :data-severity="severity"
7
- :data-size="size"
8
- :data-max-width="maxWidth"
9
- :data-truncate="truncate || undefined"
10
- :data-icon-motion="iconMotion"
11
5
  >
12
6
  <template v-if="!dot">
13
- <span v-if="leadingIcon" class="s-badge__icon" aria-hidden="true">
14
- <component :is="leadingIcon" class="s-badge__icon-graphic" />
7
+ <span v-if="leadingIcon" :class="PILL_ICON_CLASS" aria-hidden="true">
8
+ <component
9
+ :is="leadingIcon"
10
+ :class="[
11
+ PILL_ICON_GRAPHIC_CLASS,
12
+ { [PILL_ICON_SPIN_CLASS]: iconMotion === 'spin' },
13
+ ]"
14
+ />
15
+ </span>
16
+ <span :class="[PILL_LABEL_CLASS, { [PILL_LABEL_TRUNCATE_CLASS]: truncate }]">
17
+ <slot />
15
18
  </span>
16
- <span class="s-badge__label"><slot /></span>
17
19
  </template>
18
20
  </span>
19
21
  </template>
20
22
 
21
23
  <script setup lang="ts">
22
- import type { Component } from 'vue';
24
+ import { computed, type Component } from 'vue';
23
25
  import { useOwnedAttrs } from '../../../internal/ownedAttrs';
26
+ import {
27
+ PILL_ICON_CLASS,
28
+ PILL_ICON_GRAPHIC_CLASS,
29
+ PILL_ICON_SPIN_CLASS,
30
+ PILL_LABEL_CLASS,
31
+ PILL_LABEL_TRUNCATE_CLASS,
32
+ resolvePillSurfaceBindings,
33
+ type PillSurfaceIconMotion,
34
+ type PillSurfaceMaxWidth,
35
+ type PillSurfaceSeverity,
36
+ type PillSurfaceSize,
37
+ } from '../../../internal/pillSurface';
24
38
 
25
39
  defineOptions({ inheritAttrs: false });
26
40
 
27
- export type BadgeSeverity = 'primary' | 'secondary' | 'success' | 'info' | 'warn' | 'danger' | 'contrast';
41
+ export type BadgeSeverity = PillSurfaceSeverity;
28
42
 
29
43
  export interface Props {
30
44
  severity?: BadgeSeverity;
31
45
  dot?: boolean;
32
- size?: 'xs' | 'sm' | 'md';
46
+ size?: PillSurfaceSize;
33
47
  leadingIcon?: Component;
34
- iconMotion?: 'static' | 'spin';
48
+ iconMotion?: PillSurfaceIconMotion;
35
49
  truncate?: boolean;
36
- maxWidth?: 'none' | 'sm' | 'md';
50
+ maxWidth?: PillSurfaceMaxWidth;
37
51
  }
38
52
 
39
- withDefaults(defineProps<Props>(), {
53
+ const props = withDefaults(defineProps<Props>(), {
40
54
  severity: 'secondary',
41
55
  dot: false,
42
56
  size: 'md',
@@ -47,6 +61,15 @@ withDefaults(defineProps<Props>(), {
47
61
  });
48
62
 
49
63
  const ownedAttrs = useOwnedAttrs({ component: 'SBadge', owner: 'badge root' });
64
+ const surfaceBindings = computed(() => resolvePillSurfaceBindings('SBadge', {
65
+ kind: 'badge',
66
+ severity: props.severity,
67
+ size: props.size,
68
+ dot: props.dot,
69
+ closable: false,
70
+ disabled: false,
71
+ truncate: props.truncate,
72
+ maxWidth: props.maxWidth,
73
+ iconMotion: props.iconMotion,
74
+ }));
50
75
  </script>
51
-
52
- <style lang="postcss" src="./SBadge.css" scoped></style>
@@ -1,16 +1,12 @@
1
1
  <template>
2
2
  <span
3
- v-bind="ownedAttrs.bindings()"
3
+ v-bind="{ ...ownedAttrs.bindings(), ...surfaceBindings }"
4
4
  class="s-chip"
5
- :data-severity="validatedSeverity"
6
- :data-size="validatedSize"
7
- :data-closable="closable || undefined"
8
- :data-disabled="disabled || undefined"
9
5
  >
10
- <span v-if="leadingIcon" class="s-chip__icon" aria-hidden="true">
11
- <component :is="leadingIcon" class="s-chip__icon-graphic" />
6
+ <span v-if="leadingIcon" :class="PILL_ICON_CLASS" aria-hidden="true">
7
+ <component :is="leadingIcon" :class="PILL_ICON_GRAPHIC_CLASS" />
12
8
  </span>
13
- <span class="s-chip__label">{{ validatedLabel }}</span>
9
+ <span :class="[PILL_LABEL_CLASS, PILL_LABEL_TRUNCATE_CLASS]">{{ validatedLabel }}</span>
14
10
  <SButton
15
11
  v-if="closable"
16
12
  label=""
@@ -34,6 +30,13 @@
34
30
  import { computed, type Component } from 'vue'
35
31
  import { X } from '../../icons/sputnigUiIcons'
36
32
  import { useOwnedAttrs } from '../../../internal/ownedAttrs'
33
+ import {
34
+ PILL_ICON_CLASS,
35
+ PILL_ICON_GRAPHIC_CLASS,
36
+ PILL_LABEL_CLASS,
37
+ PILL_LABEL_TRUNCATE_CLASS,
38
+ resolvePillSurfaceBindings,
39
+ } from '../../../internal/pillSurface'
37
40
  import {
38
41
  validateBoolean,
39
42
  validateExactString,
@@ -80,6 +83,17 @@ const validatedSeverity = computed(() => validateExactString(
80
83
  const validatedSize = computed(() => validateExactString(
81
84
  'SChip', 'size', props.size, ['sm', 'md'] as const,
82
85
  ))
86
+ const surfaceBindings = computed(() => resolvePillSurfaceBindings('SChip', {
87
+ kind: 'chip',
88
+ severity: validatedSeverity.value,
89
+ size: validatedSize.value,
90
+ dot: false,
91
+ closable: props.closable,
92
+ disabled: props.disabled,
93
+ truncate: true,
94
+ maxWidth: 'none',
95
+ iconMotion: 'static',
96
+ }))
83
97
  const resolvedCloseLabel = computed(() => {
84
98
  validateBoolean('SChip', 'closable', props.closable)
85
99
  validateBoolean('SChip', 'disabled', props.disabled)
@@ -88,5 +102,3 @@ const resolvedCloseLabel = computed(() => {
88
102
  : validateNonEmptyString('SChip', 'closeLabel', props.closeLabel)
89
103
  })
90
104
  </script>
91
-
92
- <style lang="postcss" src="./SChip.css" scoped></style>
@@ -1,5 +1,6 @@
1
1
  <script setup lang="ts">
2
2
  import { useOwnedAttrs } from '../../../internal/ownedAttrs'
3
+ import SHeaderSurface from '../_internal/SHeaderSurface.vue'
3
4
 
4
5
  defineOptions({ inheritAttrs: false })
5
6
 
@@ -32,27 +33,22 @@ const ownedAttrs = useOwnedAttrs({ component: 'SSectionHeader', owner: 'section
32
33
  </script>
33
34
 
34
35
  <template>
35
- <header
36
+ <SHeaderSurface
36
37
  v-bind="ownedAttrs.bindings()"
37
- class="s-section-header"
38
- :data-size="size"
39
- :data-variant="variant"
40
- :data-divider="divider || undefined"
41
- :data-padding="padding"
42
- :data-spacing-after="spacingAfter"
43
- :data-trailing-layout="trailingLayout"
38
+ kind="section"
39
+ :title="title"
40
+ :eyebrow="eyebrow"
41
+ :description="description"
42
+ :meta="meta"
43
+ :heading-level="headingLevel"
44
+ :size="size"
45
+ :variant="variant"
46
+ :divider="divider"
47
+ :padding="padding"
48
+ :spacing-after="spacingAfter"
49
+ :trailing-layout="trailingLayout"
44
50
  >
45
- <div class="s-section-header__copy">
46
- <div v-if="eyebrow" class="s-section-header__eyebrow">{{ eyebrow }}</div>
47
- <component :is="`h${headingLevel}`" class="s-section-header__title">{{ title }}</component>
48
- <p v-if="description" class="s-section-header__description">{{ description }}</p>
49
- <p v-if="meta" class="s-section-header__meta">{{ meta }}</p>
50
- </div>
51
- <div v-if="$slots.badges || $slots.actions" class="s-section-header__trailing">
52
- <div v-if="$slots.badges" class="s-section-header__badges"><slot name="badges" /></div>
53
- <div v-if="$slots.actions" class="s-section-header__actions"><slot name="actions" /></div>
54
- </div>
55
- </header>
51
+ <template v-if="$slots.badges" #badges><slot name="badges" /></template>
52
+ <template v-if="$slots.actions" #actions><slot name="actions" /></template>
53
+ </SHeaderSurface>
56
54
  </template>
57
-
58
- <style lang="postcss" src="./SSectionHeader.css" scoped></style>