Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feature] Dynamic variants #721

Closed
wants to merge 20 commits into from

Conversation

ch99q
Copy link

@ch99q ch99q commented Aug 11, 2021

This introduces a new set of features in stitches called dynamic variants.

The purpose of this is to allow dynamic props to apply CSS values at static, server, and client-side.

Example of dynamic spacer component.

export const Spacer = styled("div", {
  $$spaceHeight: 0,
  $$spaceWidth: 0,
  
  minHeight: "$$spaceHeight",
  minWidth: "$$spaceWidth",

  variants: {
    size: (size: Stitches.ScaleValue<'space'>) => {
      $$spaceHeight: size,
      $$spaceWidth: size,
    },
    height: (value: Stitches.ScaleValue<'space'>) => {
      $$spaceHeight: value,
    },
    width: (value: Stitches.ScaleValue<'space'>) => {
      $$spaceWidth: value,
    }
  }
})

@ch99q ch99q changed the title [Feature] Dynamic variants [WIP] [Feature] Dynamic variants Aug 11, 2021
@codesandbox-ci
Copy link

codesandbox-ci bot commented Aug 11, 2021

This pull request is automatically built and testable in CodeSandbox.

To see build info of the built libraries, click here or the icon next to each commit SHA.

Latest deployment of this branch, based on commit 7e656fc:

Sandbox Source
Stitches CI: CRA Configuration
Stitches CI: Next.js Configuration

@peduarte
Copy link
Contributor

I cant wait to dig into this. We'll do so after v1. 🔥

@ch99q ch99q marked this pull request as ready for review September 2, 2021 06:40
@ch99q ch99q marked this pull request as draft September 2, 2021 06:51
@ch99q ch99q marked this pull request as ready for review September 2, 2021 06:51
@ch99q ch99q marked this pull request as draft September 2, 2021 06:52
@jonathantneal
Copy link
Contributor

It looks like there are still some conflicts to work out. Starting with a clean copy of stitches.d.ts and restoring your modifications (if any) from there might be easier. Let me know if you’d like some additional help. 😄

@ch99q
Copy link
Author

ch99q commented Sep 5, 2021

Thanks for the help @jonathantneal, really helped with the typings!

Okay so we finally got the typings to work properly, now I just have to ensure the tests run perfectly.

Well, I have a discussion with myself about the arguments of a function, but I realize this also includes utility functions.
So lets say we have a function e.g.

variants: {
    size: (value: Stitches.ScaleValue<'space'>) => ({
        width: value,
        height: value
   })
}

This should result in a component with the prop size="$small" a single argument. But what if a function has more than a single argument like.

variants: {
    translate: (x: number, y: number) => ({
        translate: `${x}px, ${y}px`
   })
}

Currently, the typings only support the first argument as prop value (this also applies to utils), but should we look into allowing for multiple arguments through translate={[100, 200]}?

It will not be a part of this pull request, but a general question about the future of utility functions and dynamic variants.

@pleunv
Copy link

pleunv commented Oct 3, 2021

Just started getting my feet wet with Stitches, coming from a rather large emotion + styled-system codebase. One of the first things I was also looking for was a way to map variants to a whole scale of theme tokens or valid set of css properties without having to manually map every individual value so that we could easily port over our primitives. As far as I can tell there is currently no way to achieve this, which would make this a rather cumbersome migration (and possibly also maintenance) effort.
I think this is what this proposal is suggesting so definitely a thumbs up from me as well.

edit: I guess that mapping the theme tokens could be accomplished with some utils that generate variants out of a theme definition, like:

type SpaceScale = 's00' | 's01' | 's02' | 's03' | 's04';

type SpaceTheme = Record<SpaceScale, number | string>;

const space: SpaceTheme = {
  s00: 0,
  s01: '0.25rem',
  s02: '0.5rem',
  s03: '0.75rem',
  s04: '1rem'
};

type SpaceVariant = Record<SpaceScale, CSSProperties>;

const createGapVariant = (theme: SpaceTheme): SpaceVariant =>
  Object.keys(theme).reduce(
    (acc, key) => ({ ...acc, [key]: { gap: `$${key}` } }),
    {} as SpaceVariant
  );

const Grid = styled('div', {
  display: 'grid',
  variants: {
    gap: createGapVariant(space)
  }
});

Which should have this as output:

const Grid = styled('div', {
  display: 'grid',
  variants: {
    gap: {
      s00: { gap: '$s00' },
      s01: { gap: '$s01' },
      s02: { gap: '$s02' },
      s03: { gap: '$s03' },
      s04: { gap: '$s04' }
    }
  }
});

Just not sure how well that'd play with TypeScript, suppose it should be fine. Could be turned into a generic function for some reusability as well. Very roughly something like:

const { styled, css, config, theme } = createStitches({ ... });

type CSS = Stitches.CSS<typeof config>;
type Theme = typeof theme;

type TokenByScaleName<ScaleName extends keyof Theme> = Prefixed<'$', keyof Theme[ScaleName]>;

type ScaleVariant<ScaleName extends keyof Theme> = Record<keyof Theme[ScaleName], CSS>;

type GetCss<ScaleName extends keyof Theme> = (token: TokenByScaleName<ScaleName>) => CSS;

function createScaleVariant<ScaleName extends keyof Theme>(
  scaleName: ScaleName,
  getCss: GetCss<ScaleName>
): ScaleVariant<ScaleName> {
  return Object.keys(theme[scaleName]).reduce(
    (acc, key) => ({ ...acc, [key]: getCss(`$${key}` as TokenByScaleName<ScaleName>) }),
    {} as any
  );
}

Which can then be used like this:

export const Box = styled('div', {
  boxSizing: 'border-box',
  variants: {
    mb: createScaleVariant('space', token => ({ marginBottom: token })),
    ml: createScaleVariant('space', token => ({ marginLeft: token })),
    mr: createScaleVariant('space', token => ({ marginRight: token })),
    mt: createScaleVariant('space', token => ({ marginTop: token })),
    mx: createScaleVariant('space', token => ({ marginLeft: token, marginRight: token })),
    my: createScaleVariant('space', token => ({ marginBottom: token, marginTop: token })),
    pb: createScaleVariant('space', token => ({ paddingBottom: token })),
    pl: createScaleVariant('space', token => ({ paddingLeft: token })),
    pr: createScaleVariant('space', token => ({ paddingRight: token })),
    pt: createScaleVariant('space', token => ({ paddingTop: token })),
    px: createScaleVariant('space', token => ({ paddingLeft: token, paddingRight: token })),
    py: createScaleVariant('space', token => ({ paddingBottom: token, paddingTop: token }))
  }
});

<Box mx="s04" py="s02">...</Box>

Or am I missing something here and is this really not the way Stitches is intended to be used?

@joe-bell
Copy link

joe-bell commented Oct 4, 2021

@pleunv love that createScaleVariant concept – that's exactly the kind of API I'm after (see #816) 💪

@peduarte
Copy link
Contributor

peduarte commented Oct 4, 2021

@pleunv replied here #816 (comment)

@gmlnchv
Copy link

gmlnchv commented Oct 6, 2021

Any update here? Is this being considered? Thanks!

@peduarte
Copy link
Contributor

peduarte commented Oct 6, 2021

Yeah, it is being considered but unfortunately we're currently working on other issues related to injection order, and they're higher in our priority list.

I'll keep this updated as I have more updates

@EfstathiadisD
Copy link

EfstathiadisD commented Oct 15, 2021

Very, very keen to have this baked in. I understand the issue with performance, but it is probably solvable. I am also using a variant of createScaleVariant, mainly on a <Button /> and <Label /> components, to determine compoundVariants. TS worked perfectly btw.

@darklight9811
Copy link

@ch99q, the package contributions have stopped, any updates on when this feature will be released?

@ch99q
Copy link
Author

ch99q commented Oct 24, 2021

@ch99q, the package contributions have stopped, any updates on when this feature will be released?

Waiting to see what the stitiches team say, but they seem to look after alternatives

@peduarte
Copy link
Contributor

@darklight9811 we're currently working on a relatively big feature related to injection order. Nothing will happen until that's done. Sorry, but we need to prioritise what's more important. Dynamic variants was not in the Stitches spec on purpose.

This is not to say we're not opened to it. It will be considered with an open mind.

@darklight9811
Copy link

@peduarte. I'm not complaining about prioritizing what is important. I'm just concerned that the main branch has not received any updates yet and there is no clear roadmap of features to be implemented or bugs to be fixed, I'm afraid of this package being deprecated.

But about the dynamic variants, I'm really interested in them because I need them in my project, using static variants would increase file sizes considerably (about 50-60%), so I really wanted them (besides I would like to use runtime color transformations so I wouldn't have to serialize every value beforehand).

@pleunv
Copy link

pleunv commented Oct 25, 2021

I think the maintainers have been pretty clear regarding this particular request and also regarding the status of the project and the process for requesting and discussing new functionality.

I'm afraid of this package being deprecated.

No need to jump to conclusions and spread FUD just because people haven't been pushing code for a few weeks. Stitches just had its first major release and there's plenty of activity on issues and twitter. Give them a break.

@Tatametheus
Copy link

Actually, I think this is not very import. Using css and utils partially solves the problem.
The only thing I cant find way to do is...

  variants: {
    col: {
      auto: {
        flex: '0 0 auto',
        width: 'auto',
      },
      1: { flex: '0 0 auto', width: '8.333333333333332%' },
      2: { flex: '0 0 auto', width: '16.666666666666664%' },
      3: { flex: '0 0 auto', width: '25%' },
      4: { flex: '0 0 auto', width: '33.33333333333333%' },
      5: { flex: '0 0 auto', width: '41.66666666666667%' },
      6: { flex: '0 0 auto', width: '50%' },
      7: { flex: '0 0 auto', width: '58.333333333333336%' },
      8: { flex: '0 0 auto', width: '66.66666666666666%' },
      9: { flex: '0 0 auto', width: '75%' },
      10: { flex: '0 0 auto', width: '83.33333333333334%' },
      11: { flex: '0 0 auto', width: '91.66666666666666%' },
      12: { flex: '0 0 auto', width: '100%' },
    },
    offSet: {
      1: { marginLeft: '8.333333333333332%' },
      2: { marginLeft: '16.666666666666664%' },
      3: { marginLeft: '25%' },
      4: { marginLeft: '33.33333333333333%' },
      5: { marginLeft: '41.66666666666667%' },
      6: { marginLeft: '50%' },
      7: { marginLeft: '58.333333333333336%' },
      8: { marginLeft: '66.66666666666666%' },
      9: { marginLeft: '75%' },
      10: { marginLeft: '83.33333333333334%' },
    },
  },

If there is a function, it would be simple..

@darklight9811
Copy link

@Tatamethues thats similar to what I did, but that is not really readable and can end up creating or really noisy code or unscalable code. And it becomes hardcoded in the bundle file, that can really end up increasing it.

@pleunv
Copy link

pleunv commented Oct 25, 2021

Actually, I think this is not very import. Using css and utils partially solves the problem. The only thing I cant find way to do is...

  variants: {
    col: {
      auto: {
        flex: '0 0 auto',
        width: 'auto',
      },
      1: { flex: '0 0 auto', width: '8.333333333333332%' },
      2: { flex: '0 0 auto', width: '16.666666666666664%' },
      3: { flex: '0 0 auto', width: '25%' },
      4: { flex: '0 0 auto', width: '33.33333333333333%' },
      5: { flex: '0 0 auto', width: '41.66666666666667%' },
      6: { flex: '0 0 auto', width: '50%' },
      7: { flex: '0 0 auto', width: '58.333333333333336%' },
      8: { flex: '0 0 auto', width: '66.66666666666666%' },
      9: { flex: '0 0 auto', width: '75%' },
      10: { flex: '0 0 auto', width: '83.33333333333334%' },
      11: { flex: '0 0 auto', width: '91.66666666666666%' },
      12: { flex: '0 0 auto', width: '100%' },
    },
    offSet: {
      1: { marginLeft: '8.333333333333332%' },
      2: { marginLeft: '16.666666666666664%' },
      3: { marginLeft: '25%' },
      4: { marginLeft: '33.33333333333333%' },
      5: { marginLeft: '41.66666666666667%' },
      6: { marginLeft: '50%' },
      7: { marginLeft: '58.333333333333336%' },
      8: { marginLeft: '66.66666666666666%' },
      9: { marginLeft: '75%' },
      10: { marginLeft: '83.33333333333334%' },
    },
  },

If there is a function, it would be simple..

It's a little hacky, but if it's just a 0..n range variant you could try something like this (adapted/extended from my createScaleVariant earlier in this PR - will need some base types from there):

type NumberRange<T extends number> = number extends T ? number : InnerNumberRange<T, []>;
type InnerNumberRange<T extends number, R extends unknown[]> = R['length'] extends T
  ? R['length']
  : InnerNumberRange<T, [T, ...R]> | R['length'];

type RangeVariant<Length extends number> = Record<NumberRange<Length>, CSS>;

export function createRangeVariant<Length extends number>(
  length: Length,
  get: (index: number) => CSS
): RangeVariant<Length> {
  return new Array(length).fill(0).reduce((acc, _, index) => {
    acc[index] = get(index);
    return acc;
  }, {} as any);
}

Based on this SO answer. Can probably be done better though. The difficulty here is creating a dynamic number range, which TypeScript can't really do well.
Use as:

export const Box = styled('div', {
  boxSizing: 'border-box',
  variants: {
    col: createRangeVariant(12, index => ({ flex: '0 0 auto', width: (100 / 12) * index }))
  }
});

and <Box col={5} />

@emadabdulrahim
Copy link
Contributor

emadabdulrahim commented Oct 28, 2021

Here's how I solved a similar problem for creating a component that can act as having a dynamic variant. You can do more powerful stuff that isn't just mapping 1:1 to a theme scale.

tl;dr

import "./styles.css";
import { styled, theme } from "./theme";

export const CoreVStack = styled("div", {
  "> * + *": {
    marginTop: "$$vStackSpace"
  }
});

export const VStack = ({
  space,
  ...props
}: {
  space: keyof typeof theme["space"];
  children: React.ReactNode;
}) => {
  return <CoreVStack css={{ $$vStackSpace: theme.space[space] }} {...props} />;
};

export default function App() {
  return (
    <VStack space="5">
      {Array.from({ length: 10 }, (_, i) => i + 1).map((index) => (
        <div key={index}>Item {index}</div>
      ))}
    </VStack>
  );
}

Code

https://codesandbox.io/s/stitches-dyanamic-variant-6hirg?file=/src/App.tsx:23-63

@Tatametheus
Copy link

@emadabdulrahim I'm not pro to typescript. That's why I used the silliest way. Thanks for your solution

@pstoica
Copy link

pstoica commented Nov 15, 2021

@emadabdulrahim This is the same pattern I came to before realizing there was a request for dynamic variants. I feel like having this in the docs as a recipe would be great.

While shorthand could be nice, I don't personally feel like I'm missing anything from stitches using static objects as long as I can use my token definitions as types.

@LucasUnplugged
Copy link
Contributor

Something I've found while working on a few sites with Stitches is that it's quite cumbersome to fudge this behaviour by creating a wrapper functional component and using a css prop in that wrapper — forwarding refs, merging the css prop, getting prop types for tokens etc. Because of this, use of the css prop becomes a necessary evil.

Understand the performance concern though, especially after coming from theme-ui where there are some serious performance issues on complex pages. Performance wise, is this much different from using the css prop? Is the main concern just that this encourages too much dynamic behaviour? I'd argue that the lack of this feature encourages overuse of the css prop in general — if I have to define some one-off flex settings for a Box via the css prop, I may as well add my padding/border/font/color styles there too.

So I was struggling with this kind of usage a bit at first; but since I'm creating a whole design system, with extensive theme tokens (including over 30 size/spacing tokens), I was ultimately able to use variants to cover most cases, without requiring this kind of runtime-centric solution.

For instance, here is a bit of code I'm producing, via Stitches-powered primitives (NOTE: my variants are based on styled-system):

      <Row alignItems="center">
        <Box h="7" w="6" bg="primary9" radiusLeft="4" mr="px" />
        <Box h="7" w="6" bg="secondary9" radiusRight="4" mr="3" />
        <Heading bold flat h="7" mb="1" color="secondary9">
          {title}
        </Heading>
      </Row>

Which produces this output:
Screen Shot 2021-12-11 at 3 19 44 PM

I've created some helper functions to help me generate sequences of variants, without writing them all out, but having them fully typed — e.g.:

    // Produces a sequence of 12 numbered fontSize variants, starting at 1
    fontSize: generateVariantSequence<Size>(12, (n: number) => ({
      fontSize: `$${n}`,
    })),

You can even extend it in some ways that really simplify some complex cases. For instance, I created a Grid primitive that's insanely powerful, yet really simple (and fully typed):

      <Grid columnFit="11" p="5" radius="2">
        {keys.map((key: number) => (
          <GridItem
            key={key}
            bg={`primary${key as ColorNumberKey}`}
            color={`textPrimary${key as ColorNumberKey}`}
            p="4"
          >
            {inner}
          </GridItem>
        ))}
        {keys.map((key: number) => (
          <GridItem
            key={key}
            bg={`secondary${key as ColorNumberKey}`}
            color={`textSecondary${key as ColorNumberKey}`}
            p="4"
          >
            {inner}
          </GridItem>
        ))}
      </Grid>

Which produces this output:
Screen Shot 2021-12-11 at 3 39 34 PM

The heavy lifting columnFill variant is coded as:

    columnFill: generateVariantSequence<FillVariant>(
      15,
      (n: number) => ({ gridTemplateColumns: `repeat(auto-fill, minmax($sizes$${n}, 1fr))` }),
      4
    ),

If you like the DX of something like Chakra, but want the performance and customizability of Stitches, I think this is a great solution.


Between something like what I'm doing, and the creation of your own primitives — which can map additional dynamic props to css={{...}}, I personally think it's best to actually discourage this feature (no offense to @ch99q's excellent and hard work).

One might argue and say, "well, you don't need to use it, even if they put it in", which is true, but...

  1. That's more JS that will get bundled and shipped, which many folks would have no use for.
  2. It will encourage using Stitches in a way that, to my understanding, was against to whole original premise. At that point, it becomes harder to argue your team should switch to Stitches for performance reasons, when there's a usage of it that will degrade performance as well (you'd have to introduce conventions against that... it's a whole mess).
  3. That usage will feel more natural to anyone coming from more dynamic, and less performant, styling libs, so Stitches would be contributing to the issue, instead of promoting better alternatives.

Anyhow, that's my 2 cents.

@fmal
Copy link
Contributor

fmal commented Dec 12, 2021

@LucasUnplugged really nice examples, would you mind sharing a codesandbox with the implementation of generateVariantSequence, the Grid component etc? Would be a nice reference for anyone wanting to leverage the full power of variants 😃

@LucasUnplugged
Copy link
Contributor

@LucasUnplugged really nice examples, would you mind sharing a codesandbox with the implementation of generateVariantSequence, the Grid component etc? Would be a nice reference for anyone wanting to leverage the full power of variants 😃

Yeah, I'd be happy to! I'll post here once it's ready.

@LucasUnplugged
Copy link
Contributor

LucasUnplugged commented Dec 15, 2021

As promised, here's a sandbox showcasing some of what I mentioned above; it's not a super complex example, and the typing isn't perfect (I spent quite a while on this in my design system), but it should do the trick:

https://codesandbox.io/s/stitches-variants-showcase-wtr3i

@nihgwu
Copy link

nihgwu commented Jan 11, 2022

I followed @emadabdulrahim 's idea and created this demo, though I'm not sure it's a good idea as it will generate a huge style for each component, as I have to use initial to prevent nested inherits which I think is an issue in @emadabdulrahim 's demo

@elsangedy
Copy link

const { config } = createStitches({
  theme: {
    colors: {
      primary: 'red',
    },
    space: {
      1: '',
      2: '',
      3: '',
    },
  },
});

type CSS = Stitches.CSS<typeof config>;

export const getVariant = <
  P extends keyof typeof config.theme,
  T extends keyof typeof config.theme[P],
  R extends Record<T, CSS>,
>(
  prop: P,
  map: (tokenValue: `$${T}`) => CSS,
): R => {
  const values = Object.keys(config.theme[prop]) as T[];
  return values.reduce<R>(
    (acc, tokenValue) => ({ ...acc, [tokenValue]: map(`$${tokenValue}`) }),
    {} as R,
  );
};

const Flex = styled('div', {
  boxSizing: 'border-box',
  display: 'flex',
  variants: {
    color: getVariant('colors', (tokenValue) => ({
      color: tokenValue,
    })),
    gap: getVariant('space', (tokenValue) => ({
      gap: tokenValue,
    })),
  },
});

// <Flex color="primary" gap={2} />

@rudeayelo
Copy link

@elsangedy seems like TypeScript has some issues with your example: https://codesandbox.io/s/priceless-shape-6i7ku?file=/src/App.tsx

❤️ the solution by the way

@f1lander
Copy link

f1lander commented Feb 15, 2022

const { config } = createStitches({
  theme: {
    colors: {
      primary: 'red',
    },
    space: {
      1: '',
      2: '',
      3: '',
    },
  },
});

type CSS = Stitches.CSS<typeof config>;

export const getVariant = <
  P extends keyof typeof config.theme,
  T extends keyof typeof config.theme[P],
  R extends Record<T, CSS>,
>(
  prop: P,
  map: (tokenValue: `$${T}`) => CSS,
): R => {
  const values = Object.keys(config.theme[prop]) as T[];
  return values.reduce<R>(
    (acc, tokenValue) => ({ ...acc, [tokenValue]: map(`$${tokenValue}`) }),
    {} as R,
  );
};

const Flex = styled('div', {
  boxSizing: 'border-box',
  display: 'flex',
  variants: {
    color: getVariant('colors', (tokenValue) => ({
      color: tokenValue,
    })),
    gap: getVariant('space', (tokenValue) => ({
      gap: tokenValue,
    })),
  },
});

// <Flex color="primary" gap={2} />

This was mt approach

import "./styles.css";
import type * as Stitches from "@stitches/react";
import { styled, theme, CSS, css } from "./stitches.config";

const { colors } = theme;

type TColors = {
  [K in keyof typeof colors]: { $$color: string };
};

const ColorsVariants = css({
  variants: {
    color: {
      inherit: {
        $$color: "currentColor"
      },
      ...Object.keys(colors).reduce(
        (prev, curr) => ({ ...prev, [curr]: { $$color: "$colors$" + curr } }),
        {} as TColors
      )
    }
  }
});

const SvgIcon = styled(
  "svg",
  {
    $$size: "1em",
    $$color: "$colors$black",
    lineHeight: "1em",
    verticalAlign: "middle",
    width: "$$size",
    height: "$$size",
    // here we actually select the path data with css
    "& path": {
      stroke: "$$color"
    },
    variants: {
      size: {
        xs: {
          $$size: "10px"
        },
        xl: {
          $$size: "$sizes$xl"
        }
      }
    }
  },
  ColorsVariants
);

type TSvgIcon = {
  css?: CSS;
};

const MyCoolIcon = (
  props: TSvgIcon & Stitches.VariantProps<typeof SvgIcon>
) => (
  <SvgIcon
    viewBox="0 0 10 10"
    fill="none"
    xmlns="http://www.w3.org/2000/svg"
    {...props}
  >
    <path
      d="M8.74997 1.25L1.25 8.74997M1.25 8.74997L8.75 8.75M1.25 8.74997L1.25003 1.25003"
      stroke="currentColor"
      strokeLinecap="round"
      strokeLinejoin="round"
    />
  </SvgIcon>
);

export default function App() {
  return (
    <div className="App">
      <h1>Hello CodeSandbox</h1>
      <h2>Start editing to see some magic happen!</h2>
      <MyCoolIcon size="xs" color={{ "@xs": "inherit", "@sm": "red" }} />
    </div>
  );
}

https://codesandbox.io/s/thirsty-satoshi-bbggf?file=/src/App.tsx:0-1652

I will try yours because seems more clever in matter to type the key of the locale token

@hadihallak
Copy link
Member

@ch99q Thanks for all of your hard work here.
I gave this some thought and I think like this feature can already be achieved using scoped tokens. we do however need to document these ways so I've created this issue (#1034)to track it.

I will be closing this for now but if anyone else have some more examples on dynamic values/ variants, post them in the issue that I linked as I will be creating a recipes section to document such usages

@hadihallak hadihallak closed this Jun 6, 2022
@hadihallak
Copy link
Member

in case anyone is interested in this, I did a little experiment to showcase how to handle highly dynamic variants using css custom properties and inline styles. you can check it out here

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature New feature
Projects
None yet
Development

Successfully merging this pull request may close these issues.