@teleporthq/teleport-plugin-css 0.43.0-alpha.0 → 0.43.3

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