@citizenplane/pimp 18.11.1 → 18.12.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@citizenplane/pimp",
3
- "version": "18.11.1",
3
+ "version": "18.12.0",
4
4
  "scripts": {
5
5
  "dev": "storybook dev -p 8081",
6
6
  "build-storybook": "storybook build --output-dir ./docs",
@@ -76,4 +76,14 @@
76
76
  --cp-shadow-focus-ring-accent-ring-blur: 0;
77
77
  --cp-shadow-focus-ring-accent-ring-spread: 4px;
78
78
  --cp-shadow-focus-ring-accent-ring-color: var(--cp-colors-accent-500);
79
+ --cp-shadow-focus-ring-error-gap-offset-x: 0;
80
+ --cp-shadow-focus-ring-error-gap-offset-y: 0;
81
+ --cp-shadow-focus-ring-error-gap-blur: 0;
82
+ --cp-shadow-focus-ring-error-gap-spread: 2px;
83
+ --cp-shadow-focus-ring-error-gap-color: var(--cp-colors-white);
84
+ --cp-shadow-focus-ring-error-ring-offset-x: 0;
85
+ --cp-shadow-focus-ring-error-ring-offset-y: 0;
86
+ --cp-shadow-focus-ring-error-ring-blur: 0;
87
+ --cp-shadow-focus-ring-error-ring-spread: 4px;
88
+ --cp-shadow-focus-ring-error-ring-color: var(--cp-colors-red-500);
79
89
  }
@@ -355,6 +355,14 @@
355
355
  var(--cp-shadow-focus-ring-accent-ring-offset-x) var(--cp-shadow-focus-ring-accent-ring-offset-y)
356
356
  var(--cp-shadow-focus-ring-accent-ring-blur) var(--cp-shadow-focus-ring-accent-ring-spread)
357
357
  var(--cp-shadow-focus-ring-accent-ring-color);
358
+
359
+ --cp-shadow-focus-ring-error:
360
+ var(--cp-shadow-focus-ring-error-gap-offset-x) var(--cp-shadow-focus-ring-error-gap-offset-y)
361
+ var(--cp-shadow-focus-ring-error-gap-blur) var(--cp-shadow-focus-ring-error-gap-spread)
362
+ var(--cp-shadow-focus-ring-error-gap-color),
363
+ var(--cp-shadow-focus-ring-error-ring-offset-x) var(--cp-shadow-focus-ring-error-ring-offset-y)
364
+ var(--cp-shadow-focus-ring-error-ring-blur) var(--cp-shadow-focus-ring-error-ring-spread)
365
+ var(--cp-shadow-focus-ring-error-ring-color);
358
366
  }
359
367
 
360
368
  /* stylelint-enable */
@@ -0,0 +1,210 @@
1
+ <template>
2
+ <div class="cpSegmentedControl" :class="segmentedControlDynamicClasses">
3
+ <div class="cpSegmentedControl__options">
4
+ <template v-for="(option, index) in options" :key="option.value">
5
+ <cp-segmented-control-item
6
+ :option="option"
7
+ :name="name"
8
+ :is-disabled="isDisabled"
9
+ :is-full-width="isFullWidth"
10
+ :is-invalid="isInvalid"
11
+ :is-selected="isSelected(index)"
12
+ :orientation="orientation"
13
+ v-model="selectedValue"
14
+ :size="size"
15
+ @mouseenter="handleOptionMouseEnter(index)"
16
+ @mouseleave="handleOptionMouseLeave"
17
+ @focusin="handleOptionFocusIn(index)"
18
+ @focusout="handleOptionFocusOut"
19
+ />
20
+ <div
21
+ v-if="!isLastOption(index)"
22
+ class="cpSegmentedControl__divider"
23
+ :class="getDynamicDividerClasses(index)"
24
+ aria-hidden="true"
25
+ />
26
+ </template>
27
+ </div>
28
+ </div>
29
+ </template>
30
+
31
+ <script setup lang="ts">
32
+ import { computed, ref } from 'vue'
33
+
34
+ import { capitalizeFirstLetter } from '@/helpers'
35
+ import CpSegmentedControlItem from './CpSegmentedControlItem.vue'
36
+
37
+ interface Option {
38
+ label: string
39
+ leadingIcon?: string
40
+ trailingIcon?: string
41
+ value: string
42
+ }
43
+
44
+ interface Props {
45
+ isDisabled?: boolean
46
+ isFullWidth?: boolean
47
+ isInvalid?: boolean
48
+ name: string
49
+ options: Option[]
50
+ orientation?: 'horizontal' | 'vertical'
51
+ size?: 'sm' | 'md'
52
+ }
53
+
54
+ const props = withDefaults(defineProps<Props>(), {
55
+ size: 'md',
56
+ orientation: 'horizontal',
57
+ })
58
+
59
+ const segmentedControlDynamicClasses = computed(() => ({
60
+ 'cpSegmentedControl--isInvalid': props.isInvalid,
61
+ 'cpSegmentedControl--fullWidth': props.isFullWidth,
62
+ 'cpSegmentedControl--isDisabled': props.isDisabled,
63
+ [`cpSegmentedControl--is${capitalizeFirstLetter(props.orientation)}`]: true,
64
+ }))
65
+
66
+ const selectedValue = defineModel<string | undefined>()
67
+ const hoveredIndex = ref<number | null>(null)
68
+ const focusedIndex = ref<number | null>(null)
69
+
70
+ const isSelected = (index: number) => index === selectedIndex.value
71
+
72
+ const isLastOption = (index: number) => index === props.options.length - 1
73
+
74
+ const selectedIndex = computed(() => {
75
+ if (selectedValue.value === undefined) return -1
76
+
77
+ return props.options.findIndex((option) => option.value === selectedValue.value)
78
+ })
79
+
80
+ const setAdjacentDividerIndexes = (indexes: Set<number>, optionIndex: number) => {
81
+ if (optionIndex < 0) return
82
+
83
+ if (optionIndex > 0) {
84
+ indexes.add(optionIndex - 1)
85
+ }
86
+
87
+ if (optionIndex < props.options.length - 1) {
88
+ indexes.add(optionIndex)
89
+ }
90
+ }
91
+
92
+ const selectedDividerIndexes = computed(() => {
93
+ const indexes = new Set<number>()
94
+
95
+ setAdjacentDividerIndexes(indexes, selectedIndex.value)
96
+
97
+ if (focusedIndex.value !== null) {
98
+ setAdjacentDividerIndexes(indexes, focusedIndex.value)
99
+ }
100
+
101
+ return indexes
102
+ })
103
+
104
+ const hoveredDividerIndexes = computed(() => {
105
+ const indexes = new Set<number>()
106
+
107
+ if (
108
+ hoveredIndex.value !== null
109
+ && hoveredIndex.value !== selectedIndex.value
110
+ && hoveredIndex.value !== focusedIndex.value
111
+ ) {
112
+ setAdjacentDividerIndexes(indexes, hoveredIndex.value)
113
+ }
114
+
115
+ return indexes
116
+ })
117
+
118
+ const getDynamicDividerClasses = (index: number) => {
119
+ const isActive = selectedDividerIndexes.value.has(index)
120
+
121
+ return {
122
+ 'cpSegmentedControl__divider--isActive': isActive,
123
+ 'cpSegmentedControl__divider--isHovered': !isActive && hoveredDividerIndexes.value.has(index),
124
+ }
125
+ }
126
+
127
+ const handleOptionMouseEnter = (index: number) => {
128
+ if (props.isDisabled) return
129
+
130
+ hoveredIndex.value = index
131
+ }
132
+
133
+ const handleOptionMouseLeave = () => (hoveredIndex.value = null)
134
+
135
+ const handleOptionFocusIn = (index: number) => {
136
+ if (props.isDisabled) return
137
+
138
+ focusedIndex.value = index
139
+ }
140
+
141
+ const handleOptionFocusOut = () => (focusedIndex.value = null)
142
+ </script>
143
+
144
+ <style lang="scss">
145
+ .cpSegmentedControl {
146
+ width: fit-content;
147
+ min-width: 0;
148
+ max-width: 100%;
149
+
150
+ &--fullWidth {
151
+ width: 100%;
152
+ }
153
+
154
+ &__options {
155
+ display: flex;
156
+ width: 100%;
157
+ min-width: 0;
158
+ border-radius: var(--cp-radius-md);
159
+ box-shadow: var(--cp-shadows-3xs);
160
+ }
161
+
162
+ &__divider {
163
+ width: fn.px-to-rem(1);
164
+ flex-shrink: 0;
165
+ align-self: stretch;
166
+ background-color: var(--cp-border-soft);
167
+ transition: background-color 100ms ease;
168
+
169
+ &--isHovered {
170
+ background-color: var(--cp-border-soft-hover);
171
+ }
172
+
173
+ &--isActive {
174
+ position: relative;
175
+ z-index: 5;
176
+ background-color: var(--cp-border-accent-solid);
177
+ }
178
+ }
179
+
180
+ &--isInvalid {
181
+ .cpSegmentedControl__divider {
182
+ background-color: var(--cp-border-error-solid);
183
+ }
184
+
185
+ .cpSegmentedControl__divider--isHovered,
186
+ .cpSegmentedControl__divider--isActive {
187
+ background-color: var(--cp-border-error-solid);
188
+ }
189
+ }
190
+
191
+ &--isDisabled {
192
+ .cpSegmentedControl__divider,
193
+ .cpSegmentedControl__divider--isHovered,
194
+ .cpSegmentedControl__divider--isActive {
195
+ background-color: var(--cp-border-disabled);
196
+ }
197
+ }
198
+
199
+ &--isVertical {
200
+ .cpSegmentedControl__options {
201
+ flex-direction: column;
202
+ }
203
+
204
+ .cpSegmentedControl__divider {
205
+ width: 100%;
206
+ height: fn.px-to-rem(1);
207
+ }
208
+ }
209
+ }
210
+ </style>
@@ -0,0 +1,248 @@
1
+ <template>
2
+ <div class="cpSegmentedControlItem" :class="dynamicSegmentedControlItemClasses">
3
+ <input
4
+ :id="option.value"
5
+ v-model="selectedValue"
6
+ class="cpSegmentedControlItem__input"
7
+ :disabled="isDisabled"
8
+ :name="name"
9
+ type="radio"
10
+ :value="option.value"
11
+ />
12
+ <label class="cpSegmentedControlItem__inner" :for="option.value">
13
+ <cp-icon v-if="option.leadingIcon" class="cpSegmentedControlItem__icon" size="16" :type="option.leadingIcon" />
14
+ <span class="cpSegmentedControlItem__label">{{ option.label }}</span>
15
+ <cp-icon v-if="option.trailingIcon" class="cpSegmentedControlItem__icon" size="16" :type="option.trailingIcon" />
16
+ </label>
17
+ </div>
18
+ </template>
19
+
20
+ <script setup lang="ts">
21
+ import { computed } from 'vue'
22
+
23
+ import { capitalizeFirstLetter } from '@/helpers'
24
+
25
+ interface Option {
26
+ label: string
27
+ leadingIcon?: string
28
+ trailingIcon?: string
29
+ value: string
30
+ }
31
+
32
+ interface Props {
33
+ isDisabled?: boolean
34
+ isFullWidth?: boolean
35
+ isInvalid?: boolean
36
+ isSelected?: boolean
37
+ name: string
38
+ option: Option
39
+ orientation?: 'horizontal' | 'vertical'
40
+ size?: 'sm' | 'md'
41
+ }
42
+
43
+ const props = withDefaults(defineProps<Props>(), {
44
+ orientation: 'horizontal',
45
+ })
46
+
47
+ const selectedValue = defineModel<string | undefined>()
48
+
49
+ const dynamicSegmentedControlItemClasses = computed(() => ({
50
+ 'cpSegmentedControlItem--isDisabled': props.isDisabled,
51
+ 'cpSegmentedControlItem--isInvalid': props.isInvalid,
52
+ 'cpSegmentedControlItem--isSelected': props.isSelected,
53
+ 'cpSegmentedControlItem--fullWidth': props.isFullWidth && props.orientation !== 'vertical',
54
+ [`cpSegmentedControlItem--${props.size}`]: props.size,
55
+ [`cpSegmentedControlItem--is${capitalizeFirstLetter(props.orientation)}`]: true,
56
+ }))
57
+ </script>
58
+
59
+ <style scoped lang="scss">
60
+ .cpSegmentedControlItem {
61
+ position: relative;
62
+ z-index: 1;
63
+ display: flex;
64
+ min-width: 0;
65
+ flex: 0 1 auto;
66
+ align-items: center;
67
+ border-width: fn.px-to-rem(1) 0;
68
+ border-style: solid;
69
+ border-color: var(--cp-border-soft);
70
+ background-color: var(--cp-background-primary);
71
+ color: var(--cp-text-secondary);
72
+ font-weight: 500;
73
+ transition:
74
+ background-color 100ms ease,
75
+ border-color 100ms ease,
76
+ box-shadow 100ms ease,
77
+ color 100ms ease;
78
+
79
+ &--fullWidth {
80
+ width: 100%;
81
+ }
82
+
83
+ &--md &__inner {
84
+ padding: fn.px-to-rem(13) var(--cp-spacing-xl);
85
+ }
86
+
87
+ &--sm &__inner {
88
+ padding: calc(var(--cp-spacing-sm-md) - 1px) var(--cp-spacing-xl);
89
+ }
90
+
91
+ &:first-child {
92
+ border-radius: var(--cp-radius-md) 0 0 var(--cp-radius-md);
93
+ border-left-width: fn.px-to-rem(1);
94
+ }
95
+
96
+ &:last-child {
97
+ border-radius: 0 var(--cp-radius-md) var(--cp-radius-md) 0;
98
+ border-right-width: fn.px-to-rem(1);
99
+ }
100
+
101
+ &:has(.cpSegmentedControlItem__input:focus-visible) {
102
+ z-index: 4;
103
+ border-color: var(--cp-border-accent-solid);
104
+ background-color: var(--cp-background-accent-secondary-hover);
105
+ box-shadow: var(--cp-shadow-focus-ring-accent);
106
+ color: var(--cp-foreground-accent-primary-hover);
107
+ }
108
+
109
+ &--isInvalid:has(.cpSegmentedControlItem__input:focus-visible) {
110
+ border-color: var(--cp-border-error-solid);
111
+ background-color: var(--cp-background-error-secondary-hover);
112
+ box-shadow: var(--cp-shadow-focus-ring-error);
113
+ color: var(--cp-text-error-primary-hover);
114
+ }
115
+
116
+ &:has(.cpSegmentedControlItem__input:disabled) {
117
+ background-color: var(--cp-background-primary);
118
+ color: var(--cp-text-disabled);
119
+ }
120
+
121
+ &:has(.cpSegmentedControlItem__input:disabled) > .cpSegmentedControlItem__inner {
122
+ cursor: not-allowed;
123
+ }
124
+
125
+ &--isSelected:not(:has(.cpSegmentedControlItem__input:is(:focus-visible, :disabled))) {
126
+ background-color: var(--cp-background-accent-secondary);
127
+ color: var(--cp-text-accent-primary);
128
+ }
129
+
130
+ &--isSelected.cpSegmentedControlItem--isInvalid:not(:has(.cpSegmentedControlItem__input:is(:focus-visible, :disabled))) {
131
+ background-color: var(--cp-background-error-secondary);
132
+ color: var(--cp-text-error-primary);
133
+ }
134
+
135
+ &:hover:not(.cpSegmentedControlItem--isSelected, :has(.cpSegmentedControlItem__input:disabled)) {
136
+ border-color: var(--cp-border-soft-hover);
137
+ background-color: var(--cp-background-primary-hover);
138
+ color: var(--cp-text-secondary-hover);
139
+ }
140
+
141
+ &--isSelected:not(:has(.cpSegmentedControlItem__input:disabled)) {
142
+ border-color: var(--cp-border-accent-solid);
143
+ }
144
+
145
+ &--isSelected:has(.cpSegmentedControlItem__input:disabled) {
146
+ background-color: var(--cp-background-disabled);
147
+ }
148
+
149
+ &--isSelected:hover:not(:has(.cpSegmentedControlItem__input:is(:disabled, :focus-visible))) {
150
+ border-color: var(--cp-border-accent-solid);
151
+ background-color: var(--cp-background-accent-secondary-hover);
152
+ color: var(--cp-text-accent-primary-hover);
153
+ }
154
+
155
+ &--isSelected.cpSegmentedControlItem--isInvalid:hover:not(:has(.cpSegmentedControlItem__input:is(:disabled, :focus-visible))) {
156
+ background-color: var(--cp-background-error-secondary-hover);
157
+ color: var(--cp-text-error-primary);
158
+ }
159
+
160
+ &--isInvalid {
161
+ border-color: var(--cp-border-error-solid);
162
+
163
+ &:hover:not(.cpSegmentedControlItem--isSelected, :has(.cpSegmentedControlItem__input:disabled)) {
164
+ border-color: var(--cp-border-error-solid);
165
+ background-color: var(--cp-background-error-secondary-hover);
166
+ }
167
+
168
+ &.cpSegmentedControlItem--isSelected:not(:has(.cpSegmentedControlItem__input:disabled)) {
169
+ border-color: var(--cp-border-error-solid);
170
+ }
171
+ }
172
+
173
+ &--isDisabled {
174
+ border-color: var(--cp-border-disabled);
175
+
176
+ &.cpSegmentedControlItem--isSelected {
177
+ color: var(--cp-text-disabled);
178
+ }
179
+
180
+ &.cpSegmentedControlItem--isInvalid {
181
+ border-color: var(--cp-border-disabled);
182
+ }
183
+ }
184
+
185
+ &--isVertical {
186
+ width: 100%;
187
+ border-width: 0 fn.px-to-rem(1);
188
+
189
+ &:first-child {
190
+ border-radius: var(--cp-radius-md) var(--cp-radius-md) 0 0;
191
+ border-top-width: fn.px-to-rem(1);
192
+ border-right-width: fn.px-to-rem(1);
193
+ border-left-width: fn.px-to-rem(1);
194
+ }
195
+
196
+ &:last-child {
197
+ border-radius: 0 0 var(--cp-radius-md) var(--cp-radius-md);
198
+ border-right-width: fn.px-to-rem(1);
199
+ border-bottom-width: fn.px-to-rem(1);
200
+ border-left-width: fn.px-to-rem(1);
201
+ }
202
+
203
+ &:not(:first-child, :last-child) {
204
+ border-right-width: fn.px-to-rem(1);
205
+ border-left-width: fn.px-to-rem(1);
206
+ }
207
+ }
208
+
209
+ &__inner {
210
+ display: flex;
211
+ overflow: hidden;
212
+ width: 100%;
213
+ min-width: 0;
214
+ align-items: center;
215
+ justify-content: center;
216
+ cursor: pointer;
217
+ font-size: var(--cp-text-size-sm);
218
+ gap: var(--cp-spacing-md);
219
+ line-height: var(--cp-line-height-sm);
220
+ }
221
+
222
+ &__input {
223
+ position: absolute;
224
+ z-index: 1;
225
+ width: 100%;
226
+ height: 100%;
227
+ margin: 0;
228
+ appearance: none;
229
+ background-color: transparent;
230
+ cursor: pointer;
231
+ inset: 0;
232
+
233
+ &:disabled {
234
+ cursor: not-allowed;
235
+ }
236
+ }
237
+
238
+ &__label {
239
+ @extend %u-text-ellipsis;
240
+
241
+ min-width: 0;
242
+ }
243
+
244
+ &__icon {
245
+ flex-shrink: 0;
246
+ }
247
+ }
248
+ </style>
@@ -52,6 +52,7 @@ import CpSeatMapRowFacility from './CpSeatMapRowFacility.vue'
52
52
  import CpSeatMapSeat from './CpSeatMapSeat.vue'
53
53
  import CpSeatMapSeatWrapper from './CpSeatMapSeatWrapper.vue'
54
54
  import CpSeatMapZone from './CpSeatMapZone.vue'
55
+ import CpSegmentedControl from './CpSegmentedControl.vue'
55
56
  import CpSelect from './CpSelect.vue'
56
57
  import CpSelectableButton from './CpSelectableButton.vue'
57
58
  import CpSelectMenu from './CpSelectMenu.vue'
@@ -118,6 +119,7 @@ const Components = {
118
119
  CpRadioGroup,
119
120
  CpSelectableButton,
120
121
  CpSwitch,
122
+ CpSegmentedControl,
121
123
  CpTable,
122
124
  CpTableColumnEditor,
123
125
  CpIcon,
@@ -233,6 +235,7 @@ export {
233
235
  CpSeatMapSeat,
234
236
  CpSeatMapSeatWrapper,
235
237
  CpSeatMapZone,
238
+ CpSegmentedControl,
236
239
  CpSelect,
237
240
  CpSelectableButton,
238
241
  CpSelectMenu,