@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.
Files changed (136) hide show
  1. package/README.md +80 -0
  2. package/dist/component-registry.d.ts +90 -0
  3. package/dist/component-registry.d.ts.map +1 -0
  4. package/dist/component-registry.js +93 -0
  5. package/dist/component-registry.js.map +1 -0
  6. package/dist/css-props/getCSSPropRawValue.d.ts +4 -0
  7. package/dist/css-props/getCSSPropRawValue.d.ts.map +1 -0
  8. package/dist/css-props/getCSSPropRawValue.js +41 -0
  9. package/dist/css-props/getCSSPropRawValue.js.map +1 -0
  10. package/dist/css-props/index.d.ts +5 -0
  11. package/dist/css-props/index.d.ts.map +1 -0
  12. package/dist/css-props/index.js +5 -0
  13. package/dist/css-props/index.js.map +1 -0
  14. package/dist/css-props/parseCSSPropValue.d.ts +7 -0
  15. package/dist/css-props/parseCSSPropValue.d.ts.map +1 -0
  16. package/dist/css-props/parseCSSPropValue.js +70 -0
  17. package/dist/css-props/parseCSSPropValue.js.map +1 -0
  18. package/dist/css-props/registerCSSProps.d.ts +7 -0
  19. package/dist/css-props/registerCSSProps.d.ts.map +1 -0
  20. package/dist/css-props/registerCSSProps.js +231 -0
  21. package/dist/css-props/registerCSSProps.js.map +1 -0
  22. package/dist/css-props/types.d.ts +29 -0
  23. package/dist/css-props/types.d.ts.map +1 -0
  24. package/dist/css-props/types.js +2 -0
  25. package/dist/css-props/types.js.map +1 -0
  26. package/dist/css-states/index.d.ts +3 -0
  27. package/dist/css-states/index.d.ts.map +1 -0
  28. package/dist/css-states/index.js +3 -0
  29. package/dist/css-states/index.js.map +1 -0
  30. package/dist/css-states/registerCSSStates.d.ts +23 -0
  31. package/dist/css-states/registerCSSStates.d.ts.map +1 -0
  32. package/dist/css-states/registerCSSStates.js +119 -0
  33. package/dist/css-states/registerCSSStates.js.map +1 -0
  34. package/dist/css-states/states.d.ts +71 -0
  35. package/dist/css-states/states.d.ts.map +1 -0
  36. package/dist/css-states/states.js +140 -0
  37. package/dist/css-states/states.js.map +1 -0
  38. package/dist/design-tokens.generated.d.ts +1167 -0
  39. package/dist/design-tokens.generated.d.ts.map +1 -0
  40. package/dist/design-tokens.generated.js +2675 -0
  41. package/dist/design-tokens.generated.js.map +1 -0
  42. package/dist/index.d.ts +7 -0
  43. package/dist/index.d.ts.map +1 -0
  44. package/dist/index.js +7 -0
  45. package/dist/index.js.map +1 -0
  46. package/dist/tailwind-customizations/index.d.ts +3 -0
  47. package/dist/tailwind-customizations/index.d.ts.map +1 -0
  48. package/dist/tailwind-customizations/index.js +3 -0
  49. package/dist/tailwind-customizations/index.js.map +1 -0
  50. package/dist/tailwind-customizations/registerUtilities.d.ts +9 -0
  51. package/dist/tailwind-customizations/registerUtilities.d.ts.map +1 -0
  52. package/dist/tailwind-customizations/registerUtilities.js +59 -0
  53. package/dist/tailwind-customizations/registerUtilities.js.map +1 -0
  54. package/dist/tailwind-customizations/registerVariants.d.ts +8 -0
  55. package/dist/tailwind-customizations/registerVariants.d.ts.map +1 -0
  56. package/dist/tailwind-customizations/registerVariants.js +197 -0
  57. package/dist/tailwind-customizations/registerVariants.js.map +1 -0
  58. package/dist/tailwind-customizations/variants.d.ts +72 -0
  59. package/dist/tailwind-customizations/variants.d.ts.map +1 -0
  60. package/dist/tailwind-customizations/variants.js +153 -0
  61. package/dist/tailwind-customizations/variants.js.map +1 -0
  62. package/dist/tailwind-plugin.d.ts +4 -0
  63. package/dist/tailwind-plugin.d.ts.map +1 -0
  64. package/dist/tailwind-plugin.js +12 -0
  65. package/dist/tailwind-plugin.js.map +1 -0
  66. package/dist/types.d.ts +4 -0
  67. package/dist/types.d.ts.map +1 -0
  68. package/dist/types.js +2 -0
  69. package/dist/types.js.map +1 -0
  70. package/dist/utils/cssUnescape.d.ts +20 -0
  71. package/dist/utils/cssUnescape.d.ts.map +1 -0
  72. package/dist/utils/cssUnescape.js +44 -0
  73. package/dist/utils/cssUnescape.js.map +1 -0
  74. package/dist/utils/index.d.ts +6 -0
  75. package/dist/utils/index.d.ts.map +1 -0
  76. package/dist/utils/index.js +6 -0
  77. package/dist/utils/index.js.map +1 -0
  78. package/dist/utils/pxToTw.d.ts +3 -0
  79. package/dist/utils/pxToTw.d.ts.map +1 -0
  80. package/dist/utils/pxToTw.js +5 -0
  81. package/dist/utils/pxToTw.js.map +1 -0
  82. package/dist/utils/twToPx.d.ts +3 -0
  83. package/dist/utils/twToPx.d.ts.map +1 -0
  84. package/dist/utils/twToPx.js +5 -0
  85. package/dist/utils/twToPx.js.map +1 -0
  86. package/dist/utils/twToRem.d.ts +3 -0
  87. package/dist/utils/twToRem.d.ts.map +1 -0
  88. package/dist/utils/twToRem.js +5 -0
  89. package/dist/utils/twToRem.js.map +1 -0
  90. package/dist/utils/wrapSelector.d.ts +10 -0
  91. package/dist/utils/wrapSelector.d.ts.map +1 -0
  92. package/dist/utils/wrapSelector.js +57 -0
  93. package/dist/utils/wrapSelector.js.map +1 -0
  94. package/package.json +65 -0
  95. package/src/component-registry.ts +213 -0
  96. package/src/css-props/getCSSPropRawValue.ts +52 -0
  97. package/src/css-props/index.ts +4 -0
  98. package/src/css-props/parseCSSPropValue.ts +81 -0
  99. package/src/css-props/registerCSSProps.ts +274 -0
  100. package/src/css-props/types.ts +35 -0
  101. package/src/css-states/index.ts +2 -0
  102. package/src/css-states/registerCSSStates.ts +136 -0
  103. package/src/css-states/states.ts +160 -0
  104. package/src/design-tokens.generated.ts +2799 -0
  105. package/src/index.ts +6 -0
  106. package/src/tailwind-customizations/index.ts +2 -0
  107. package/src/tailwind-customizations/registerUtilities.ts +65 -0
  108. package/src/tailwind-customizations/registerVariants.ts +296 -0
  109. package/src/tailwind-customizations/variants.ts +190 -0
  110. package/src/tailwind-plugin.ts +14 -0
  111. package/src/types.ts +4 -0
  112. package/src/utils/cssUnescape.ts +49 -0
  113. package/src/utils/index.ts +5 -0
  114. package/src/utils/pxToTw.ts +4 -0
  115. package/src/utils/twToPx.ts +4 -0
  116. package/src/utils/twToRem.ts +4 -0
  117. package/src/utils/wrapSelector.ts +60 -0
  118. package/styles/fonts/EuclidCircularA-Bold.woff2 +0 -0
  119. package/styles/fonts/EuclidCircularA-BoldItalic.woff2 +0 -0
  120. package/styles/fonts/EuclidCircularA-Light.woff2 +0 -0
  121. package/styles/fonts/EuclidCircularA-LightItalic.woff2 +0 -0
  122. package/styles/fonts/EuclidCircularA-Medium.woff2 +0 -0
  123. package/styles/fonts/EuclidCircularA-MediumItalic.woff2 +0 -0
  124. package/styles/fonts/EuclidCircularA-Regular.woff2 +0 -0
  125. package/styles/fonts/EuclidCircularA-RegularItalic.woff2 +0 -0
  126. package/styles/fonts/EuclidCircularA-Semibold.woff2 +0 -0
  127. package/styles/fonts/EuclidCircularA-SemiboldItalic.woff2 +0 -0
  128. package/styles/fonts.css +83 -0
  129. package/styles/global.css +203 -0
  130. package/styles/layers.css +8 -0
  131. package/styles/tailwind.css +13 -0
  132. package/styles/tailwind.vscode.css +11 -0
  133. package/styles/theme.css +420 -0
  134. package/styles/typography.css +198 -0
  135. package/styles/utilities.css +305 -0
  136. package/styles/variants.css +34 -0
package/src/index.ts ADDED
@@ -0,0 +1,6 @@
1
+ export * from './component-registry.ts'
2
+ export * from './css-props/index.ts'
3
+ export * from './css-states/index.ts'
4
+ export * from './tailwind-customizations/index.ts'
5
+ export * from './utils/index.ts'
6
+ export * from './design-tokens.generated.ts'
@@ -0,0 +1,2 @@
1
+ export { registerUtilities } from './registerUtilities.ts'
2
+ export { registerVariants } from './registerVariants.ts'
@@ -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,4 @@
1
+ import type createPlugin from 'tailwindcss/plugin'
2
+
3
+ /** Types for Tailwind CSS plugin API. */
4
+ export type PluginAPI = Parameters<Parameters<typeof createPlugin>[0]>[0]
@@ -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
+ }
@@ -0,0 +1,5 @@
1
+ export { cssUnescape } from './cssUnescape.ts'
2
+ export { pxToTw } from './pxToTw.ts'
3
+ export { twToPx } from './twToPx.ts'
4
+ export { twToRem } from './twToRem.ts'
5
+ export { wrapSelector } from './wrapSelector.ts'
@@ -0,0 +1,4 @@
1
+ /** Converts a value in CSS pixels to a Tailwind length. */
2
+ export function pxToTw(px: number): number {
3
+ return px * 0.25 // 1px = 0.25 tw unit, 4px = 1 tw unit
4
+ }
@@ -0,0 +1,4 @@
1
+ /** Converts a Tailwind length to `px`. */
2
+ export function twToPx(tw: number): number {
3
+ return tw * 4 // 1 tw unit = 4px
4
+ }
@@ -0,0 +1,4 @@
1
+ /** Converts a Tailwind length to `rem`. */
2
+ export function twToRem(tw: number): number {
3
+ return tw * 0.25 // 1 tw unit = 0.25rem (4px at 16px base)
4
+ }