@graphprotocol/gds-css 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/dist/component-registry.d.ts +112 -40
  2. package/dist/component-registry.d.ts.map +1 -1
  3. package/dist/component-registry.js +42 -7
  4. package/dist/component-registry.js.map +1 -1
  5. package/dist/css-props/index.d.ts +1 -1
  6. package/dist/css-props/index.d.ts.map +1 -1
  7. package/dist/css-props/index.js +1 -1
  8. package/dist/css-props/index.js.map +1 -1
  9. package/dist/css-props/parseCSSPropValue.js +1 -1
  10. package/dist/css-props/parseCSSPropValue.js.map +1 -1
  11. package/dist/css-props/setupCSSProps.d.ts +7 -0
  12. package/dist/css-props/setupCSSProps.d.ts.map +1 -0
  13. package/dist/css-props/{registerCSSProps.js → setupCSSProps.js} +30 -42
  14. package/dist/css-props/setupCSSProps.js.map +1 -0
  15. package/dist/css-states/index.d.ts +1 -1
  16. package/dist/css-states/index.d.ts.map +1 -1
  17. package/dist/css-states/index.js +1 -1
  18. package/dist/css-states/index.js.map +1 -1
  19. package/dist/css-states/setupCSSStates.d.ts +20 -0
  20. package/dist/css-states/setupCSSStates.d.ts.map +1 -0
  21. package/dist/css-states/{registerCSSStates.js → setupCSSStates.js} +21 -18
  22. package/dist/css-states/setupCSSStates.js.map +1 -0
  23. package/dist/css-states/states.d.ts +1 -0
  24. package/dist/css-states/states.d.ts.map +1 -1
  25. package/dist/css-states/states.js +2 -1
  26. package/dist/css-states/states.js.map +1 -1
  27. package/dist/design-tokens.generated.d.ts +44 -24
  28. package/dist/design-tokens.generated.d.ts.map +1 -1
  29. package/dist/design-tokens.generated.js +82 -10
  30. package/dist/design-tokens.generated.js.map +1 -1
  31. package/dist/tailwind-customizations/index.d.ts +2 -2
  32. package/dist/tailwind-customizations/index.d.ts.map +1 -1
  33. package/dist/tailwind-customizations/index.js +2 -2
  34. package/dist/tailwind-customizations/index.js.map +1 -1
  35. package/dist/tailwind-customizations/setupComponents.d.ts +8 -0
  36. package/dist/tailwind-customizations/setupComponents.d.ts.map +1 -0
  37. package/dist/tailwind-customizations/setupComponents.js +75 -0
  38. package/dist/tailwind-customizations/setupComponents.js.map +1 -0
  39. package/dist/tailwind-customizations/setupVariants.d.ts +8 -0
  40. package/dist/tailwind-customizations/setupVariants.d.ts.map +1 -0
  41. package/dist/tailwind-customizations/{registerVariants.js → setupVariants.js} +7 -7
  42. package/dist/tailwind-customizations/setupVariants.js.map +1 -0
  43. package/dist/tailwind-customizations/variants.d.ts.map +1 -1
  44. package/dist/tailwind-customizations/variants.js +2 -0
  45. package/dist/tailwind-customizations/variants.js.map +1 -1
  46. package/dist/tailwind-plugin.js +7 -7
  47. package/dist/tailwind-plugin.js.map +1 -1
  48. package/dist/utils/cssUnescape.d.ts +5 -3
  49. package/dist/utils/cssUnescape.d.ts.map +1 -1
  50. package/dist/utils/cssUnescape.js +5 -3
  51. package/dist/utils/cssUnescape.js.map +1 -1
  52. package/package.json +5 -5
  53. package/src/component-registry.ts +205 -71
  54. package/src/css-props/index.ts +1 -1
  55. package/src/css-props/parseCSSPropValue.ts +1 -1
  56. package/src/css-props/{registerCSSProps.ts → setupCSSProps.ts} +31 -46
  57. package/src/css-states/index.ts +1 -1
  58. package/src/css-states/{registerCSSStates.ts → setupCSSStates.ts} +20 -17
  59. package/src/css-states/states.ts +2 -1
  60. package/src/design-tokens.generated.ts +82 -10
  61. package/src/tailwind-customizations/index.ts +2 -2
  62. package/src/tailwind-customizations/setupComponents.ts +87 -0
  63. package/src/tailwind-customizations/{registerVariants.ts → setupVariants.ts} +6 -6
  64. package/src/tailwind-customizations/variants.ts +11 -15
  65. package/src/tailwind-plugin.ts +7 -7
  66. package/src/utils/cssUnescape.ts +5 -3
  67. package/styles/global.css +4 -3
  68. package/styles/theme.css +16 -12
  69. package/styles/typography.css +27 -14
  70. package/styles/utilities.css +9 -0
  71. package/dist/css-props/registerCSSProps.d.ts +0 -7
  72. package/dist/css-props/registerCSSProps.d.ts.map +0 -1
  73. package/dist/css-props/registerCSSProps.js.map +0 -1
  74. package/dist/css-states/registerCSSStates.d.ts +0 -23
  75. package/dist/css-states/registerCSSStates.d.ts.map +0 -1
  76. package/dist/css-states/registerCSSStates.js.map +0 -1
  77. package/dist/tailwind-customizations/registerUtilities.d.ts +0 -9
  78. package/dist/tailwind-customizations/registerUtilities.d.ts.map +0 -1
  79. package/dist/tailwind-customizations/registerUtilities.js +0 -59
  80. package/dist/tailwind-customizations/registerUtilities.js.map +0 -1
  81. package/dist/tailwind-customizations/registerVariants.d.ts +0 -8
  82. package/dist/tailwind-customizations/registerVariants.d.ts.map +0 -1
  83. package/dist/tailwind-customizations/registerVariants.js.map +0 -1
  84. package/src/tailwind-customizations/registerUtilities.ts +0 -65
@@ -1,5 +1,3 @@
1
- import { objectEntries } from 'ts-extras'
2
-
3
1
  import { camelToKebab, type DeepRecord } from '@graphprotocol/gds-utils'
4
2
 
5
3
  import type {
@@ -14,45 +12,85 @@ import type {
14
12
 
15
13
  export type CSSPropsConfig = Record<string, CSSPropDefinition>
16
14
 
17
- type CSSProp<T extends CSSPropDefinition = CSSPropDefinition> = T & {
15
+ type CSSProp<Def extends CSSPropDefinition = CSSPropDefinition> = Def & {
18
16
  readonly name: string
19
17
  readonly kebabName: string
20
- readonly initialValue: CSSPropType<T>
18
+ /** The container name to use in style queries for this prop. */
19
+ readonly containerName: string
20
+ /** The CSS custom property that holds this prop's value (e.g. `--gds-button-size`). */
21
+ readonly cssProperty: string
22
+ /** The initial value of this prop's CSS custom property. */
23
+ readonly initialValue: CSSPropType<Def>
24
+ /** Whether this prop is defined or overridden in this component (not inherited via `extends`). */
25
+ readonly own: boolean
26
+ /** The data attribute used by `useCSSPropsPolyfill` (e.g. `data-gds-prop-polyfill-size`). */
27
+ readonly polyfillAttribute: string
21
28
  }
22
29
 
23
- type CSSProps<P extends CSSPropsConfig> = {
24
- readonly [K in keyof P]: P[K] & CSSProp<P[K]>
30
+ type CSSProps<Props extends CSSPropsConfig> = {
31
+ readonly [K in keyof Props]: Props[K] & CSSProp<Props[K]>
25
32
  }
26
33
 
27
- type VarsConfig = Record<string, VarCSS>
34
+ type IsolateOption = true | 'allow-inheritance' | false
35
+
28
36
  type VarCSS = string | DeepRecord<string>
37
+ type VarsConfig = Record<string, VarCSS>
29
38
 
30
- export class GDSComponent<P extends CSSPropsConfig = CSSPropsConfig, A extends boolean = boolean> {
39
+ export class GDSComponent<
40
+ Props extends CSSPropsConfig = CSSPropsConfig,
41
+ AddonCompatible extends boolean = boolean,
42
+ > {
31
43
  readonly name: string
32
44
  readonly kebabName: string
33
45
  readonly className: string
34
- readonly containerName: string | null
35
- readonly addonCompatible: A
36
- readonly cssProps: CSSProps<P>
46
+ readonly containerName: string
47
+ readonly extends: GDSComponent | undefined
48
+ readonly isolate: IsolateOption
49
+ readonly addonCompatible: AddonCompatible
50
+ readonly cssProps: CSSProps<Props>
37
51
  readonly vars: VarsConfig | undefined
38
52
 
39
53
  constructor(
40
54
  name: string,
41
55
  config?: {
42
- containerName?: string | null | undefined
43
- addonCompatible?: A | undefined
44
- cssProps?: P | undefined
56
+ extends?: GDSComponent | undefined
57
+ isolate?: IsolateOption | undefined
58
+ addonCompatible?: AddonCompatible | undefined
59
+ cssProps?: Props | undefined
45
60
  vars?: VarsConfig | undefined
46
61
  },
47
62
  ) {
48
63
  this.name = name
49
64
  this.kebabName = camelToKebab(name)
50
65
  this.className = `gds-${this.kebabName}`
51
- this.containerName = config?.containerName !== undefined ? config.containerName : this.className
52
- this.addonCompatible = (config?.addonCompatible ?? false) as A
66
+ this.containerName = `gds-${this.kebabName}`
67
+ this.extends = config?.extends
68
+ this.isolate = config?.isolate ?? (this.extends ? false : true)
69
+ this.addonCompatible = (config?.addonCompatible ?? false) as AddonCompatible
70
+ this.vars = config?.vars
71
+
72
+ // Merge `cssProps` from parent with own, allowing `null` to remove inherited props
73
+ const parentCSSProps = this.extends?.cssProps ?? {}
74
+ const ownCSSProps = (config?.cssProps ?? {}) as Props
75
+ const ownPropNames = new Set(Object.keys(ownCSSProps))
76
+ const mergedCSSProps = new Map<string, CSSPropDefinition>()
77
+ for (const [propName, prop] of Object.entries(parentCSSProps)) {
78
+ if (ownCSSProps[propName] !== null) {
79
+ mergedCSSProps.set(propName, prop)
80
+ }
81
+ }
82
+ for (const [propName, prop] of Object.entries(ownCSSProps)) {
83
+ if (prop !== null) {
84
+ mergedCSSProps.set(propName, prop)
85
+ }
86
+ }
87
+
53
88
  this.cssProps = Object.freeze(
54
89
  Object.fromEntries(
55
- objectEntries(config?.cssProps ?? ({} as P)).map(([propName, prop]) => {
90
+ [...mergedCSSProps.entries()].map(([propName, prop]) => {
91
+ const kebabName = camelToKebab(propName)
92
+ const own = ownPropNames.has(propName)
93
+ const parentProp = parentCSSProps[propName]
56
94
  const initialValue =
57
95
  prop.defaultValue ??
58
96
  (() => {
@@ -74,21 +112,35 @@ export class GDSComponent<P extends CSSPropsConfig = CSSPropsConfig, A extends b
74
112
  {
75
113
  ...prop,
76
114
  name: propName,
77
- kebabName: camelToKebab(propName),
115
+ kebabName,
116
+ // Inherited props keep their parent's `containerName` and `cssProperty`
117
+ containerName: (!own && parentProp?.containerName) || this.containerName,
118
+ cssProperty:
119
+ (!own && parentProp?.cssProperty) || `--gds-${this.kebabName}-${kebabName}`,
78
120
  initialValue,
121
+ own,
122
+ polyfillAttribute: `data-gds-prop-polyfill-${kebabName}`,
79
123
  },
80
124
  ]
81
125
  }),
82
126
  ),
83
- ) as CSSProps<P>
84
- this.vars = config?.vars
127
+ ) as unknown as CSSProps<Props>
85
128
  }
86
129
 
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}"`)
130
+ /** Returns the chain of container names from the `extends` hierarchy as an array. */
131
+ getContainerNameChain() {
132
+ const containerNames = [this.containerName]
133
+ let parent = this.extends
134
+ while (parent) {
135
+ containerNames.unshift(parent.containerName)
136
+ parent = parent.extends
91
137
  }
138
+ return containerNames
139
+ }
140
+
141
+ getCSSPropByName<Name extends keyof Props & string>(name: Name): CSSProps<Props>[Name] {
142
+ const prop = this.cssProps[name]
143
+ if (!prop) throw new Error(`Unknown CSS prop "${name}" for component "${this.name}"`)
92
144
  return prop
93
145
  }
94
146
 
@@ -102,81 +154,163 @@ export class GDSComponent<P extends CSSPropsConfig = CSSPropsConfig, A extends b
102
154
  }
103
155
  }
104
156
 
105
- /** Type to validate CSS prop definitions. */
106
- type ValidateCSSPropDefinition<P> = P extends {
157
+ /**
158
+ * Validates a CSS prop definition. Ensures:
159
+ *
160
+ * - `values` type props have at least one value.
161
+ * - `defaultValue` (if provided) is one of the `values`
162
+ */
163
+ type ValidateCSSPropDefinition<Def> = Def extends {
107
164
  type: 'values'
108
- values: readonly unknown[]
165
+ values: readonly [unknown, ...unknown[]]
166
+ defaultValue: infer Default
109
167
  }
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]>
168
+ ? Default extends Def['values'][number]
169
+ ? Def
170
+ : Omit<Def, 'defaultValue'> & { defaultValue: Def['values'][number] }
171
+ : Def extends { type: 'values'; values: readonly [] }
172
+ ? never
173
+ : Def
174
+
175
+ /** Validates all CSS props in an object. Allows `null` to remove inherited props. */
176
+ type ValidateCSSPropsConfig<Config extends Record<string, unknown>> = {
177
+ [K in keyof Config]: Config[K] extends null ? null : ValidateCSSPropDefinition<Config[K]>
178
+ }
179
+
180
+ /** Validates that `vars` names don't conflict with `cssProps` names. */
181
+ type ValidateVarsConfig<CSSPropsKeys, Vars extends VarsConfig> = {
182
+ [K in keyof Vars]: K extends CSSPropsKeys
183
+ ? `Var "${K & string}" conflicts with cssProps. Choose a different name.`
184
+ : Vars[K]
124
185
  }
125
186
 
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
187
+ /**
188
+ * Merges parent `cssProps` with own. Own props override parent props, `null` removes inherited
189
+ * props.
190
+ */
191
+ type MergeCSSProps<
192
+ ParentProps extends CSSPropsConfig | undefined,
193
+ OwnProps extends Record<string, CSSPropDefinition | null>,
194
+ > = ParentProps extends CSSPropsConfig
195
+ ? {
196
+ // Keys from parent that aren't overridden with `null`
197
+ [K in keyof ParentProps as K extends keyof OwnProps
198
+ ? OwnProps[K] extends null
199
+ ? never
200
+ : K
201
+ : K]: K extends keyof OwnProps
202
+ ? OwnProps[K] extends CSSPropDefinition
203
+ ? OwnProps[K]
204
+ : ParentProps[K]
205
+ : ParentProps[K]
206
+ } & {
207
+ // Keys from own that aren't in parent and aren't `null`
208
+ [K in keyof OwnProps as K extends keyof ParentProps
209
+ ? never
210
+ : OwnProps[K] extends null
211
+ ? never
212
+ : K]: OwnProps[K] extends CSSPropDefinition ? OwnProps[K] : never
213
+ }
129
214
  : {
130
- [K in keyof V]: K extends keyof P
131
- ? `Var "${K & string}" conflicts with cssProps. Choose a different name.`
132
- : V[K]
215
+ [K in keyof OwnProps as OwnProps[K] extends null
216
+ ? never
217
+ : K]: OwnProps[K] extends CSSPropDefinition ? OwnProps[K] : never
133
218
  }
134
219
 
220
+ /** Computed merged `cssProps` type for `createComponent`. */
221
+ type MergedCSSProps<
222
+ Extends extends GDSComponent | undefined,
223
+ OwnProps extends Record<string, CSSPropDefinition | null>,
224
+ > = MergeCSSProps<
225
+ Extends extends GDSComponent<infer ParentProps> ? ParentProps : undefined,
226
+ OwnProps
227
+ >
228
+
229
+ /**
230
+ * Empty object type without index signature (unlike `Record<string, never>` which has `keyof
231
+ * Record<string, never>` = `string`, not `never`).
232
+ */
233
+ type EmptyObject = { [K in never]: never }
234
+
135
235
  /** Helper function to create a `GDSComponent` instance with proper type inference. */
136
236
  export function createComponent<
137
- const P extends CSSPropsConfig,
138
- const V extends VarsConfig,
139
- const A extends boolean,
237
+ const Extends extends GDSComponent | undefined = undefined,
238
+ const OwnCSSProps extends Record<string, CSSPropDefinition | null> = EmptyObject,
239
+ const Vars extends VarsConfig = EmptyObject,
240
+ const AddonCompatible extends boolean = false,
140
241
  >(
141
242
  name: string,
142
- config?: ConstructorParameters<typeof GDSComponent<P>>[1] & {
143
- addonCompatible?: A
144
- cssProps?: ValidateCSSPropsConfig<P>
145
- vars?: ValidateVarsConfig<P, V>
243
+ config?: {
244
+ /**
245
+ * The parent component that this component extends. When set:
246
+ *
247
+ * - The component inherits all parent container names for CSS container queries (e.g. if
248
+ * `Checkbox` extends `Checkable`, the following CSS will be generated: `.gds-checkbox {
249
+ * container-name: gds-checkable gds-checkbox; }`).
250
+ * - The component inherits all parent `cssProps`, which can be overridden or removed (with
251
+ * `null`).
252
+ * - The `isolate` option defaults to `false` since the parent already handles isolation (if not,
253
+ * it can be set to `true` or `'allow-inheritance'`, but `isolate: false` will not cancel/undo
254
+ * the parent's isolation).
255
+ */
256
+ extends?: Extends
257
+ /**
258
+ * - `true`: add `isolation: isolate` and reset all inherited font/text properties.
259
+ * - `'allow-inheritance'`: only add `isolation: isolate` (keep inheriting font/text properties).
260
+ * - `false`: do nothing.
261
+ *
262
+ * @default true // or if `extends` is set, `false`
263
+ */
264
+ isolate?: IsolateOption
265
+ /**
266
+ * Whether the component can be used as an addon (e.g. `<Button addonBefore={Component}>`).
267
+ * Setting this to `true` also includes this component's class in the selector generated by the
268
+ * `addon-compatible` custom variant.
269
+ *
270
+ * @default false
271
+ */
272
+ addonCompatible?: AddonCompatible
273
+ cssProps?: ValidateCSSPropsConfig<OwnCSSProps>
274
+ vars?: ValidateVarsConfig<keyof MergedCSSProps<Extends, OwnCSSProps>, Vars>
146
275
  },
147
- ): GDSComponent<P, A> {
148
- return new GDSComponent(name, config)
276
+ ) {
277
+ return new GDSComponent(
278
+ name,
279
+ config as ConstructorParameters<typeof GDSComponent>[1],
280
+ ) as unknown as GDSComponent<MergedCSSProps<Extends, OwnCSSProps>, AddonCompatible>
149
281
  }
150
282
 
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
283
+ /** Extracts the CSS props config type from a `GDSComponent` instance. */
284
+ export type ComponentCSSPropsConfig<Component extends GDSComponent> =
285
+ Component extends GDSComponent<infer Props> ? Props : never
154
286
 
155
287
  /**
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`.
288
+ * Extracts CSS props types from a component for use in React props. Props without a `defaultValue`
289
+ * are required, those with a `defaultValue` are optional. Explicitly includes `undefined` in the
290
+ * type to work with `exactOptionalPropertyTypes`.
159
291
  */
160
- export type ComponentCSSProps<C extends GDSComponent> = {
161
- [K in keyof ComponentCSSPropsConfig<C> as string extends K
292
+ export type ComponentCSSProps<Component extends GDSComponent> = {
293
+ // Required props (no `defaultValue`)
294
+ [K in keyof ComponentCSSPropsConfig<Component> as string extends K
162
295
  ? never
163
- : 'defaultValue' extends keyof ComponentCSSPropsConfig<C>[K]
296
+ : 'defaultValue' extends keyof ComponentCSSPropsConfig<Component>[K]
164
297
  ? never
165
- : K]: CSSPropType<ComponentCSSPropsConfig<C>[K]>
298
+ : K]: CSSPropType<ComponentCSSPropsConfig<Component>[K]>
166
299
  } & {
167
- [K in keyof ComponentCSSPropsConfig<C> as string extends K
300
+ // Optional props (have `defaultValue`)
301
+ [K in keyof ComponentCSSPropsConfig<Component> as string extends K
168
302
  ? never
169
- : 'defaultValue' extends keyof ComponentCSSPropsConfig<C>[K]
303
+ : 'defaultValue' extends keyof ComponentCSSPropsConfig<Component>[K]
170
304
  ? K
171
- : never]?: CSSPropType<ComponentCSSPropsConfig<C>[K]> | undefined
305
+ : never]?: CSSPropType<ComponentCSSPropsConfig<Component>[K]> | undefined
172
306
  }
173
307
 
174
308
  /**
175
309
  * Combined props type for GDS components. Includes CSS props and, for addon-compatible components,
176
310
  * a hidden prop to mark them as such.
177
311
  */
178
- export type GDSComponentProps<C extends GDSComponent> = ComponentCSSProps<C> &
179
- (C extends GDSComponent<CSSPropsConfig, true>
312
+ export type GDSComponentProps<Component extends GDSComponent> = ComponentCSSProps<Component> &
313
+ (Component extends GDSComponent<CSSPropsConfig, true>
180
314
  ? { __addonCompatible?: undefined }
181
315
  : { __addonCompatible?: never })
182
316
 
@@ -1,4 +1,4 @@
1
1
  export * from './types.ts'
2
2
  export { getCSSPropRawValue } from './getCSSPropRawValue.ts'
3
3
  export { parseCSSPropValue } from './parseCSSPropValue.ts'
4
- export { registerCSSProps } from './registerCSSProps.ts'
4
+ export { setupCSSProps } from './setupCSSProps.ts'
@@ -47,7 +47,7 @@ export function parseCSSPropValue<P extends CSSPropDefinition>(
47
47
  const numberValue = parseNumber(rawValue.slice(0, -2)) // Remove 'px'
48
48
  return pxToTw(numberValue ?? 0) satisfies CSSPropLength as CSSPropType<P>
49
49
  }
50
- // Otherwise keep as-is (e.g., "1rem", "100%")
50
+ // Otherwise keep as-is (e.g. '1rem' or '100%')
51
51
  return rawValue as CSSPropLength as CSSPropType<P>
52
52
  }
53
53
  case 'color': {
@@ -12,10 +12,10 @@ import { getCSSPropRawValue } from './getCSSPropRawValue.ts'
12
12
  import type { CSSPropColor } from './types.ts'
13
13
 
14
14
  /**
15
- * Registers CSS Props functionality in the Tailwind plugin. This includes custom properties,
16
- * utilities, and variants for prop-based styling.
15
+ * Sets up the CSS props system via the Tailwind plugin. This includes custom properties, utilities,
16
+ * and variants for prop-based styling.
17
17
  */
18
- export function registerCSSProps(api: PluginAPI) {
18
+ export function setupCSSProps(api: PluginAPI) {
19
19
  const componentsByCSSProp = getRegisteredComponents(true).reduce((outerMap, component) => {
20
20
  for (const cssProp of Object.values(component.cssProps)) {
21
21
  const innerMap =
@@ -46,9 +46,13 @@ export function registerCSSProps(api: PluginAPI) {
46
46
  })
47
47
  }
48
48
 
49
- /** Register a custom property for each component-prop pair to restrict the allowed values. */
50
49
  for (const component of getRegisteredComponents()) {
51
- for (const cssProp of Object.values(component.cssProps)) {
50
+ // Only process own props (not inherited via `extends`); inherited props are handled by the parent component
51
+ const ownCSSProps = Object.values(component.cssProps).filter((cssProp) => cssProp.own)
52
+ if (ownCSSProps.length === 0) continue
53
+
54
+ /** Register a custom property for each component-prop pair to restrict the allowed values. */
55
+ for (const cssProp of ownCSSProps) {
52
56
  const syntax = (() => {
53
57
  switch (cssProp.type) {
54
58
  case 'values':
@@ -66,7 +70,7 @@ export function registerCSSProps(api: PluginAPI) {
66
70
  }
67
71
  })()
68
72
  api.addBase({
69
- [`@property --gds-${component.kebabName}-${cssProp.kebabName}`]: {
73
+ [`@property ${cssProp.cssProperty}`]: {
70
74
  syntax,
71
75
  /**
72
76
  * We only need this property to inherit if the type is not `values`, because `values` CSS
@@ -89,26 +93,20 @@ export function registerCSSProps(api: PluginAPI) {
89
93
  }
90
94
 
91
95
  /**
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.
96
+ * Add base styles for each component-prop pair, defining the default value of each CSS prop and
97
+ * applying the value passed via the React prop, if any. See `getCSSPropRawValue` for details on
98
+ * how each prop type is handled.
95
99
  */
96
100
  api.addBase({
97
101
  '@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
- },
102
+ [`.${component.className}`]: ownCSSProps.reduce<Record<string, string>>(
103
+ (rules, cssProp) => ({
104
+ ...rules,
105
+ [`--gds-passed-or-default-${cssProp.kebabName}`]: `var(--gds-passed-${cssProp.kebabName}, var(--gds-default-${cssProp.kebabName}, ${getCSSPropRawValue(cssProp, cssProp.initialValue)}))`,
106
+ [cssProp.cssProperty]: `var(--gds-prop-${cssProp.kebabName}, var(--gds-passed-or-default-${cssProp.kebabName}))`,
107
+ }),
108
+ {},
109
+ ),
112
110
  },
113
111
  })
114
112
  }
@@ -200,19 +198,12 @@ export function registerCSSProps(api: PluginAPI) {
200
198
  variant,
201
199
  (cssPropKebabNameAndValue, { modifier: componentKebabName }) => {
202
200
  /**
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.
201
+ * If the modifier is missing, return some dummy CSS so that the variant is suggested by
202
+ * Tailwind IntelliSense. If a modifier is present but is not a valid component name, then
203
+ * return an empty array (no CSS is generated and the variant is considered invalid).
215
204
  */
205
+ if (componentKebabName === null) return '@container dummy'
206
+ const component = getRegisteredComponent(componentKebabName)
216
207
  if (!component) return []
217
208
  try {
218
209
  const { cssPropKebabName, value } = JSON.parse(cssPropKebabNameAndValue)
@@ -223,20 +214,14 @@ export function registerCSSProps(api: PluginAPI) {
223
214
  return []
224
215
  const cssProp = component.findCSSPropByKebabName(cssPropKebabName)
225
216
  /**
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).
217
+ * If no such prop exists on the component, or if one exists but is not of type `values`
218
+ * or doesn't support the specified value, consider the variant invalid.
229
219
  */
230
- if (
231
- (!cssProp || cssProp.type !== 'values' || !cssProp.values.includes(value)) &&
232
- component.kebabName !== 'dummy'
233
- ) {
234
- return []
235
- }
220
+ if (!cssProp || cssProp.type !== 'values' || !cssProp.values.includes(value)) return []
236
221
  const rawValue = cssProp ? getCSSPropRawValue(cssProp, value) : String(value)
237
222
  return [
238
223
  /** Style query selector for modern browsers. */
239
- `@container ${component.containerName} ${variant === '@prop-not' ? 'not' : ''} style(--gds-${component.kebabName}-${cssPropKebabName}: ${rawValue})`,
224
+ `@container ${cssProp.containerName} ${variant === '@prop-not' ? 'not' : ''} style(${cssProp.cssProperty}: ${rawValue})`,
240
225
  /**
241
226
  * Fallback selector for browsers without style query support, which unfortunately
242
227
  * cannot be queried, but happens to be the same browsers that don't support view
@@ -246,7 +231,7 @@ export function registerCSSProps(api: PluginAPI) {
246
231
  * polyfill attributes are present even when the polyfill is not needed (see
247
232
  * `useCSSPropsPolyfill` for an explanation why).
248
233
  */
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 })}]) *) }`,
234
+ `@supports (not (view-transition-name: none)) or (-moz-orient: inline) { &:where(.${component.className}${variant === '@prop-not' ? ':not' : ':is'}([${cssProp.polyfillAttribute}=${cssesc(rawValue, { isIdentifier: true })}]) *) }`,
250
235
  ]
251
236
  } catch {
252
237
  /**
@@ -1,2 +1,2 @@
1
1
  export { cssStates, cssStateVariables, type CSSState, type CSSStateValue } from './states.ts'
2
- export { registerCSSStates } from './registerCSSStates.ts'
2
+ export { setupCSSStates } from './setupCSSStates.ts'
@@ -5,9 +5,8 @@ import type { PluginAPI } from '../types.ts'
5
5
  import { cssStates, cssStateVariables, getCSSStateVariable } from './states.ts'
6
6
 
7
7
  /**
8
- * Registers CSS States functionality in the Tailwind plugin.
9
- *
10
- * This system abstracts away CSS's native states in order to:
8
+ * Sets up the CSS states system via the Tailwind plugin. This system abstracts away CSS's native
9
+ * states in order to:
11
10
  *
12
11
  * - Allow overriding the state of components (e.g. `<Button className="state-active" />`).
13
12
  * - Allow reading the state of components from their root element even when the native state is
@@ -18,24 +17,24 @@ import { cssStates, cssStateVariables, getCSSStateVariable } from './states.ts'
18
17
  * `@state-[highlighted]:bg-strong` on a child; also supports key-values like `state-[foo=bar]
19
18
  * lg:state-[foo=baz]`).
20
19
  *
21
- * While this system is separate from the CSS Props one, it also leverages style queries and relies
22
- * on the fact that component classes (e.g. `gds-button`) establish a container that can be queried.
23
- * Furthermore, it uses some of the same techniques in the fallback implementation (querying by data
24
- * attribute, `view-transition-name` to target browsers that don't support style queries, etc.),
25
- * though the CSS props polyfill is not used. See `useCSSState` for more details.
20
+ * Like CSS props, this system leverages style queries and uses a similar fallback mechanism
21
+ * (querying by data attribute + using `view-transition-name` to target browsers that don't support
22
+ * style queries), though the CSS props polyfill is not used. See `useCSSState` for more details.
26
23
  */
27
- export function registerCSSStates(api: PluginAPI) {
24
+ export function setupCSSStates(api: PluginAPI) {
28
25
  /**
29
- * Register a couple custom properties for each state variable.
26
+ * Register a couple custom properties for each non-arbitrary state variable.
30
27
  *
31
28
  * - All of them are non-inherited to prevent leaking.
32
29
  * - Normally, we would only need `--gds-exposed-*` and `--gds-state-*` (with no `initial-value` on
33
30
  * `--gds-exposed-*`), but due to a limitation in the `style-observer` library
34
31
  * (https://github.com/LeaVerou/style-observer/issues/133), we need to set an `initial-value` on
35
- * any custom property we want to observe in JS. But we also need `--gds-state-*` to NOT have an
36
- * initial value so that we can define a fallback when a state is not set at all (see
37
- * `--gds-clickable-*`). So we register an additional `--gds-observed-*` property that is
38
- * guaranteed to have a valid value and can be observed.
32
+ * any custom property we want to observe in JS (and we want to observe both in `useCSSState`).
33
+ * But we also need `--gds-state-*` to NOT have an initial value so that we can define a
34
+ * fallback when a state is not set at all (see `--gds-clickable-*`). So we register an
35
+ * additional `--gds-observed-*` property that is guaranteed to have a valid value and can be
36
+ * observed. Note that non-registered `--gds-state-*` properties (i.e. arbitrary states) don't
37
+ * have this problem, so arbitrary state utilities don't need to set `--gds-observed-*`.
39
38
  */
40
39
  for (const [variableName, { values, defaultValue }] of Object.entries(cssStateVariables)) {
41
40
  api.addBase({
@@ -69,14 +68,18 @@ export function registerCSSStates(api: PluginAPI) {
69
68
  return {
70
69
  [`--gds-exposed-${stateVariable.name}`]: stateVariable.value,
71
70
  [`--gds-state-${stateVariable.name}`]: stateVariable.value,
72
- [`--gds-observed-${stateVariable.name}`]: stateVariable.value,
71
+ ...(!stateVariable.isArbitrary && {
72
+ [`--gds-observed-${stateVariable.name}`]: stateVariable.value,
73
+ }),
73
74
  }
74
75
  },
75
76
  state: (state) => {
76
77
  const stateVariable = getCSSStateVariable(state)
77
78
  return {
78
79
  [`--gds-state-${stateVariable.name}`]: stateVariable.value,
79
- [`--gds-observed-${stateVariable.name}`]: stateVariable.value,
80
+ ...(!stateVariable.isArbitrary && {
81
+ [`--gds-observed-${stateVariable.name}`]: stateVariable.value,
82
+ }),
80
83
  }
81
84
  },
82
85
  },
@@ -108,7 +111,7 @@ export function registerCSSStates(api: PluginAPI) {
108
111
  .join(' or ')})`,
109
112
  ]
110
113
  if (containerName) {
111
- // Fallback selector for browsers without style query support; see `css-props/registerCSSProps.ts` for more details
114
+ // Fallback selector for browsers without style query support; see `setupCSSProps` for more details
112
115
  selectors.push(
113
116
  `@supports (not (view-transition-name: none)) or (-moz-orient: inline) { &:where(.${containerName}${variant === '@state-not' ? ':not' : ':is'}(${stateVariables
114
117
  .map(
@@ -101,13 +101,14 @@ export function getCSSStateVariable(state: string) {
101
101
  name: variableName,
102
102
  value: String(value),
103
103
  isDefault: value === defaultValue,
104
+ isArbitrary: false,
104
105
  }
105
106
  }
106
107
  }
107
108
  }
108
109
  // Support arbitrary states in the format `state-[foo]` or `state-[foo=bar]`
109
110
  const [stateName, stateValue] = state.split('=')
110
- return { name: stateName!, value: stateValue ?? 'true', isDefault: false }
111
+ return { name: stateName!, value: stateValue ?? 'true', isDefault: false, isArbitrary: true }
111
112
  }
112
113
 
113
114
  export function getPointerStateSelector(state: 'idle' | 'hover' | 'active') {