@citizenplane/pimp 15.0.0 → 15.1.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@citizenplane/pimp",
3
- "version": "15.0.0",
3
+ "version": "15.1.1",
4
4
  "scripts": {
5
5
  "dev": "storybook dev -p 8080",
6
6
  "build-storybook": "storybook build --output-dir ./docs",
@@ -1,5 +1,6 @@
1
1
  :root {
2
2
  --cp-colors-black: #000000;
3
+ --cp-colors-white: #ffffff;
3
4
 
4
5
  --cp-colors-blue-50: #f6faff;
5
6
  --cp-colors-blue-100: #e5efff;
@@ -68,4 +68,15 @@
68
68
  --cp-drop-shadow-side-panel-offset-x: 0;
69
69
  --cp-drop-shadow-side-panel-offset-y: 0;
70
70
  --cp-drop-shadow-side-panel-spread: 0;
71
+
72
+ --cp-shadow-focus-ring-accent-gap-offset-x: 0;
73
+ --cp-shadow-focus-ring-accent-gap-offset-y: 0;
74
+ --cp-shadow-focus-ring-accent-gap-blur: 0;
75
+ --cp-shadow-focus-ring-accent-gap-spread: 2px;
76
+ --cp-shadow-focus-ring-accent-gap-color: var(--cp-colors-white);
77
+ --cp-shadow-focus-ring-accent-ring-offset-x: 0;
78
+ --cp-shadow-focus-ring-accent-ring-offset-y: 0;
79
+ --cp-shadow-focus-ring-accent-ring-blur: 0;
80
+ --cp-shadow-focus-ring-accent-ring-spread: 4px;
81
+ --cp-shadow-focus-ring-accent-ring-color: var(--cp-colors-accent-500);
71
82
  }
@@ -392,4 +392,13 @@
392
392
  var(--cp-drop-shadow-overlay-4-blur) var(--cp-drop-shadow-overlay-4-spread) var(--cp-drop-shadow-overlay-4-color),
393
393
  var(--cp-drop-shadow-overlay-5-offset-x) var(--cp-drop-shadow-overlay-5-offset-y)
394
394
  var(--cp-drop-shadow-overlay-5-blur) var(--cp-drop-shadow-overlay-5-spread) var(--cp-drop-shadow-overlay-5-color);
395
+
396
+ /* Focus ring tokens */
397
+ --cp-shadow-focus-ring-accent:
398
+ var(--cp-shadow-focus-ring-accent-gap-offset-x) var(--cp-shadow-focus-ring-accent-gap-offset-y)
399
+ var(--cp-shadow-focus-ring-accent-gap-blur) var(--cp-shadow-focus-ring-accent-gap-spread)
400
+ var(--cp-shadow-focus-ring-accent-gap-color),
401
+ var(--cp-shadow-focus-ring-accent-ring-offset-x) var(--cp-shadow-focus-ring-accent-ring-offset-y)
402
+ var(--cp-shadow-focus-ring-accent-ring-blur) var(--cp-shadow-focus-ring-accent-ring-spread)
403
+ var(--cp-shadow-focus-ring-accent-ring-color);
395
404
  }
@@ -3,58 +3,59 @@
3
3
  <slot name="leading-icon">
4
4
  <cp-icon v-if="leadingIcon" class="cpBadge__icon" :type="leadingIcon" />
5
5
  </slot>
6
- <span v-if="label" class="cpBadge__label">
6
+ <span v-if="hasLabel" class="cpBadge__label">
7
7
  <slot>{{ label }}</slot>
8
8
  </span>
9
- <slot v-if="!hasClose" name="trailing-icon">
10
- <cp-icon v-if="trailingIcon" class="cpBadge__icon" :type="trailingIcon" />
11
- </slot>
12
- <button v-if="hasClose" class="cpBadge__clear" :disabled="disabled" type="button" @click="emit('onClear')">
9
+ <button v-if="isClearable" class="cpBadge__clear" :disabled="disabled" type="button" @click="emit('onClear')">
13
10
  <cp-icon class="cpBadge__clearIcon" type="x" />
14
11
  </button>
12
+ <slot v-else name="trailing-icon">
13
+ <cp-icon v-if="trailingIcon" class="cpBadge__icon" :type="trailingIcon" />
14
+ </slot>
15
15
  </div>
16
16
  </template>
17
17
 
18
18
  <script setup lang="ts">
19
- import { computed } from 'vue'
19
+ import { computed, useSlots } from 'vue'
20
20
 
21
21
  import type { Colors, Sizes } from '@/constants'
22
22
  import { capitalizeFirstLetter } from '@/helpers'
23
23
 
24
- interface Emits {
25
- (e: 'onClear'): void
26
- }
27
-
28
24
  type BadgeSizes = Extract<Sizes, '2xs' | 'xs' | 'sm' | 'md'>
29
- type BadgeStyles = 'outline' | 'soft' | 'solid'
25
+ type BadgeVariants = 'outline' | 'soft' | 'solid'
30
26
 
31
27
  interface Props {
32
28
  color?: Colors
33
29
  disabled?: boolean
34
- hasClose?: boolean
30
+ isClearable?: boolean
35
31
  isSquare?: boolean
36
32
  label?: string
37
33
  leadingIcon?: string
38
34
  size?: BadgeSizes
39
- style?: BadgeStyles
40
35
  trailingIcon?: string
36
+ variant?: BadgeVariants
41
37
  }
42
38
 
43
39
  const props = withDefaults(defineProps<Props>(), {
40
+ isClearable: false,
44
41
  color: 'neutral',
45
42
  size: 'md',
46
- style: 'outline',
43
+ variant: 'soft',
47
44
  label: '',
48
45
  leadingIcon: '',
49
46
  trailingIcon: '',
50
47
  })
51
48
 
52
- const emit = defineEmits<Emits>()
49
+ const emit = defineEmits<{ onClear: [] }>()
50
+
51
+ const slots = useSlots()
52
+
53
+ const hasLabel = computed(() => !!props.label || !!slots.default)
53
54
 
54
55
  const componentDynamicClasses = computed(() => [
55
56
  `cpBadge--${props.size}`,
56
57
  `cpBadge--is${capitalizeFirstLetter(props.color)}`,
57
- `cpBadge--is${capitalizeFirstLetter(props.style)}`,
58
+ `cpBadge--is${capitalizeFirstLetter(props.variant)}`,
58
59
  { 'cpBadge--isSquare': props.isSquare },
59
60
  { 'cpBadge--isDisabled': props.disabled },
60
61
  ])
@@ -1,10 +1,10 @@
1
1
  <template>
2
- <div>
2
+ <div class="cpRadio">
3
3
  <label
4
4
  v-for="({ label, value, description, additionalData, disabled }, index) in options"
5
5
  :key="getRadioId(index)"
6
- class="cpRadio"
7
- :class="computedClasses({ value, disabled })"
6
+ class="cpRadio__item"
7
+ :class="computedClasses"
8
8
  :for="getRadioId(index)"
9
9
  >
10
10
  <input
@@ -17,19 +17,15 @@
17
17
  :value="value"
18
18
  @input="onChange(value)"
19
19
  />
20
- <span class="cpRadio__content">
21
- <span class="cpRadio__information">
22
- <span class="cpRadio__label">{{ label }}</span>
23
- <span v-if="description" class="cpRadio__description">{{ description }}</span>
24
- </span>
25
- <span v-if="additionalData" class="cpRadio__additionalData">{{ additionalData }}</span>
26
- </span>
20
+ <span class="cpRadio__label">{{ label }}</span>
21
+ <span class="cpRadio__description">{{ description }}</span>
22
+ <span class="cpRadio__additionalData">{{ additionalData }}</span>
27
23
  </label>
28
24
  </div>
29
25
  </template>
30
26
 
31
27
  <script setup lang="ts">
32
- import { useId } from 'vue'
28
+ import { computed, useId } from 'vue'
33
29
 
34
30
  import { ToggleColors } from '@/constants'
35
31
  import { capitalizeFirstLetter } from '@/helpers'
@@ -70,20 +66,12 @@ const getRadioId = (index: number): string => `${radioUniqueId}${index}`
70
66
 
71
67
  const isActive = (value: string): boolean => value === props.modelValue
72
68
 
73
- const computedClasses = ({ value, disabled }: { disabled?: boolean; value: string }) => {
74
- return [
75
- {
76
- 'cpRadio--isActive': isActive(value),
77
- 'cpRadio--isDisabled': disabled,
78
- },
79
- `cpRadio--is${capitalizeFirstLetter(props.color)}`,
80
- ]
81
- }
69
+ const computedClasses = computed(() => `cpRadio--is${capitalizeFirstLetter(props.color)}`)
82
70
  </script>
83
71
 
84
72
  <style lang="scss">
85
73
  @mixin cp-radio-style($color, $className) {
86
- &--is#{$className}#{&}--isActive {
74
+ &--is#{$className}:has(input:checked) {
87
75
  border-color: $color;
88
76
  }
89
77
 
@@ -91,11 +79,9 @@ const computedClasses = ({ value, disabled }: { disabled?: boolean; value: strin
91
79
  background-color: $color;
92
80
  border-color: $color;
93
81
 
94
- & ~ span .cpRadio {
95
- &__label,
96
- &__additionalData {
97
- color: $color;
98
- }
82
+ & ~ .cpRadio__label,
83
+ & ~ .cpRadio__additionalData {
84
+ color: $color;
99
85
  }
100
86
  }
101
87
 
@@ -112,53 +98,70 @@ const computedClasses = ({ value, disabled }: { disabled?: boolean; value: strin
112
98
  .cpRadio {
113
99
  $cp-radio-base-width: var(--cp-dimensions-5);
114
100
 
115
- position: relative;
116
- border: var(--cp-dimensions-0_25) solid var(--cp-border-soft);
117
- border-radius: var(--cp-radius-md-lg);
118
- padding: var(--cp-spacing-xl) var(--cp-spacing-lg);
119
- display: grid;
120
- grid-template-columns: min-content 1fr;
121
- grid-gap: var(--cp-spacing-lg);
122
- align-items: center;
123
- width: 100%;
124
-
125
- &:not(#{&}--isDisabled),
126
- &:not(#{&}--isDisabled) * {
127
- cursor: pointer;
128
- }
101
+ display: flex;
102
+ flex-direction: column;
103
+ gap: var(--cp-spacing-lg);
129
104
 
130
- @include cp-radio-style(var(--cp-foreground-accent-primary), 'Accent');
105
+ &__item {
106
+ position: relative;
107
+ border: var(--cp-dimensions-0_25) solid var(--cp-border-soft);
108
+ border-radius: var(--cp-radius-md-lg);
109
+ padding: var(--cp-spacing-xl) var(--cp-spacing-lg);
110
+ display: grid;
111
+ grid-template-columns: min-content 1fr;
112
+ grid-template-rows: auto auto auto;
113
+ align-items: center;
114
+ column-gap: var(--cp-spacing-lg);
115
+ row-gap: 0;
116
+ width: 100%;
131
117
 
132
- @include cp-radio-style(var(--cp-foreground-blue-primary), 'Blue');
118
+ &:not(:has(input:disabled)),
119
+ &:not(:has(input:disabled)) * {
120
+ cursor: pointer;
121
+ }
133
122
 
134
- @include cp-radio-style(var(--cp-foreground-accent-primary), 'Purple'); // TODO: Sould be replace by ACCENT
123
+ &:has(input:disabled) {
124
+ background-color: var(--cp-background-disabled);
125
+ color: var(--cp-foreground-disabled);
126
+ border-color: var(--cp-border-disabled);
135
127
 
136
- &--isDisabled {
137
- background-color: var(--cp-background-disabled);
138
- color: var(--cp-foreground-disabled);
139
- border-color: var(--cp-border-disabled);
128
+ &,
129
+ * {
130
+ cursor: not-allowed;
131
+ }
140
132
 
141
- &,
142
- * {
143
- cursor: not-allowed;
133
+ &:hover,
134
+ &:focus,
135
+ &:has(input:checked) {
136
+ box-shadow: none;
137
+ border-color: var(--cp-border-disabled);
138
+ }
144
139
  }
140
+ }
145
141
 
146
- &:hover,
147
- &:focus {
148
- box-shadow: none;
149
- border-color: var(--cp-border-disabled);
150
- }
142
+ .cpRadio__label {
143
+ grid-column: 2;
144
+ grid-row: 1;
151
145
  }
152
146
 
153
- &--isActive#{&}--isDisabled,
154
- &--isActive#{&}--isDisabled:hover {
155
- border-color: var(--cp-border-disabled);
147
+ .cpRadio__description {
148
+ grid-column: 2;
149
+ grid-row: 2;
156
150
  }
157
151
 
158
- &:not(:last-of-type) {
159
- margin-bottom: var(--cp-spacing-lg);
152
+ .cpRadio__additionalData {
153
+ grid-column: 2;
154
+ grid-row: 3;
155
+ margin-left: 0;
156
+ text-align: left;
160
157
  }
161
158
 
159
+ @include cp-radio-style(var(--cp-foreground-accent-primary), 'Accent');
160
+
161
+ @include cp-radio-style(var(--cp-foreground-blue-primary), 'Blue');
162
+
163
+ @include cp-radio-style(var(--cp-foreground-accent-primary), 'Purple'); // TODO: Sould be replace by ACCENT
164
+
162
165
  input {
163
166
  -webkit-appearance: none;
164
167
  -moz-appearance: none;
@@ -169,6 +172,9 @@ const computedClasses = ({ value, disabled }: { disabled?: boolean; value: strin
169
172
  width: $cp-radio-base-width;
170
173
  height: $cp-radio-base-width;
171
174
  transition: transform 0.1s linear;
175
+ grid-column: 1;
176
+ grid-row: 1 / -1;
177
+ align-self: center;
172
178
 
173
179
  &:before {
174
180
  content: '';
@@ -198,34 +204,16 @@ const computedClasses = ({ value, disabled }: { disabled?: boolean; value: strin
198
204
  background-color: var(--cp-foreground-disabled);
199
205
  }
200
206
 
201
- &:checked:disabled ~ span .cpRadio {
202
- &__label,
203
- &__additionalData {
204
- color: var(--cp-foreground-disabled);
205
- }
207
+ &:checked:disabled ~ .cpRadio__label,
208
+ &:checked:disabled ~ .cpRadio__additionalData {
209
+ color: var(--cp-foreground-disabled);
206
210
  }
207
211
  }
208
212
 
209
- &__content {
210
- display: flex;
211
- align-items: center;
212
- line-height: 1.3;
213
- }
214
-
215
- &__information {
216
- flex-grow: 2;
217
- display: flex;
218
- align-items: center;
219
- justify-content: space-between;
220
- flex-wrap: wrap;
221
- text-transform: capitalize;
222
- margin: 0 calc(var(--cp-spacing-md) * -1);
223
- }
224
-
225
213
  &__label,
226
214
  &__description {
227
- margin: 0 var(--cp-spacing-md);
228
- flex-grow: 1;
215
+ line-height: 1.3;
216
+ text-transform: capitalize;
229
217
  }
230
218
 
231
219
  &__label,
@@ -242,9 +230,52 @@ const computedClasses = ({ value, disabled }: { disabled?: boolean; value: strin
242
230
  white-space: nowrap;
243
231
  }
244
232
 
245
- &__additionalData {
246
- text-align: right;
247
- margin-left: var(--cp-spacing-xl);
233
+ &__description:empty {
234
+ display: none;
235
+ }
236
+
237
+ &__additionalData:empty {
238
+ display: none;
239
+ }
240
+ }
241
+
242
+ @media (min-width: 768px) {
243
+ .cpRadio {
244
+ display: grid;
245
+ grid-template-columns: min-content max-content minmax(0, 1fr) auto;
246
+ column-gap: var(--cp-spacing-lg);
247
+ row-gap: var(--cp-spacing-lg);
248
+ }
249
+
250
+ .cpRadio__item {
251
+ grid-template-columns: subgrid;
252
+ grid-template-rows: auto;
253
+ grid-column: 1 / -1;
254
+ row-gap: 0;
255
+
256
+ input {
257
+ grid-column: auto;
258
+ grid-row: auto;
259
+ align-self: center;
260
+ }
261
+
262
+ .cpRadio__label {
263
+ grid-column: auto;
264
+ grid-row: auto;
265
+ }
266
+
267
+ .cpRadio__description {
268
+ grid-column: auto;
269
+ grid-row: auto;
270
+ min-width: 0;
271
+ }
272
+
273
+ .cpRadio__additionalData {
274
+ grid-column: auto;
275
+ grid-row: auto;
276
+ text-align: right;
277
+ margin-left: var(--cp-spacing-xl);
278
+ }
248
279
  }
249
280
  }
250
281
  </style>
@@ -0,0 +1,146 @@
1
+ <template>
2
+ <div class="cpRadioGroup" :class="cpRadioGroupDynamicClasses">
3
+ <div v-if="hasGroupHeader" class="cpRadioGroup__header">
4
+ <span v-if="groupLabel" :id="radioGroupLabelId" class="cpRadioGroup__label">
5
+ {{ groupLabel }}
6
+ <span v-if="required" class="cpRadioGroup__required">*</span>
7
+ <span v-else class="cpRadioGroup__optional">(Optional)</span>
8
+ </span>
9
+ <span v-if="groupHelperText" :id="radioGroupHelperTextId" class="cpRadioGroup__helperText">{{
10
+ groupHelperText
11
+ }}</span>
12
+ </div>
13
+ <div class="cpRadioGroup__options">
14
+ <cp-radio-new v-for="option in options" :key="option.value" :option="option" />
15
+ </div>
16
+ </div>
17
+ </template>
18
+
19
+ <script setup lang="ts">
20
+ import { computed, provide, reactive, toRef, useId } from 'vue'
21
+
22
+ import CpRadioNew, { type RadioOption } from './CpRadioNew.vue'
23
+ import { capitalizeFirstLetter } from '@/helpers'
24
+
25
+ interface Props {
26
+ autofocus?: boolean
27
+ direction?: 'horizontal' | 'vertical'
28
+ groupHelperText?: string
29
+ groupLabel?: string
30
+ groupName?: string
31
+ modelValue: string
32
+ options: RadioOption[]
33
+ required?: boolean
34
+ size?: 'md' | 'lg'
35
+ }
36
+
37
+ interface Emits {
38
+ (e: 'update:modelValue', value: string): void
39
+ }
40
+
41
+ const props = withDefaults(defineProps<Props>(), {
42
+ groupName: '',
43
+ autofocus: false,
44
+ direction: 'vertical',
45
+ groupLabel: '',
46
+ groupHelperText: '',
47
+ size: 'md',
48
+ })
49
+
50
+ const emit = defineEmits<Emits>()
51
+
52
+ const radioGroudId = useId()
53
+ const radioGroupLabelId = useId()
54
+ const radioGroupHelperTextId = useId()
55
+
56
+ const onChange = (value: string): void => emit('update:modelValue', value)
57
+
58
+ const cpRadioGroupDynamicClasses = computed(() => [
59
+ `cpRadioGroup--${props.size}`,
60
+ `cpRadioGroup--is${capitalizeFirstLetter(props.direction)}`,
61
+ ])
62
+ const hasGroupHeader = computed(() => props.groupLabel || props.groupHelperText)
63
+ const hasOptionRequiredVisible = computed(() => props.required && !props.groupLabel)
64
+ const hasOptionNotRequiredVisible = computed(() => !props.required && !props.groupLabel)
65
+
66
+ provide(
67
+ 'radioGroup',
68
+ reactive({
69
+ autofocus: toRef(props, 'autofocus'),
70
+ groupName: toRef(props, 'groupName'),
71
+ modelValue: toRef(props, 'modelValue'),
72
+ onChange,
73
+ radioGroupHelperTextId,
74
+ radioGroupLabelId,
75
+ showOptional: hasOptionNotRequiredVisible,
76
+ showRequired: hasOptionRequiredVisible,
77
+ size: toRef(props, 'size'),
78
+ radioGroudId,
79
+ }),
80
+ )
81
+ </script>
82
+
83
+ <style lang="scss">
84
+ .cpRadioGroup {
85
+ display: flex;
86
+ gap: var(--cp-spacing-md);
87
+ flex-direction: column;
88
+ width: 100%;
89
+
90
+ &__options {
91
+ display: flex;
92
+ flex-direction: column;
93
+ gap: var(--cp-spacing-md);
94
+ }
95
+
96
+ &--isHorizontal .cpRadioGroup__options {
97
+ flex-direction: row;
98
+ gap: var(--cp-spacing-2xl);
99
+ flex-wrap: wrap;
100
+ align-items: flex-start;
101
+ }
102
+
103
+ &__header {
104
+ display: flex;
105
+ flex-direction: column;
106
+ font-family: 'Figtree', sans-serif;
107
+ font-size: var(--cp-text-size-sm);
108
+ line-height: var(--cp-line-height-sm);
109
+ }
110
+
111
+ &--lg .cpRadioGroup__header {
112
+ font-size: var(--cp-text-size-md);
113
+ line-height: var(--cp-line-height-md);
114
+ }
115
+
116
+ &__label {
117
+ font-weight: 500;
118
+ color: var(--cp-text-primary);
119
+ display: flex;
120
+ gap: var(--cp-spacing-sm);
121
+ align-items: baseline;
122
+ }
123
+
124
+ &__helperText {
125
+ font-weight: 400;
126
+ color: var(--cp-text-secondary);
127
+ }
128
+
129
+ &__required {
130
+ color: var(--cp-text-error-primary);
131
+ font-weight: 700;
132
+ }
133
+
134
+ &__optional {
135
+ color: var(--cp-text-secondary);
136
+ font-weight: 400;
137
+ font-size: var(--cp-text-size-xs);
138
+ line-height: var(--cp-line-height-xs);
139
+ }
140
+
141
+ &--lg &__optional {
142
+ font-size: var(--cp-text-size-sm);
143
+ line-height: var(--cp-line-height-sm);
144
+ }
145
+ }
146
+ </style>