@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
|
@@ -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,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']>
|