@graphprotocol/gds-css 0.0.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/README.md +80 -0
- package/dist/component-registry.d.ts +90 -0
- package/dist/component-registry.d.ts.map +1 -0
- package/dist/component-registry.js +93 -0
- package/dist/component-registry.js.map +1 -0
- package/dist/css-props/getCSSPropRawValue.d.ts +4 -0
- package/dist/css-props/getCSSPropRawValue.d.ts.map +1 -0
- package/dist/css-props/getCSSPropRawValue.js +41 -0
- package/dist/css-props/getCSSPropRawValue.js.map +1 -0
- package/dist/css-props/index.d.ts +5 -0
- package/dist/css-props/index.d.ts.map +1 -0
- package/dist/css-props/index.js +5 -0
- package/dist/css-props/index.js.map +1 -0
- package/dist/css-props/parseCSSPropValue.d.ts +7 -0
- package/dist/css-props/parseCSSPropValue.d.ts.map +1 -0
- package/dist/css-props/parseCSSPropValue.js +70 -0
- package/dist/css-props/parseCSSPropValue.js.map +1 -0
- package/dist/css-props/registerCSSProps.d.ts +7 -0
- package/dist/css-props/registerCSSProps.d.ts.map +1 -0
- package/dist/css-props/registerCSSProps.js +231 -0
- package/dist/css-props/registerCSSProps.js.map +1 -0
- package/dist/css-props/types.d.ts +29 -0
- package/dist/css-props/types.d.ts.map +1 -0
- package/dist/css-props/types.js +2 -0
- package/dist/css-props/types.js.map +1 -0
- package/dist/css-states/index.d.ts +3 -0
- package/dist/css-states/index.d.ts.map +1 -0
- package/dist/css-states/index.js +3 -0
- package/dist/css-states/index.js.map +1 -0
- package/dist/css-states/registerCSSStates.d.ts +23 -0
- package/dist/css-states/registerCSSStates.d.ts.map +1 -0
- package/dist/css-states/registerCSSStates.js +119 -0
- package/dist/css-states/registerCSSStates.js.map +1 -0
- package/dist/css-states/states.d.ts +71 -0
- package/dist/css-states/states.d.ts.map +1 -0
- package/dist/css-states/states.js +140 -0
- package/dist/css-states/states.js.map +1 -0
- package/dist/design-tokens.generated.d.ts +1167 -0
- package/dist/design-tokens.generated.d.ts.map +1 -0
- package/dist/design-tokens.generated.js +2675 -0
- package/dist/design-tokens.generated.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/tailwind-customizations/index.d.ts +3 -0
- package/dist/tailwind-customizations/index.d.ts.map +1 -0
- package/dist/tailwind-customizations/index.js +3 -0
- package/dist/tailwind-customizations/index.js.map +1 -0
- package/dist/tailwind-customizations/registerUtilities.d.ts +9 -0
- package/dist/tailwind-customizations/registerUtilities.d.ts.map +1 -0
- package/dist/tailwind-customizations/registerUtilities.js +59 -0
- package/dist/tailwind-customizations/registerUtilities.js.map +1 -0
- package/dist/tailwind-customizations/registerVariants.d.ts +8 -0
- package/dist/tailwind-customizations/registerVariants.d.ts.map +1 -0
- package/dist/tailwind-customizations/registerVariants.js +197 -0
- package/dist/tailwind-customizations/registerVariants.js.map +1 -0
- package/dist/tailwind-customizations/variants.d.ts +72 -0
- package/dist/tailwind-customizations/variants.d.ts.map +1 -0
- package/dist/tailwind-customizations/variants.js +153 -0
- package/dist/tailwind-customizations/variants.js.map +1 -0
- package/dist/tailwind-plugin.d.ts +4 -0
- package/dist/tailwind-plugin.d.ts.map +1 -0
- package/dist/tailwind-plugin.js +12 -0
- package/dist/tailwind-plugin.js.map +1 -0
- package/dist/types.d.ts +4 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/cssUnescape.d.ts +20 -0
- package/dist/utils/cssUnescape.d.ts.map +1 -0
- package/dist/utils/cssUnescape.js +44 -0
- package/dist/utils/cssUnescape.js.map +1 -0
- package/dist/utils/index.d.ts +6 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +6 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/pxToTw.d.ts +3 -0
- package/dist/utils/pxToTw.d.ts.map +1 -0
- package/dist/utils/pxToTw.js +5 -0
- package/dist/utils/pxToTw.js.map +1 -0
- package/dist/utils/twToPx.d.ts +3 -0
- package/dist/utils/twToPx.d.ts.map +1 -0
- package/dist/utils/twToPx.js +5 -0
- package/dist/utils/twToPx.js.map +1 -0
- package/dist/utils/twToRem.d.ts +3 -0
- package/dist/utils/twToRem.d.ts.map +1 -0
- package/dist/utils/twToRem.js +5 -0
- package/dist/utils/twToRem.js.map +1 -0
- package/dist/utils/wrapSelector.d.ts +10 -0
- package/dist/utils/wrapSelector.d.ts.map +1 -0
- package/dist/utils/wrapSelector.js +57 -0
- package/dist/utils/wrapSelector.js.map +1 -0
- package/package.json +65 -0
- package/src/component-registry.ts +213 -0
- package/src/css-props/getCSSPropRawValue.ts +52 -0
- package/src/css-props/index.ts +4 -0
- package/src/css-props/parseCSSPropValue.ts +81 -0
- package/src/css-props/registerCSSProps.ts +274 -0
- package/src/css-props/types.ts +35 -0
- package/src/css-states/index.ts +2 -0
- package/src/css-states/registerCSSStates.ts +136 -0
- package/src/css-states/states.ts +160 -0
- package/src/design-tokens.generated.ts +2799 -0
- package/src/index.ts +6 -0
- package/src/tailwind-customizations/index.ts +2 -0
- package/src/tailwind-customizations/registerUtilities.ts +65 -0
- package/src/tailwind-customizations/registerVariants.ts +296 -0
- package/src/tailwind-customizations/variants.ts +190 -0
- package/src/tailwind-plugin.ts +14 -0
- package/src/types.ts +4 -0
- package/src/utils/cssUnescape.ts +49 -0
- package/src/utils/index.ts +5 -0
- package/src/utils/pxToTw.ts +4 -0
- package/src/utils/twToPx.ts +4 -0
- package/src/utils/twToRem.ts +4 -0
- package/src/utils/wrapSelector.ts +60 -0
- package/styles/fonts/EuclidCircularA-Bold.woff2 +0 -0
- package/styles/fonts/EuclidCircularA-BoldItalic.woff2 +0 -0
- package/styles/fonts/EuclidCircularA-Light.woff2 +0 -0
- package/styles/fonts/EuclidCircularA-LightItalic.woff2 +0 -0
- package/styles/fonts/EuclidCircularA-Medium.woff2 +0 -0
- package/styles/fonts/EuclidCircularA-MediumItalic.woff2 +0 -0
- package/styles/fonts/EuclidCircularA-Regular.woff2 +0 -0
- package/styles/fonts/EuclidCircularA-RegularItalic.woff2 +0 -0
- package/styles/fonts/EuclidCircularA-Semibold.woff2 +0 -0
- package/styles/fonts/EuclidCircularA-SemiboldItalic.woff2 +0 -0
- package/styles/fonts.css +83 -0
- package/styles/global.css +203 -0
- package/styles/layers.css +8 -0
- package/styles/tailwind.css +13 -0
- package/styles/tailwind.vscode.css +11 -0
- package/styles/theme.css +420 -0
- package/styles/typography.css +198 -0
- package/styles/utilities.css +305 -0
- package/styles/variants.css +34 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { DeepRecord } from '@graphprotocol/gds-utils'
|
|
2
|
+
|
|
3
|
+
import { getRegisteredComponents } from '../component-registry.ts'
|
|
4
|
+
import type { PluginAPI } from '../types.ts'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Registers custom utilities and extends/overrides some built-in ones in the Tailwind plugin. Since
|
|
8
|
+
* Tailwind 4 it is preferable to do this in CSS, and we do it whenever we can (see
|
|
9
|
+
* `utilities.css`), but `@utility` doesn't support all use cases (e.g. advanced conditions, string
|
|
10
|
+
* manipulation, etc.).
|
|
11
|
+
*/
|
|
12
|
+
export function registerUtilities(api: PluginAPI) {
|
|
13
|
+
/**
|
|
14
|
+
* Apply the default "var" values for registered components, and register `var-[var=value]`
|
|
15
|
+
* utilities to override them.
|
|
16
|
+
*/
|
|
17
|
+
for (const component of getRegisteredComponents()) {
|
|
18
|
+
if (!component.vars) continue
|
|
19
|
+
api.addBase({
|
|
20
|
+
'@layer components': {
|
|
21
|
+
[`.${component.className}`]: {
|
|
22
|
+
...Object.fromEntries(
|
|
23
|
+
Object.keys(component.vars).map((varName) => {
|
|
24
|
+
return [`--gds-var-${varName}`, 'initial']
|
|
25
|
+
}),
|
|
26
|
+
),
|
|
27
|
+
'& > *': {
|
|
28
|
+
'&': Object.entries(component.vars).flatMap(([varName, varCSS]) => {
|
|
29
|
+
function getDefaultVarRule(cssOrValue: typeof varCSS): DeepRecord<string> {
|
|
30
|
+
if (!cssOrValue) {
|
|
31
|
+
return {}
|
|
32
|
+
}
|
|
33
|
+
if (typeof cssOrValue === 'string') {
|
|
34
|
+
return {
|
|
35
|
+
[`--gds-default-${varName}`]: cssOrValue,
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return Object.fromEntries(
|
|
39
|
+
Object.entries(cssOrValue).map(([selector, nestedCSSOrValue]) => {
|
|
40
|
+
return [selector || '&', getDefaultVarRule(nestedCSSOrValue)]
|
|
41
|
+
}),
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
return [
|
|
45
|
+
getDefaultVarRule(varCSS),
|
|
46
|
+
{
|
|
47
|
+
[`--gds-${component.kebabName}-${varName}`]: `var(--gds-var-${varName}, var(--gds-default-${varName}))`,
|
|
48
|
+
},
|
|
49
|
+
]
|
|
50
|
+
}),
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
})
|
|
55
|
+
}
|
|
56
|
+
api.matchUtilities({
|
|
57
|
+
var: (varNameAndValue) => {
|
|
58
|
+
const [varName, ...valueParts] = varNameAndValue.split('=')
|
|
59
|
+
const value = valueParts.join('')
|
|
60
|
+
return {
|
|
61
|
+
[`--gds-var-${varName}`]: value,
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
})
|
|
65
|
+
}
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
import cssesc from 'cssesc'
|
|
2
|
+
import { objectEntries } from 'ts-extras'
|
|
3
|
+
|
|
4
|
+
import { getRegisteredComponents } from '../component-registry.ts'
|
|
5
|
+
import { getCSSStateVariable, type CSSState } from '../css-states/states.ts'
|
|
6
|
+
import type { PluginAPI } from '../types.ts'
|
|
7
|
+
import { wrapSelector } from '../utils/index.ts'
|
|
8
|
+
import { getComponentVariantSelectors, getVariantSelectors } from './variants.ts'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Detect if running in VS Code's Tailwind IntelliSense extension to skip expensive variants. This
|
|
12
|
+
* dramatically improves autocomplete performance by avoiding exponential variant combinations.
|
|
13
|
+
*/
|
|
14
|
+
const IS_INTELLISENSE =
|
|
15
|
+
typeof process !== 'undefined' ? Boolean(process.env.VSCODE_PID) && !process.env.PRETTIER : false
|
|
16
|
+
|
|
17
|
+
function getNestedSelector(modifier: string | null) {
|
|
18
|
+
const separateGroupAndNestedNames = modifier?.split('--') ?? []
|
|
19
|
+
const nestedName = separateGroupAndNestedNames[1] ?? separateGroupAndNestedNames[0]
|
|
20
|
+
return `.${cssesc(nestedName ? `nested/${nestedName}` : 'nested', { isIdentifier: true })}`
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function getGroupSelector(modifier: string | null) {
|
|
24
|
+
const groupName = modifier?.split('--')[0]
|
|
25
|
+
return `.${cssesc(groupName ? `group/${groupName}` : 'group', { isIdentifier: true })}`
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function getPeerSelector(modifier: string | null) {
|
|
29
|
+
return `.${cssesc(modifier ? `peer/${modifier}` : 'peer', { isIdentifier: true })}`
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function addSimpleVariant(
|
|
33
|
+
api: PluginAPI,
|
|
34
|
+
variant: string,
|
|
35
|
+
selector: string,
|
|
36
|
+
supportsArbitrary: boolean,
|
|
37
|
+
) {
|
|
38
|
+
if (supportsArbitrary) {
|
|
39
|
+
api.matchVariant(
|
|
40
|
+
variant,
|
|
41
|
+
(value, { modifier }) => {
|
|
42
|
+
if (modifier !== null) return []
|
|
43
|
+
return value ? `&${selector}${wrapSelector(':is', value)}` : `&${selector}`
|
|
44
|
+
},
|
|
45
|
+
{ values: { DEFAULT: '' } },
|
|
46
|
+
)
|
|
47
|
+
} else {
|
|
48
|
+
api.addVariant(variant, `&${selector}`)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Registers custom variants in the Tailwind plugin. Since Tailwind 4 it is preferable to do this in
|
|
54
|
+
* CSS, and we do it whenever we can (see `variants.css`), but `@custom-variant` doesn't support
|
|
55
|
+
* dynamic variants or modifiers.
|
|
56
|
+
*/
|
|
57
|
+
export function registerVariants(api: PluginAPI) {
|
|
58
|
+
const variantSelectors = getVariantSelectors()
|
|
59
|
+
const registeredComponents = getRegisteredComponents()
|
|
60
|
+
const componentKebabNames = new Set(registeredComponents.map((component) => component.kebabName))
|
|
61
|
+
const componentVariantSelectors = getComponentVariantSelectors({
|
|
62
|
+
includeStates: !IS_INTELLISENSE,
|
|
63
|
+
includePositional: !IS_INTELLISENSE,
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
/** Register the base variants, including some overrides required by the CSS States system. */
|
|
67
|
+
for (const [variant, selector] of objectEntries(variantSelectors)) {
|
|
68
|
+
addSimpleVariant(api, variant, selector, variant === 'clickable')
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Register component variants. */
|
|
72
|
+
for (const [variant, selector] of objectEntries(componentVariantSelectors)) {
|
|
73
|
+
const isPlainComponentVariant = componentKebabNames.has(variant)
|
|
74
|
+
addSimpleVariant(api, variant, selector, isPlainComponentVariant)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Register `(group-)(not-)has-nested(-not)(-has)-{variant}/{name}` variants to style the current
|
|
79
|
+
* element based on a nested element's presence or state.
|
|
80
|
+
*/
|
|
81
|
+
for (const groupPrefix of ['group-', ''] as const) {
|
|
82
|
+
for (const notPrefix of ['not-', ''] as const) {
|
|
83
|
+
// Register `(group-)(not-)has-nested-not-*` variant
|
|
84
|
+
api.matchVariant(
|
|
85
|
+
`${groupPrefix}${notPrefix}has-nested-not`,
|
|
86
|
+
(value, { modifier }) => {
|
|
87
|
+
if (!value || modifier === '') return []
|
|
88
|
+
const selector = wrapSelector(
|
|
89
|
+
notPrefix === 'not-' ? ':not' : ':is',
|
|
90
|
+
`:has(${getNestedSelector(modifier)}${wrapSelector(':not', value)})`,
|
|
91
|
+
)
|
|
92
|
+
return groupPrefix === 'group-'
|
|
93
|
+
? `&:is(${getGroupSelector(modifier)}${selector} *)`
|
|
94
|
+
: `&${selector}`
|
|
95
|
+
},
|
|
96
|
+
{ values: variantSelectors },
|
|
97
|
+
)
|
|
98
|
+
// Register `(group-)(not-)has-nested-*` variant
|
|
99
|
+
api.matchVariant(
|
|
100
|
+
`${groupPrefix}${notPrefix}has-nested`,
|
|
101
|
+
(value, { modifier }) => {
|
|
102
|
+
if (modifier === '') return []
|
|
103
|
+
const selector = wrapSelector(
|
|
104
|
+
notPrefix === 'not-' ? ':not' : ':is',
|
|
105
|
+
`:has(${getNestedSelector(modifier)}${wrapSelector(':is', value)})`,
|
|
106
|
+
)
|
|
107
|
+
return groupPrefix === 'group-'
|
|
108
|
+
? `&:is(${getGroupSelector(modifier)}${selector} *)`
|
|
109
|
+
: `&${selector}`
|
|
110
|
+
},
|
|
111
|
+
{ values: { DEFAULT: '', ...variantSelectors } },
|
|
112
|
+
)
|
|
113
|
+
// Register `(group-)(not-)has-nested-not-has-*` variant
|
|
114
|
+
api.matchVariant(
|
|
115
|
+
`${groupPrefix}${notPrefix}has-nested-not-has`,
|
|
116
|
+
(value, { modifier }) => {
|
|
117
|
+
if (!value || modifier === '') return []
|
|
118
|
+
const selector = wrapSelector(
|
|
119
|
+
notPrefix === 'not-' ? ':not' : ':is',
|
|
120
|
+
`:has(${getNestedSelector(modifier)}):not(:has(${getNestedSelector(modifier)} ${wrapSelector(':is', value)}))`,
|
|
121
|
+
)
|
|
122
|
+
return groupPrefix === 'group-'
|
|
123
|
+
? `&:is(${getGroupSelector(modifier)}${selector} *)`
|
|
124
|
+
: `&${selector}`
|
|
125
|
+
},
|
|
126
|
+
{ values: { ...variantSelectors, ...componentVariantSelectors } },
|
|
127
|
+
)
|
|
128
|
+
// Register `(group-)(not-)has-nested-has-*` variant
|
|
129
|
+
api.matchVariant(
|
|
130
|
+
`${groupPrefix}${notPrefix}has-nested-has`,
|
|
131
|
+
(value, { modifier }) => {
|
|
132
|
+
if (!value || modifier === '') return []
|
|
133
|
+
const selector = wrapSelector(
|
|
134
|
+
notPrefix === 'not-' ? ':not' : ':is',
|
|
135
|
+
`:has(${getNestedSelector(modifier)} ${wrapSelector(':is', value)})`,
|
|
136
|
+
)
|
|
137
|
+
return groupPrefix === 'group-'
|
|
138
|
+
? `&:is(${getGroupSelector(modifier)}${selector} *)`
|
|
139
|
+
: `&${selector}`
|
|
140
|
+
},
|
|
141
|
+
{ values: { ...variantSelectors, ...componentVariantSelectors } },
|
|
142
|
+
)
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Register `(not-)in-group/{group}` variants. */
|
|
147
|
+
for (const notPrefix of ['not-', ''] as const) {
|
|
148
|
+
api.matchVariant(
|
|
149
|
+
`${notPrefix}in-group`,
|
|
150
|
+
(value, { modifier }) => {
|
|
151
|
+
if (value || modifier === '') return []
|
|
152
|
+
return wrapSelector(
|
|
153
|
+
notPrefix === 'not-' ? ':not' : ':is',
|
|
154
|
+
`${getGroupSelector(modifier)} *`,
|
|
155
|
+
)
|
|
156
|
+
},
|
|
157
|
+
{ values: { DEFAULT: '' } },
|
|
158
|
+
)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Register `(not-)preceded-by-peer/{peer}` and `(not-)followed-by-peer/{peer}` variants. */
|
|
162
|
+
for (const notPrefix of ['not-', ''] as const) {
|
|
163
|
+
api.matchVariant(
|
|
164
|
+
`${notPrefix}preceded-by-peer`,
|
|
165
|
+
(value, { modifier }) => {
|
|
166
|
+
if (value || modifier === '') return []
|
|
167
|
+
const selector = wrapSelector(
|
|
168
|
+
notPrefix === 'not-' ? ':not' : ':is',
|
|
169
|
+
`${getPeerSelector(modifier)} + *`,
|
|
170
|
+
)
|
|
171
|
+
return `&${selector}`
|
|
172
|
+
},
|
|
173
|
+
{ values: { DEFAULT: '' } },
|
|
174
|
+
)
|
|
175
|
+
api.matchVariant(
|
|
176
|
+
`${notPrefix}followed-by-peer`,
|
|
177
|
+
(value, { modifier }) => {
|
|
178
|
+
if (value || modifier === '') return []
|
|
179
|
+
const selector = wrapSelector(
|
|
180
|
+
notPrefix === 'not-' ? ':not' : ':is',
|
|
181
|
+
`:has(+ ${getPeerSelector(modifier)})`,
|
|
182
|
+
)
|
|
183
|
+
return `&${selector}`
|
|
184
|
+
},
|
|
185
|
+
{ values: { DEFAULT: '' } },
|
|
186
|
+
)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** Register `(not-)nearest-clickable-*` variants. */
|
|
190
|
+
for (const notPrefix of ['not-', ''] as const) {
|
|
191
|
+
for (const nearestClickableState of [
|
|
192
|
+
'open',
|
|
193
|
+
'current',
|
|
194
|
+
'checked',
|
|
195
|
+
'indeterminate',
|
|
196
|
+
'unchecked',
|
|
197
|
+
'hocus-visible',
|
|
198
|
+
'hover',
|
|
199
|
+
'focus-visible',
|
|
200
|
+
'active',
|
|
201
|
+
'enabled',
|
|
202
|
+
'disabled',
|
|
203
|
+
] as const satisfies (CSSState | 'hocus-visible' | 'focus-visible')[]) {
|
|
204
|
+
const states: CSSState[] =
|
|
205
|
+
nearestClickableState === 'hocus-visible'
|
|
206
|
+
? ['hover', 'active', 'focus']
|
|
207
|
+
: nearestClickableState === 'hover'
|
|
208
|
+
? ['hover', 'active']
|
|
209
|
+
: nearestClickableState === 'focus-visible'
|
|
210
|
+
? ['focus']
|
|
211
|
+
: [nearestClickableState]
|
|
212
|
+
const stateVariables = states.map((state) => getCSSStateVariable(state))
|
|
213
|
+
const selectors = [
|
|
214
|
+
`@container ${notPrefix === 'not-' ? 'not' : ''} (${stateVariables
|
|
215
|
+
.map(
|
|
216
|
+
(stateVariable) =>
|
|
217
|
+
`style(--gds-clickable-${stateVariable.name}: ${stateVariable.value})`,
|
|
218
|
+
)
|
|
219
|
+
.join(' or ')})`,
|
|
220
|
+
]
|
|
221
|
+
// Fallback selector for browsers without style query support; see `css-props/registerCSSProps.ts` for more details
|
|
222
|
+
const onlyEnabledClickables =
|
|
223
|
+
states.includes('hover') || states.includes('active') || states.includes('focus')
|
|
224
|
+
selectors.push(
|
|
225
|
+
`@supports (not (view-transition-name: none)) or (-moz-orient: inline) { &${notPrefix === 'not-' ? ':not' : ':is'}(${variantSelectors[onlyEnabledClickables ? 'clickable' : 'any-clickable']}:where(${stateVariables
|
|
226
|
+
.map(
|
|
227
|
+
(stateVariable) =>
|
|
228
|
+
`[data-gds-state-polyfill-${stateVariable.name}${stateVariable.value ? `=${cssesc(stateVariable.value, { isIdentifier: true })}` : ''}]`,
|
|
229
|
+
)
|
|
230
|
+
.join(', ')}) *) }`,
|
|
231
|
+
)
|
|
232
|
+
api.addVariant(`${notPrefix}nearest-clickable-${nearestClickableState}`, selectors)
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** Register a `style-group/{group}` variant to apply styles to an element from a descendant. */
|
|
237
|
+
api.matchVariant(
|
|
238
|
+
'style-group',
|
|
239
|
+
(value, { modifier }) => {
|
|
240
|
+
if (value || modifier === '') return []
|
|
241
|
+
return `${getGroupSelector(modifier)}:has(&)`
|
|
242
|
+
},
|
|
243
|
+
{ values: { DEFAULT: '' } },
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
/** Register `style-(previous|next)-peer(s)` variants to apply styles to peers. */
|
|
247
|
+
api.matchVariant(
|
|
248
|
+
'style-previous-peer',
|
|
249
|
+
(value, { modifier }) => {
|
|
250
|
+
if (value || modifier === '') return []
|
|
251
|
+
return `${getPeerSelector(modifier)}:has(+ &)`
|
|
252
|
+
},
|
|
253
|
+
{ values: { DEFAULT: '' } },
|
|
254
|
+
)
|
|
255
|
+
api.matchVariant(
|
|
256
|
+
'style-previous-peers',
|
|
257
|
+
(value, { modifier }) => {
|
|
258
|
+
if (value || modifier === '') return []
|
|
259
|
+
return `${getPeerSelector(modifier)}:has(~ &)`
|
|
260
|
+
},
|
|
261
|
+
{ values: { DEFAULT: '' } },
|
|
262
|
+
)
|
|
263
|
+
api.matchVariant(
|
|
264
|
+
'style-next-peer',
|
|
265
|
+
(value, { modifier }) => {
|
|
266
|
+
if (value || modifier === '') return []
|
|
267
|
+
return `& + ${getPeerSelector(modifier)}`
|
|
268
|
+
},
|
|
269
|
+
{ values: { DEFAULT: '' } },
|
|
270
|
+
)
|
|
271
|
+
api.matchVariant(
|
|
272
|
+
'style-next-peers',
|
|
273
|
+
(value, { modifier }) => {
|
|
274
|
+
if (value || modifier === '') return []
|
|
275
|
+
return `& ~ ${getPeerSelector(modifier)}`
|
|
276
|
+
},
|
|
277
|
+
{ values: { DEFAULT: '' } },
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
/** Register a `style-nested/{name}` variant to apply styles to nested elements. */
|
|
281
|
+
api.matchVariant(
|
|
282
|
+
'style-nested',
|
|
283
|
+
(value, { modifier }) => {
|
|
284
|
+
if (value || !modifier) return []
|
|
285
|
+
return `${getNestedSelector(modifier)}:is(& *)`
|
|
286
|
+
},
|
|
287
|
+
{ values: { DEFAULT: '' } },
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
/** Register a style query variant (e.g. `@style-[--prop-name=value]/optional-container-name`). */
|
|
291
|
+
api.matchVariant(
|
|
292
|
+
'@style',
|
|
293
|
+
(condition, { modifier }) =>
|
|
294
|
+
`@container ${modifier ?? ''} style(${condition.replace(/(?<![<>])=/g, ':')})`,
|
|
295
|
+
)
|
|
296
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { getRegisteredComponents } from '../component-registry.ts'
|
|
2
|
+
import { cssStates, cssStateSelectors, getFocusStateSelector } from '../css-states/states.ts'
|
|
3
|
+
import { wrapSelector } from '../utils/index.ts'
|
|
4
|
+
|
|
5
|
+
function getClickableSelector(enabled: boolean | 'any' = true) {
|
|
6
|
+
return `:where(
|
|
7
|
+
:any-link,
|
|
8
|
+
button,
|
|
9
|
+
input:is([type='button'], [type='submit'], [type='reset']),
|
|
10
|
+
summary,
|
|
11
|
+
[role='button'],
|
|
12
|
+
[role='link'],
|
|
13
|
+
[role='menuitem'],
|
|
14
|
+
[role='menuitemcheckbox'],
|
|
15
|
+
[role='menuitemradio'],
|
|
16
|
+
[role='option'],
|
|
17
|
+
[role='tab'],
|
|
18
|
+
.gds-checkable-label
|
|
19
|
+
)${enabled !== 'any' ? (enabled ? cssStateSelectors.enabled : cssStateSelectors.disabled) : ''}`
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Static list of Tailwind's built-in variants and our own custom ones. Maintains the same order as
|
|
24
|
+
* `https://github.com/tailwindlabs/tailwindcss/blob/main/packages/tailwindcss/src/variants.ts`
|
|
25
|
+
*/
|
|
26
|
+
const staticVariantSelectors = {
|
|
27
|
+
// Positional
|
|
28
|
+
first: ':first-child',
|
|
29
|
+
last: ':last-child',
|
|
30
|
+
only: ':only-child',
|
|
31
|
+
odd: ':nth-child(odd)',
|
|
32
|
+
even: ':nth-child(even)',
|
|
33
|
+
'first-of-type': ':first-of-type',
|
|
34
|
+
'last-of-type': ':last-of-type',
|
|
35
|
+
'only-of-type': ':only-of-type',
|
|
36
|
+
|
|
37
|
+
// State
|
|
38
|
+
visited: ':visited',
|
|
39
|
+
target: ':target',
|
|
40
|
+
open: cssStateSelectors.open,
|
|
41
|
+
|
|
42
|
+
// Forms
|
|
43
|
+
current: cssStateSelectors.current, // custom CSS state
|
|
44
|
+
default: ':default',
|
|
45
|
+
checked: cssStateSelectors.checked,
|
|
46
|
+
indeterminate: cssStateSelectors.indeterminate,
|
|
47
|
+
unchecked: cssStateSelectors.unchecked, // custom CSS state, different from `not-checked` which can still be indeterminate
|
|
48
|
+
'placeholder-shown': ':placeholder-shown',
|
|
49
|
+
autofill: ':autofill',
|
|
50
|
+
optional: ':optional',
|
|
51
|
+
required: ':required',
|
|
52
|
+
valid: ':valid',
|
|
53
|
+
invalid: ':invalid',
|
|
54
|
+
'user-valid': ':user-valid',
|
|
55
|
+
'user-invalid': ':user-invalid',
|
|
56
|
+
'in-range': ':in-range',
|
|
57
|
+
'out-of-range': ':out-of-range',
|
|
58
|
+
'read-only': cssStateSelectors['read-only'],
|
|
59
|
+
|
|
60
|
+
// Content
|
|
61
|
+
empty: ':empty',
|
|
62
|
+
blank: cssStateSelectors.blank, // custom CSS state
|
|
63
|
+
|
|
64
|
+
// Interactive
|
|
65
|
+
idle: cssStateSelectors.idle, // custom CSS state
|
|
66
|
+
'focus-within': getFocusStateSelector(false, true),
|
|
67
|
+
'hocus-visible': `:is(${cssStateSelectors.hover}, ${getFocusStateSelector(true, false)})`, // custom
|
|
68
|
+
hover: cssStateSelectors.hover,
|
|
69
|
+
focus: getFocusStateSelector(false, false),
|
|
70
|
+
'focus-visible': getFocusStateSelector(true, false),
|
|
71
|
+
active: cssStateSelectors.active,
|
|
72
|
+
enabled: cssStateSelectors.enabled,
|
|
73
|
+
disabled: cssStateSelectors.disabled,
|
|
74
|
+
|
|
75
|
+
// Other
|
|
76
|
+
inert: ':is([inert], [inert] *)',
|
|
77
|
+
|
|
78
|
+
// Clickable (all custom)
|
|
79
|
+
'any-clickable': getClickableSelector('any'),
|
|
80
|
+
clickable: getClickableSelector(),
|
|
81
|
+
'any-clickable-open': `${getClickableSelector('any')}${cssStateSelectors.open}`,
|
|
82
|
+
'clickable-open': `${getClickableSelector()}${cssStateSelectors.open}`,
|
|
83
|
+
'any-clickable-current': `${getClickableSelector('any')}${cssStateSelectors.current}`,
|
|
84
|
+
'clickable-current': `${getClickableSelector()}${cssStateSelectors.current}`,
|
|
85
|
+
'any-clickable-checked': `${getClickableSelector('any')}${cssStateSelectors.checked}`,
|
|
86
|
+
'clickable-checked': `${getClickableSelector()}${cssStateSelectors.checked}`,
|
|
87
|
+
'any-clickable-indeterminate': `${getClickableSelector('any')}${cssStateSelectors.indeterminate}`,
|
|
88
|
+
'clickable-indeterminate': `${getClickableSelector()}${cssStateSelectors.indeterminate}`,
|
|
89
|
+
'any-clickable-unchecked': `${getClickableSelector('any')}${cssStateSelectors.unchecked}`,
|
|
90
|
+
'clickable-unchecked': `${getClickableSelector()}${cssStateSelectors.unchecked}`,
|
|
91
|
+
'clickable-idle': `${getClickableSelector()}${cssStateSelectors.idle}`,
|
|
92
|
+
'clickable-focus-within': `${getClickableSelector()}${getFocusStateSelector(false, true)}`,
|
|
93
|
+
'clickable-hocus-visible': `:is(${getClickableSelector()}${cssStateSelectors.hover}, ${getClickableSelector()}${getFocusStateSelector(true, false)})`,
|
|
94
|
+
'clickable-hover': `${getClickableSelector()}${cssStateSelectors.hover}`,
|
|
95
|
+
'clickable-focus': `${getClickableSelector()}${getFocusStateSelector(false, false)}`,
|
|
96
|
+
'clickable-focus-visible': `${getClickableSelector()}${getFocusStateSelector(true, false)}`,
|
|
97
|
+
'clickable-active': `${getClickableSelector()}${cssStateSelectors.active}`,
|
|
98
|
+
'clickable-disabled': getClickableSelector(false),
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const positionalVariants = ['first', 'last', 'only', 'odd', 'even', 'inert'] as const
|
|
102
|
+
|
|
103
|
+
/** Generate positional variant selectors (`{prefix}-first`, `{prefix}-last`, etc.) */
|
|
104
|
+
function generatePositionalVariantSelectors(prefix: string, baseSelector: string) {
|
|
105
|
+
return {
|
|
106
|
+
...positionalVariants.reduce(
|
|
107
|
+
(result, variant) => {
|
|
108
|
+
result[`${prefix}-not-${variant}`] =
|
|
109
|
+
`${baseSelector}${wrapSelector(':not', staticVariantSelectors[variant])}`
|
|
110
|
+
return result
|
|
111
|
+
},
|
|
112
|
+
{} as Record<string, string>,
|
|
113
|
+
),
|
|
114
|
+
...positionalVariants.reduce(
|
|
115
|
+
(result, variant) => {
|
|
116
|
+
result[`${prefix}-${variant}`] = `${baseSelector}${staticVariantSelectors[variant]}`
|
|
117
|
+
return result
|
|
118
|
+
},
|
|
119
|
+
{} as Record<string, string>,
|
|
120
|
+
),
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Get all variant selectors including dynamically generated ones based on registered components. */
|
|
125
|
+
export function getVariantSelectors() {
|
|
126
|
+
const dynamicVariantSelectors: Record<string, string> = {}
|
|
127
|
+
|
|
128
|
+
// Add `addon-compatible` and `addon-compatible-{variant}` variants if there are addon-compatible components
|
|
129
|
+
const addonCompatibleComponents = getRegisteredComponents().filter(
|
|
130
|
+
(component) => component.addonCompatible,
|
|
131
|
+
)
|
|
132
|
+
if (addonCompatibleComponents.length > 0) {
|
|
133
|
+
const addonCompatibleSelector = `:is(${addonCompatibleComponents.map((component) => `.${component.className}`).join(', ')})`
|
|
134
|
+
dynamicVariantSelectors['addon-compatible'] = addonCompatibleSelector
|
|
135
|
+
Object.assign(
|
|
136
|
+
dynamicVariantSelectors,
|
|
137
|
+
generatePositionalVariantSelectors('addon-compatible', addonCompatibleSelector),
|
|
138
|
+
)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
...staticVariantSelectors,
|
|
143
|
+
...dynamicVariantSelectors,
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Get component variant selectors for all registered components. These include plain component
|
|
149
|
+
* selectors, state-based selectors, and positional ones.
|
|
150
|
+
*/
|
|
151
|
+
export function getComponentVariantSelectors(
|
|
152
|
+
options: {
|
|
153
|
+
includePlain?: boolean
|
|
154
|
+
includeStates?: boolean
|
|
155
|
+
includePositional?: boolean
|
|
156
|
+
} = {},
|
|
157
|
+
) {
|
|
158
|
+
const { includePlain = true, includeStates = true, includePositional = true } = options
|
|
159
|
+
const componentVariantSelectors: Record<string, string> = {}
|
|
160
|
+
|
|
161
|
+
for (const component of getRegisteredComponents()) {
|
|
162
|
+
// Plain `{component}` variant
|
|
163
|
+
if (includePlain) {
|
|
164
|
+
componentVariantSelectors[component.kebabName] = `.${component.className}`
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// CSS state variants (`{component}-state` and `{component}-not-state`)
|
|
168
|
+
if (includeStates) {
|
|
169
|
+
for (const state of cssStates) {
|
|
170
|
+
// Skip `not-*` states for `{component}-not-*` to prevent `{component}-not-not-*`
|
|
171
|
+
if (!state.startsWith('not-')) {
|
|
172
|
+
componentVariantSelectors[`${component.kebabName}-not-${state}`] =
|
|
173
|
+
`.${component.className}${wrapSelector(':not', cssStateSelectors[state])}`
|
|
174
|
+
}
|
|
175
|
+
componentVariantSelectors[`${component.kebabName}-${state}`] =
|
|
176
|
+
`.${component.className}${cssStateSelectors[state]}`
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Positional variants (`{component}-first`, `{component}-last`, etc.)
|
|
181
|
+
if (includePositional) {
|
|
182
|
+
Object.assign(
|
|
183
|
+
componentVariantSelectors,
|
|
184
|
+
generatePositionalVariantSelectors(component.kebabName, `.${component.className}`),
|
|
185
|
+
)
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return componentVariantSelectors
|
|
190
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import createPlugin from 'tailwindcss/plugin'
|
|
2
|
+
|
|
3
|
+
import { registerCSSProps } from './css-props/registerCSSProps.ts'
|
|
4
|
+
import { registerCSSStates } from './css-states/registerCSSStates.ts'
|
|
5
|
+
import { registerUtilities, registerVariants } from './tailwind-customizations/index.ts'
|
|
6
|
+
|
|
7
|
+
const gdsTailwindPlugin: ReturnType<typeof createPlugin> = createPlugin((api) => {
|
|
8
|
+
registerUtilities(api)
|
|
9
|
+
registerVariants(api)
|
|
10
|
+
registerCSSStates(api)
|
|
11
|
+
registerCSSProps(api)
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
export default gdsTailwindPlugin
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unescapes CSS-escaped strings, reversing the escaping done by libraries like `cssesc`.
|
|
3
|
+
*
|
|
4
|
+
* Handles three types of CSS escapes:
|
|
5
|
+
*
|
|
6
|
+
* 1. Hex escapes: `\20` or `\00020 ` → space character.
|
|
7
|
+
* 2. Character escapes: `\.` → `.`
|
|
8
|
+
* 3. Line continuations: `\<newline>` → (removed)
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
*
|
|
12
|
+
* cssUnescape('foo\.bar') // 'foo.bar'
|
|
13
|
+
* cssUnescape('hello\20world') // 'hello world'
|
|
14
|
+
* cssUnescape('test\2d case') // 'test-case'
|
|
15
|
+
*
|
|
16
|
+
* @param input - The CSS-escaped string to unescape.
|
|
17
|
+
* @returns The unescaped string.
|
|
18
|
+
*/
|
|
19
|
+
export function cssUnescape(input: string): string {
|
|
20
|
+
// Matches: hex escapes | line continuations | single char escapes
|
|
21
|
+
const escapePattern = /\\([0-9a-f]{1,6}[ \t\r\n\f]?|$)|\\(\r?\n|\r|\f)|\\(.)/gi
|
|
22
|
+
|
|
23
|
+
return input.replace(escapePattern, (_match, hex, newline, char) => {
|
|
24
|
+
// Single character escape: "\." → "."
|
|
25
|
+
if (char) {
|
|
26
|
+
return char
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Line continuation: backslash followed by newline is removed
|
|
30
|
+
if (newline) {
|
|
31
|
+
return ''
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Lone backslash at end of string
|
|
35
|
+
if (!hex) {
|
|
36
|
+
return '\uFFFD' // Replacement character
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Hex escape: parse the hex digits (trim removes optional trailing whitespace)
|
|
40
|
+
const codePoint = Number.parseInt(hex.trim(), 16)
|
|
41
|
+
|
|
42
|
+
// Invalid code points: null, surrogates, or out of Unicode range
|
|
43
|
+
if (codePoint === 0 || codePoint > 0x10ffff || (codePoint >= 0xd800 && codePoint <= 0xdfff)) {
|
|
44
|
+
return '\uFFFD' // Replacement character
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return String.fromCodePoint(codePoint)
|
|
48
|
+
})
|
|
49
|
+
}
|