unocss / unocss

The instant on-demand atomic CSS engine.
https://unocss.dev
MIT License
16.31k stars 822 forks source link

proposal: support dynamic layer (and layer order?) in rules #4131

Open olets opened 1 week ago

olets commented 1 week ago

Clear and concise description of the problem

I'd like to be to have rules which put CSS in an arbitrary layer based on the class name, and set the layer order based on the class name. Just like we can currently use the rule regex to set parent, selector, property, and value, I'd like to be able to use the rule regex to set layer and to set that layer's order.

Suggested solution

I've thought of three possible solutions

  1. Have the layers config generate a block at-rule. Supports a subset of possible needs

    export default defineConfig({
      layers: {
        stuff: 1,
        things: 2,
      },
      rules: [
        [
          /^m-([a-z]+)-(\d+)$/,
          ([, layer, size], { symbols }) => ({
            [symbols.parent]: `@layer ${layer}`,
            margin: `${size / 4}rem`
          }),
        ],
      ],
      // must leave `outputToCssLayers` false
    })
    <div class="m-stuff-4"></div>
    + @layer stuff, things;
    
    @layer stuff {
      .m-stuff-4 {
        margin: 1rem;
      }
    }

    Downsides:

    • symbols.parent can't be used for anything else in the same rule
    • outputToCssLayers can't be used (from UnoCSS's perspective, the CSS is in the default layer)
    • layer ordering can't itself be dynamic

    Benefits:

    • smallest change
  2. Teach symbols about layers. 🌟 I like this forth best

    export default defineConfig({
      layers: {
        stuff: 1,
        things: 2,
      },
      rules: [
        [
          /^m-([a-z]+)-(\d+)$/,
          ([, layer, size], { symbols }) => ({
            margin: `${size / 4}rem`
          }),
        ],
      ],
      // optional: add `outputToCssLayers: true,`
    })
    <div class="m-stuff-4"></div>
    /* layer: stuff */
    .m-stuff-4 {
      margin: 1rem;
    }

    Downsides:

    • Two handles for controlling layer, one in the rule's second argument and one in the third argument.

    Benefits:

    • can use symbols.parent for other things
    • can choose to use outputToCssLayers or not
    • potential for teaching symbols.layers about order 🌟🌟 I like this third best
      export default defineConfig({
        layers: {
          stuff: 1,
          things: 2,
        },
        rules: [
          [
            /^m-([a-z]+)(-\d+)?-(\d+)$/,
            ([, layer, layerOrder, size], { symbols }) => {
              return {
                [symbols.layer]: [layer, layerOrder], // would also accept `[symbols.layer]: [layer]`. maybe also accept `[symbols.layer]: layer`? or maybe use an object instead?
                margin: `${size / 4}rem`,
              }
            },
          ],
        ],
        // optional: add `outputToCssLayers: true,`
      })
      <div class="m-stuff-4"></div>
      <div class="m-other-0-4"></div>
      <div class="m-more-3-4"></div>
      /* layer: other */
      .m-other-4 {
        margin: 1rem;
      }
      /* layer: stuff */
      .m-stuff-4 {
        margin: 1rem;
      }
      /* layer: more */
      .m-stuff-4 {
        margin: 1rem;
      }

      Would have to decide whether layers listed in the layers config get special treatment. I think probably no: simply override the config… In the above example, adding <div class="m-stuff-4-4"></div> would move the "stuff" layer to order 4.

  3. Support dynamic layer values in rules' third arguments. Like the second argument, the rule regex's capture groups are available. 🌟🌟🌟 I like this second best
    export default defineConfig({
      layers: {
        stuff: 1,
        things: 2,
      },
      rules: [
        [
          /^m-([a-z]+)-(\d+)$/,
          ([,, size]) => ({
            margin: `${size / 4}rem`,
          }),
          ([, layer]) => ({
            layer: layer,
          }),
        ],
        // third argument would still accept the current shape
        [
          /^test$/,
          {
            margin: '1rem',
          },
          {
            layer: 'things',
          },
        ],
      ],
      // optional: add `outputToCssLayers: true,`
    })
    <div class="m-stuff-4"></div>
    /* layer: stuff */
    .m-stuff-4 {
      margin: 1rem;
    }

    Benefits

    • only one handle for controlling layer, in the rule's third argument
    • can use symbols.parent for other things
    • can choose to use outputToCssLayers or not
    • potential for teaching symbols.layers about order 🌟🌟🌟🌟 I like this best
      export default defineConfig({
        layers: {
          stuff: 1,
          things: 2,
        },
        rules: [
          [
            /^m-([a-z]+)(-\d+)?-(\d+)$/,
            ([,,, size]) => ({
              margin: `${size / 4}rem`,
            }),
            ([, layer, order]) => ({
              layer: [layer, order], // would also accept `layer: [layer]`. maybe also accept `layer: layer`? or maybe use an object instead?
            })
          ],
          // third argument would still accept the current shape
          [
            /^test$/,
           {
              margin: '1rem',
            },
            {
              layer: 'things',
            },
          ],
        ],
        // optional: add `outputToCssLayers: true,`
      })
      <div class="m-stuff-4"></div>
      <div class="m-other-0-4"></div>
      <div class="m-more-3-4"></div>
      /* layer: other */
      .m-other-4 {
        margin: 1rem;
      }
      /* layer: stuff */
      .m-stuff-4 {
        margin: 1rem;
      }
      /* layer: more */
      .m-stuff-4 {
        margin: 1rem;
      }

      Again, would have to decide whether layers listed in the layers config get special treatment. I think probably no: simply override the config… in the above example, adding <div class="m-stuff-4-4"></div> would move the "stuff" layer to order 4.

Alternative

No response

Additional context

No response

Validations

henrikvilhelmberglund commented 1 week ago

There is a variant for setting the CSS layer: play

I think changing the order of layers in rules would be confusing. It's an interesting feature though.

olets commented 1 week ago

I think changing the order of layers in rules would be confusing.

It's not something that everyone will want or need, but it's a CSS feature https://developer.mozilla.org/en-US/docs/Web/CSS/@layer#description and in theory is already supported by UnoCSS https://unocss.dev/config/layers#ordering.

There is a variant for setting the CSS layer

Thanks for sharing that example! I didn't know about preset-mini's layer-…: variant. It has the same shortcoming as the rules: because it modifies the parent selectors (https://github.com/unocss/unocss/blob/main/packages/preset-mini/src/_variants/misc.ts#L33) rather than using UnoCSS's layers system (notice the /* layer: default */ comment in your example's output CSS), it can't be combined with outputToCssLayers: true and the layer order can't be controlled with the layers config key.

Here's an illustration of the difference between using the layers system and modifying the parent to approximate layers play

I've updated this feature proposal to cover rules and variants.

henrikvilhelmberglund commented 1 week ago

It's not something that everyone will want or need, but it's a CSS feature https://developer.mozilla.org/en-US/docs/Web/CSS/@layer#description and in theory is already supported by UnoCSS https://unocss.dev/config/layers#ordering.

That's pretty much my point, there's already a place to set the order and being able to change the order for all utilities under a certain layer within a single rule seems like it would make it too easy to break things by mistake.

I also found another variant (not sure how I missed this one 🀯: uno-layer

olets commented 6 days ago

I also found another variant

Nice find, so there's undocumented support for setting layer in a variant. A minimal example:

    (matcher) => {
      const matched = matcher.match(/^my-layer-(.+)?:(.+)$/)

      if (!matched) {
        return
      }

      const [, layer, rest] = matched

      return {
        matcher: rest,
        layer,
      }
    }

play

Reverted this issue back to being about rules, not rules and variants.

there's already a place to set the order

I think we're talking about different things. The "layer" we can assign via a rule is not a layer in the same sense as preset-mini's default and shortcuts layers. It's a modification of the parent; the styles are put in the default layer. The distinction matters with the configuration outputToCssLayers: true.

henrikvilhelmberglund commented 5 days ago

I think we're talking about different things. The "layer" we can assign via a rule is not a layer in the same sense as preset-mini's default and shortcuts layers. It's a modification of the parent; the styles are put in the default layer. The distinction matters with the configuration outputToCssLayers: true.

I don't really understand, eg. play, from what I can tell your proposal 3 would allow a user to change the order via a rule so for example test2 to 0 even though it is specified in layers: as layer 3 which imo isn't great. Setting an order value when the layer isn't listed in layers would be fine for me though.

MrFoxPro commented 6 hours ago

proposal

I'm not sure this is exactly related, but what I found is that custom specified layers are not sorted properly when used in combination with media queries and other things.

One thing I found that seems like fixes that is:

const v_layers = new Array(10).fill(null).reduce((p, _, i) => ({ ...p, [`v${i + 1}`]: i + 1 }), {})
export default defineConfig({
    layers: v_layers,
        postprocess: [
        (util) => {
            const layer_name = util.parent?.split("@layer ")[1]?.split(" ")[0]
            let v_layer_order: number
            if (v_layer_order = v_layers[layer_name]) {
                util.layer = layer_name
                util.sort = v_layer_order
            }
        },
    ],
})
layer-v3:xl:(bg-red-8 hover:c-blue) layer-v1:bg-green layer-v2:md:bg-yellow

will place v1, v2, v3 in correct order