@citizenplane/pimp 16.0.2 → 16.1.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 (62) hide show
  1. package/dist/pimp.es.js +343 -311
  2. package/dist/pimp.umd.js +21 -21
  3. package/dist/style.css +1 -1
  4. package/package.json +2 -1
  5. package/src/components/CpAlert.vue +38 -30
  6. package/src/components/CpHeading.vue +4 -5
  7. package/src/components/CpText.vue +141 -0
  8. package/src/components/index.ts +2 -0
  9. package/src/stories/BaseInputLabel.stories.ts +36 -9
  10. package/src/stories/Colors.mdx +9 -0
  11. package/src/stories/Colors.stories.ts +177 -0
  12. package/src/stories/CpAccordion.stories.ts +187 -158
  13. package/src/stories/CpAccordionGroup.stories.ts +50 -94
  14. package/src/stories/CpAirlineLogo.stories.ts +49 -28
  15. package/src/stories/CpAlert.stories.ts +194 -157
  16. package/src/stories/CpBadge.stories.ts +259 -193
  17. package/src/stories/CpButton.stories.ts +257 -426
  18. package/src/stories/CpCheckbox.stories.ts +101 -29
  19. package/src/stories/CpContextualMenu.stories.ts +9 -8
  20. package/src/stories/CpDate.stories.ts +52 -25
  21. package/src/stories/CpDatepicker.stories.ts +57 -88
  22. package/src/stories/CpDialog.stories.ts +22 -1
  23. package/src/stories/CpHeading.stories.ts +59 -20
  24. package/src/stories/CpIcon.stories.ts +98 -31
  25. package/src/stories/CpInput.stories.ts +142 -67
  26. package/src/stories/CpItemActions.stories.ts +22 -27
  27. package/src/stories/CpLoader.stories.ts +54 -6
  28. package/src/stories/CpMenuItem.stories.ts +52 -26
  29. package/src/stories/CpMultiselect.stories.ts +52 -71
  30. package/src/stories/CpPartnerBadge.stories.ts +53 -74
  31. package/src/stories/CpRadio.stories.ts +44 -48
  32. package/src/stories/CpRadioGroup.stories.ts +46 -39
  33. package/src/stories/CpSelect.stories.ts +98 -39
  34. package/src/stories/CpSelectMenu.stories.ts +49 -57
  35. package/src/stories/CpSelectableButton.stories.ts +170 -81
  36. package/src/stories/CpSwitch.stories.ts +135 -133
  37. package/src/stories/CpTable.stories.ts +54 -1
  38. package/src/stories/CpTableEmptyState.stories.ts +11 -7
  39. package/src/stories/CpTabs.stories.ts +22 -4
  40. package/src/stories/CpTelInput.stories.ts +25 -23
  41. package/src/stories/CpText.stories.ts +131 -0
  42. package/src/stories/CpTextarea.stories.ts +59 -23
  43. package/src/stories/CpToast.stories.ts +53 -103
  44. package/src/stories/CpTooltip.stories.ts +82 -77
  45. package/src/stories/CpTransitionCounter.stories.ts +4 -0
  46. package/src/stories/CpTransitionExpand.stories.ts +11 -6
  47. package/src/stories/CpTransitionListItems.stories.ts +5 -0
  48. package/src/stories/CpTransitionSize.stories.ts +8 -0
  49. package/src/stories/CpTransitionSlide.stories.ts +4 -0
  50. package/src/stories/CpTransitionTabContent.stories.ts +4 -0
  51. package/src/stories/Dimensions.mdx +9 -0
  52. package/src/stories/Dimensions.stories.ts +119 -0
  53. package/src/stories/Easings.mdx +9 -0
  54. package/src/stories/Easings.stories.ts +101 -0
  55. package/src/stories/FocusRings.mdx +9 -0
  56. package/src/stories/FocusRings.stories.ts +74 -0
  57. package/src/stories/Shadows.mdx +9 -0
  58. package/src/stories/Shadows.stories.ts +100 -0
  59. package/src/stories/Typography.mdx +9 -0
  60. package/src/stories/Typography.stories.ts +181 -0
  61. package/src/stories/documentationStyles.ts +2 -10
  62. package/src/stories/tokenUtils.ts +259 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@citizenplane/pimp",
3
- "version": "16.0.2",
3
+ "version": "16.1.0",
4
4
  "scripts": {
5
5
  "dev": "storybook dev -p 8081",
6
6
  "build-storybook": "storybook build --output-dir ./docs",
@@ -48,6 +48,7 @@
48
48
  "@commitlint/cli": "20.4.2",
49
49
  "@eslint/eslintrc": "^3.3.3",
50
50
  "@eslint/js": "^9.39.2",
51
+ "@storybook/addon-docs": "^9.1.20",
51
52
  "@storybook/addon-onboarding": "^9.1.17",
52
53
  "@storybook/addon-vitest": "^9.0.18",
53
54
  "@storybook/preset-scss": "^1.0.3",
@@ -98,37 +98,39 @@ const emit = defineEmits<{
98
98
 
99
99
  const slots = useSlots()
100
100
 
101
- const colorProps = {
102
- neutral: {
103
- icon: 'dashed-circle',
104
- primaryActionAppearance: 'secondary',
105
- secondaryActionAppearance: 'tertiary',
106
- },
107
- accent: {
108
- icon: 'info',
109
- primaryActionAppearance: 'primary',
101
+ const colorProps = computed(() => {
102
+ const defaultProps = {
103
+ primaryActionAppearance: isExpanded.value ? 'primary' : 'secondary',
110
104
  secondaryActionAppearance: 'secondary',
111
- },
112
- success: {
113
- icon: 'check',
114
- primaryActionAppearance: 'primary',
115
- secondaryActionAppearance: 'secondary',
116
- },
117
- warning: {
118
- icon: 'alert-triangle',
119
- primaryActionAppearance: 'primary',
120
- secondaryActionAppearance: 'secondary',
121
- },
122
- error: {
123
- icon: 'x-octagon',
124
- primaryActionAppearance: 'primary',
125
- secondaryActionAppearance: 'secondary',
126
- },
127
- }
105
+ }
106
+ return {
107
+ neutral: {
108
+ icon: 'dashed-circle',
109
+ primaryActionAppearance: isExpanded.value ? 'secondary' : 'tertiary',
110
+ secondaryActionAppearance: 'tertiary',
111
+ },
112
+ accent: {
113
+ ...defaultProps,
114
+ icon: 'info',
115
+ },
116
+ success: {
117
+ ...defaultProps,
118
+ icon: 'check',
119
+ },
120
+ warning: {
121
+ ...defaultProps,
122
+ icon: 'alert-triangle',
123
+ },
124
+ error: {
125
+ ...defaultProps,
126
+ icon: 'x-octagon',
127
+ },
128
+ }
129
+ })
128
130
 
129
131
  const alertIcon = computed(() => {
130
132
  if (props.icon) return props.icon
131
- return colorProps[props.color].icon
133
+ return colorProps.value[props.color].icon
132
134
  })
133
135
 
134
136
  const isExpanded = computed(() => props.type === 'expanded')
@@ -136,9 +138,11 @@ const hasTitle = computed(() => !!props.title || !!slots.title)
136
138
  const hasContent = computed(() => isExpanded.value && (!!props.content || !!slots.default))
137
139
  const hasActions = computed(() => hasPrimaryAction.value || hasSecondaryAction.value)
138
140
  const hasPrimaryAction = computed(() => !!props.primaryActionLabel || !!slots['primary-action'])
139
- const hasSecondaryAction = computed(() => !!props.secondaryActionLabel || !!slots['secondary-action'])
140
- const primaryActionAppearance = computed(() => colorProps[props.color].primaryActionAppearance)
141
- const secondaryActionAppearance = computed(() => colorProps[props.color].secondaryActionAppearance)
141
+ const hasSecondaryAction = computed(
142
+ () => isExpanded.value && (!!props.secondaryActionLabel || !!slots['secondary-action']),
143
+ )
144
+ const primaryActionAppearance = computed(() => colorProps.value[props.color].primaryActionAppearance)
145
+ const secondaryActionAppearance = computed(() => colorProps.value[props.color].secondaryActionAppearance)
142
146
  const actionsSize = computed(() => (isExpanded.value ? 'sm' : 'xs'))
143
147
 
144
148
  const dynamicClasses = computed(() => [
@@ -197,6 +201,10 @@ const dynamicClasses = computed(() => [
197
201
  gap: var(--cp-spacing-md);
198
202
  }
199
203
 
204
+ &__close {
205
+ align-self: flex-start;
206
+ }
207
+
200
208
  &__title {
201
209
  font-size: var(--cp-text-size-sm);
202
210
  line-height: var(--cp-line-height-sm);
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <component :is="$props.headingLevel" v-bind="$attrs" :class="`cpHeading--${size}`" class="cpHeading">
2
+ <component :is="$props.headingLevel" class="cpHeading" :class="`cpHeading--${size}`" v-bind="$attrs">
3
3
  <slot />
4
4
  </component>
5
5
  </template>
@@ -7,12 +7,11 @@
7
7
  <script setup lang="ts">
8
8
  import { HeadingLevels } from '@/constants'
9
9
 
10
- /**
11
- * @deprecated This component is deprecated. Please use directly CSS variables instead.
12
- */
13
-
14
10
  type HeadingSize = 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900
15
11
 
12
+ /**
13
+ * @deprecated `CpHeading` is deprecated. Use CpText instead.
14
+ */
16
15
  interface Props {
17
16
  headingLevel?: HeadingLevels
18
17
  size?: HeadingSize
@@ -0,0 +1,141 @@
1
+ <template>
2
+ <component :is="tag" class="cpText" :class="dynamicClasses" v-bind="$attrs">
3
+ <slot />
4
+ </component>
5
+ </template>
6
+
7
+ <script setup lang="ts">
8
+ import { computed } from 'vue'
9
+
10
+ import { capitalizeFirstLetter } from '@/helpers'
11
+
12
+ export type CpTextSize =
13
+ | 'xs'
14
+ | 'sm'
15
+ | 'md'
16
+ | 'lg'
17
+ | 'xl'
18
+ | '2xl'
19
+ | '3xl'
20
+ | '4xl'
21
+ | '5xl'
22
+ | '6xl'
23
+ | '7xl'
24
+ | '8xl'
25
+ | '9xl'
26
+
27
+ export type CpTextWeight = 400 | 500 | 600 | 700
28
+
29
+ interface Props {
30
+ size?: CpTextSize
31
+ tag?: string
32
+ weight?: CpTextWeight
33
+ }
34
+
35
+ const props = withDefaults(defineProps<Props>(), {
36
+ size: 'md',
37
+ tag: 'p',
38
+ weight: 400,
39
+ })
40
+
41
+ const weightList = {
42
+ 400: 'normal',
43
+ 500: 'medium',
44
+ 600: 'semibold',
45
+ 700: 'bold',
46
+ }
47
+
48
+ const dynamicClasses = computed(() => [
49
+ `cpText--${props.size}`,
50
+ `cpText--is${capitalizeFirstLetter(weightList[props.weight])}`,
51
+ ])
52
+
53
+ defineOptions({
54
+ inheritAttrs: false,
55
+ })
56
+ </script>
57
+
58
+ <style lang="scss">
59
+ .cpText {
60
+ &--xs {
61
+ font-size: var(--cp-text-size-xs);
62
+ line-height: var(--cp-line-height-xs);
63
+ }
64
+
65
+ &--sm {
66
+ font-size: var(--cp-text-size-sm);
67
+ line-height: var(--cp-line-height-sm);
68
+ }
69
+
70
+ &--md {
71
+ font-size: var(--cp-text-size-md);
72
+ line-height: var(--cp-line-height-md);
73
+ }
74
+
75
+ &--lg {
76
+ font-size: var(--cp-text-size-lg);
77
+ line-height: var(--cp-line-height-lg);
78
+ }
79
+
80
+ &--xl {
81
+ font-size: var(--cp-text-size-xl);
82
+ line-height: var(--cp-line-height-xl);
83
+ }
84
+
85
+ &--2xl {
86
+ font-size: var(--cp-text-size-2xl);
87
+ line-height: var(--cp-line-height-2xl);
88
+ }
89
+
90
+ &--3xl {
91
+ font-size: var(--cp-text-size-3xl);
92
+ line-height: var(--cp-line-height-3xl);
93
+ }
94
+
95
+ &--4xl {
96
+ font-size: var(--cp-text-size-4xl);
97
+ line-height: var(--cp-line-height-4xl);
98
+ }
99
+
100
+ &--5xl {
101
+ font-size: var(--cp-text-size-5xl);
102
+ line-height: var(--cp-line-height-5xl);
103
+ }
104
+
105
+ &--6xl {
106
+ font-size: var(--cp-text-size-6xl);
107
+ line-height: var(--cp-line-height-6xl);
108
+ }
109
+
110
+ &--7xl {
111
+ font-size: var(--cp-text-size-7xl);
112
+ line-height: var(--cp-line-height-7xl);
113
+ }
114
+
115
+ &--8xl {
116
+ font-size: var(--cp-text-size-8xl);
117
+ line-height: var(--cp-line-height-8xl);
118
+ }
119
+
120
+ &--9xl {
121
+ font-size: var(--cp-text-size-9xl);
122
+ line-height: var(--cp-line-height-9xl);
123
+ }
124
+
125
+ &--isNormal {
126
+ font-weight: 400;
127
+ }
128
+
129
+ &--isMedium {
130
+ font-weight: 500;
131
+ }
132
+
133
+ &--isSemibold {
134
+ font-weight: 600;
135
+ }
136
+
137
+ &--isBold {
138
+ font-weight: 700;
139
+ }
140
+ }
141
+ </style>
@@ -46,6 +46,7 @@ import CpTable from './CpTable.vue'
46
46
  import CpTableColumnEditor from './CpTableColumnEditor.vue'
47
47
  import CpTabs from './CpTabs.vue'
48
48
  import CpTelInput from './CpTelInput.vue'
49
+ import CpText from './CpText.vue'
49
50
  import CpTextarea from './CpTextarea.vue'
50
51
  import CpToast from './CpToast.vue'
51
52
  import CpTooltip from './CpTooltip.vue'
@@ -87,6 +88,7 @@ const Components = {
87
88
  CpAlert,
88
89
  CpLoader,
89
90
  CpInput,
91
+ CpText,
90
92
  CpTextarea,
91
93
  CpSelect,
92
94
  CpSelectMenu,
@@ -1,9 +1,11 @@
1
- import type { Meta, StoryObj } from '@storybook/vue3'
1
+ import type { Args, Meta, StoryObj } from '@storybook/vue3'
2
2
 
3
3
  import BaseInputLabel from '@/components/BaseInputLabel.vue'
4
4
 
5
+ import { docCellStyle, docLabelStyle, docRowWrapStyle } from '@/stories/documentationStyles'
6
+
5
7
  const meta = {
6
- title: 'Form/BaseInputLabel',
8
+ title: 'Atoms/BaseInputLabel',
7
9
  component: BaseInputLabel,
8
10
  argTypes: {
9
11
  isInvalid: {
@@ -16,20 +18,45 @@ const meta = {
16
18
  export default meta
17
19
  type Story = StoryObj<typeof meta>
18
20
 
21
+ /**
22
+ * Default label used above form controls. Use the controls to experiment
23
+ * with each prop in isolation.
24
+ */
19
25
  export const Default: Story = {
20
- args: {
21
- isInvalid: false,
22
- },
23
- render: (args) => ({
26
+ args: { isInvalid: false },
27
+ render: (args: Args) => ({
24
28
  components: { BaseInputLabel },
25
29
  setup() {
26
30
  return { args }
27
31
  },
28
32
  template: `
29
33
  <div class="baseInputLabel-story">
30
- <BaseInputLabel v-bind="args">
31
- Default Label
32
- </BaseInputLabel>
34
+ <BaseInputLabel v-bind="args">Default Label</BaseInputLabel>
35
+ </div>
36
+ `,
37
+ }),
38
+ }
39
+
40
+ /**
41
+ * Default and invalid states side by side.
42
+ */
43
+ export const States: Story = {
44
+ parameters: { controls: { disable: true } },
45
+ render: () => ({
46
+ components: { BaseInputLabel },
47
+ setup() {
48
+ return { docCellStyle, docLabelStyle, docRowWrapStyle }
49
+ },
50
+ template: `
51
+ <div :style="docRowWrapStyle">
52
+ <div :style="docCellStyle">
53
+ <span :style="docLabelStyle">Default</span>
54
+ <BaseInputLabel>Default Label</BaseInputLabel>
55
+ </div>
56
+ <div :style="docCellStyle">
57
+ <span :style="docLabelStyle">Invalid</span>
58
+ <BaseInputLabel :is-invalid="true">Invalid Label</BaseInputLabel>
59
+ </div>
33
60
  </div>
34
61
  `,
35
62
  }),
@@ -0,0 +1,9 @@
1
+ import { Meta, Title, Description, Stories } from '@storybook/addon-docs/blocks'
2
+
3
+ import * as ColorsStories from './Colors.stories'
4
+
5
+ <Meta of={ColorsStories} />
6
+
7
+ <Title />
8
+ <Description />
9
+ <Stories />
@@ -0,0 +1,177 @@
1
+ import type { Meta, StoryObj } from '@storybook/vue3'
2
+
3
+ import { docLabelStyle } from '@/stories/documentationStyles'
4
+ import type { Token } from '@/stories/tokenUtils'
5
+ import { copyableClass, copyableCopiedClass, readTokens, useCopier } from '@/stories/tokenUtils'
6
+
7
+ const meta: Meta = {
8
+ title: 'Foundations/Colors',
9
+ tags: ['!dev'],
10
+ parameters: {
11
+ layout: 'padded',
12
+ controls: { disable: true },
13
+ docs: { source: { code: null } },
14
+ },
15
+ }
16
+
17
+ export default meta
18
+
19
+ type Story = StoryObj
20
+
21
+ const FAMILIES = ['accent', 'blue', 'error', 'magenta', 'neutral', 'pink', 'success', 'warning', 'yellow'] as const
22
+
23
+ const FAMILY_ORDER: readonly string[] = [
24
+ 'base',
25
+ 'accent',
26
+ 'error',
27
+ 'warning',
28
+ 'success',
29
+ 'blue',
30
+ 'pink',
31
+ 'magenta',
32
+ 'yellow',
33
+ 'neutral',
34
+ ]
35
+
36
+ const FAMILY_LABEL: Record<string, string> = {
37
+ base: 'Base',
38
+ accent: 'Accent',
39
+ error: 'Error',
40
+ warning: 'Warning',
41
+ success: 'Success',
42
+ blue: 'Blue',
43
+ pink: 'Pink',
44
+ magenta: 'Magenta',
45
+ yellow: 'Yellow',
46
+ neutral: 'Neutral',
47
+ }
48
+
49
+ const gridStyle = 'display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 16px;'
50
+
51
+ const cardStyle =
52
+ 'display: flex; flex-direction: column; border: 1px solid #e9eaf6; border-radius: 8px; overflow: hidden; background: #ffffff;'
53
+
54
+ const swatchStyle = (tokenName: string) =>
55
+ `height: 72px; background: var(${tokenName}); border-bottom: 1px solid rgba(0, 0, 0, 0.06);`
56
+
57
+ const cardBodyStyle = 'display: flex; flex-direction: column; gap: 2px; padding: 10px 12px;'
58
+
59
+ const cardNameStyle =
60
+ 'font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 11px; color: #111; word-break: break-all;'
61
+
62
+ const cardValueStyle =
63
+ 'font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 11px; color: #6b7280; word-break: break-all;'
64
+
65
+ const familyGroupStyle = 'display: flex; flex-direction: column; gap: 10px; margin-bottom: 24px;'
66
+
67
+ type Family = { label: string; tokens: Token[] }
68
+
69
+ const familyOf = (name: string, prefix: string): string => {
70
+ const rest = name.startsWith(prefix) ? name.slice(prefix.length) : name
71
+ const first = rest.split('-')[0]
72
+ return (FAMILIES as readonly string[]).includes(first) ? first : 'base'
73
+ }
74
+
75
+ const groupByFamily = (tokens: Token[], prefix: string): Family[] => {
76
+ const byFamily = new Map<string, Token[]>()
77
+ for (const token of tokens) {
78
+ const key = familyOf(token.name, prefix)
79
+ const bucket = byFamily.get(key) ?? []
80
+ bucket.push(token)
81
+ byFamily.set(key, bucket)
82
+ }
83
+ return FAMILY_ORDER.filter((key) => byFamily.has(key)).map((key) => ({
84
+ label: FAMILY_LABEL[key] ?? key,
85
+ tokens: byFamily.get(key) ?? [],
86
+ }))
87
+ }
88
+
89
+ type SectionArgs = { exclude?: (RegExp | string)[]; prefix: string }
90
+
91
+ const makeSectionStory = (section: SectionArgs): Story => ({
92
+ render: () => ({
93
+ setup() {
94
+ const tokens = readTokens([section.prefix], section.exclude ?? [])
95
+ const { copiedKey, copy } = useCopier()
96
+ return {
97
+ count: tokens.length,
98
+ families: groupByFamily(tokens, section.prefix),
99
+ copiedKey,
100
+ copy,
101
+ labelStyle: docLabelStyle,
102
+ groupStyle: familyGroupStyle,
103
+ grid: gridStyle,
104
+ card: cardStyle,
105
+ body: cardBodyStyle,
106
+ nameText: cardNameStyle,
107
+ valueText: cardValueStyle,
108
+ copyClass: copyableClass,
109
+ copiedClass: copyableCopiedClass,
110
+ swatch: (tokenName: string) => swatchStyle(tokenName),
111
+ }
112
+ },
113
+ template: `
114
+ <div v-if="count > 0">
115
+ <div v-for="family in families" :key="family.label" :style="groupStyle">
116
+ <span :style="labelStyle">{{ family.label }} ({{ family.tokens.length }})</span>
117
+ <div :style="grid">
118
+ <div v-for="token in family.tokens" :key="token.name" :style="card">
119
+ <div :style="swatch(token.name)"></div>
120
+ <div :style="body">
121
+ <span
122
+ :style="nameText"
123
+ :class="[copyClass, copiedKey === token.name ? copiedClass : '']"
124
+ role="button"
125
+ tabindex="0"
126
+ :title="'Click to copy ' + token.name"
127
+ @click="copy(token.name)"
128
+ @keydown.enter.prevent="copy(token.name)"
129
+ @keydown.space.prevent="copy(token.name)"
130
+ >{{ copiedKey === token.name ? 'Copied!' : token.name }}</span>
131
+ <span :style="valueText">{{ token.value }}</span>
132
+ </div>
133
+ </div>
134
+ </div>
135
+ </div>
136
+ </div>
137
+ <div v-else style="padding: 12px 0; color: #9ca3af; font-size: 12px;">No tokens</div>
138
+ `,
139
+ }),
140
+ })
141
+
142
+ /**
143
+ * Surface colors used for page, card and control backgrounds.
144
+ */
145
+ export const Background: Story = makeSectionStory({ prefix: '--cp-background-' })
146
+
147
+ /**
148
+ * Border colors for dividers, card outlines and control edges.
149
+ */
150
+ export const Border: Story = makeSectionStory({ prefix: '--cp-border-' })
151
+
152
+ /**
153
+ * Foreground colors used for icons, illustrations and decorative surfaces
154
+ * that sit on top of the background.
155
+ */
156
+ export const Foreground: Story = makeSectionStory({ prefix: '--cp-foreground-' })
157
+
158
+ /**
159
+ * Text colors. Typography size, line-height and letter-spacing tokens live
160
+ * under the Typography section.
161
+ */
162
+ export const Text: Story = makeSectionStory({
163
+ prefix: '--cp-text-',
164
+ exclude: [/-line-height$/, /-letter-spacing$/, /^--cp-text-size-/],
165
+ })
166
+
167
+ /**
168
+ * Utility colors for transient UI surfaces such as overlays, shadows or
169
+ * state indicators.
170
+ */
171
+ export const Utility: Story = makeSectionStory({ prefix: '--cp-utility-' })
172
+
173
+ /**
174
+ * Dedicated colors used by focus rings. See the Focus Rings section for
175
+ * composite ring tokens.
176
+ */
177
+ export const Focus: Story = makeSectionStory({ prefix: '--cp-focus' })