@teleporthq/teleport-plugin-css 0.44.0-alpha.0 → 0.44.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.
package/src/index.ts CHANGED
@@ -1,9 +1,18 @@
1
- import { StringUtils, UIDLUtils } from '@teleporthq/teleport-shared'
2
- import { StyleUtils, StyleBuilders, HASTUtils, ASTUtils } from '@teleporthq/teleport-plugin-common'
1
+ import { join } from 'path'
2
+ import { StringUtils, UIDLUtils, GenericUtils } from '@teleporthq/teleport-shared'
3
+ import {
4
+ StyleUtils,
5
+ StyleBuilders,
6
+ HASTUtils,
7
+ HASTBuilders,
8
+ ASTUtils,
9
+ createBinaryExpression,
10
+ } from '@teleporthq/teleport-plugin-common'
3
11
  import * as types from '@babel/types'
4
12
  import {
5
13
  ComponentPluginFactory,
6
14
  ComponentPlugin,
15
+ UIDLConditionExpressionEntry,
7
16
  UIDLDynamicReference,
8
17
  UIDLStyleDefinitions,
9
18
  ChunkType,
@@ -12,8 +21,17 @@ import {
12
21
  UIDLElementNodeReferenceStyles,
13
22
  UIDLStyleMediaQueryScreenSizeCondition,
14
23
  PluginCSS,
24
+ UIDLElement,
25
+ UIDLElementNode,
26
+ UIDLExpressionValue,
27
+ StateDefaultValueTypes,
28
+ PropDefaultValueTypes,
29
+ UIDLStyleInlineAsset,
30
+ UIDLFontAsset,
31
+ UIDLGlobalStateDefinition,
15
32
  } from '@teleporthq/teleport-types'
16
33
  import { createStyleSheetPlugin } from './style-sheet'
34
+ import { createConditionalStatement } from './utils'
17
35
 
18
36
  interface CSSPluginConfig {
19
37
  chunkName: string
@@ -21,11 +39,38 @@ interface CSSPluginConfig {
21
39
  componentDecoratorChunkName: string
22
40
  inlineStyleAttributeKey: string // style vs :style vs ...
23
41
  classAttributeName: string // class vs className
24
- forceScoping: boolean // class names get the component name prefix
25
42
  templateStyle: 'html' | 'jsx'
26
43
  declareDependency: 'import' | 'decorator' | 'none'
27
44
  dynamicVariantPrefix?: string
28
45
  staticPropReferences?: boolean
46
+ standaloneHtmlComponents?: boolean
47
+ /**
48
+ * An element's own styles written under its class twice (`.title.title`),
49
+ * the weight of two classes. The Next export's styled-jsx scopes them with a
50
+ * second class and the editor weighs them the same, so an element's own
51
+ * colour beats a two-class combinator like `.hero .title` there; under one
52
+ * class it lost in the static HTML export, which showed another colour.
53
+ */
54
+ doubleElementClass?: boolean
55
+ }
56
+
57
+ const prefixUrlPathsInCss = (css: string, folderPath: string[]): string => {
58
+ const relativePrefix = GenericUtils.localRelativePath(join(...folderPath), './')
59
+ if (!relativePrefix) {
60
+ return css
61
+ }
62
+
63
+ return css.replace(/url\(["']?(.*?)["']?\)/g, (match, url) => {
64
+ if (
65
+ url.startsWith('http') ||
66
+ url.startsWith('data:') ||
67
+ url.startsWith('../') ||
68
+ url.startsWith('#')
69
+ ) {
70
+ return match
71
+ }
72
+ return `url("${join(relativePrefix, url)}")`
73
+ })
29
74
  }
30
75
 
31
76
  const createCSSPlugin: ComponentPluginFactory<CSSPluginConfig> = (config) => {
@@ -37,15 +82,27 @@ const createCSSPlugin: ComponentPluginFactory<CSSPluginConfig> = (config) => {
37
82
  classAttributeName = 'class',
38
83
  templateStyle = 'html',
39
84
  declareDependency = 'none',
40
- forceScoping = false,
41
85
  dynamicVariantPrefix,
42
86
  staticPropReferences = false,
87
+ standaloneHtmlComponents = false,
88
+ doubleElementClass = false,
43
89
  } = config || {}
44
90
 
45
91
  const cssPlugin: ComponentPlugin = async (structure) => {
46
92
  const { uidl, chunks, dependencies, options } = structure
47
- const { node, styleSetDefinitions: componentStyleSet = {}, propDefinitions = {} } = uidl
48
- const { projectStyleSet, designLanguage: { tokens = {} } = {}, isRootComponent } = options || {}
93
+ const {
94
+ node,
95
+ styleSetDefinitions: componentStyleSet = {},
96
+ propDefinitions = {},
97
+ stateDefinitions = {},
98
+ } = uidl
99
+ const {
100
+ projectStyleSet,
101
+ designLanguage: { tokens = {} } = {},
102
+ isRootComponent,
103
+ globalStateDefinitions = {},
104
+ prefixInlineClasses,
105
+ } = options || {}
49
106
  const {
50
107
  styleSetDefinitions = {},
51
108
  fileName: projectStyleSheetName,
@@ -71,7 +128,7 @@ const createCSSPlugin: ComponentPluginFactory<CSSPluginConfig> = (config) => {
71
128
  (chunk) => chunk.name === componentDecoratorChunkName
72
129
  )
73
130
 
74
- const templateLookup = templateChunk.meta.nodesLookup as Record<
131
+ const jsxNodesLookup = templateChunk.meta.nodesLookup as Record<
75
132
  string,
76
133
  HastNode | types.JSXElement
77
134
  >
@@ -81,49 +138,65 @@ const createCSSPlugin: ComponentPluginFactory<CSSPluginConfig> = (config) => {
81
138
  : ('' as string)
82
139
 
83
140
  const cssMap: string[] = []
141
+ // A doubled element class nests its rule one level down (see `doubleElementClass`).
84
142
  const mediaStylesMap: Record<
85
143
  string,
86
- Array<{ [x: string]: Record<string, string | number> }>
144
+ Array<{ [x: string]: Record<string, string | number | Record<string, string | number>> }>
87
145
  > = {}
146
+ const usedProjectStyleIds: Set<string> = new Set()
88
147
 
89
- UIDLUtils.traverseElements(node, (element) => {
148
+ const generateStylesForElementNode = (element: UIDLElement) => {
90
149
  const classNamesToAppend: Set<string> = new Set()
91
- const dynamicVariantsToAppend: Set<string> = new Set()
150
+ const dynamicVariantsToAppend: Set<string | types.ConditionalExpression> = new Set()
92
151
  const {
93
152
  style = {},
94
153
  key,
95
154
  referencedStyles = {},
96
- dependency,
97
155
  attrs = {},
98
156
  elementType,
157
+ dependency,
99
158
  } = element
100
- const elementClassName = StringUtils.camelCaseToDashCase(key)
159
+ const { dynamicStyleBindings } = element
160
+ const hasDynamicBindings =
161
+ dynamicStyleBindings && Object.keys(dynamicStyleBindings).length > 0
101
162
 
102
- if (forceScoping && dependency?.type === 'local') {
163
+ const root = jsxNodesLookup[key]
164
+ if (!root) {
165
+ return
166
+ }
167
+
168
+ // Refer to line 323 all component scoped styles are appended with component name by default
169
+ if (dependency?.type === 'local') {
103
170
  StyleBuilders.setPropValueForCompStyle({
104
171
  attrs,
105
- key,
106
- jsxNodesLookup: templateLookup,
172
+ root,
107
173
  templateStyle,
174
+ // elementType is used here in-order to target the component name that the class is actually defined.
175
+ // Here we are appendigng to the node where the component is being called.
108
176
  getClassName: (styleName: string) =>
109
- getClassName(forceScoping, StringUtils.camelCaseToDashCase(elementType), styleName),
177
+ StringUtils.camelCaseToDashCase(elementType + styleName),
110
178
  })
111
179
  }
112
180
 
113
181
  if (
114
182
  Object.keys(style).length === 0 &&
115
183
  Object.keys(referencedStyles).length === 0 &&
116
- Object.keys(componentStyleSet).length === 0
184
+ Object.keys(componentStyleSet).length === 0 &&
185
+ !hasDynamicBindings
117
186
  ) {
118
187
  return
119
188
  }
120
189
 
121
- const root = templateLookup[key]
122
- if (!root) {
123
- throw new PluginCSS(`Node - ${key} is missing from the template chunk`)
124
- }
125
-
126
- const className = getClassName(forceScoping, uidl.name, elementClassName)
190
+ // Inline (node-key-derived) classes can share a name with a real project
191
+ // class. When `prefixInlineClasses` is set (AI editor codegen only), tag
192
+ // every inline class so the consumer can tell fake from real by name.
193
+ const className = prefixInlineClasses
194
+ ? `${prefixInlineClasses}${StringUtils.camelCaseToDashCase(key)}`
195
+ : StringUtils.camelCaseToDashCase(key)
196
+ // `&` is the class; doubled it reads `.name.name`. Nested, because JSS
197
+ // escapes a dot written into the class name itself. The element still
198
+ // wears the class once.
199
+ const own = doubleElementClass ? `&.${className}` : '&'
127
200
 
128
201
  const { staticStyles, dynamicStyles, tokenStyles } =
129
202
  UIDLUtils.splitDynamicAndStaticStyles(style)
@@ -134,32 +207,64 @@ const createCSSPlugin: ComponentPluginFactory<CSSPluginConfig> = (config) => {
134
207
  ...StyleUtils.getCSSVariablesContentFromTokenStyles(tokenStyles),
135
208
  } as Record<string, string | number>
136
209
 
137
- cssMap.push(StyleBuilders.createCSSClass(className, collectedStyles))
210
+ cssMap.push(
211
+ doubleElementClass
212
+ ? StyleBuilders.createCSSClassWithSelector(className, own, collectedStyles)
213
+ : StyleBuilders.createCSSClass(className, collectedStyles)
214
+ )
138
215
  classNamesToAppend.add(className)
139
216
  }
140
217
 
218
+ let jsxInlineStyles: Record<string, unknown> | null = null
219
+
141
220
  if (Object.keys(dynamicStyles).length > 0) {
221
+ for (const styleValue of Object.values(dynamicStyles)) {
222
+ if (
223
+ styleValue.type === 'dynamic' &&
224
+ styleValue.content.referenceType === 'global' &&
225
+ templateChunk.meta.globalReferences
226
+ ) {
227
+ ;(templateChunk.meta.globalReferences as string[]).push(styleValue.content.id)
228
+ }
229
+ }
230
+
142
231
  /* If dynamic styles are on nested-styles they are unfortunately lost,
143
232
  since inline style does not support that */
144
233
  if (templateStyle === 'html') {
145
234
  const inlineStyles = createDynamicInlineStyle(dynamicStyles)
146
- HASTUtils.addAttributeToNode(
147
- root as HastNode,
148
- inlineStyleAttributeKey,
149
- `{${inlineStyles}}`
150
- )
235
+ const bindingParts = hasDynamicBindings
236
+ ? createDynamicBindingInlineStyle(dynamicStyleBindings)
237
+ : ''
238
+ const combined = bindingParts ? `${inlineStyles}, ${bindingParts}` : inlineStyles
239
+ HASTUtils.addAttributeToNode(root as HastNode, inlineStyleAttributeKey, `{${combined}}`)
151
240
  } else {
152
- const inlineStyles = UIDLUtils.transformDynamicStyles(dynamicStyles, (styleValue) =>
241
+ jsxInlineStyles = UIDLUtils.transformDynamicStyles(dynamicStyles, (styleValue) =>
153
242
  StyleBuilders.createDynamicStyleExpression(styleValue, propsPrefix)
154
243
  )
155
- ASTUtils.addAttributeToJSXTag(
156
- root as types.JSXElement,
157
- inlineStyleAttributeKey,
158
- inlineStyles
159
- )
244
+ }
245
+ } else if (hasDynamicBindings && templateStyle === 'html') {
246
+ const bindingParts = createDynamicBindingInlineStyle(dynamicStyleBindings)
247
+ HASTUtils.addAttributeToNode(root as HastNode, inlineStyleAttributeKey, `{${bindingParts}}`)
248
+ }
249
+
250
+ if (hasDynamicBindings && templateStyle !== 'html') {
251
+ if (!jsxInlineStyles) {
252
+ jsxInlineStyles = {}
253
+ }
254
+ for (const [cssProperty, binding] of Object.entries(dynamicStyleBindings)) {
255
+ const camelCaseProperty = StringUtils.cssPropertyToInlineStyleKey(cssProperty)
256
+ jsxInlineStyles[camelCaseProperty] = StyleBuilders.createDynamicBindingExpression(binding)
160
257
  }
161
258
  }
162
259
 
260
+ if (jsxInlineStyles) {
261
+ ASTUtils.addAttributeToJSXTag(
262
+ root as types.JSXElement,
263
+ inlineStyleAttributeKey,
264
+ jsxInlineStyles
265
+ )
266
+ }
267
+
163
268
  Object.values(referencedStyles).forEach((styleRef: UIDLElementNodeReferenceStyles) => {
164
269
  switch (styleRef.content.mapType) {
165
270
  case 'inlined': {
@@ -176,14 +281,16 @@ const createCSSPlugin: ComponentPluginFactory<CSSPluginConfig> = (config) => {
176
281
  if (!mediaStylesMap[String(maxWidth)]) {
177
282
  mediaStylesMap[String(maxWidth)] = []
178
283
  }
179
- mediaStylesMap[String(maxWidth)].push({ [className]: collectedStyles })
284
+ mediaStylesMap[String(maxWidth)].push({
285
+ [className]: doubleElementClass ? { [own]: collectedStyles } : collectedStyles,
286
+ })
180
287
  }
181
288
 
182
289
  if (condition.conditionType === 'element-state') {
183
290
  cssMap.push(
184
291
  StyleBuilders.createCSSClassWithSelector(
185
292
  className,
186
- `&:${condition.content}`,
293
+ `${own}:${condition.content}`,
187
294
  collectedStyles
188
295
  )
189
296
  )
@@ -206,24 +313,21 @@ const createCSSPlugin: ComponentPluginFactory<CSSPluginConfig> = (config) => {
206
313
  propDefinitions[styleRef.content.content.content.id]?.defaultValue
207
314
 
208
315
  if (defaultPropValue) {
209
- /* Changing the default value of the prop.
210
- When forceScoping is enabled the classnames change. So, we need to change the default prop too. */
211
- propDefinitions[styleRef.content.content.content.id].defaultValue = getClassName(
212
- forceScoping,
213
- uidl.name,
214
- String(defaultPropValue)
215
- )
316
+ propDefinitions[styleRef.content.content.content.id].defaultValue =
317
+ StringUtils.camelCaseToDashCase(String(defaultPropValue))
216
318
  }
217
319
 
320
+ // staticPropReferences flag is only used for just html-code generation.
321
+ // This is used to append the class name to the node where the component is being called.
322
+ // Instead of how frameworks handle them using props at runtime. This makes the props
323
+ // to behave statically during the generation time instead by appending them directly.
218
324
  if (staticPropReferences) {
219
- if (!defaultPropValue) {
325
+ if (defaultPropValue === undefined || typeof defaultPropValue !== 'string') {
220
326
  return
221
327
  }
222
- if (staticPropReferences) {
223
- classNamesToAppend.add(
224
- getClassName(forceScoping, uidl.name, String(defaultPropValue))
225
- )
226
- }
328
+ classNamesToAppend.add(
329
+ StringUtils.camelCaseToDashCase(uidl.name + defaultPropValue)
330
+ )
227
331
  } else {
228
332
  dynamicVariantsToAppend.add(styleRef.content.content.content.id)
229
333
  }
@@ -239,7 +343,7 @@ const createCSSPlugin: ComponentPluginFactory<CSSPluginConfig> = (config) => {
239
343
  )
240
344
  }
241
345
  classNamesToAppend.add(
242
- getClassName(forceScoping, uidl.name, String(styleRef.content.content.content.id))
346
+ StringUtils.camelCaseToDashCase(uidl.name + styleRef.content.content.content.id)
243
347
  )
244
348
  }
245
349
 
@@ -250,12 +354,252 @@ const createCSSPlugin: ComponentPluginFactory<CSSPluginConfig> = (config) => {
250
354
  const { content } = styleRef
251
355
  const referedStyle = styleSetDefinitions[content.referenceId]
252
356
  if (!referedStyle) {
253
- throw new PluginCSS(
254
- `Style used from global stylesheet is missing - ${content.referenceId}`
255
- )
357
+ return
256
358
  }
257
359
 
258
- classNamesToAppend.add(content.referenceId)
360
+ usedProjectStyleIds.add(content.referenceId)
361
+
362
+ if (styleRef.content.condition) {
363
+ if (templateStyle === 'html') {
364
+ const {
365
+ value: staticValue,
366
+ reference,
367
+ expression: { matchingCriteria },
368
+ } = styleRef.content.condition
369
+ // Class conditions are single-reference flat chains: no writer
370
+ // emits per-entry references or nested groups here.
371
+ const conditions = styleRef.content.condition.expression
372
+ .conditions as UIDLConditionExpressionEntry[]
373
+
374
+ const {
375
+ content: { referenceType, id, refPath = [] },
376
+ } = reference
377
+
378
+ switch (referenceType) {
379
+ case 'prop': {
380
+ const usedProp = propDefinitions[id]
381
+ if (usedProp === undefined || usedProp.defaultValue === undefined) {
382
+ throw new PluginCSS(`Prop with ${id} is missing in the propDefinitions.`)
383
+ }
384
+
385
+ let defaultValue = usedProp.defaultValue
386
+ let operandDefaultValue: StateDefaultValueTypes | PropDefaultValueTypes | string
387
+ for (const path of refPath) {
388
+ defaultValue = (defaultValue as Record<string, unknown[]>)?.[path]
389
+ }
390
+
391
+ // If defaultValue is undefined or null after path traversal, use original default
392
+ defaultValue = defaultValue ?? usedProp.defaultValue
393
+
394
+ // our conditions can be of type 'expr' or they can be a dynamic reference
395
+ const rightSideCondition = conditions[0].operand
396
+ const rightSideConditionType = typeof rightSideCondition
397
+ if (rightSideConditionType === 'object') {
398
+ const type = (
399
+ rightSideCondition as UIDLDynamicReference | UIDLExpressionValue
400
+ ).type
401
+ if (type === 'dynamic') {
402
+ const dynamicRef = rightSideCondition as UIDLDynamicReference
403
+ if (dynamicRef.content.referenceType === 'prop') {
404
+ const prop = propDefinitions[dynamicRef.content.id]
405
+ if (prop && prop.defaultValue) {
406
+ operandDefaultValue = prop.defaultValue
407
+ }
408
+ }
409
+ if (dynamicRef.content.referenceType === 'state') {
410
+ const state = stateDefinitions[dynamicRef.content.id]
411
+ if (state && state.defaultValue) {
412
+ defaultValue = state.defaultValue
413
+ }
414
+ }
415
+ }
416
+ if (type === 'expr') {
417
+ operandDefaultValue = (rightSideCondition as UIDLExpressionValue).content
418
+ }
419
+ } else {
420
+ operandDefaultValue = rightSideCondition
421
+ }
422
+
423
+ // Since we know the operand and the default value from the prop.
424
+ // We can try building the condition and check if the condition is true or false.
425
+ // @todo: You can only use a 'value' in UIDL or 'conditions' but not both.
426
+ // UIDL validations need to be improved on this aspect.
427
+ const resolvedConditions = [
428
+ {
429
+ operation: conditions[0].operation,
430
+ operand: operandDefaultValue as string,
431
+ },
432
+ ]
433
+
434
+ const dynamicConditions = createConditionalStatement(
435
+ staticValue !== undefined
436
+ ? [{ operand: staticValue, operation: '===' }]
437
+ : resolvedConditions,
438
+ defaultValue
439
+ )
440
+ const matchCondition =
441
+ matchingCriteria && matchingCriteria === 'all' ? '&&' : '||'
442
+ const conditionString = dynamicConditions.join(` ${matchCondition} `)
443
+
444
+ // tslint:disable-next-line function-constructor
445
+ const isConditionPassing = new Function(`return ${conditionString}`)()
446
+ if (isConditionPassing) {
447
+ classNamesToAppend.add(styleRef.content.referenceId)
448
+ return
449
+ }
450
+ return
451
+ }
452
+ case 'state': {
453
+ const usedState = stateDefinitions[id]
454
+ if (usedState === undefined || usedState.defaultValue === undefined) {
455
+ throw new PluginCSS(`State with ${id} is missing in the stateDefinitions.`)
456
+ }
457
+ let defaultValue = usedState.defaultValue
458
+ for (const path of refPath) {
459
+ defaultValue = (defaultValue as Record<string, unknown[]>)?.[path]
460
+ }
461
+ if (
462
+ typeof defaultValue === 'object' &&
463
+ defaultValue !== null &&
464
+ 'type' in (defaultValue as Record<string, unknown>) &&
465
+ 'content' in (defaultValue as Record<string, unknown>)
466
+ ) {
467
+ const entry = defaultValue as { type: string; content: unknown }
468
+ if (entry.type === 'static') {
469
+ defaultValue = entry.content as typeof defaultValue
470
+ }
471
+ }
472
+ defaultValue = defaultValue ?? usedState.defaultValue
473
+
474
+ const dynamicConditions = createConditionalStatement(
475
+ staticValue !== undefined
476
+ ? [{ operand: staticValue, operation: '===' }]
477
+ : conditions,
478
+ defaultValue
479
+ )
480
+ const matchCondition =
481
+ matchingCriteria && matchingCriteria === 'all' ? '&&' : '||'
482
+ const conditionString = dynamicConditions.join(` ${matchCondition} `)
483
+ // tslint:disable-next-line function-constructor
484
+ const isConditionPassing = new Function(`return ${conditionString}`)()
485
+ if (isConditionPassing) {
486
+ classNamesToAppend.add(styleRef.content.referenceId)
487
+ return
488
+ }
489
+ return
490
+ }
491
+
492
+ case 'globalState' as any: {
493
+ const gsDef = findGlobalStateDefinitionById(id, globalStateDefinitions)
494
+ if (!gsDef) {
495
+ classNamesToAppend.add(styleRef.content.referenceId)
496
+ return
497
+ }
498
+ let defaultValue: StateDefaultValueTypes | PropDefaultValueTypes =
499
+ gsDef.defaultValue as StateDefaultValueTypes
500
+ for (const path of refPath) {
501
+ defaultValue = (defaultValue as Record<string, unknown>)?.[
502
+ path
503
+ ] as typeof defaultValue
504
+ }
505
+ defaultValue = defaultValue ?? (gsDef.defaultValue as StateDefaultValueTypes)
506
+ const dynamicConditions = createConditionalStatement(
507
+ staticValue !== undefined
508
+ ? [{ operand: staticValue, operation: '===' }]
509
+ : conditions,
510
+ defaultValue
511
+ )
512
+ const matchCondition =
513
+ matchingCriteria && matchingCriteria === 'all' ? '&&' : '||'
514
+ const conditionString = dynamicConditions.join(` ${matchCondition} `)
515
+ // tslint:disable-next-line function-constructor
516
+ const isConditionPassing = new Function(`return ${conditionString}`)()
517
+ if (isConditionPassing) {
518
+ classNamesToAppend.add(styleRef.content.referenceId)
519
+ return
520
+ }
521
+ return
522
+ }
523
+
524
+ default: {
525
+ classNamesToAppend.add(styleRef.content.referenceId)
526
+ return
527
+ }
528
+ }
529
+ } else {
530
+ const referenceContent = styleRef.content.condition.reference.content
531
+ const referenceType = referenceContent.referenceType
532
+ const nameToAppend =
533
+ referenceType === 'local' && referenceContent.refPath?.length
534
+ ? referenceContent.refPath.join('.')
535
+ : referenceContent.id
536
+
537
+ // Same flat-leaf contract as the html branch above.
538
+ const conditions = styleRef.content.condition.expression
539
+ .conditions as UIDLConditionExpressionEntry[]
540
+
541
+ const operator = conditions[0].operation as '===' | '!==' | '<' | '<=' | '>' | '>='
542
+ const right = conditions[0].operand as string | number | boolean
543
+
544
+ let binaryExpressionType = ''
545
+ switch (referenceType) {
546
+ case 'prop': {
547
+ binaryExpressionType = propDefinitions[nameToAppend].type
548
+ break
549
+ }
550
+ case 'state': {
551
+ binaryExpressionType = stateDefinitions[nameToAppend].type
552
+ break
553
+ }
554
+ case 'local': {
555
+ binaryExpressionType = typeof right as string
556
+ break
557
+ }
558
+ default: {
559
+ if ((referenceType as string) === 'globalState') {
560
+ const gsDef = findGlobalStateDefinitionById(
561
+ nameToAppend,
562
+ globalStateDefinitions
563
+ )
564
+ if (gsDef) {
565
+ binaryExpressionType = gsDef.type
566
+ }
567
+ break
568
+ }
569
+ throw new PluginCSS(
570
+ `Un-supported reference type ${referenceType} for ${nameToAppend}`
571
+ )
572
+ }
573
+ }
574
+
575
+ let binaryKey = nameToAppend
576
+ if (referenceType === 'prop') {
577
+ binaryKey = 'props?.' + nameToAppend
578
+ } else if ((referenceType as string) === 'globalState') {
579
+ const gsDef = findGlobalStateDefinitionById(nameToAppend, globalStateDefinitions)
580
+ binaryKey = gsDef ? gsDef.name : nameToAppend
581
+ }
582
+
583
+ const binaryExpression = createBinaryExpression(
584
+ { operation: operator, operand: right },
585
+ {
586
+ key: binaryKey,
587
+ type: binaryExpressionType,
588
+ }
589
+ )
590
+
591
+ const conditionalExpression = types.conditionalExpression(
592
+ binaryExpression,
593
+ types.stringLiteral(content.referenceId),
594
+ types.stringLiteral('')
595
+ )
596
+
597
+ dynamicVariantsToAppend.add(conditionalExpression)
598
+ return
599
+ }
600
+ } else {
601
+ classNamesToAppend.add(content.referenceId)
602
+ }
259
603
  return
260
604
  }
261
605
 
@@ -296,18 +640,35 @@ const createCSSPlugin: ComponentPluginFactory<CSSPluginConfig> = (config) => {
296
640
  ? types.identifier(dynamicVariantPrefix)
297
641
  : types.identifier(propsPrefix)
298
642
 
299
- return types.memberExpression(dynamicAttrValueIdentifier, types.identifier(variant))
643
+ if (typeof variant === 'string') {
644
+ return types.memberExpression(dynamicAttrValueIdentifier, types.identifier(variant))
645
+ } else {
646
+ // variant is a ConditionalExpression, so return it directly
647
+ return variant
648
+ }
300
649
  })
301
650
  )
302
651
  }
303
- })
652
+ }
653
+
654
+ UIDLUtils.traverseElements(node, generateStylesForElementNode)
655
+ for (const prop of Object.values(propDefinitions)) {
656
+ if (prop.type === 'element' && prop.defaultValue) {
657
+ UIDLUtils.traverseElements(
658
+ prop.defaultValue as UIDLElementNode,
659
+ generateStylesForElementNode
660
+ )
661
+ }
662
+ }
304
663
 
305
664
  if (Object.keys(componentStyleSet).length > 0) {
306
665
  StyleBuilders.generateStylesFromStyleSetDefinitions(
307
666
  componentStyleSet,
308
667
  cssMap,
309
668
  mediaStylesMap,
310
- (styleName: string) => getClassName(forceScoping, uidl.name, styleName)
669
+ (styleName: string) => {
670
+ return StringUtils.camelCaseToDashCase(uidl.name + styleName)
671
+ }
311
672
  )
312
673
  }
313
674
 
@@ -315,7 +676,210 @@ const createCSSPlugin: ComponentPluginFactory<CSSPluginConfig> = (config) => {
315
676
  cssMap.push(...StyleBuilders.generateMediaStyle(mediaStylesMap))
316
677
  }
317
678
 
318
- if (cssMap.length > 0) {
679
+ // Handle inline style tag for HTML templates (self-contained fragments)
680
+ if (standaloneHtmlComponents && templateStyle === 'html' && templateChunk) {
681
+ const inlineCssMap: string[] = []
682
+ const hasTokens = Object.keys(tokens).length > 0
683
+ const hasUsedProjectStyles = usedProjectStyleIds.size > 0
684
+ const globalAssets = options.globalAssets || []
685
+
686
+ // Get the scoped root class from the template chunk meta
687
+ const scopedRootClass = templateChunk.meta?.scopedRootClass as string | undefined
688
+
689
+ // Utility function to scope CSS selectors
690
+ const scopeCssSelectors = (css: string, rootSelector: string): string => {
691
+ // Replace :root with scoped root class
692
+ let scopedCss = css.replace(/:root\s*\{/g, `${rootSelector} {`)
693
+
694
+ // Scope regular selectors (but not inside @media or @keyframes)
695
+ // This regex matches CSS rules outside of @ blocks
696
+ scopedCss = scopedCss.replace(/(?:^|\})\s*([^@{}]+?)\s*\{/gm, (match, selectors) => {
697
+ // Skip if it's just closing brace or empty
698
+ if (!selectors || selectors.trim() === '') {
699
+ return match
700
+ }
701
+
702
+ // Skip if selector is already the root class
703
+ if (selectors.trim() === rootSelector) {
704
+ return match
705
+ }
706
+
707
+ // Handle multiple selectors (comma-separated)
708
+ const scopedSelectors = selectors
709
+ .split(',')
710
+ .map((sel: string) => {
711
+ const trimmedSel = sel.trim()
712
+ // Skip empty selectors
713
+ if (!trimmedSel) {
714
+ return ''
715
+ }
716
+ // Skip if already scoped
717
+ if (trimmedSel.startsWith(rootSelector)) {
718
+ return trimmedSel
719
+ }
720
+ // Replace html/body with root selector
721
+ if (trimmedSel === 'html' || trimmedSel === 'body') {
722
+ return rootSelector
723
+ }
724
+ // For universal selector, scope it
725
+ if (trimmedSel === '*') {
726
+ return `${rootSelector} *`
727
+ }
728
+ // Prefix other selectors with root
729
+ return `${rootSelector} ${trimmedSel}`
730
+ })
731
+ .filter(Boolean)
732
+ .join(', ')
733
+
734
+ return match.replace(selectors, scopedSelectors)
735
+ })
736
+
737
+ return scopedCss
738
+ }
739
+
740
+ // 0. Add reset and default styles from global assets (scoped)
741
+ const inlineStyleAssets = globalAssets.filter(
742
+ (asset): asset is UIDLStyleInlineAsset => asset.type === 'style' && 'content' in asset
743
+ )
744
+ for (const styleAsset of inlineStyleAssets) {
745
+ if (styleAsset.content) {
746
+ const css = scopedRootClass
747
+ ? scopeCssSelectors(styleAsset.content, `.${scopedRootClass}`)
748
+ : styleAsset.content
749
+ inlineCssMap.push(css)
750
+ }
751
+ }
752
+
753
+ // 1. Add CSS variables (tokens) - scoped to root class instead of :root
754
+ if (hasTokens) {
755
+ const tokenSelector = scopedRootClass ? `.${scopedRootClass}` : ':root'
756
+ inlineCssMap.push(
757
+ StyleBuilders.createCSSClassWithSelector(
758
+ '@global',
759
+ tokenSelector,
760
+ StyleUtils.getTokensContentFromTokensObject(tokens)
761
+ )
762
+ )
763
+ }
764
+
765
+ // 2. Add project-referenced styles that this component uses (scoped)
766
+ if (hasUsedProjectStyles) {
767
+ const usedProjectStyles = Array.from(usedProjectStyleIds)
768
+ // Include exact matches and compound selectors that reference the same base class
769
+ const filteredDefinitions = Object.fromEntries(
770
+ Object.entries(styleSetDefinitions).filter(([key, definition]) => {
771
+ // Include if exact match
772
+ if (usedProjectStyles.includes(key)) {
773
+ return true
774
+ }
775
+ // Include compound selectors where className matches a used project style
776
+ // e.g., "navigation-mobile-overlay.navigation-mobile-overlay-active" has className "navigation-mobile-overlay"
777
+ const className = (definition as { className?: string }).className
778
+ if (className && usedProjectStyles.includes(className)) {
779
+ return true
780
+ }
781
+ // Include compound selectors where the base class (before the dot or space) is a used style
782
+ // e.g., "dashboard-sidebar.collapsed" should be included if "dashboard-sidebar" is used
783
+ // e.g., "dashboard-sidebar.collapsed .sidebar-brand-text" should also be included
784
+ const dotIdx = key.indexOf('.')
785
+ const spaceIdx = key.indexOf(' ')
786
+ if (dotIdx > 0) {
787
+ const baseClass = key.substring(0, dotIdx)
788
+ if (usedProjectStyles.includes(baseClass)) {
789
+ return true
790
+ }
791
+ }
792
+ if (spaceIdx > 0 && dotIdx < 0) {
793
+ const baseClass = key.substring(0, spaceIdx)
794
+ if (usedProjectStyles.includes(baseClass)) {
795
+ return true
796
+ }
797
+ }
798
+ return false
799
+ })
800
+ )
801
+ const projectStylesCssMap: string[] = []
802
+ const projectMediaStylesMap: Record<
803
+ string,
804
+ Array<{ [x: string]: Record<string, string | number> }>
805
+ > = {}
806
+ StyleBuilders.generateStylesFromStyleSetDefinitions(
807
+ filteredDefinitions,
808
+ projectStylesCssMap,
809
+ projectMediaStylesMap,
810
+ (styleName) => styleName
811
+ )
812
+ // Scope the project styles if scoped root class is available
813
+ if (scopedRootClass) {
814
+ const scopedProjectStyles = projectStylesCssMap.map((css) =>
815
+ scopeCssSelectors(css, `.${scopedRootClass}`)
816
+ )
817
+ inlineCssMap.push(...scopedProjectStyles)
818
+ } else {
819
+ inlineCssMap.push(...projectStylesCssMap)
820
+ }
821
+ if (Object.keys(projectMediaStylesMap).length > 0) {
822
+ const mediaStyles = StyleBuilders.generateMediaStyle(projectMediaStylesMap)
823
+ if (scopedRootClass) {
824
+ const scopedMediaStyles = mediaStyles.map((css) =>
825
+ scopeCssSelectors(css, `.${scopedRootClass}`)
826
+ )
827
+ inlineCssMap.push(...scopedMediaStyles)
828
+ } else {
829
+ inlineCssMap.push(...mediaStyles)
830
+ }
831
+ }
832
+ }
833
+
834
+ // 3. Add component-specific styles (scoped)
835
+ if (scopedRootClass) {
836
+ // Scope the component CSS
837
+ const scopedCssMap = cssMap.map((css) => scopeCssSelectors(css, `.${scopedRootClass}`))
838
+ inlineCssMap.push(...scopedCssMap)
839
+ } else {
840
+ inlineCssMap.push(...cssMap)
841
+ }
842
+
843
+ // 4. Create font link tags from global assets
844
+ const fontAssets = globalAssets.filter(
845
+ (asset): asset is UIDLFontAsset => asset.type === 'font' && 'path' in asset
846
+ )
847
+ const fontLinkNodes: HastNode[] = []
848
+ for (const fontAsset of fontAssets) {
849
+ const linkNode = HASTBuilders.createHTMLNode('link')
850
+ HASTUtils.addAttributeToNode(linkNode, 'rel', 'stylesheet')
851
+ HASTUtils.addAttributeToNode(linkNode, 'href', fontAsset.path)
852
+ if (fontAsset.attrs) {
853
+ for (const [attrKey, attrValue] of Object.entries(fontAsset.attrs)) {
854
+ const value =
855
+ typeof attrValue === 'object' && 'content' in attrValue
856
+ ? String(attrValue.content)
857
+ : String(attrValue)
858
+ HASTUtils.addAttributeToNode(linkNode, attrKey, value)
859
+ }
860
+ }
861
+ fontLinkNodes.push(linkNode)
862
+ }
863
+
864
+ // 5. Create <style> node and prepend to HTML (fonts first, then styles)
865
+ if (inlineCssMap.length > 0 || fontLinkNodes.length > 0) {
866
+ const nodesToPrepend: HastNode[] = []
867
+
868
+ // Add font links first
869
+ nodesToPrepend.push(...fontLinkNodes)
870
+
871
+ // Add style tag with all CSS
872
+ if (inlineCssMap.length > 0) {
873
+ const styleNode = HASTBuilders.createHTMLNode('style')
874
+ const cssContent = HASTBuilders.createTextNode(inlineCssMap.join('\n\n'))
875
+ styleNode.children.push(cssContent)
876
+ nodesToPrepend.push(styleNode)
877
+ }
878
+
879
+ ;(templateChunk.content as HastNode).children.unshift(...nodesToPrepend)
880
+ }
881
+ } else if (cssMap.length > 0) {
882
+ // Original behavior: create separate CSS file
319
883
  /**
320
884
  * Setup an import statement for the styles
321
885
  * The name of the file is either in the meta of the component generator
@@ -344,11 +908,17 @@ const createCSSPlugin: ComponentPluginFactory<CSSPluginConfig> = (config) => {
344
908
  }
345
909
  }
346
910
 
911
+ const folderPath = uidl.outputOptions?.folderPath
912
+ const cssContent =
913
+ folderPath?.length > 0
914
+ ? cssMap.map((css) => prefixUrlPathsInCss(css, folderPath)).join('\n \n')
915
+ : cssMap.join('\n \n')
916
+
347
917
  chunks.push({
348
918
  type: ChunkType.STRING,
349
919
  name: chunkName,
350
920
  fileType: FileType.CSS,
351
- content: cssMap.join('\n'),
921
+ content: cssContent,
352
922
  linkAfter: [],
353
923
  })
354
924
  }
@@ -363,6 +933,18 @@ export { createStyleSheetPlugin, createCSSPlugin }
363
933
 
364
934
  export default createCSSPlugin()
365
935
 
936
+ const findGlobalStateDefinitionById = (
937
+ id: string,
938
+ definitions: Record<string, UIDLGlobalStateDefinition>
939
+ ): UIDLGlobalStateDefinition | undefined => {
940
+ for (const def of Object.values(definitions)) {
941
+ if (def.id === id) {
942
+ return def
943
+ }
944
+ }
945
+ return undefined
946
+ }
947
+
366
948
  const createDynamicInlineStyle = (styles: UIDLStyleDefinitions) => {
367
949
  return Object.keys(styles)
368
950
  .map((styleKey) => {
@@ -371,8 +953,25 @@ const createDynamicInlineStyle = (styles: UIDLStyleDefinitions) => {
371
953
  .join(', ')
372
954
  }
373
955
 
374
- const getClassName = (scoping: boolean, uidlName: string, nodeStyleName: string) => {
375
- return scoping
376
- ? StringUtils.camelCaseToDashCase(`${uidlName}-${nodeStyleName}`)
377
- : StringUtils.camelCaseToDashCase(nodeStyleName)
956
+ const createDynamicBindingInlineStyle = (
957
+ bindings: Record<
958
+ string,
959
+ {
960
+ referenceType: string
961
+ stateKey: string
962
+ defaultValue: string
963
+ contextName?: string
964
+ stateDefinitionId?: string
965
+ }
966
+ >
967
+ ): string => {
968
+ return Object.entries(bindings)
969
+ .map(([prop, binding]) => {
970
+ const value =
971
+ binding.referenceType === 'ctx' && binding.contextName
972
+ ? `${binding.contextName}.${binding.stateKey}`
973
+ : binding.stateKey
974
+ return `${prop}: ${value} != null ? ${value} : '${binding.defaultValue || ''}'`
975
+ })
976
+ .join(', ')
378
977
  }