@@ -37,23 +37,25 @@ const tailwindToWebstudioMappings: Record<number, undefined | number> = {
3737 1536 : 1440 ,
3838} ;
3939
40+ type StyleDecl = Omit < ParsedStyleDecl , "selector" > ;
41+
4042type Breakpoint = {
41- key : string ;
43+ styleDecl : StyleDecl ;
4244 minWidth ?: number ;
4345 maxWidth ?: number ;
4446} ;
4547
4648type Range = {
47- key : string ;
49+ styleDecl : StyleDecl ;
4850 start : number ;
4951 end : number ;
5052} ;
5153
5254const serializeBreakpoint = ( breakpoint : Breakpoint ) => {
53- if ( breakpoint ?. minWidth ) {
55+ if ( breakpoint ?. minWidth !== undefined ) {
5456 return `(min-width: ${ breakpoint . minWidth } px)` ;
5557 }
56- if ( breakpoint ?. maxWidth ) {
58+ if ( breakpoint ?. maxWidth !== undefined ) {
5759 return `(max-width: ${ breakpoint . maxWidth } px)` ;
5860 }
5961} ;
@@ -63,17 +65,17 @@ const UPPER_BOUND = Number.MAX_SAFE_INTEGER;
6365const breakpointsToRanges = ( breakpoints : Breakpoint [ ] ) => {
6466 // collect lower bounds and ids
6567 const values = new Set < number > ( [ 0 ] ) ;
66- const keys = new Map < undefined | number , string > ( ) ;
68+ const styles = new Map < undefined | number , StyleDecl > ( ) ;
6769 for ( const breakpoint of breakpoints ) {
6870 if ( breakpoint . minWidth !== undefined ) {
6971 values . add ( breakpoint . minWidth ) ;
70- keys . set ( breakpoint . minWidth , breakpoint . key ) ;
72+ styles . set ( breakpoint . minWidth , breakpoint . styleDecl ) ;
7173 } else if ( breakpoint . maxWidth !== undefined ) {
7274 values . add ( breakpoint . maxWidth + 1 ) ;
73- keys . set ( breakpoint . maxWidth , breakpoint . key ) ;
75+ styles . set ( breakpoint . maxWidth , breakpoint . styleDecl ) ;
7476 } else {
7577 // base breakpoint
76- keys . set ( undefined , breakpoint . key ) ;
78+ styles . set ( undefined , breakpoint . styleDecl ) ;
7779 }
7880 }
7981 const sortedValues = Array . from ( values ) . sort ( ( left , right ) => left - right ) ;
@@ -86,46 +88,58 @@ const breakpointsToRanges = (breakpoints: Breakpoint[]) => {
8688 } else {
8789 end = sortedValues [ index + 1 ] - 1 ;
8890 }
89- const key = keys . get ( start ) ?? keys . get ( end ) ?? keys . get ( undefined ) ;
90- if ( key ) {
91- ranges . push ( { key, start, end } ) ;
91+ const styleDecl =
92+ styles . get ( start ) ?? styles . get ( end ) ?? styles . get ( undefined ) ;
93+ if ( styleDecl ) {
94+ ranges . push ( { styleDecl, start, end } ) ;
95+ continue ;
96+ }
97+ // when declaration is missing add new one with unset value
98+ // to fill the hole in breakpoints
99+ // for example
100+ // "sm:opacity-20" has a hole at the start
101+ // "max-sm:opacity-10 md:opacity-20" has a whole in the middle
102+ const example = Array . from ( styles . values ( ) ) [ 0 ] ;
103+ if ( example ) {
104+ const newStyleDecl : StyleDecl = {
105+ ...example ,
106+ value : { type : "keyword" , value : "unset" } ,
107+ } ;
108+ ranges . push ( { styleDecl : newStyleDecl , start, end } ) ;
92109 }
93110 }
94111 return ranges ;
95112} ;
96113
97114const rangesToBreakpoints = ( ranges : Range [ ] ) => {
98115 const breakpoints : Breakpoint [ ] = [ ] ;
99- for ( const range of ranges ) {
116+ for ( const { styleDecl , start , end } of ranges ) {
100117 let matchedBreakpoint ;
101118 for ( const breakpoint of availableBreakpoints ) {
102- if ( breakpoint . minWidth === range . start ) {
103- matchedBreakpoint = { key : range . key , minWidth : range . start } ;
119+ if ( breakpoint . minWidth === start ) {
120+ matchedBreakpoint = { styleDecl , minWidth : start } ;
104121 }
105- if ( breakpoint . maxWidth === range . end ) {
106- matchedBreakpoint = { key : range . key , maxWidth : range . end } ;
122+ if ( breakpoint . maxWidth === end ) {
123+ matchedBreakpoint = { styleDecl , maxWidth : end } ;
107124 }
108125 if (
109126 breakpoint . minWidth === undefined &&
110127 breakpoint . maxWidth === undefined
111128 ) {
112- matchedBreakpoint ??= { key : range . key } ;
129+ matchedBreakpoint ??= { styleDecl } ;
113130 }
114131 }
115132 if ( matchedBreakpoint ) {
133+ styleDecl . breakpoint = serializeBreakpoint ( matchedBreakpoint ) ;
116134 breakpoints . push ( matchedBreakpoint ) ;
117135 }
118136 }
119137 return breakpoints ;
120138} ;
121139
122- const adaptBreakpoints = (
123- parsedStyles : Omit < ParsedStyleDecl , "selector" > [ ]
124- ) => {
125- const newStyles : typeof parsedStyles = [ ] ;
140+ const adaptBreakpoints = ( parsedStyles : StyleDecl [ ] ) => {
126141 const breakpointGroups = new Map < string , Breakpoint [ ] > ( ) ;
127142 for ( const styleDecl of parsedStyles ) {
128- newStyles . push ( styleDecl ) ;
129143 const mediaQuery = styleDecl . breakpoint
130144 ? parseMediaQuery ( styleDecl . breakpoint )
131145 : undefined ;
@@ -143,27 +157,14 @@ const adaptBreakpoints = (
143157 group = [ ] ;
144158 breakpointGroups . set ( groupKey , group ) ;
145159 }
146- const styleDeclKey = `${ styleDecl . breakpoint ?? "" } :${ styleDecl . property } :${ styleDecl . state ?? "" } ` ;
147- group . push ( { key : styleDeclKey , ...mediaQuery } ) ;
160+ group . push ( { styleDecl, ...mediaQuery } ) ;
148161 }
149- const breakpointsByKey = new Map < string , Breakpoint > ( ) ;
150- for ( let group of breakpointGroups . values ( ) ) {
162+ const newStyles : typeof parsedStyles = [ ] ;
163+ for ( const group of breakpointGroups . values ( ) ) {
151164 const ranges = breakpointsToRanges ( group ) ;
152- // adapt breakpoints only when first range is defined
153- // for example opacity-10 sm:opacity-20 will work
154- // but sm:opacity-20 alone does not have the base to switch to
155- if ( ranges [ 0 ] . start === 0 ) {
156- group = rangesToBreakpoints ( ranges ) ;
157- }
158- for ( const breakpoint of group ) {
159- breakpointsByKey . set ( breakpoint . key , breakpoint ) ;
160- }
161- }
162- for ( const styleDecl of newStyles ) {
163- const styleDeclKey = `${ styleDecl . breakpoint ?? "" } :${ styleDecl . property } :${ styleDecl . state ?? "" } ` ;
164- const breakpoint = breakpointsByKey . get ( styleDeclKey ) ;
165- if ( breakpoint ) {
166- styleDecl . breakpoint = serializeBreakpoint ( breakpoint ) ;
165+ const newGroup = rangesToBreakpoints ( ranges ) ;
166+ for ( const { styleDecl } of newGroup ) {
167+ newStyles . push ( styleDecl ) ;
167168 }
168169 }
169170 return newStyles ;
@@ -215,7 +216,7 @@ const parseTailwindClasses = async (classes: string) => {
215216 const generated = await generator . generate ( classes ) ;
216217 // use tailwind prefix instead of unocss one
217218 const css = generated . css . replaceAll ( "--un-" , "--tw-" ) ;
218- let parsedStyles : Omit < ParsedStyleDecl , "selector" > [ ] = [ ] ;
219+ let parsedStyles : StyleDecl [ ] = [ ] ;
219220 // @todo probably builtin in v4
220221 if ( css . includes ( "border" ) ) {
221222 // Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116)
@@ -264,7 +265,7 @@ const parseTailwindClasses = async (classes: string) => {
264265 }
265266 ) ;
266267 }
267- adaptBreakpoints ( parsedStyles ) ;
268+ parsedStyles = adaptBreakpoints ( parsedStyles ) ;
268269 const newClasses = classes
269270 . split ( " " )
270271 . filter ( ( item ) => ! generated . matched . has ( item ) )
@@ -353,7 +354,7 @@ export const generateFragmentFromTailwind = async (
353354 } ;
354355 const createOrMergeLocalStyles = (
355356 instanceId : Instance [ "id" ] ,
356- newStyles : Omit < ParsedStyleDecl , "selector" > [ ]
357+ newStyles : StyleDecl [ ]
357358 ) => {
358359 const localStyleSource =
359360 getLocalStyleSource ( instanceId ) ?? createLocalStyleSource ( instanceId ) ;
0 commit comments