@citizenplane/pimp 16.0.3 → 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 (61) hide show
  1. package/dist/pimp.es.js +313 -285
  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/CpHeading.vue +4 -5
  6. package/src/components/CpText.vue +141 -0
  7. package/src/components/index.ts +2 -0
  8. package/src/stories/BaseInputLabel.stories.ts +36 -9
  9. package/src/stories/Colors.mdx +9 -0
  10. package/src/stories/Colors.stories.ts +177 -0
  11. package/src/stories/CpAccordion.stories.ts +187 -158
  12. package/src/stories/CpAccordionGroup.stories.ts +50 -94
  13. package/src/stories/CpAirlineLogo.stories.ts +49 -28
  14. package/src/stories/CpAlert.stories.ts +195 -158
  15. package/src/stories/CpBadge.stories.ts +259 -193
  16. package/src/stories/CpButton.stories.ts +257 -426
  17. package/src/stories/CpCheckbox.stories.ts +101 -29
  18. package/src/stories/CpContextualMenu.stories.ts +9 -8
  19. package/src/stories/CpDate.stories.ts +52 -25
  20. package/src/stories/CpDatepicker.stories.ts +57 -88
  21. package/src/stories/CpDialog.stories.ts +22 -1
  22. package/src/stories/CpHeading.stories.ts +59 -20
  23. package/src/stories/CpIcon.stories.ts +98 -31
  24. package/src/stories/CpInput.stories.ts +142 -67
  25. package/src/stories/CpItemActions.stories.ts +22 -27
  26. package/src/stories/CpLoader.stories.ts +54 -6
  27. package/src/stories/CpMenuItem.stories.ts +52 -26
  28. package/src/stories/CpMultiselect.stories.ts +52 -71
  29. package/src/stories/CpPartnerBadge.stories.ts +53 -74
  30. package/src/stories/CpRadio.stories.ts +44 -48
  31. package/src/stories/CpRadioGroup.stories.ts +46 -39
  32. package/src/stories/CpSelect.stories.ts +98 -39
  33. package/src/stories/CpSelectMenu.stories.ts +49 -57
  34. package/src/stories/CpSelectableButton.stories.ts +170 -81
  35. package/src/stories/CpSwitch.stories.ts +135 -133
  36. package/src/stories/CpTable.stories.ts +54 -1
  37. package/src/stories/CpTableEmptyState.stories.ts +11 -7
  38. package/src/stories/CpTabs.stories.ts +22 -4
  39. package/src/stories/CpTelInput.stories.ts +25 -23
  40. package/src/stories/CpText.stories.ts +131 -0
  41. package/src/stories/CpTextarea.stories.ts +59 -23
  42. package/src/stories/CpToast.stories.ts +53 -103
  43. package/src/stories/CpTooltip.stories.ts +82 -77
  44. package/src/stories/CpTransitionCounter.stories.ts +4 -0
  45. package/src/stories/CpTransitionExpand.stories.ts +11 -6
  46. package/src/stories/CpTransitionListItems.stories.ts +5 -0
  47. package/src/stories/CpTransitionSize.stories.ts +8 -0
  48. package/src/stories/CpTransitionSlide.stories.ts +4 -0
  49. package/src/stories/CpTransitionTabContent.stories.ts +4 -0
  50. package/src/stories/Dimensions.mdx +9 -0
  51. package/src/stories/Dimensions.stories.ts +119 -0
  52. package/src/stories/Easings.mdx +9 -0
  53. package/src/stories/Easings.stories.ts +101 -0
  54. package/src/stories/FocusRings.mdx +9 -0
  55. package/src/stories/FocusRings.stories.ts +74 -0
  56. package/src/stories/Shadows.mdx +9 -0
  57. package/src/stories/Shadows.stories.ts +100 -0
  58. package/src/stories/Typography.mdx +9 -0
  59. package/src/stories/Typography.stories.ts +181 -0
  60. package/src/stories/documentationStyles.ts +2 -10
  61. package/src/stories/tokenUtils.ts +259 -0
@@ -1,9 +1,12 @@
1
- import type { Meta, StoryObj } from '@storybook/vue3'
1
+ import type { Args, Meta, StoryObj } from '@storybook/vue3'
2
2
 
3
3
  import CpTooltip from '@/components/CpTooltip.vue'
4
4
 
5
+ const tooltipColors = ['accent', 'neutral'] as const
6
+ const tooltipPlacements = ['top', 'right', 'bottom', 'left'] as const
7
+
5
8
  const meta = {
6
- title: 'Feedback/CpTooltip',
9
+ title: 'Atoms/CpTooltip',
7
10
  component: CpTooltip,
8
11
  argTypes: {
9
12
  content: {
@@ -16,12 +19,12 @@ const meta = {
16
19
  },
17
20
  color: {
18
21
  control: 'select',
19
- options: ['accent', 'neutral'],
22
+ options: tooltipColors,
20
23
  description: 'The color variant',
21
24
  },
22
25
  placement: {
23
26
  control: 'select',
24
- options: ['top', 'right', 'bottom', 'left'],
27
+ options: tooltipPlacements,
25
28
  description: 'The placement of the tooltip',
26
29
  },
27
30
  distance: {
@@ -34,53 +37,53 @@ const meta = {
34
37
  export default meta
35
38
  type Story = StoryObj<typeof meta>
36
39
 
40
+ const paddedRender = (args: Args) => ({
41
+ components: { CpTooltip },
42
+ setup() {
43
+ return { args }
44
+ },
45
+ template: `
46
+ <div style="padding: 100px; text-align: center;">
47
+ <CpTooltip v-bind="args">
48
+ <button>Hover me</button>
49
+ </CpTooltip>
50
+ </div>
51
+ `,
52
+ })
53
+
54
+ /**
55
+ * Default tooltip. Hover the trigger to reveal it. Use the controls to
56
+ * experiment with each prop in isolation.
57
+ */
37
58
  export const Default: Story = {
38
59
  args: {
39
60
  content: 'Tooltip content',
40
61
  placement: 'top',
41
62
  distance: 8,
42
63
  },
43
- render: (args) => ({
44
- components: { CpTooltip },
45
- setup() {
46
- return { args }
47
- },
48
- template: `
49
- <div style="padding: 100px; text-align: center;">
50
- <CpTooltip v-bind="args">
51
- <button>Hover me</button>
52
- </CpTooltip>
53
- </div>
54
- `,
55
- }),
64
+ render: paddedRender,
56
65
  }
57
66
 
58
- export const AllPlacements: Story = {
59
- render: (args) => ({
67
+ /* -------------------------------------------------------------------------- */
68
+ /* Placements */
69
+ /* -------------------------------------------------------------------------- */
70
+
71
+ /**
72
+ * All four placements rendered on one page. Tooltips flip automatically if
73
+ * they don't fit on the chosen side.
74
+ */
75
+ export const Placements: Story = {
76
+ parameters: { controls: { disable: true } },
77
+ render: () => ({
60
78
  components: { CpTooltip },
61
79
  setup() {
62
- return { args }
80
+ return { tooltipPlacements }
63
81
  },
64
82
  template: `
65
- <div style="display: flex; flex-direction: column; gap: 20px; padding: 100px;">
66
- <div>
67
- <CpTooltip content="Top tooltip" placement="top">
68
- <button>Top</button>
69
- </CpTooltip>
70
- </div>
71
- <div>
72
- <CpTooltip content="Right tooltip" placement="right">
73
- <button>Right</button>
74
- </CpTooltip>
75
- </div>
76
- <div>
77
- <CpTooltip content="Bottom tooltip" placement="bottom">
78
- <button>Bottom</button>
79
- </CpTooltip>
80
- </div>
81
- <div>
82
- <CpTooltip content="Left tooltip" placement="left">
83
- <button>Left</button>
83
+ <div style="display: flex; gap: 32px; padding: 100px; justify-content: center; flex-wrap: wrap;">
84
+ <div v-for="placement in tooltipPlacements" :key="placement">
85
+ <CpTooltip :content="placement + ' tooltip'" :placement="placement">
86
+ <button>{{ placement }}</button>
84
87
  </CpTooltip>
85
88
  </div>
86
89
  </div>
@@ -88,11 +91,17 @@ export const AllPlacements: Story = {
88
91
  }),
89
92
  }
90
93
 
94
+ /* -------------------------------------------------------------------------- */
95
+ /* Content */
96
+ /* -------------------------------------------------------------------------- */
97
+
98
+ /**
99
+ * Pass rich HTML content via the `#content` slot — perfect for links,
100
+ * formatting or small compositions.
101
+ */
91
102
  export const WithHTMLContent: Story = {
92
- args: {
93
- placement: 'top',
94
- },
95
- render: (args) => ({
103
+ args: { placement: 'top' },
104
+ render: (args: Args) => ({
96
105
  components: { CpTooltip },
97
106
  setup() {
98
107
  return { args }
@@ -110,55 +119,51 @@ export const WithHTMLContent: Story = {
110
119
  }),
111
120
  }
112
121
 
113
- export const WithSubcontentSlot: Story = {
114
- args: {
115
- content: 'Main content',
116
- placement: 'top',
117
- },
118
- render: (args) => ({
119
- components: { CpTooltip },
120
- setup() {
121
- return { args }
122
- },
123
- template: `
124
- <div style="padding: 100px; text-align: center;">
125
- <CpTooltip v-bind="args">
126
- <button type="button">Hover me</button>
127
- <template #subcontent>
128
- <p>Optional subcontent via slot, it can be a long text to see how it works and includes <b>HTML</b> <u>tags</u></p>
129
- </template>
130
- </CpTooltip>
131
- </div>
132
- `,
133
- }),
134
- }
135
-
136
- export const WithSubcontentProp: Story = {
122
+ /**
123
+ * A secondary line of content displayed below the main content. Can be
124
+ * provided as a `subcontent` prop or through the `#subcontent` slot.
125
+ */
126
+ export const WithSubcontent: Story = {
137
127
  args: {
138
128
  content: 'Main content',
139
129
  subcontent: 'Optional subcontent via prop',
140
130
  placement: 'top',
141
131
  },
142
- render: (args) => ({
132
+ render: (args: Args) => ({
143
133
  components: { CpTooltip },
144
134
  setup() {
145
135
  return { args }
146
136
  },
147
137
  template: `
148
- <div style="padding: 100px; text-align: center;">
149
- <CpTooltip v-bind="args">
150
- <button type="button">Hover me</button>
151
- </CpTooltip>
138
+ <div style="display: flex; gap: 48px; padding: 100px; justify-content: center; flex-wrap: wrap;">
139
+ <div>
140
+ <CpTooltip v-bind="args">
141
+ <button type="button">Subcontent via prop</button>
142
+ </CpTooltip>
143
+ </div>
144
+ <div>
145
+ <CpTooltip :content="args.content" :placement="args.placement">
146
+ <button type="button">Subcontent via slot</button>
147
+ <template #subcontent>
148
+ <p>Optional subcontent via slot, includes <b>HTML</b> <u>tags</u></p>
149
+ </template>
150
+ </CpTooltip>
151
+ </div>
152
152
  </div>
153
153
  `,
154
154
  }),
155
155
  }
156
156
 
157
+ /* -------------------------------------------------------------------------- */
158
+ /* States */
159
+ /* -------------------------------------------------------------------------- */
160
+
161
+ /**
162
+ * When `disabled` is `true` the tooltip never appears, even on hover.
163
+ */
157
164
  export const Disabled: Story = {
158
- args: {
159
- disabled: true,
160
- },
161
- render: (args) => ({
165
+ args: { disabled: true },
166
+ render: (args: Args) => ({
162
167
  components: { CpTooltip },
163
168
  setup() {
164
169
  return { args }
@@ -166,7 +171,7 @@ export const Disabled: Story = {
166
171
  template: `
167
172
  <div style="padding: 100px; text-align: center;">
168
173
  <CpTooltip v-bind="args" content="You should not see me">
169
- <div type="button">Disabled</div>
174
+ <div>Disabled trigger</div>
170
175
  </CpTooltip>
171
176
  </div>
172
177
  `,
@@ -24,6 +24,10 @@ const rowStyle = 'padding: 24px; display: inline-flex; align-items: center; just
24
24
 
25
25
  const counterTextStyle = 'font-weight: bold; font-size: 4.5rem; line-height: 1; font-variant-numeric: tabular-nums;'
26
26
 
27
+ /**
28
+ * Counter transition that animates number changes. Use the +/- buttons
29
+ * (or the `duration` control) to see the slide-in / slide-out behaviour.
30
+ */
27
31
  export const Default: Story = {
28
32
  args: {
29
33
  duration: 150,
@@ -17,6 +17,10 @@ type Story = StoryObj<typeof meta>
17
17
 
18
18
  const wrapperStyle = 'display: flex; flex-direction: column; align-items: center; gap: 12px;'
19
19
 
20
+ /**
21
+ * Animated height expansion. Wrap any element in `<CpTransitionExpand>`
22
+ * and toggle its rendering with `v-if` to smoothly expand/collapse.
23
+ */
20
24
  export const Default: Story = {
21
25
  render: () => ({
22
26
  components: { CpTransitionExpand },
@@ -35,14 +39,11 @@ export const Default: Story = {
35
39
  </CpButton>
36
40
 
37
41
  <CpTransitionExpand>
38
- <div v-if="isExpanded" style="
39
- background: #F3F4F6;
40
- border-radius: 6px;
41
- ">
42
+ <div v-if="isExpanded" style="background: #F3F4F6; border-radius: 6px; padding: 16px;">
42
43
  <h3 style="margin: 0 0 8px 0;">Expanded Content</h3>
43
44
  <p style="margin: 0;">
44
- This content will smoothly expand and collapse with a nice animation.
45
- The height transition is handled automatically by the CpTransitionExpand component.
45
+ This content smoothly expands and collapses. The height transition is handled
46
+ automatically by the CpTransitionExpand component.
46
47
  </p>
47
48
  </div>
48
49
  </CpTransitionExpand>
@@ -51,6 +52,10 @@ export const Default: Story = {
51
52
  }),
52
53
  }
53
54
 
55
+ /**
56
+ * The transition works with content of any height — no manual value
57
+ * needed.
58
+ */
54
59
  export const WithLongContent: Story = {
55
60
  render: () => ({
56
61
  components: { CpTransitionExpand },
@@ -51,6 +51,11 @@ const fabWrapperStyle = {
51
51
  alignItems: 'flex-end',
52
52
  }
53
53
 
54
+ /**
55
+ * Animate list insertions, removals and reorders with FLIP-based
56
+ * transitions. Use the + / shuffle buttons to see the enter, leave and
57
+ * move animations.
58
+ */
54
59
  export const Default: Story = {
55
60
  args: {
56
61
  disableOnLoad: false,
@@ -21,6 +21,10 @@ type Story = StoryObj<typeof meta>
21
21
 
22
22
  const wrapperStyle = 'display: flex; flex-direction: column; align-items: center; gap: 12px;'
23
23
 
24
+ /**
25
+ * Animate a button's width when its inner label changes. Click the
26
+ * button to swap between a short and a longer label.
27
+ */
24
28
  export const Default: Story = {
25
29
  args: {
26
30
  type: 'width',
@@ -50,6 +54,10 @@ export const Default: Story = {
50
54
  }),
51
55
  }
52
56
 
57
+ /**
58
+ * Height mode — smoothly animates a dialog's body height as the step
59
+ * content (and therefore its size) changes.
60
+ */
53
61
  export const HeightTransition: Story = {
54
62
  args: {
55
63
  type: 'height',
@@ -28,6 +28,10 @@ const wrapperStyle = 'display: flex; flex-direction: column; align-items: center
28
28
  const panelStyle =
29
29
  'min-width: 260px; max-width: 320px; padding: 20px; border-radius: 12px; background: #f3f4f6; text-align: center;'
30
30
 
31
+ /**
32
+ * Slide between two panels. The `slideTo` prop controls direction
33
+ * (`top` or `left`) and `mode` follows Vue's `<Transition mode>` rules.
34
+ */
31
35
  export const Toggle: Story = {
32
36
  name: 'Toggle',
33
37
  args: {
@@ -36,6 +36,10 @@ const layoutStyle = 'display: flex; flex-direction: column; gap: 20px; min-width
36
36
  const panelStyle =
37
37
  'min-height: 120px; padding: 20px; border-radius: 12px; background: #f3f4f6; font-size: 15px; line-height: 1.5;'
38
38
 
39
+ /**
40
+ * Directional slide between tab panels. `direction` is driven by the
41
+ * current vs. previous tab index to slide forward or backward.
42
+ */
39
43
  export const Default: Story = {
40
44
  args: {
41
45
  duration: 300,
@@ -0,0 +1,9 @@
1
+ import { Meta, Title, Description, Stories } from '@storybook/addon-docs/blocks'
2
+
3
+ import * as DimensionsStories from './Dimensions.stories'
4
+
5
+ <Meta of={DimensionsStories} />
6
+
7
+ <Title />
8
+ <Description />
9
+ <Stories />
@@ -0,0 +1,119 @@
1
+ import type { Meta, StoryObj } from '@storybook/vue3'
2
+
3
+ import type { Token } from '@/stories/tokenUtils'
4
+ import {
5
+ copyableClass,
6
+ copyableCopiedClass,
7
+ readTokens,
8
+ sortTokensBySize,
9
+ splitRemPx,
10
+ tokenTableClass,
11
+ useCopier,
12
+ } from '@/stories/tokenUtils'
13
+
14
+ const meta: Meta = {
15
+ title: 'Foundations/Dimensions',
16
+ tags: ['!dev'],
17
+ parameters: {
18
+ layout: 'padded',
19
+ controls: { disable: true },
20
+ docs: { source: { code: null } },
21
+ },
22
+ }
23
+
24
+ export default meta
25
+
26
+ type Story = StoryObj
27
+
28
+ type DimensionKind = 'bar' | 'none' | 'radius' | 'square'
29
+
30
+ const previewStyle = (kind: DimensionKind, tokenName: string): string => {
31
+ const ref = `var(${tokenName})`
32
+ switch (kind) {
33
+ case 'bar':
34
+ return `height: 12px; width: ${ref}; max-width: 100%; background: var(--cp-background-accent-solid, #603dfd); border-radius: 4px;`
35
+ case 'radius':
36
+ return `width: 56px; height: 56px; background: var(--cp-background-accent-primary, #eaecff); border: 1px solid var(--cp-border-accent-primary, #ccd0ff); border-radius: ${ref};`
37
+ case 'square':
38
+ return `width: ${ref}; height: ${ref}; background: var(--cp-background-accent-solid, #603dfd); border-radius: 2px;`
39
+ case 'none':
40
+ return ''
41
+ }
42
+ }
43
+
44
+ type SectionArgs = { kind: DimensionKind; prefix: string }
45
+
46
+ const makeSectionStory = (section: SectionArgs): Story => ({
47
+ render: () => ({
48
+ setup() {
49
+ const tokens = sortTokensBySize(readTokens([section.prefix]))
50
+ const { copiedKey, copy } = useCopier()
51
+ return {
52
+ kind: section.kind,
53
+ tokens,
54
+ copiedKey,
55
+ copy,
56
+ tableClass: tokenTableClass,
57
+ copyClass: copyableClass,
58
+ copiedClass: copyableCopiedClass,
59
+ preview: (kind: DimensionKind, tokenName: string) => previewStyle(kind, tokenName),
60
+ split: (token: Token) => splitRemPx(token),
61
+ }
62
+ },
63
+ template: `
64
+ <table v-if="tokens.length > 0" :class="tableClass">
65
+ <thead>
66
+ <tr>
67
+ <th scope="col">Token</th>
68
+ <th scope="col">rem</th>
69
+ <th scope="col">px</th>
70
+ <th scope="col">Preview</th>
71
+ </tr>
72
+ </thead>
73
+ <tbody>
74
+ <tr v-for="token in tokens" :key="token.name">
75
+ <td>
76
+ <span
77
+ :class="[copyClass, copiedKey === token.name ? copiedClass : '']"
78
+ role="button"
79
+ tabindex="0"
80
+ :title="'Click to copy ' + token.name"
81
+ @click="copy(token.name)"
82
+ @keydown.enter.prevent="copy(token.name)"
83
+ @keydown.space.prevent="copy(token.name)"
84
+ >{{ copiedKey === token.name ? 'Copied!' : token.name }}</span>
85
+ </td>
86
+ <td>{{ split(token).rem }}</td>
87
+ <td>{{ split(token).px }}</td>
88
+ <td class="cp-token-table__preview">
89
+ <div :style="preview(kind, token.name)"></div>
90
+ </td>
91
+ </tr>
92
+ </tbody>
93
+ </table>
94
+ <div v-else style="padding: 12px 0; color: #9ca3af; font-size: 12px;">No tokens</div>
95
+ `,
96
+ }),
97
+ })
98
+
99
+ /**
100
+ * Responsive breakpoints. These values match the `min-width` media queries
101
+ * the design system uses to adapt layouts.
102
+ */
103
+ export const Breakpoint: Story = makeSectionStory({ prefix: '--cp-breakpoint-', kind: 'none' })
104
+
105
+ /**
106
+ * Border radius scale, from sharp corners to fully rounded pill shapes.
107
+ */
108
+ export const Radius: Story = makeSectionStory({ prefix: '--cp-radius-', kind: 'radius' })
109
+
110
+ /**
111
+ * Spacing scale used for padding, margin and gap. Rendered as horizontal
112
+ * bars to compare relative widths.
113
+ */
114
+ export const Spacing: Story = makeSectionStory({ prefix: '--cp-spacing-', kind: 'bar' })
115
+
116
+ /**
117
+ * Width tokens used for icons, avatars and small fixed-size surfaces.
118
+ */
119
+ export const Width: Story = makeSectionStory({ prefix: '--cp-width-', kind: 'square' })
@@ -0,0 +1,9 @@
1
+ import { Meta, Title, Description, Stories } from '@storybook/addon-docs/blocks'
2
+
3
+ import * as EasingsStories from './Easings.stories'
4
+
5
+ <Meta of={EasingsStories} />
6
+
7
+ <Title />
8
+ <Description />
9
+ <Stories />
@@ -0,0 +1,101 @@
1
+ import type { Meta, StoryObj } from '@storybook/vue3'
2
+
3
+ import { copyableClass, copyableCopiedClass, readTokens, tokenTableClass, useCopier } from '@/stories/tokenUtils'
4
+
5
+ const meta: Meta = {
6
+ title: 'Foundations/Easings',
7
+ tags: ['!dev'],
8
+ parameters: {
9
+ layout: 'padded',
10
+ controls: { disable: true },
11
+ docs: { source: { code: null } },
12
+ },
13
+ }
14
+
15
+ export default meta
16
+
17
+ type Story = StoryObj
18
+
19
+ /**
20
+ * Cubic-bezier curves used for the design-system transitions. Hover (or
21
+ * focus) any track to play the animation and compare curves side by side.
22
+ */
23
+ export const Curves: Story = {
24
+ render: () => ({
25
+ setup() {
26
+ const easings = readTokens(['--cp-easing-'])
27
+ const { copiedKey, copy } = useCopier()
28
+ return {
29
+ easings,
30
+ copiedKey,
31
+ copy,
32
+ tableClass: tokenTableClass,
33
+ copyClass: copyableClass,
34
+ copiedClass: copyableCopiedClass,
35
+ ballStyle: (tokenName: string) => `transition-timing-function: var(${tokenName});`,
36
+ }
37
+ },
38
+ template: `
39
+ <div>
40
+ <style>
41
+ .cp-easing-track {
42
+ position: relative;
43
+ width: 220px;
44
+ height: 36px;
45
+ background: #f3f4f6;
46
+ border-radius: 18px;
47
+ overflow: hidden;
48
+ }
49
+ .cp-easing-ball {
50
+ position: absolute;
51
+ top: 4px;
52
+ left: 4px;
53
+ width: 28px;
54
+ height: 28px;
55
+ border-radius: 50%;
56
+ background: var(--cp-background-accent-solid, #603dfd);
57
+ transition-property: transform;
58
+ transition-duration: 900ms;
59
+ transform: translateX(0);
60
+ }
61
+ .cp-easing-track:hover .cp-easing-ball,
62
+ .cp-easing-track:focus-within .cp-easing-ball {
63
+ transform: translateX(184px);
64
+ }
65
+ </style>
66
+ <p style="margin: 0 0 12px 0; color: #6b7280; font-size: 12px;">Hover a track to play the transition.</p>
67
+ <table v-if="easings.length > 0" :class="tableClass">
68
+ <thead>
69
+ <tr>
70
+ <th scope="col">Token</th>
71
+ <th scope="col">Value</th>
72
+ <th scope="col">Preview</th>
73
+ </tr>
74
+ </thead>
75
+ <tbody>
76
+ <tr v-for="token in easings" :key="token.name">
77
+ <td>
78
+ <span
79
+ :class="[copyClass, copiedKey === token.name ? copiedClass : '']"
80
+ role="button"
81
+ tabindex="0"
82
+ :title="'Click to copy ' + token.name"
83
+ @click="copy(token.name)"
84
+ @keydown.enter.prevent="copy(token.name)"
85
+ @keydown.space.prevent="copy(token.name)"
86
+ >{{ copiedKey === token.name ? 'Copied!' : token.name }}</span>
87
+ </td>
88
+ <td>{{ token.value }}</td>
89
+ <td class="cp-token-table__preview">
90
+ <div class="cp-easing-track" tabindex="0">
91
+ <div class="cp-easing-ball" :style="ballStyle(token.name)"></div>
92
+ </div>
93
+ </td>
94
+ </tr>
95
+ </tbody>
96
+ </table>
97
+ <div v-else style="padding: 12px 0; color: #9ca3af; font-size: 12px;">No tokens</div>
98
+ </div>
99
+ `,
100
+ }),
101
+ }
@@ -0,0 +1,9 @@
1
+ import { Meta, Title, Description, Stories } from '@storybook/addon-docs/blocks'
2
+
3
+ import * as FocusRingsStories from './FocusRings.stories'
4
+
5
+ <Meta of={FocusRingsStories} />
6
+
7
+ <Title />
8
+ <Description />
9
+ <Stories />
@@ -0,0 +1,74 @@
1
+ import type { Meta, StoryObj } from '@storybook/vue3'
2
+
3
+ import { copyableClass, copyableCopiedClass, readTokens, tokenTableClass, useCopier } from '@/stories/tokenUtils'
4
+
5
+ const meta: Meta = {
6
+ title: 'Foundations/Focus Rings',
7
+ tags: ['!dev'],
8
+ parameters: {
9
+ layout: 'padded',
10
+ controls: { disable: true },
11
+ docs: { source: { code: null } },
12
+ },
13
+ }
14
+
15
+ export default meta
16
+
17
+ type Story = StoryObj
18
+
19
+ const previewStyle = (tokenName: string): string =>
20
+ `display: inline-flex; align-items: center; justify-content: center; height: 40px; padding: 0 18px; border-radius: 8px; background: #ffffff; border: 1px solid #e9eaf6; color: #36384d; font-size: 14px; box-shadow: var(${tokenName});`
21
+
22
+ /**
23
+ * Composite focus-ring tokens (one token per color role). Each token is a
24
+ * multi-layered `box-shadow` that produces a consistent, accessible focus
25
+ * outline when applied to an element.
26
+ */
27
+ export const Composite: Story = {
28
+ render: () => ({
29
+ setup() {
30
+ const composites = readTokens([/^--cp-shadow-focus-ring-[^-]+$/])
31
+ const { copiedKey, copy } = useCopier()
32
+ return {
33
+ composites,
34
+ copiedKey,
35
+ copy,
36
+ tableClass: tokenTableClass,
37
+ copyClass: copyableClass,
38
+ copiedClass: copyableCopiedClass,
39
+ preview: (tokenName: string) => previewStyle(tokenName),
40
+ }
41
+ },
42
+ template: `
43
+ <table v-if="composites.length > 0" :class="tableClass">
44
+ <thead>
45
+ <tr>
46
+ <th scope="col">Token</th>
47
+ <th scope="col">Value</th>
48
+ <th scope="col">Preview</th>
49
+ </tr>
50
+ </thead>
51
+ <tbody>
52
+ <tr v-for="token in composites" :key="token.name">
53
+ <td>
54
+ <span
55
+ :class="[copyClass, copiedKey === token.name ? copiedClass : '']"
56
+ role="button"
57
+ tabindex="0"
58
+ :title="'Click to copy ' + token.name"
59
+ @click="copy(token.name)"
60
+ @keydown.enter.prevent="copy(token.name)"
61
+ @keydown.space.prevent="copy(token.name)"
62
+ >{{ copiedKey === token.name ? 'Copied!' : token.name }}</span>
63
+ </td>
64
+ <td>{{ token.value }}</td>
65
+ <td class="cp-token-table__preview">
66
+ <span :style="preview(token.name)">Focused</span>
67
+ </td>
68
+ </tr>
69
+ </tbody>
70
+ </table>
71
+ <div v-else style="padding: 12px 0; color: #9ca3af; font-size: 12px;">No tokens</div>
72
+ `,
73
+ }),
74
+ }
@@ -0,0 +1,9 @@
1
+ import { Meta, Title, Description, Stories } from '@storybook/addon-docs/blocks'
2
+
3
+ import * as ShadowsStories from './Shadows.stories'
4
+
5
+ <Meta of={ShadowsStories} />
6
+
7
+ <Title />
8
+ <Description />
9
+ <Stories />