@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
@@ -0,0 +1,213 @@
1
+ import { objectEntries } from 'ts-extras'
2
+
3
+ import { camelToKebab, type DeepRecord } from '@graphprotocol/gds-utils'
4
+
5
+ import type {
6
+ CSSPropColor,
7
+ CSSPropDefinition,
8
+ CSSPropLength,
9
+ CSSPropNumber,
10
+ CSSPropString,
11
+ CSSPropType,
12
+ CSSPropValue,
13
+ } from './css-props/types.ts'
14
+
15
+ export type CSSPropsConfig = Record<string, CSSPropDefinition>
16
+
17
+ type CSSProp<T extends CSSPropDefinition = CSSPropDefinition> = T & {
18
+ readonly name: string
19
+ readonly kebabName: string
20
+ readonly initialValue: CSSPropType<T>
21
+ }
22
+
23
+ type CSSProps<P extends CSSPropsConfig> = {
24
+ readonly [K in keyof P]: P[K] & CSSProp<P[K]>
25
+ }
26
+
27
+ type VarsConfig = Record<string, VarCSS>
28
+ type VarCSS = string | DeepRecord<string>
29
+
30
+ export class GDSComponent<P extends CSSPropsConfig = CSSPropsConfig, A extends boolean = boolean> {
31
+ readonly name: string
32
+ readonly kebabName: string
33
+ readonly className: string
34
+ readonly containerName: string | null
35
+ readonly addonCompatible: A
36
+ readonly cssProps: CSSProps<P>
37
+ readonly vars: VarsConfig | undefined
38
+
39
+ constructor(
40
+ name: string,
41
+ config?: {
42
+ containerName?: string | null | undefined
43
+ addonCompatible?: A | undefined
44
+ cssProps?: P | undefined
45
+ vars?: VarsConfig | undefined
46
+ },
47
+ ) {
48
+ this.name = name
49
+ this.kebabName = camelToKebab(name)
50
+ this.className = `gds-${this.kebabName}`
51
+ this.containerName = config?.containerName !== undefined ? config.containerName : this.className
52
+ this.addonCompatible = (config?.addonCompatible ?? false) as A
53
+ this.cssProps = Object.freeze(
54
+ Object.fromEntries(
55
+ objectEntries(config?.cssProps ?? ({} as P)).map(([propName, prop]) => {
56
+ const initialValue =
57
+ prop.defaultValue ??
58
+ (() => {
59
+ switch (prop.type) {
60
+ case 'values':
61
+ return (prop.values[0] ?? '') satisfies CSSPropValue
62
+ case 'string':
63
+ return '' satisfies CSSPropString
64
+ case 'number':
65
+ return 0 satisfies CSSPropNumber
66
+ case 'length':
67
+ return '0px' satisfies CSSPropLength
68
+ case 'color':
69
+ return 'current' satisfies CSSPropColor
70
+ }
71
+ })()
72
+ return [
73
+ propName,
74
+ {
75
+ ...prop,
76
+ name: propName,
77
+ kebabName: camelToKebab(propName),
78
+ initialValue,
79
+ },
80
+ ]
81
+ }),
82
+ ),
83
+ ) as CSSProps<P>
84
+ this.vars = config?.vars
85
+ }
86
+
87
+ getCSSPropByName<T extends keyof P & string>(name: T): CSSProps<P>[T] {
88
+ const prop = this.cssProps[name]
89
+ if (!prop) {
90
+ throw new Error(`Unknown CSS prop "${name}" for component "${this.name}"`)
91
+ }
92
+ return prop
93
+ }
94
+
95
+ findCSSPropByKebabName(kebabName: string) {
96
+ for (const key in this.cssProps) {
97
+ if (this.cssProps[key].kebabName === kebabName) {
98
+ return this.cssProps[key]
99
+ }
100
+ }
101
+ return undefined
102
+ }
103
+ }
104
+
105
+ /** Type to validate CSS prop definitions. */
106
+ type ValidateCSSPropDefinition<P> = P extends {
107
+ type: 'values'
108
+ values: readonly unknown[]
109
+ }
110
+ ? P['values'] extends readonly [unknown, ...unknown[]]
111
+ ? P extends {
112
+ defaultValue?: unknown
113
+ }
114
+ ? P['defaultValue'] extends P['values'][number]
115
+ ? P
116
+ : never
117
+ : P
118
+ : never
119
+ : P
120
+
121
+ /** Type to validate all CSS props in an object. */
122
+ type ValidateCSSPropsConfig<P extends Record<string, unknown>> = {
123
+ [K in keyof P]: ValidateCSSPropDefinition<P[K]>
124
+ }
125
+
126
+ /** Type to validate that vars don't conflict with cssProps names. */
127
+ type ValidateVarsConfig<P extends CSSPropsConfig, V extends VarsConfig> = {} extends P
128
+ ? V
129
+ : {
130
+ [K in keyof V]: K extends keyof P
131
+ ? `Var "${K & string}" conflicts with cssProps. Choose a different name.`
132
+ : V[K]
133
+ }
134
+
135
+ /** Helper function to create a `GDSComponent` instance with proper type inference. */
136
+ export function createComponent<
137
+ const P extends CSSPropsConfig,
138
+ const V extends VarsConfig,
139
+ const A extends boolean,
140
+ >(
141
+ name: string,
142
+ config?: ConstructorParameters<typeof GDSComponent<P>>[1] & {
143
+ addonCompatible?: A
144
+ cssProps?: ValidateCSSPropsConfig<P>
145
+ vars?: ValidateVarsConfig<P, V>
146
+ },
147
+ ): GDSComponent<P, A> {
148
+ return new GDSComponent(name, config)
149
+ }
150
+
151
+ /** Helper type to extract the CSS props config type from a `GDSComponent` instance. */
152
+ export type ComponentCSSPropsConfig<C extends GDSComponent> =
153
+ C extends GDSComponent<infer P> ? P : never
154
+
155
+ /**
156
+ * Helper type to extract CSS props types from components created with `createComponent`. Props
157
+ * without a `defaultValue` are required, while those with a `defaultValue` are optional. Explicitly
158
+ * includes `undefined` in the type to work with `exactOptionalPropertyTypes`.
159
+ */
160
+ export type ComponentCSSProps<C extends GDSComponent> = {
161
+ [K in keyof ComponentCSSPropsConfig<C> as string extends K
162
+ ? never
163
+ : 'defaultValue' extends keyof ComponentCSSPropsConfig<C>[K]
164
+ ? never
165
+ : K]: CSSPropType<ComponentCSSPropsConfig<C>[K]>
166
+ } & {
167
+ [K in keyof ComponentCSSPropsConfig<C> as string extends K
168
+ ? never
169
+ : 'defaultValue' extends keyof ComponentCSSPropsConfig<C>[K]
170
+ ? K
171
+ : never]?: CSSPropType<ComponentCSSPropsConfig<C>[K]> | undefined
172
+ }
173
+
174
+ /**
175
+ * Combined props type for GDS components. Includes CSS props and, for addon-compatible components,
176
+ * a hidden prop to mark them as such.
177
+ */
178
+ export type GDSComponentProps<C extends GDSComponent> = ComponentCSSProps<C> &
179
+ (C extends GDSComponent<CSSPropsConfig, true>
180
+ ? { __addonCompatible?: undefined }
181
+ : { __addonCompatible?: never })
182
+
183
+ const componentRegistry = new Map<string, GDSComponent>()
184
+
185
+ /** Register multiple components at once. */
186
+ export function registerComponents(components: GDSComponent[]) {
187
+ for (const component of components) {
188
+ componentRegistry.set(component.kebabName, component)
189
+ }
190
+ }
191
+
192
+ /**
193
+ * Get a registered component by its kebab-case name.
194
+ *
195
+ * @param kebabName - The kebab-case name of the component.
196
+ * @returns The component if found, `undefined` otherwise.
197
+ */
198
+ export function getRegisteredComponent(kebabName: string) {
199
+ return componentRegistry.get(kebabName)
200
+ }
201
+
202
+ /**
203
+ * Get all registered components.
204
+ *
205
+ * @param includeDummy - If true, don't filter out the dummy component.
206
+ * @returns Array of registered components.
207
+ */
208
+ export function getRegisteredComponents(includeDummy = false) {
209
+ const components = [...componentRegistry.values()]
210
+ return includeDummy
211
+ ? components
212
+ : components.filter((component) => component.kebabName !== 'dummy')
213
+ }
@@ -0,0 +1,52 @@
1
+ import cssesc from 'cssesc'
2
+
3
+ import { twToRem } from '../utils/index.ts'
4
+ import type {
5
+ CSSPropColor,
6
+ CSSPropDefinition,
7
+ CSSPropLength,
8
+ CSSPropType,
9
+ CSSPropValue,
10
+ } from './types.ts'
11
+
12
+ /** Converts a CSS prop value to its raw string representation for CSS custom properties. */
13
+ export function getCSSPropRawValue<P extends CSSPropDefinition>(
14
+ prop: P,
15
+ value: CSSPropType<P>,
16
+ ): string {
17
+ switch (prop.type) {
18
+ case 'values': {
19
+ const valuesValue = value as CSSPropValue
20
+ // Prefix the value with `gds-` because cssnano (which Next.js uses) incorrectly minifies `--gds-prop-variant: white` to `--gds-prop-variant: #fff`
21
+ return `gds-${valuesValue}`
22
+ }
23
+ case 'string': {
24
+ return `'${cssesc(String(value))}'`
25
+ }
26
+ case 'number': {
27
+ return String(value)
28
+ }
29
+ case 'length': {
30
+ const lengthValue = value as CSSPropLength
31
+ if (typeof lengthValue === 'number') {
32
+ return `${twToRem(lengthValue)}rem`
33
+ } else {
34
+ return lengthValue
35
+ }
36
+ }
37
+ case 'color': {
38
+ const colorValue = value as CSSPropColor
39
+ const [color, opacity] = colorValue.split('/')
40
+ const colorVariable =
41
+ color === 'current'
42
+ ? 'currentcolor'
43
+ : color === 'transparent'
44
+ ? 'transparent'
45
+ : `var(--color-${color})`
46
+ if (opacity && opacity !== '100') {
47
+ return `color-mix(in oklab, ${colorVariable} ${opacity}%, transparent)`
48
+ }
49
+ return colorVariable
50
+ }
51
+ }
52
+ }
@@ -0,0 +1,4 @@
1
+ export * from './types.ts'
2
+ export { getCSSPropRawValue } from './getCSSPropRawValue.ts'
3
+ export { parseCSSPropValue } from './parseCSSPropValue.ts'
4
+ export { registerCSSProps } from './registerCSSProps.ts'
@@ -0,0 +1,81 @@
1
+ import { parseNumber } from '@graphprotocol/gds-utils'
2
+
3
+ import { cssUnescape, pxToTw } from '../utils/index.ts'
4
+ import type {
5
+ CSSPropColor,
6
+ CSSPropDefinition,
7
+ CSSPropLength,
8
+ CSSPropNumber,
9
+ CSSPropString,
10
+ CSSPropType,
11
+ CSSPropValue,
12
+ } from './types.ts'
13
+
14
+ /**
15
+ * Parses a raw CSS prop value back to its typed representation. This is the inverse of
16
+ * `getCSSPropRawValue`.
17
+ */
18
+ export function parseCSSPropValue<P extends CSSPropDefinition>(
19
+ prop: P,
20
+ rawValue: string,
21
+ ): CSSPropType<P> {
22
+ switch (prop.type) {
23
+ case 'values': {
24
+ // Remove the `gds-` prefix added by `getCSSPropRawValue`
25
+ const value = rawValue.replace(/^(gds-)?/, '')
26
+ // Check if it looks like a boolean
27
+ if (value === 'true') return true satisfies CSSPropValue as CSSPropType<P>
28
+ if (value === 'false') return false satisfies CSSPropValue as CSSPropType<P>
29
+ // Check if it looks like a number
30
+ const numberValue = parseNumber(value)
31
+ if (numberValue !== null) {
32
+ return numberValue satisfies CSSPropValue as CSSPropType<P>
33
+ }
34
+ // Otherwise it's a string
35
+ return value satisfies CSSPropValue as CSSPropType<P>
36
+ }
37
+ case 'string': {
38
+ return cssUnescape(rawValue.slice(1, -1)) satisfies CSSPropString as CSSPropType<P>
39
+ }
40
+ case 'number': {
41
+ const numberValue = parseNumber(rawValue)
42
+ return (numberValue ?? 0) satisfies CSSPropNumber as CSSPropType<P>
43
+ }
44
+ case 'length': {
45
+ // Check if it's in pixels and convert to Tailwind units
46
+ if (/^\d+(?:\.\d+)?px$/.test(rawValue)) {
47
+ const numberValue = parseNumber(rawValue.slice(0, -2)) // Remove 'px'
48
+ return pxToTw(numberValue ?? 0) satisfies CSSPropLength as CSSPropType<P>
49
+ }
50
+ // Otherwise keep as-is (e.g., "1rem", "100%")
51
+ return rawValue as CSSPropLength as CSSPropType<P>
52
+ }
53
+ case 'color': {
54
+ if (rawValue === 'currentcolor') {
55
+ return 'current' satisfies CSSPropColor as CSSPropType<P>
56
+ }
57
+ if (rawValue === 'transparent') {
58
+ return 'transparent' satisfies CSSPropColor as CSSPropType<P>
59
+ }
60
+ // Check for `color-mix()` (color with opacity)
61
+ const colorMixMatch = rawValue.match(/^color-mix\(in oklab, (.+) (\d+)%, transparent\)$/)
62
+ if (colorMixMatch) {
63
+ const [, colorVar, opacity] = colorMixMatch
64
+ if (colorVar && opacity) {
65
+ const varMatch = colorVar.match(/^var\(--color-(.+)\)$/)
66
+ if (varMatch && varMatch[1]) {
67
+ const color = varMatch[1]
68
+ return `${color}/${opacity}` as CSSPropColor as CSSPropType<P>
69
+ }
70
+ }
71
+ }
72
+ // Check for simple color variable
73
+ const varMatch = rawValue.match(/^var\(--color-(.+)\)$/)
74
+ if (varMatch && varMatch[1]) {
75
+ return varMatch[1] as CSSPropColor as CSSPropType<P>
76
+ }
77
+ // Fallback - shouldn't normally happen
78
+ return rawValue as CSSPropType<P>
79
+ }
80
+ }
81
+ }
@@ -0,0 +1,274 @@
1
+ import cssesc from 'cssesc'
2
+
3
+ import { parseNumber } from '@graphprotocol/gds-utils'
4
+
5
+ import {
6
+ getRegisteredComponent,
7
+ getRegisteredComponents,
8
+ type GDSComponent,
9
+ } from '../component-registry.ts'
10
+ import type { PluginAPI } from '../types.ts'
11
+ import { getCSSPropRawValue } from './getCSSPropRawValue.ts'
12
+ import type { CSSPropColor } from './types.ts'
13
+
14
+ /**
15
+ * Registers CSS Props functionality in the Tailwind plugin. This includes custom properties,
16
+ * utilities, and variants for prop-based styling.
17
+ */
18
+ export function registerCSSProps(api: PluginAPI) {
19
+ const componentsByCSSProp = getRegisteredComponents(true).reduce((outerMap, component) => {
20
+ for (const cssProp of Object.values(component.cssProps)) {
21
+ const innerMap =
22
+ outerMap.get(cssProp.kebabName) ??
23
+ outerMap.set(cssProp.kebabName, new Map<string, GDSComponent>()).get(cssProp.kebabName)!
24
+ innerMap.set(component.kebabName, component)
25
+ }
26
+ return outerMap
27
+ }, new Map<string, Map<string, GDSComponent>>())
28
+
29
+ const cssPropKebabNames = [...componentsByCSSProp.keys()]
30
+
31
+ /** Register a couple non-inherited custom properties for each prop name. */
32
+ for (const cssPropKebabName of cssPropKebabNames) {
33
+ api.addBase({
34
+ [`@property --gds-default-${cssPropKebabName}`]: {
35
+ syntax: "'*'",
36
+ inherits: 'false',
37
+ },
38
+ [`@property --gds-passed-${cssPropKebabName}`]: {
39
+ syntax: "'*'",
40
+ inherits: 'false',
41
+ },
42
+ [`@property --gds-prop-${cssPropKebabName}`]: {
43
+ syntax: "'*'",
44
+ inherits: 'false',
45
+ },
46
+ })
47
+ }
48
+
49
+ /** Register a custom property for each component-prop pair to restrict the allowed values. */
50
+ for (const component of getRegisteredComponents()) {
51
+ for (const cssProp of Object.values(component.cssProps)) {
52
+ const syntax = (() => {
53
+ switch (cssProp.type) {
54
+ case 'values':
55
+ return `'${cssProp.values.map((value) => getCSSPropRawValue(cssProp, value)).join(' | ')}'`
56
+ case 'string':
57
+ return "'<string>'"
58
+ case 'number':
59
+ return "'<number>'"
60
+ case 'length':
61
+ return "'<length-percentage>'"
62
+ case 'color':
63
+ return "'<color>'"
64
+ default:
65
+ return "'*'"
66
+ }
67
+ })()
68
+ api.addBase({
69
+ [`@property --gds-${component.kebabName}-${cssProp.kebabName}`]: {
70
+ syntax,
71
+ /**
72
+ * We only need this property to inherit if the type is not `values`, because `values` CSS
73
+ * props are accessed via style queries.
74
+ */
75
+ inherits: cssProp.type === 'values' ? 'false' : 'true',
76
+ 'initial-value': (() => {
77
+ if (
78
+ cssProp.type === 'length' &&
79
+ typeof cssProp.initialValue === 'string' &&
80
+ /(cap|ch|em|ex|ic|lh)$/.test(cssProp.initialValue)
81
+ ) {
82
+ // `initial-value` cannot be font-size-relative
83
+ return '100%'
84
+ }
85
+ return getCSSPropRawValue(cssProp, cssProp.initialValue)
86
+ })(),
87
+ },
88
+ })
89
+ }
90
+
91
+ /**
92
+ * Base styles for each component, including defining the default value of each CSS prop and
93
+ * applying the value passed via the React prop, if any. See `getCSSPropsAttributes` for details
94
+ * on how each prop type is handled.
95
+ */
96
+ api.addBase({
97
+ '@layer components': {
98
+ [`.${component.className}`]: {
99
+ '--tw-sort': 'container-type',
100
+ ...(component.containerName ? { 'container-name': component.containerName } : {}),
101
+ ...Object.values(component.cssProps).reduce(
102
+ (rules, cssProp) => {
103
+ return {
104
+ ...rules,
105
+ [`--gds-passed-or-default-${cssProp.kebabName}`]: `var(--gds-passed-${cssProp.kebabName}, var(--gds-default-${cssProp.kebabName}, ${getCSSPropRawValue(cssProp, cssProp.initialValue)}))`,
106
+ [`--gds-${component.kebabName}-${cssProp.kebabName}`]: `var(--gds-prop-${cssProp.kebabName}, var(--gds-passed-or-default-${cssProp.kebabName}))`,
107
+ }
108
+ },
109
+ {} as Record<string, string>,
110
+ ),
111
+ },
112
+ },
113
+ })
114
+ }
115
+
116
+ function parseBareValueNumber(bareValue: string, increment = 0.25) {
117
+ const number = parseNumber(bareValue, { strict: true })
118
+ if (number === null || number < 0 || !Number.isInteger(number * (1 / increment))) return null
119
+ return number
120
+ }
121
+
122
+ type BareValueFunction =
123
+ NonNullable<Parameters<typeof api.matchUtilities>[1]> extends {
124
+ values?: { __BARE_VALUE__?: infer B }
125
+ }
126
+ ? B
127
+ : never
128
+
129
+ /** Register `prop-{prop}-{value}` and `default-{prop}-{value}` utilities. */
130
+ for (const [cssPropKebabName, components] of componentsByCSSProp) {
131
+ const cssProps = [...components.values()].flatMap((component) => {
132
+ const cssProp = component.findCSSPropByKebabName(cssPropKebabName)
133
+ return cssProp ? [cssProp] : []
134
+ })
135
+ const cssPropTypes = cssProps.map((cssProp) => cssProp.type)
136
+ const matchUtilitiesType = ['any']
137
+ const matchUtilitiesValues: Record<string, string> = {
138
+ inherit: 'inherit',
139
+ }
140
+ cssProps.forEach((cssProp) => {
141
+ ;[...(cssProp.values ?? []), ...(cssProp.suggestedValues ?? [])].forEach((value) => {
142
+ matchUtilitiesValues[String(value)] = getCSSPropRawValue(cssProp, value)
143
+ })
144
+ })
145
+ if (cssPropTypes.includes('number') && !cssPropTypes.includes('length')) {
146
+ const bareValueFunction: BareValueFunction = (value) => {
147
+ const numberValue = parseBareValueNumber(value.value)
148
+ if (numberValue === null) return undefined
149
+ return String(numberValue)
150
+ }
151
+ matchUtilitiesValues['__BARE_VALUE__'] = bareValueFunction as unknown as string
152
+ }
153
+ if (cssPropTypes.includes('length')) {
154
+ for (const [spacingKey, spacingValue] of Object.entries(
155
+ api.theme('spacing') as Record<string, string>,
156
+ )) {
157
+ matchUtilitiesValues[spacingKey] =
158
+ parseNumber(spacingKey, { strict: true }) !== null
159
+ ? `--spacing(${spacingKey})`
160
+ : spacingValue
161
+ }
162
+ const bareValueFunction: BareValueFunction = (value) => {
163
+ const numberValue = parseBareValueNumber(value.value)
164
+ if (numberValue === null) return undefined
165
+ return `--spacing(${numberValue})`
166
+ }
167
+ matchUtilitiesValues['__BARE_VALUE__'] = bareValueFunction as unknown as string
168
+ }
169
+ if (cssPropTypes.includes('color')) {
170
+ matchUtilitiesType.push('color') // Allows using opacity modifiers
171
+ for (const colorKey in api.theme('colors') as Record<string, string>) {
172
+ matchUtilitiesValues[colorKey] = getCSSPropRawValue(
173
+ { type: 'color', defaultValue: 'current' },
174
+ colorKey as CSSPropColor,
175
+ )
176
+ }
177
+ }
178
+ api.matchUtilities(
179
+ Object.fromEntries(
180
+ (['prop', 'default'] as const).map((utilityType) => [
181
+ `${utilityType}-${cssPropKebabName}`,
182
+ (value) => ({
183
+ [`--gds-${utilityType}-${cssPropKebabName}`]: value,
184
+ }),
185
+ ]),
186
+ ),
187
+ {
188
+ type: matchUtilitiesType,
189
+ values: matchUtilitiesValues,
190
+ },
191
+ )
192
+ }
193
+
194
+ /**
195
+ * Register `@prop-{prop}-{value}/{component}` and `@prop-not-{prop}-{value}/{component}` variants
196
+ * for props of type `values`, for components to use internally.
197
+ */
198
+ for (const variant of ['@prop-not', '@prop'] as const) {
199
+ api.matchVariant(
200
+ variant,
201
+ (cssPropKebabNameAndValue, { modifier: componentKebabName }) => {
202
+ /**
203
+ * If the modifier is explicitly `dummy`, consider the variant invalid by returning an empty
204
+ * selector list (no CSS will be generated for it).
205
+ */
206
+ if (componentKebabName === 'dummy') return []
207
+ /**
208
+ * Default a missing modifier to `dummy` instead of considering the variant invalid so that
209
+ * it's suggested by Tailwind IntelliSense.
210
+ */
211
+ const component = getRegisteredComponent(componentKebabName ?? 'dummy')
212
+ /**
213
+ * If no component is found (i.e. the modifier is not a valid component name), consider the
214
+ * variant invalid.
215
+ */
216
+ if (!component) return []
217
+ try {
218
+ const { cssPropKebabName, value } = JSON.parse(cssPropKebabNameAndValue)
219
+ if (
220
+ typeof cssPropKebabName !== 'string' ||
221
+ (typeof value !== 'string' && typeof value !== 'number' && typeof value !== 'boolean')
222
+ )
223
+ return []
224
+ const cssProp = component.findCSSPropByKebabName(cssPropKebabName)
225
+ /**
226
+ * Only consider the variant valid if the prop exists on the component and is of type
227
+ * `values`, and the value is valid for it (unless the component is `dummy`, which is used
228
+ * to list all variants in IntelliSense regardless of the component).
229
+ */
230
+ if (
231
+ (!cssProp || cssProp.type !== 'values' || !cssProp.values.includes(value)) &&
232
+ component.kebabName !== 'dummy'
233
+ ) {
234
+ return []
235
+ }
236
+ const rawValue = cssProp ? getCSSPropRawValue(cssProp, value) : String(value)
237
+ return [
238
+ /** Style query selector for modern browsers. */
239
+ `@container ${component.containerName} ${variant === '@prop-not' ? 'not' : ''} style(--gds-${component.kebabName}-${cssPropKebabName}: ${rawValue})`,
240
+ /**
241
+ * Fallback selector for browsers without style query support, which unfortunately
242
+ * cannot be queried, but happens to be the same browsers that don't support view
243
+ * transitions (except for Firefox which introduced support for view transitions in
244
+ * version 144 but not style queries). Note that we don't want the polyfill rules to be
245
+ * applied when style queries are supported to prevent bugs on the first render, where
246
+ * polyfill attributes are present even when the polyfill is not needed (see
247
+ * `useCSSPropsPolyfill` for an explanation why).
248
+ */
249
+ `@supports (not (view-transition-name: none)) or (-moz-orient: inline) { &:where(.${component.className}${variant === '@prop-not' ? ':not' : ':is'}([data-gds-prop-polyfill-${cssPropKebabName}=${cssesc(rawValue, { isIdentifier: true })}]) *) }`,
250
+ ]
251
+ } catch {
252
+ /**
253
+ * If the value is invalid JSON (e.g. `@prop-[some-arbitrary-string]`), consider the
254
+ * variant invalid.
255
+ */
256
+ return []
257
+ }
258
+ },
259
+ {
260
+ values: Object.fromEntries(
261
+ [...componentsByCSSProp.entries()].flatMap(([cssPropKebabName, components]) => {
262
+ const cssPropValues = [...components.values()].flatMap(
263
+ (component) => component.findCSSPropByKebabName(cssPropKebabName)?.values ?? [],
264
+ )
265
+ return cssPropValues.map((value) => [
266
+ `${cssPropKebabName}-${value}`,
267
+ JSON.stringify({ cssPropKebabName, value }),
268
+ ])
269
+ }),
270
+ ),
271
+ },
272
+ )
273
+ }
274
+ }
@@ -0,0 +1,35 @@
1
+ import type { GDSColor } from '../design-tokens.generated.ts'
2
+
3
+ type CSSUnit = 'px' | 'em' | 'rem' | 'lh' | 'rlh' | '%' | 'vw' | 'vh' | 'vmin' | 'vmax'
4
+ type CSSLength = number | `${number}${CSSUnit}`
5
+
6
+ export type CSSPropValue = string | number | boolean
7
+ export type CSSPropString = string
8
+ export type CSSPropNumber = number
9
+ export type CSSPropLength = CSSLength
10
+ export type CSSPropColor = GDSColor | `${GDSColor}/${number}`
11
+
12
+ type Type = 'values' | 'string' | 'number' | 'length' | 'color'
13
+ type TypeToValue<T extends Type, V extends CSSPropValue = CSSPropValue> = T extends 'values'
14
+ ? V
15
+ : T extends 'string'
16
+ ? CSSPropString
17
+ : T extends 'number'
18
+ ? CSSPropNumber
19
+ : T extends 'length'
20
+ ? CSSPropLength
21
+ : CSSPropColor
22
+
23
+ export type CSSPropDefinition<
24
+ T extends Type = Type,
25
+ V extends CSSPropValue[] = CSSPropValue[],
26
+ D extends TypeToValue<T, V[number]> = TypeToValue<T, V[number]>,
27
+ > = T extends 'values'
28
+ ? { type: T; values: V; suggestedValues?: never; defaultValue?: D & TypeToValue<T, V[number]> }
29
+ : T extends 'number'
30
+ ? { type: T; values?: never; suggestedValues?: number[]; defaultValue?: D & TypeToValue<T> }
31
+ : { type: T; values?: never; suggestedValues?: never; defaultValue?: D & TypeToValue<T> }
32
+
33
+ export type CSSPropType<P extends CSSPropDefinition> = P['values'] extends CSSPropValue[]
34
+ ? TypeToValue<P['type'], P['values'][number]>
35
+ : TypeToValue<P['type']>
@@ -0,0 +1,2 @@
1
+ export { cssStates, cssStateVariables, type CSSState, type CSSStateValue } from './states.ts'
2
+ export { registerCSSStates } from './registerCSSStates.ts'