@citizenplane/pimp 16.0.3 → 16.2.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 (71) hide show
  1. package/dist/pimp.es.js +781 -757
  2. package/dist/pimp.umd.js +21 -21
  3. package/dist/style.css +1 -1
  4. package/package.json +10 -8
  5. package/src/components/CpDate.vue +3 -1
  6. package/src/components/CpHeading.vue +4 -5
  7. package/src/components/CpMultiselect.vue +2 -5
  8. package/src/components/CpTable.vue +4 -2
  9. package/src/components/CpTelInput.vue +18 -12
  10. package/src/components/CpText.vue +141 -0
  11. package/src/components/CpToast.vue +1 -1
  12. package/src/components/CpTransitionExpand.vue +23 -20
  13. package/src/components/index.ts +2 -0
  14. package/src/libs/CoreDatepicker.vue +1 -0
  15. package/src/stories/BaseInputLabel.stories.ts +36 -9
  16. package/src/stories/Colors.mdx +9 -0
  17. package/src/stories/Colors.stories.ts +177 -0
  18. package/src/stories/CpAccordion.stories.ts +188 -159
  19. package/src/stories/CpAccordionGroup.stories.ts +51 -95
  20. package/src/stories/CpAirlineLogo.stories.ts +52 -28
  21. package/src/stories/CpAlert.stories.ts +196 -159
  22. package/src/stories/CpBadge.stories.ts +260 -194
  23. package/src/stories/CpButton.stories.ts +257 -426
  24. package/src/stories/CpCheckbox.stories.ts +102 -30
  25. package/src/stories/CpContextualMenu.stories.ts +14 -9
  26. package/src/stories/CpDate.stories.ts +53 -26
  27. package/src/stories/CpDatepicker.stories.ts +53 -80
  28. package/src/stories/CpDialog.stories.ts +23 -2
  29. package/src/stories/CpHeading.stories.ts +60 -20
  30. package/src/stories/CpIcon.stories.ts +98 -31
  31. package/src/stories/CpInput.stories.ts +164 -73
  32. package/src/stories/CpItemActions.stories.ts +23 -12
  33. package/src/stories/CpLoader.stories.ts +55 -7
  34. package/src/stories/CpMenuItem.stories.ts +53 -27
  35. package/src/stories/CpMultiselect.stories.ts +53 -72
  36. package/src/stories/CpPartnerBadge.stories.ts +58 -76
  37. package/src/stories/CpRadio.stories.ts +45 -49
  38. package/src/stories/CpRadioGroup.stories.ts +47 -40
  39. package/src/stories/CpSelect.stories.ts +108 -34
  40. package/src/stories/CpSelectMenu.stories.ts +66 -55
  41. package/src/stories/CpSelectableButton.stories.ts +170 -81
  42. package/src/stories/CpSwitch.stories.ts +136 -134
  43. package/src/stories/CpTable.stories.ts +69 -13
  44. package/src/stories/CpTableEmptyState.stories.ts +11 -7
  45. package/src/stories/CpTabs.stories.ts +23 -5
  46. package/src/stories/CpTelInput.stories.ts +28 -19
  47. package/src/stories/CpText.stories.ts +131 -0
  48. package/src/stories/CpTextarea.stories.ts +74 -27
  49. package/src/stories/CpToast.stories.ts +75 -109
  50. package/src/stories/CpTooltip.stories.ts +82 -77
  51. package/src/stories/CpTransitionCounter.stories.ts +5 -1
  52. package/src/stories/CpTransitionExpand.stories.ts +12 -7
  53. package/src/stories/CpTransitionListItems.stories.ts +6 -1
  54. package/src/stories/CpTransitionSize.stories.ts +9 -1
  55. package/src/stories/CpTransitionSlide.stories.ts +5 -1
  56. package/src/stories/CpTransitionTabContent.stories.ts +5 -1
  57. package/src/stories/Dimensions.mdx +9 -0
  58. package/src/stories/Dimensions.stories.ts +119 -0
  59. package/src/stories/Easings.mdx +9 -0
  60. package/src/stories/Easings.stories.ts +101 -0
  61. package/src/stories/FocusRings.mdx +9 -0
  62. package/src/stories/FocusRings.stories.ts +74 -0
  63. package/src/stories/Shadows.mdx +9 -0
  64. package/src/stories/Shadows.stories.ts +100 -0
  65. package/src/stories/Typography.mdx +9 -0
  66. package/src/stories/Typography.stories.ts +181 -0
  67. package/src/stories/documentationStyles.ts +2 -10
  68. package/src/stories/tokenUtils.ts +259 -0
  69. package/src/types/primevue-toasteventbus.d.ts +14 -0
  70. package/tsconfig.json +1 -0
  71. package/.lintstagedrc.json +0 -4
@@ -1,48 +1,48 @@
1
1
  import { ref } from 'vue'
2
2
 
3
- import type { Meta, StoryObj } from '@storybook/vue3'
3
+ import type { Meta, StoryObj } from '@storybook/vue3-vite'
4
4
 
5
5
  import CpSelectMenu from '@/components/CpSelectMenu.vue'
6
6
 
7
+ interface SelectValue {
8
+ label: string
9
+ value: string
10
+ }
11
+
7
12
  const meta = {
8
- title: 'Form/CpSelectMenu',
13
+ title: 'Molecules/CpSelectMenu',
9
14
  component: CpSelectMenu,
10
15
  argTypes: {
11
- values: {
12
- control: 'object',
13
- description: 'Array of options to display',
14
- },
15
- selectedValue: {
16
- control: 'object',
17
- description: 'Currently selected value',
18
- },
19
- hasFilter: {
20
- control: 'boolean',
21
- description: 'Whether to show search filter',
22
- },
23
- dropdownTitle: {
24
- control: 'text',
25
- description: 'Title text for the dropdown',
26
- },
27
- dropdownFilterPlaceholder: {
28
- control: 'text',
29
- description: 'Placeholder text for the filter input',
30
- },
16
+ values: { control: 'object', description: 'Array of options to display' },
17
+ selectedValue: { control: 'object', description: 'Currently selected value' },
18
+ hasFilter: { control: 'boolean', description: 'Whether to show search filter' },
19
+ dropdownTitle: { control: 'text', description: 'Title text for the dropdown' },
20
+ dropdownFilterPlaceholder: { control: 'text', description: 'Placeholder text for the filter input' },
31
21
  dropdownEmptyViewPlaceholder: {
32
22
  control: 'text',
33
23
  description: 'Text to show when no options match the filter',
34
24
  },
35
- closeOnSelect: {
36
- control: 'boolean',
37
- description: 'Whether to close dropdown after selection',
38
- },
25
+ closeOnSelect: { control: 'boolean', description: 'Whether to close dropdown after selection' },
39
26
  },
40
- decorators: [() => ({ template: '<div style="min-height: 30vh;"><story/></div>' })],
27
+ decorators: [() => ({ template: '<div style="min-height: 30vh;"><story /></div>' })],
41
28
  } satisfies Meta<typeof CpSelectMenu>
42
29
 
43
30
  export default meta
31
+
44
32
  type Story = StoryObj<typeof meta>
45
33
 
34
+ type SelectMenuStoryArgs = NonNullable<Story['args']>
35
+
36
+ function pickSelectMenuBindProps(args: SelectMenuStoryArgs) {
37
+ return {
38
+ closeOnSelect: args.closeOnSelect,
39
+ dropdownEmptyViewPlaceholder: args.dropdownEmptyViewPlaceholder,
40
+ dropdownFilterPlaceholder: args.dropdownFilterPlaceholder,
41
+ dropdownTitle: args.dropdownTitle,
42
+ hasFilter: args.hasFilter,
43
+ }
44
+ }
45
+
46
46
  const sampleOptions = [
47
47
  { value: '1', label: 'Apple' },
48
48
  { value: '2', label: 'Banana' },
@@ -53,6 +53,10 @@ const sampleOptions = [
53
53
  { value: '7', label: 'Cherry' },
54
54
  ]
55
55
 
56
+ /**
57
+ * Default select menu. Use the controls to experiment with each prop in
58
+ * isolation.
59
+ */
56
60
  export const Default: Story = {
57
61
  args: {
58
62
  values: sampleOptions,
@@ -63,69 +67,76 @@ export const Default: Story = {
63
67
  dropdownEmptyViewPlaceholder: 'No option found',
64
68
  closeOnSelect: true,
65
69
  },
66
- render: (args) => ({
70
+ render: (args: SelectMenuStoryArgs) => ({
67
71
  components: { CpSelectMenu },
68
72
  setup() {
69
- const selectedValue = ref(args.selectedValue)
70
- return { args, selectedValue }
73
+ const selectedValue = ref<SelectValue>({ ...args.selectedValue })
74
+ const values = ref<SelectValue[]>([...(args.values ?? [])])
75
+ const menuProps = pickSelectMenuBindProps(args)
76
+
77
+ const onUpdateSelectedValue = (data: SelectValue) => {
78
+ selectedValue.value = data
79
+ }
80
+
81
+ return { menuProps, onUpdateSelectedValue, selectedValue, values }
71
82
  },
72
83
  template: `
73
84
  <div style="max-width: 400px; padding: 20px;">
74
85
  <CpSelectMenu
75
86
  v-model:selectedValue="selectedValue"
76
- v-bind="args"
87
+ v-bind="menuProps"
88
+ :values="values"
77
89
  @update:selected-value="onUpdateSelectedValue"
78
90
  />
79
91
  </div>
80
92
  `,
81
- methods: {
82
- onUpdateSelectedValue: (data) => {
83
- args.selectedValue = data
84
- },
85
- },
86
93
  }),
87
94
  }
88
95
 
89
- const searchQuery = ref('')
90
-
91
96
  export const WithFilter: Story = {
92
97
  args: {
93
98
  ...Default.args,
94
99
  hasFilter: true,
95
100
  dropdownTitle: 'Search for a fruit',
96
- selectedValue: { value: '1', label: 'Apple' },
97
- values: sampleOptions,
98
101
  },
99
- render: (args) => ({
102
+ render: (args: SelectMenuStoryArgs) => ({
100
103
  components: { CpSelectMenu },
101
104
  setup() {
102
- const selectedValue = ref(args.selectedValue)
103
- return { args, selectedValue }
105
+ const selectedValue = ref<SelectValue>({ ...args.selectedValue })
106
+ const values = ref<SelectValue[]>([...(args.values ?? [])])
107
+ const menuProps = pickSelectMenuBindProps(args)
108
+ const searchQuery = ref('')
109
+
110
+ const onUpdateSelectedValue = (data: SelectValue) => {
111
+ selectedValue.value = data
112
+ }
113
+
114
+ const onFilterChange = (data: string) => {
115
+ searchQuery.value = data
116
+ values.value = sampleOptions.filter((option) =>
117
+ option.label.toLowerCase().includes(searchQuery.value.toLowerCase()),
118
+ )
119
+ }
120
+
121
+ return { menuProps, onFilterChange, onUpdateSelectedValue, selectedValue, values }
104
122
  },
105
123
  template: `
106
124
  <div style="max-width: 400px; padding: 20px;">
107
125
  <CpSelectMenu
108
126
  v-model:selectedValue="selectedValue"
109
- v-bind="args"
127
+ v-bind="menuProps"
128
+ :values="values"
110
129
  @on-filter-change="onFilterChange"
111
130
  @update:selected-value="onUpdateSelectedValue"
112
131
  />
113
132
  </div>
114
133
  `,
115
- methods: {
116
- onFilterChange: (data) => {
117
- searchQuery.value = data
118
- args.values = sampleOptions.filter((option) =>
119
- option.label.toLowerCase().includes(searchQuery.value.toLowerCase()),
120
- )
121
- },
122
- onUpdateSelectedValue: (data) => {
123
- args.selectedValue = data
124
- },
125
- },
126
134
  }),
127
135
  }
128
136
 
137
+ /**
138
+ * Override the dropdown title via `dropdownTitle`.
139
+ */
129
140
  export const CustomTitle: Story = {
130
141
  args: {
131
142
  ...Default.args,
@@ -1,16 +1,19 @@
1
- import { ref } from 'vue'
2
-
3
- import type { Meta, StoryObj } from '@storybook/vue3'
1
+ import type { Args, Meta, StoryObj } from '@storybook/vue3-vite'
4
2
 
5
3
  import CpSelectableButton from '@/components/CpSelectableButton.vue'
6
4
 
5
+ import { docCellStyle, docLabelStyle, docRowWrapStyle } from '@/stories/documentationStyles'
6
+
7
+ const selectableAppearances = ['primary', 'secondary', 'tertiary', 'quaternary', 'inverse'] as const
8
+ const selectableSizes = ['3xs', '2xs', 'xs', 'sm', 'md'] as const
9
+
7
10
  const meta = {
8
- title: 'Form/CpSelectableButton',
11
+ title: 'Atoms/CpSelectableButton',
9
12
  component: CpSelectableButton,
10
13
  argTypes: {
11
14
  appearance: {
12
15
  control: 'select',
13
- options: ['primary', 'secondary', 'tertiary', 'quaternary', 'inverse'],
16
+ options: selectableAppearances,
14
17
  description: 'Visual style of the selectable button',
15
18
  },
16
19
  isSelected: {
@@ -23,7 +26,7 @@ const meta = {
23
26
  },
24
27
  size: {
25
28
  control: 'select',
26
- options: ['3xs', '2xs', 'xs', 'sm', 'md'],
29
+ options: selectableSizes,
27
30
  description: 'Size of the button',
28
31
  },
29
32
  disabled: {
@@ -40,6 +43,22 @@ const meta = {
40
43
  export default meta
41
44
  type Story = StoryObj<typeof meta>
42
45
 
46
+ const defaultRender = (args: Args) => ({
47
+ components: { CpSelectableButton },
48
+ setup() {
49
+ return { args }
50
+ },
51
+ template: `
52
+ <div style="display: flex; justify-content: center; align-items: center; min-height: 120px; min-width: 240px;">
53
+ <CpSelectableButton v-bind="args" />
54
+ </div>
55
+ `,
56
+ })
57
+
58
+ /**
59
+ * Default selectable button. Use the controls to experiment with each prop
60
+ * in isolation.
61
+ */
43
62
  export const Default: Story = {
44
63
  args: {
45
64
  appearance: 'primary',
@@ -49,72 +68,150 @@ export const Default: Story = {
49
68
  isSelected: false,
50
69
  isExpanded: false,
51
70
  },
52
- render: (args) => ({
71
+ render: defaultRender,
72
+ }
73
+
74
+ /* -------------------------------------------------------------------------- */
75
+ /* Appearances */
76
+ /* -------------------------------------------------------------------------- */
77
+
78
+ /**
79
+ * Every appearance rendered side by side. The `inverse` appearance is meant
80
+ * for dark backgrounds.
81
+ */
82
+ export const Appearances: Story = {
83
+ parameters: { controls: { disable: true } },
84
+ render: () => ({
53
85
  components: { CpSelectableButton },
54
86
  setup() {
55
- return { args }
87
+ return { selectableAppearances, docCellStyle, docLabelStyle, docRowWrapStyle }
88
+ },
89
+ template: `
90
+ <div :style="docRowWrapStyle">
91
+ <div
92
+ v-for="appearance in selectableAppearances"
93
+ :key="appearance"
94
+ :style="[docCellStyle, appearance === 'inverse' ? 'background: #111; color: #fff; padding: 16px; border-radius: 8px;' : '']"
95
+ >
96
+ <span :style="[docLabelStyle, appearance === 'inverse' ? 'color: #e5e7eb;' : '']">{{ appearance }}</span>
97
+ <CpSelectableButton :appearance="appearance" label="Label" />
98
+ </div>
99
+ </div>
100
+ `,
101
+ }),
102
+ }
103
+
104
+ /* -------------------------------------------------------------------------- */
105
+ /* Sizes */
106
+ /* -------------------------------------------------------------------------- */
107
+
108
+ /**
109
+ * All sizes rendered side by side, from `3xs` to `md`.
110
+ */
111
+ export const Sizes: Story = {
112
+ parameters: { controls: { disable: true } },
113
+ render: () => ({
114
+ components: { CpSelectableButton },
115
+ setup() {
116
+ return { selectableSizes, docCellStyle, docLabelStyle, docRowWrapStyle }
117
+ },
118
+ template: `
119
+ <div :style="docRowWrapStyle">
120
+ <div v-for="size in selectableSizes" :key="size" :style="docCellStyle">
121
+ <span :style="docLabelStyle">{{ size }}</span>
122
+ <CpSelectableButton appearance="primary" :size="size" label="Label" />
123
+ </div>
124
+ </div>
125
+ `,
126
+ }),
127
+ }
128
+
129
+ /* -------------------------------------------------------------------------- */
130
+ /* States */
131
+ /* -------------------------------------------------------------------------- */
132
+
133
+ /**
134
+ * The three selection states: default, `isExpanded` and `isSelected`.
135
+ */
136
+ export const States: Story = {
137
+ parameters: { controls: { disable: true } },
138
+ render: () => ({
139
+ components: { CpSelectableButton },
140
+ setup() {
141
+ return { docCellStyle, docLabelStyle, docRowWrapStyle }
56
142
  },
57
143
  template: `
58
- <div style="display: flex; justify-content: center; align-items: center; min-height: 240px; min-width:240px;">
59
- <CpSelectableButton
60
- v-bind="args"
61
- />
144
+ <div :style="docRowWrapStyle">
145
+ <div :style="docCellStyle">
146
+ <span :style="docLabelStyle">Default</span>
147
+ <CpSelectableButton appearance="primary" label="Label" />
148
+ </div>
149
+ <div :style="docCellStyle">
150
+ <span :style="docLabelStyle">Expanded</span>
151
+ <CpSelectableButton appearance="primary" label="Label" :is-expanded="true" />
152
+ </div>
153
+ <div :style="docCellStyle">
154
+ <span :style="docLabelStyle">Selected</span>
155
+ <CpSelectableButton appearance="primary" label="Label" :is-selected="true" />
156
+ </div>
157
+ <div :style="docCellStyle">
158
+ <span :style="docLabelStyle">Disabled</span>
159
+ <CpSelectableButton appearance="primary" label="Label" :disabled="true" />
160
+ </div>
62
161
  </div>
63
162
  `,
64
163
  }),
65
164
  }
66
165
 
166
+ /* -------------------------------------------------------------------------- */
167
+ /* Icons */
168
+ /* -------------------------------------------------------------------------- */
169
+
170
+ /**
171
+ * Slot `#leading-icon` and `#trailing-icon` accept any icon component.
172
+ */
67
173
  export const WithIcons: Story = {
68
174
  args: {
69
175
  appearance: 'primary',
70
176
  size: 'md',
71
- disabled: false,
72
177
  label: 'Label',
73
- isSelected: false,
74
- isExpanded: false,
75
178
  },
76
- render: (args) => ({
179
+ render: (args: Args) => ({
77
180
  components: { CpSelectableButton },
78
181
  setup() {
79
182
  return { args }
80
183
  },
81
184
  template: `
82
- <div style="display: flex; justify-content: center; align-items: center; min-height: 240px; min-width:240px;">
83
- <CpSelectableButton
84
- v-bind="args"
85
- >
86
- <template #leading-icon>
87
- <CpIcon type="circle" />
88
- </template>
89
- <template #trailing-icon>
90
- <CpIcon type="circle" />
91
- </template>
92
- </CpSelectableButton>
93
- </div>
185
+ <CpSelectableButton v-bind="args">
186
+ <template #leading-icon>
187
+ <CpIcon type="circle" />
188
+ </template>
189
+ <template #trailing-icon>
190
+ <CpIcon type="circle" />
191
+ </template>
192
+ </CpSelectableButton>
94
193
  `,
95
194
  }),
96
195
  }
97
196
 
197
+ /**
198
+ * Stretch the button to fill its container by setting `width: 100%`.
199
+ */
98
200
  export const FullWidth: Story = {
99
201
  args: {
100
202
  appearance: 'secondary',
101
203
  size: 'md',
102
- disabled: false,
103
204
  label: 'Label',
104
205
  isSelected: true,
105
- isExpanded: false,
106
206
  },
107
- render: (args) => ({
207
+ render: (args: Args) => ({
108
208
  components: { CpSelectableButton },
109
209
  setup() {
110
210
  return { args }
111
211
  },
112
212
  template: `
113
- <div style="display: flex; justify-content: center; align-items: center; min-height: 240px; width:500px;">
114
- <CpSelectableButton
115
- style="width: 100%;"
116
- v-bind="args"
117
- >
213
+ <div style="width: 500px;">
214
+ <CpSelectableButton style="width: 100%;" v-bind="args">
118
215
  <template #leading-icon>
119
216
  <CpIcon type="circle" />
120
217
  </template>
@@ -127,56 +224,48 @@ export const FullWidth: Story = {
127
224
  }),
128
225
  }
129
226
 
130
- export const Sizing: Story = {
131
- args: {
132
- disabled: false,
133
- label: 'Label',
134
- },
135
- render: (args) => ({
227
+ /* -------------------------------------------------------------------------- */
228
+ /* Matrix */
229
+ /* -------------------------------------------------------------------------- */
230
+
231
+ /**
232
+ * Full appearance × size matrix, each cell showing default / expanded /
233
+ * selected side by side. Useful for visual regression.
234
+ */
235
+ export const Matrix: Story = {
236
+ parameters: { controls: { disable: true } },
237
+ render: () => ({
136
238
  components: { CpSelectableButton },
137
239
  setup() {
138
- const defaultValue = ref('default')
139
- const expandedValue = ref('expanded')
140
- const selectedValue = ref('selected')
141
- return { args, defaultValue, expandedValue, selectedValue }
240
+ return { selectableAppearances, selectableSizes }
142
241
  },
143
242
  template: `
144
- <div v-for="appearance in ['primary', 'secondary', 'tertiary', 'quaternary', 'inverse']" :style="['display: flex; flex-direction: row; gap: 48px; align-items: center; padding: 48px', {'background-color': appearance === 'inverse' ? 'black' : 'transparent', 'color': appearance === 'inverse' ? 'white' : 'black'}]">
145
- <p style="min-width: 80px;">{{ appearance }}</p>
146
- <div v-for="size in ['3xs', '2xs', 'xs', 'sm', 'md']" style="display: flex; justify-content: center; align-items: center; gap: 8px;">
147
- <p>{{ size }}</p>
148
- <CpSelectableButton
149
- v-bind="{ ...args, appearance, size }"
150
- >
151
- <template #leading-icon>
152
- <CpIcon type="circle" />
153
- </template>
154
- <template #trailing-icon>
155
- <CpIcon type="circle" />
156
- </template>
157
- </CpSelectableButton>
158
- <CpSelectableButton
159
- v-bind="{ ...args, appearance, size }"
160
- isExpanded
161
- >
162
- <template #leading-icon>
163
- <CpIcon type="circle" />
164
- </template>
165
- <template #trailing-icon>
166
- <CpIcon type="circle" />
167
- </template>
168
- </CpSelectableButton>
169
- <CpSelectableButton
170
- v-bind="{ ...args, appearance, size }"
171
- isSelected
243
+ <div>
244
+ <div
245
+ v-for="appearance in selectableAppearances"
246
+ :key="appearance"
247
+ :style="['display: flex; flex-direction: row; gap: 48px; align-items: center; padding: 48px', appearance === 'inverse' ? 'background-color: black; color: white;' : '']"
248
+ >
249
+ <p style="min-width: 80px; margin: 0;">{{ appearance }}</p>
250
+ <div
251
+ v-for="size in selectableSizes"
252
+ :key="size"
253
+ style="display: flex; justify-content: center; align-items: center; gap: 8px;"
172
254
  >
173
- <template #leading-icon>
174
- <CpIcon type="circle" />
175
- </template>
176
- <template #trailing-icon>
177
- <CpIcon type="circle" />
178
- </template>
179
- </CpSelectableButton>
255
+ <p style="margin: 0;">{{ size }}</p>
256
+ <CpSelectableButton :appearance="appearance" :size="size" label="Label">
257
+ <template #leading-icon><CpIcon type="circle" /></template>
258
+ <template #trailing-icon><CpIcon type="circle" /></template>
259
+ </CpSelectableButton>
260
+ <CpSelectableButton :appearance="appearance" :size="size" label="Label" :is-expanded="true">
261
+ <template #leading-icon><CpIcon type="circle" /></template>
262
+ <template #trailing-icon><CpIcon type="circle" /></template>
263
+ </CpSelectableButton>
264
+ <CpSelectableButton :appearance="appearance" :size="size" label="Label" :is-selected="true">
265
+ <template #leading-icon><CpIcon type="circle" /></template>
266
+ <template #trailing-icon><CpIcon type="circle" /></template>
267
+ </CpSelectableButton>
268
+ </div>
180
269
  </div>
181
270
  </div>
182
271
  `,