@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.
- package/dist/pimp.es.js +313 -285
- package/dist/pimp.umd.js +21 -21
- package/dist/style.css +1 -1
- package/package.json +2 -1
- package/src/components/CpHeading.vue +4 -5
- package/src/components/CpText.vue +141 -0
- package/src/components/index.ts +2 -0
- package/src/stories/BaseInputLabel.stories.ts +36 -9
- package/src/stories/Colors.mdx +9 -0
- package/src/stories/Colors.stories.ts +177 -0
- package/src/stories/CpAccordion.stories.ts +187 -158
- package/src/stories/CpAccordionGroup.stories.ts +50 -94
- package/src/stories/CpAirlineLogo.stories.ts +49 -28
- package/src/stories/CpAlert.stories.ts +195 -158
- package/src/stories/CpBadge.stories.ts +259 -193
- package/src/stories/CpButton.stories.ts +257 -426
- package/src/stories/CpCheckbox.stories.ts +101 -29
- package/src/stories/CpContextualMenu.stories.ts +9 -8
- package/src/stories/CpDate.stories.ts +52 -25
- package/src/stories/CpDatepicker.stories.ts +57 -88
- package/src/stories/CpDialog.stories.ts +22 -1
- package/src/stories/CpHeading.stories.ts +59 -20
- package/src/stories/CpIcon.stories.ts +98 -31
- package/src/stories/CpInput.stories.ts +142 -67
- package/src/stories/CpItemActions.stories.ts +22 -27
- package/src/stories/CpLoader.stories.ts +54 -6
- package/src/stories/CpMenuItem.stories.ts +52 -26
- package/src/stories/CpMultiselect.stories.ts +52 -71
- package/src/stories/CpPartnerBadge.stories.ts +53 -74
- package/src/stories/CpRadio.stories.ts +44 -48
- package/src/stories/CpRadioGroup.stories.ts +46 -39
- package/src/stories/CpSelect.stories.ts +98 -39
- package/src/stories/CpSelectMenu.stories.ts +49 -57
- package/src/stories/CpSelectableButton.stories.ts +170 -81
- package/src/stories/CpSwitch.stories.ts +135 -133
- package/src/stories/CpTable.stories.ts +54 -1
- package/src/stories/CpTableEmptyState.stories.ts +11 -7
- package/src/stories/CpTabs.stories.ts +22 -4
- package/src/stories/CpTelInput.stories.ts +25 -23
- package/src/stories/CpText.stories.ts +131 -0
- package/src/stories/CpTextarea.stories.ts +59 -23
- package/src/stories/CpToast.stories.ts +53 -103
- package/src/stories/CpTooltip.stories.ts +82 -77
- package/src/stories/CpTransitionCounter.stories.ts +4 -0
- package/src/stories/CpTransitionExpand.stories.ts +11 -6
- package/src/stories/CpTransitionListItems.stories.ts +5 -0
- package/src/stories/CpTransitionSize.stories.ts +8 -0
- package/src/stories/CpTransitionSlide.stories.ts +4 -0
- package/src/stories/CpTransitionTabContent.stories.ts +4 -0
- package/src/stories/Dimensions.mdx +9 -0
- package/src/stories/Dimensions.stories.ts +119 -0
- package/src/stories/Easings.mdx +9 -0
- package/src/stories/Easings.stories.ts +101 -0
- package/src/stories/FocusRings.mdx +9 -0
- package/src/stories/FocusRings.stories.ts +74 -0
- package/src/stories/Shadows.mdx +9 -0
- package/src/stories/Shadows.stories.ts +100 -0
- package/src/stories/Typography.mdx +9 -0
- package/src/stories/Typography.stories.ts +181 -0
- package/src/stories/documentationStyles.ts +2 -10
- package/src/stories/tokenUtils.ts +259 -0
|
@@ -0,0 +1,100 @@
|
|
|
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
|
+
tokenTableClass,
|
|
10
|
+
useCopier,
|
|
11
|
+
} from '@/stories/tokenUtils'
|
|
12
|
+
|
|
13
|
+
const meta: Meta = {
|
|
14
|
+
title: 'Foundations/Shadows',
|
|
15
|
+
tags: ['!dev'],
|
|
16
|
+
parameters: {
|
|
17
|
+
layout: 'padded',
|
|
18
|
+
controls: { disable: true },
|
|
19
|
+
docs: { source: { code: null } },
|
|
20
|
+
},
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export default meta
|
|
24
|
+
|
|
25
|
+
type Story = StoryObj
|
|
26
|
+
|
|
27
|
+
const previewStyle = (tokenName: string): string =>
|
|
28
|
+
`width: 160px; height: 80px; background: #ffffff; border-radius: 8px; box-shadow: var(${tokenName});`
|
|
29
|
+
|
|
30
|
+
const makeSectionStory = (loader: () => Token[]): Story => ({
|
|
31
|
+
render: () => ({
|
|
32
|
+
setup() {
|
|
33
|
+
const { copiedKey, copy } = useCopier()
|
|
34
|
+
return {
|
|
35
|
+
tokens: loader(),
|
|
36
|
+
copiedKey,
|
|
37
|
+
copy,
|
|
38
|
+
tableClass: tokenTableClass,
|
|
39
|
+
copyClass: copyableClass,
|
|
40
|
+
copiedClass: copyableCopiedClass,
|
|
41
|
+
preview: (tokenName: string) => previewStyle(tokenName),
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
template: `
|
|
45
|
+
<table v-if="tokens.length > 0" :class="tableClass">
|
|
46
|
+
<thead>
|
|
47
|
+
<tr>
|
|
48
|
+
<th scope="col">Token</th>
|
|
49
|
+
<th scope="col">Value</th>
|
|
50
|
+
<th scope="col">Preview</th>
|
|
51
|
+
</tr>
|
|
52
|
+
</thead>
|
|
53
|
+
<tbody>
|
|
54
|
+
<tr v-for="token in tokens" :key="token.name">
|
|
55
|
+
<td>
|
|
56
|
+
<span
|
|
57
|
+
:class="[copyClass, copiedKey === token.name ? copiedClass : '']"
|
|
58
|
+
role="button"
|
|
59
|
+
tabindex="0"
|
|
60
|
+
:title="'Click to copy ' + token.name"
|
|
61
|
+
@click="copy(token.name)"
|
|
62
|
+
@keydown.enter.prevent="copy(token.name)"
|
|
63
|
+
@keydown.space.prevent="copy(token.name)"
|
|
64
|
+
>{{ copiedKey === token.name ? 'Copied!' : token.name }}</span>
|
|
65
|
+
</td>
|
|
66
|
+
<td>{{ token.value }}</td>
|
|
67
|
+
<td class="cp-token-table__preview">
|
|
68
|
+
<div :style="preview(token.name)"></div>
|
|
69
|
+
</td>
|
|
70
|
+
</tr>
|
|
71
|
+
</tbody>
|
|
72
|
+
</table>
|
|
73
|
+
<div v-else style="padding: 12px 0; color: #9ca3af; font-size: 12px;">No tokens</div>
|
|
74
|
+
`,
|
|
75
|
+
}),
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Drop shadows ordered from the most to the least prominent. Use them to
|
|
80
|
+
* lift cards, popovers and menus off the surface.
|
|
81
|
+
*/
|
|
82
|
+
export const DropShadows: Story = makeSectionStory(() =>
|
|
83
|
+
sortTokensBySize(readTokens(['--cp-shadows-'], [/-inset$/, /^--cp-shadows-overlay$/, /^--cp-shadows-side-panel$/])),
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Inset shadows sit inside the element. Useful for pressed states and
|
|
88
|
+
* recessed surfaces like form fields.
|
|
89
|
+
*/
|
|
90
|
+
export const InsetShadows: Story = makeSectionStory(() => sortTokensBySize(readTokens([/^--cp-shadows-.*-inset$/])))
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Dedicated shadow for the side panel container.
|
|
94
|
+
*/
|
|
95
|
+
export const SidePanel: Story = makeSectionStory(() => readTokens(['--cp-shadows-side-panel']))
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Multi-layered shadow used for modals and full-screen overlays.
|
|
99
|
+
*/
|
|
100
|
+
export const Overlay: Story = makeSectionStory(() => readTokens(['--cp-shadows-overlay']))
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/vue3'
|
|
2
|
+
|
|
3
|
+
import type { ProbeProperty, Token } from '@/stories/tokenUtils'
|
|
4
|
+
import {
|
|
5
|
+
copyableClass,
|
|
6
|
+
copyableCopiedClass,
|
|
7
|
+
getTokenPixels,
|
|
8
|
+
readTokens,
|
|
9
|
+
sortTokensBySize,
|
|
10
|
+
splitRemPx,
|
|
11
|
+
tokenTableClass,
|
|
12
|
+
useCopier,
|
|
13
|
+
} from '@/stories/tokenUtils'
|
|
14
|
+
|
|
15
|
+
const meta: Meta = {
|
|
16
|
+
title: 'Foundations/Typography',
|
|
17
|
+
tags: ['!dev'],
|
|
18
|
+
parameters: {
|
|
19
|
+
layout: 'padded',
|
|
20
|
+
controls: { disable: true },
|
|
21
|
+
docs: { source: { code: null } },
|
|
22
|
+
},
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export default meta
|
|
26
|
+
|
|
27
|
+
type Story = StoryObj
|
|
28
|
+
|
|
29
|
+
type PreviewKind = 'letter-spacing' | 'line-height' | 'text-size'
|
|
30
|
+
|
|
31
|
+
const previewStyle = (kind: PreviewKind, tokenName: string): string => {
|
|
32
|
+
const ref = `var(${tokenName})`
|
|
33
|
+
switch (kind) {
|
|
34
|
+
case 'text-size':
|
|
35
|
+
return `font-size: ${ref}; line-height: 1.1; color: #36384d;`
|
|
36
|
+
case 'line-height':
|
|
37
|
+
return `font-size: 14px; line-height: ${ref}; color: #36384d; max-width: 280px; width:280px; display:block; white-space: initial;`
|
|
38
|
+
case 'letter-spacing':
|
|
39
|
+
return `font-size: 14px; letter-spacing: ${ref}; color: #36384d;`
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const sampleForKind = (kind: PreviewKind): string => {
|
|
44
|
+
switch (kind) {
|
|
45
|
+
case 'text-size':
|
|
46
|
+
return 'The quick brown fox'
|
|
47
|
+
case 'line-height':
|
|
48
|
+
return 'Cras mattis consectetur purus sit amet fermentum. Vivamus sagittis lacus vel augue laoreet rutrum faucibus dolor auctor.'
|
|
49
|
+
case 'letter-spacing':
|
|
50
|
+
return 'AVERAGE spacing sample'
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Raw text sizes, sorted from the biggest to the smallest. Prefer the
|
|
56
|
+
* semantic `--cp-text-*-size` tokens (Text scale) in application code.
|
|
57
|
+
*/
|
|
58
|
+
export const TextSize: Story = {
|
|
59
|
+
render: () => ({
|
|
60
|
+
setup() {
|
|
61
|
+
const tokens = sortTokensBySize(readTokens(['--cp-text-size-']))
|
|
62
|
+
const { copiedKey, copy } = useCopier()
|
|
63
|
+
return {
|
|
64
|
+
tokens,
|
|
65
|
+
copiedKey,
|
|
66
|
+
copy,
|
|
67
|
+
tableClass: tokenTableClass,
|
|
68
|
+
copyClass: copyableClass,
|
|
69
|
+
copiedClass: copyableCopiedClass,
|
|
70
|
+
preview: (tokenName: string) => previewStyle('text-size', tokenName),
|
|
71
|
+
sample: sampleForKind('text-size'),
|
|
72
|
+
split: (token: Token) => splitRemPx(token),
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
template: `
|
|
76
|
+
<table v-if="tokens.length > 0" :class="tableClass">
|
|
77
|
+
<thead>
|
|
78
|
+
<tr>
|
|
79
|
+
<th scope="col">Token</th>
|
|
80
|
+
<th scope="col">rem</th>
|
|
81
|
+
<th scope="col">px</th>
|
|
82
|
+
<th scope="col">Preview</th>
|
|
83
|
+
</tr>
|
|
84
|
+
</thead>
|
|
85
|
+
<tbody>
|
|
86
|
+
<tr v-for="token in tokens" :key="token.name">
|
|
87
|
+
<td>
|
|
88
|
+
<span
|
|
89
|
+
:class="[copyClass, copiedKey === token.name ? copiedClass : '']"
|
|
90
|
+
role="button"
|
|
91
|
+
tabindex="0"
|
|
92
|
+
:title="'Click to copy ' + token.name"
|
|
93
|
+
@click="copy(token.name)"
|
|
94
|
+
@keydown.enter.prevent="copy(token.name)"
|
|
95
|
+
@keydown.space.prevent="copy(token.name)"
|
|
96
|
+
>{{ copiedKey === token.name ? 'Copied!' : token.name }}</span>
|
|
97
|
+
</td>
|
|
98
|
+
<td>{{ split(token).rem }}</td>
|
|
99
|
+
<td>{{ split(token).px }}</td>
|
|
100
|
+
<td class="cp-token-table__preview">
|
|
101
|
+
<span :style="preview(token.name)">{{ sample }}</span>
|
|
102
|
+
</td>
|
|
103
|
+
</tr>
|
|
104
|
+
</tbody>
|
|
105
|
+
</table>
|
|
106
|
+
<div v-else style="padding: 12px 0; color: #9ca3af; font-size: 12px;">No tokens</div>
|
|
107
|
+
`,
|
|
108
|
+
}),
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
type ValueSectionArgs = { kind: 'letter-spacing' | 'line-height'; loader: () => Token[] }
|
|
112
|
+
|
|
113
|
+
const makeValueStory = ({ kind, loader }: ValueSectionArgs): Story => ({
|
|
114
|
+
render: () => ({
|
|
115
|
+
setup() {
|
|
116
|
+
const { copiedKey, copy } = useCopier()
|
|
117
|
+
const probeProperty: ProbeProperty = kind
|
|
118
|
+
return {
|
|
119
|
+
kind,
|
|
120
|
+
tokens: loader(),
|
|
121
|
+
copiedKey,
|
|
122
|
+
copy,
|
|
123
|
+
tableClass: tokenTableClass,
|
|
124
|
+
copyClass: copyableClass,
|
|
125
|
+
copiedClass: copyableCopiedClass,
|
|
126
|
+
preview: (k: PreviewKind, tokenName: string) => previewStyle(k, tokenName),
|
|
127
|
+
sample: (k: PreviewKind) => sampleForKind(k),
|
|
128
|
+
px: (tokenName: string) => getTokenPixels(tokenName, probeProperty) ?? '—',
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
template: `
|
|
132
|
+
<table v-if="tokens.length > 0" :class="tableClass">
|
|
133
|
+
<thead>
|
|
134
|
+
<tr>
|
|
135
|
+
<th scope="col">Token</th>
|
|
136
|
+
<th scope="col">Value</th>
|
|
137
|
+
<th scope="col">px</th>
|
|
138
|
+
<th scope="col">Preview</th>
|
|
139
|
+
</tr>
|
|
140
|
+
</thead>
|
|
141
|
+
<tbody>
|
|
142
|
+
<tr v-for="token in tokens" :key="token.name">
|
|
143
|
+
<td>
|
|
144
|
+
<span
|
|
145
|
+
:class="[copyClass, copiedKey === token.name ? copiedClass : '']"
|
|
146
|
+
role="button"
|
|
147
|
+
tabindex="0"
|
|
148
|
+
:title="'Click to copy ' + token.name"
|
|
149
|
+
@click="copy(token.name)"
|
|
150
|
+
@keydown.enter.prevent="copy(token.name)"
|
|
151
|
+
@keydown.space.prevent="copy(token.name)"
|
|
152
|
+
>{{ copiedKey === token.name ? 'Copied!' : token.name }}</span>
|
|
153
|
+
</td>
|
|
154
|
+
<td>{{ token.value }}</td>
|
|
155
|
+
<td>{{ px(token.name) }}</td>
|
|
156
|
+
<td class="cp-token-table__preview">
|
|
157
|
+
<span :style="preview(kind, token.name)">{{ sample(kind) }}</span>
|
|
158
|
+
</td>
|
|
159
|
+
</tr>
|
|
160
|
+
</tbody>
|
|
161
|
+
</table>
|
|
162
|
+
<div v-else style="padding: 12px 0; color: #9ca3af; font-size: 12px;">No tokens</div>
|
|
163
|
+
`,
|
|
164
|
+
}),
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Raw line-heights. The gradient line marks the baseline of each sample.
|
|
169
|
+
*/
|
|
170
|
+
export const LineHeight: Story = makeValueStory({
|
|
171
|
+
kind: 'line-height',
|
|
172
|
+
loader: () => sortTokensBySize(readTokens(['--cp-line-height-'])),
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Letter-spacing (tracking) scale.
|
|
177
|
+
*/
|
|
178
|
+
export const LetterSpacing: Story = makeValueStory({
|
|
179
|
+
kind: 'letter-spacing',
|
|
180
|
+
loader: () => sortTokensBySize(readTokens(['--cp-letter-spacing-'])),
|
|
181
|
+
})
|
|
@@ -1,16 +1,8 @@
|
|
|
1
|
-
export const docPageStyle =
|
|
2
|
-
'padding: 24px; background: #f3f4f6; min-height: 100vh; width: 70vw; box-sizing: border-box;'
|
|
3
|
-
|
|
4
|
-
export const docSectionStyle =
|
|
5
|
-
'background: #fff; border-radius: 8px; padding: 24px; margin-bottom: 28px; box-shadow: 0 1px 3px rgba(0,0,0,0.08);'
|
|
6
|
-
|
|
7
|
-
export const docTitleStyle =
|
|
8
|
-
'margin: 0 0 16px 0; font-size: 12px; font-weight: 600; color: #6b7280; text-transform: uppercase; letter-spacing: 0.05em;'
|
|
9
|
-
|
|
10
1
|
export const docRowWrapStyle = 'display: flex; flex-wrap: wrap; gap: 24px 32px; align-items: flex-end;'
|
|
11
2
|
|
|
12
3
|
export const docRowColumnStyle = 'display: flex; flex-direction: column; gap: 24px;'
|
|
13
4
|
|
|
14
|
-
export const docCellStyle =
|
|
5
|
+
export const docCellStyle =
|
|
6
|
+
'display: flex; flex-direction: column; gap: 10px; align-items: flex-start; justify-content: space-between; align-self: stretch;'
|
|
15
7
|
|
|
16
8
|
export const docLabelStyle = 'font-size: 12px; color: #6b7280;'
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import { ref } from 'vue'
|
|
2
|
+
|
|
3
|
+
export type Token = { name: string; value: string }
|
|
4
|
+
|
|
5
|
+
export async function copyText(text: string): Promise<boolean> {
|
|
6
|
+
if (typeof navigator === 'undefined' || typeof window === 'undefined') return false
|
|
7
|
+
try {
|
|
8
|
+
if (navigator.clipboard?.writeText) {
|
|
9
|
+
await navigator.clipboard.writeText(text)
|
|
10
|
+
return true
|
|
11
|
+
}
|
|
12
|
+
} catch {
|
|
13
|
+
// fall through to fallback
|
|
14
|
+
}
|
|
15
|
+
try {
|
|
16
|
+
const textarea = document.createElement('textarea')
|
|
17
|
+
textarea.value = text
|
|
18
|
+
textarea.setAttribute('readonly', '')
|
|
19
|
+
textarea.style.position = 'fixed'
|
|
20
|
+
textarea.style.opacity = '0'
|
|
21
|
+
document.body.appendChild(textarea)
|
|
22
|
+
textarea.select()
|
|
23
|
+
const ok = document.execCommand('copy')
|
|
24
|
+
document.body.removeChild(textarea)
|
|
25
|
+
return ok
|
|
26
|
+
} catch {
|
|
27
|
+
return false
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function useCopier() {
|
|
32
|
+
const copiedKey = ref<null | string>(null)
|
|
33
|
+
let timer: null | ReturnType<typeof setTimeout> = null
|
|
34
|
+
|
|
35
|
+
const copy = async (key: string, text: string = key) => {
|
|
36
|
+
const ok = await copyText(text)
|
|
37
|
+
if (!ok) return
|
|
38
|
+
if (timer) clearTimeout(timer)
|
|
39
|
+
copiedKey.value = key
|
|
40
|
+
timer = setTimeout(() => {
|
|
41
|
+
copiedKey.value = null
|
|
42
|
+
}, 1200)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return { copiedKey, copy }
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
type MatcherFn = (name: string) => boolean
|
|
49
|
+
type Matcher = string | RegExp | MatcherFn
|
|
50
|
+
|
|
51
|
+
const toMatcherFn = (m: Matcher): MatcherFn => {
|
|
52
|
+
if (typeof m === 'function') return m
|
|
53
|
+
if (m instanceof RegExp) return (name: string) => m.test(name)
|
|
54
|
+
return (name: string) => name.startsWith(m)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function readTokens(include: Matcher[], exclude: Matcher[] = []): Token[] {
|
|
58
|
+
if (typeof window === 'undefined' || typeof document === 'undefined') return []
|
|
59
|
+
const styles = getComputedStyle(document.documentElement)
|
|
60
|
+
const includeFns = include.map(toMatcherFn)
|
|
61
|
+
const excludeFns = exclude.map(toMatcherFn)
|
|
62
|
+
const out: Token[] = []
|
|
63
|
+
for (let i = 0; i < styles.length; i++) {
|
|
64
|
+
const name = styles[i]
|
|
65
|
+
if (!name.startsWith('--cp-')) continue
|
|
66
|
+
if (!includeFns.some((fn) => fn(name))) continue
|
|
67
|
+
if (excludeFns.some((fn) => fn(name))) continue
|
|
68
|
+
out.push({ name, value: styles.getPropertyValue(name).trim() })
|
|
69
|
+
}
|
|
70
|
+
return out.sort((a, b) => a.name.localeCompare(b.name))
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export type ProbeProperty = 'letter-spacing' | 'line-height' | 'width'
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Apply a token as the given CSS property on a hidden probe element and read
|
|
77
|
+
* back the computed value. Defaults to `width`, which is the right choice for
|
|
78
|
+
* length tokens (rem/px). Use `line-height` or `letter-spacing` to resolve
|
|
79
|
+
* values that depend on the current font size.
|
|
80
|
+
*/
|
|
81
|
+
export function getTokenPixels(tokenName: string, property: ProbeProperty = 'width'): null | string {
|
|
82
|
+
if (typeof window === 'undefined' || typeof document === 'undefined') return null
|
|
83
|
+
const probe = document.createElement('div')
|
|
84
|
+
probe.style.position = 'absolute'
|
|
85
|
+
probe.style.visibility = 'hidden'
|
|
86
|
+
probe.style.pointerEvents = 'none'
|
|
87
|
+
probe.style.fontSize = '16px'
|
|
88
|
+
if (property === 'width') {
|
|
89
|
+
probe.style.width = `var(${tokenName})`
|
|
90
|
+
} else if (property === 'line-height') {
|
|
91
|
+
probe.style.lineHeight = `var(${tokenName})`
|
|
92
|
+
probe.textContent = 'M'
|
|
93
|
+
} else {
|
|
94
|
+
probe.style.letterSpacing = `var(${tokenName})`
|
|
95
|
+
}
|
|
96
|
+
document.body.appendChild(probe)
|
|
97
|
+
const style = getComputedStyle(probe)
|
|
98
|
+
let computed: string
|
|
99
|
+
if (property === 'width') computed = style.width
|
|
100
|
+
else if (property === 'line-height') computed = style.lineHeight
|
|
101
|
+
else computed = style.letterSpacing
|
|
102
|
+
document.body.removeChild(probe)
|
|
103
|
+
if (!computed || computed === 'auto' || computed === 'normal') return null
|
|
104
|
+
return computed
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const tshirtRank: Record<string, number> = {
|
|
108
|
+
none: -1,
|
|
109
|
+
'3xs': 0,
|
|
110
|
+
'2xs': 1,
|
|
111
|
+
xs: 2,
|
|
112
|
+
sm: 3,
|
|
113
|
+
'sm-md': 3.5,
|
|
114
|
+
md: 4,
|
|
115
|
+
'md-lg': 4.5,
|
|
116
|
+
lg: 5,
|
|
117
|
+
xl: 6,
|
|
118
|
+
'2xl': 7,
|
|
119
|
+
'3xl': 8,
|
|
120
|
+
'4xl': 9,
|
|
121
|
+
'5xl': 10,
|
|
122
|
+
'6xl': 11,
|
|
123
|
+
'7xl': 12,
|
|
124
|
+
'8xl': 13,
|
|
125
|
+
'9xl': 14,
|
|
126
|
+
full: 9999,
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function extractSizeRank(name: string): null | number {
|
|
130
|
+
const parts = name.replace(/^--cp-/, '').split('-')
|
|
131
|
+
for (let i = parts.length - 1; i >= 0; i--) {
|
|
132
|
+
const one = parts[i]
|
|
133
|
+
const two = i > 0 ? `${parts[i - 1]}-${one}` : null
|
|
134
|
+
if (two && two in tshirtRank) return tshirtRank[two]
|
|
135
|
+
if (one in tshirtRank) return tshirtRank[one]
|
|
136
|
+
}
|
|
137
|
+
return null
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function parsePxNumber(px: null | string): null | number {
|
|
141
|
+
if (!px) return null
|
|
142
|
+
const m = px.match(/^(-?\d+(?:\.\d+)?)px$/)
|
|
143
|
+
return m ? parseFloat(m[1]) : null
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function sortTokensBySize(tokens: Token[], direction: 'asc' | 'desc' = 'desc'): Token[] {
|
|
147
|
+
const factor = direction === 'desc' ? -1 : 1
|
|
148
|
+
return [...tokens].sort((a, b) => {
|
|
149
|
+
const rankA = extractSizeRank(a.name)
|
|
150
|
+
const rankB = extractSizeRank(b.name)
|
|
151
|
+
if (rankA !== null && rankB !== null && rankA !== rankB) {
|
|
152
|
+
return factor * (rankA - rankB)
|
|
153
|
+
}
|
|
154
|
+
if (rankA !== null && rankB === null) return -1
|
|
155
|
+
if (rankA === null && rankB !== null) return 1
|
|
156
|
+
const pxA = parsePxNumber(getTokenPixels(a.name))
|
|
157
|
+
const pxB = parsePxNumber(getTokenPixels(b.name))
|
|
158
|
+
if (pxA !== null && pxB !== null && pxA !== pxB) {
|
|
159
|
+
return factor * (pxA - pxB)
|
|
160
|
+
}
|
|
161
|
+
return a.name.localeCompare(b.name)
|
|
162
|
+
})
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export const copyableClass = 'cp-copyable'
|
|
166
|
+
|
|
167
|
+
export const copyableCopiedClass = 'cp-copyable--copied'
|
|
168
|
+
|
|
169
|
+
export const tokenTableClass = 'cp-token-table'
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Split a token value into a rem part and a px part. When the raw value is
|
|
173
|
+
* expressed in rem, both columns are filled (value in rem + computed px).
|
|
174
|
+
* Otherwise the value goes into whichever column matches its unit.
|
|
175
|
+
*/
|
|
176
|
+
export function splitRemPx(token: Token): { px: string; rem: string } {
|
|
177
|
+
const value = token.value.trim()
|
|
178
|
+
const resolvedPx = getTokenPixels(token.name) ?? ''
|
|
179
|
+
const dash = '—'
|
|
180
|
+
const isRem = /^-?\d*\.?\d+rem$/.test(value)
|
|
181
|
+
if (isRem) {
|
|
182
|
+
return { rem: value, px: resolvedPx || dash }
|
|
183
|
+
}
|
|
184
|
+
const isPx = /^-?\d*\.?\d+px$/.test(value)
|
|
185
|
+
if (isPx) {
|
|
186
|
+
return { rem: dash, px: value }
|
|
187
|
+
}
|
|
188
|
+
return { rem: dash, px: resolvedPx || dash }
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function ensureFoundationStyles(): void {
|
|
192
|
+
if (typeof document === 'undefined') return
|
|
193
|
+
const id = 'cp-foundation-styles'
|
|
194
|
+
if (document.getElementById(id)) return
|
|
195
|
+
const style = document.createElement('style')
|
|
196
|
+
style.id = id
|
|
197
|
+
style.textContent = `
|
|
198
|
+
.${copyableClass} {
|
|
199
|
+
cursor: pointer;
|
|
200
|
+
border-radius: 4px;
|
|
201
|
+
padding: 2px 6px;
|
|
202
|
+
margin: -2px -6px;
|
|
203
|
+
transition: background-color 0.15s ease, color 0.15s ease;
|
|
204
|
+
position: relative;
|
|
205
|
+
}
|
|
206
|
+
.${copyableClass}:hover {
|
|
207
|
+
background-color: #eef0ff;
|
|
208
|
+
}
|
|
209
|
+
.${copyableClass}:focus-visible {
|
|
210
|
+
outline: 2px solid #6366f1;
|
|
211
|
+
outline-offset: 2px;
|
|
212
|
+
}
|
|
213
|
+
.${copyableClass}.${copyableCopiedClass} {
|
|
214
|
+
background-color: #dcfce7;
|
|
215
|
+
color: #065f46;
|
|
216
|
+
}
|
|
217
|
+
.${tokenTableClass} {
|
|
218
|
+
width: 100%;
|
|
219
|
+
border-collapse: collapse;
|
|
220
|
+
text-align: left;
|
|
221
|
+
}
|
|
222
|
+
.${tokenTableClass} thead th {
|
|
223
|
+
font-family: ui-sans-serif, system-ui, sans-serif;
|
|
224
|
+
font-size: 11px;
|
|
225
|
+
font-weight: 600;
|
|
226
|
+
text-transform: uppercase;
|
|
227
|
+
letter-spacing: 0.04em;
|
|
228
|
+
color: #6b7280;
|
|
229
|
+
padding: 10px 12px;
|
|
230
|
+
border-bottom: 1px solid #e5e7eb;
|
|
231
|
+
background: #f9fafb;
|
|
232
|
+
}
|
|
233
|
+
.${tokenTableClass} tbody td {
|
|
234
|
+
padding: 10px 12px;
|
|
235
|
+
border-bottom: 1px solid #f1f2f6;
|
|
236
|
+
vertical-align: middle;
|
|
237
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
238
|
+
font-size: 12px;
|
|
239
|
+
color: #6b7280;
|
|
240
|
+
word-break: break-all;
|
|
241
|
+
white-space: nowrap;
|
|
242
|
+
}
|
|
243
|
+
.${tokenTableClass} tbody td:first-child {
|
|
244
|
+
color: #111;
|
|
245
|
+
}
|
|
246
|
+
.${tokenTableClass} tbody tr:last-child td {
|
|
247
|
+
border-bottom: none;
|
|
248
|
+
}
|
|
249
|
+
.${tokenTableClass} tbody td.cp-token-table__preview {
|
|
250
|
+
width: 1%;
|
|
251
|
+
white-space: nowrap;
|
|
252
|
+
}
|
|
253
|
+
`
|
|
254
|
+
document.head.appendChild(style)
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (typeof document !== 'undefined') {
|
|
258
|
+
ensureFoundationStyles()
|
|
259
|
+
}
|