Contribution

Learn on how to contribute to sigma-ui.

Introduction

If you'd like to contribute to this project, please take a moment to read this page before submitting your first pull request. It's also strongly recommended that you check for open issues and pull requests to see if someone else is working on something similar.

About this repository

This repository is a monorepo. It uses pnpm and workspaces for development.

Structure

It is structured as follows:

  1. packages - Contains source code for supporting nuxt as a module and the cli to add new components.

  2. apps/www - The main folder that holds the source code for the website and every sigma-ui component. This folder contains important sub-folders and is a subproject with its own package.json.

  3. .vitepress - Contains the configuration and source code for vitepress and the sigma-ui website.

  4. src - Hosts the main source code for every sigma-ui component or demo and their documentation on the website.

  5. __registry__ - Holds the registry file generated by scripts/build-registry.ts to serve components for the cli. This folder's content is auto-generated and should not be edited manually.

  6. scripts - Contains various helper scripts, such as build-registry.ts, which automatically generates the __registry__ folder.

  7. content - This folder holds all the documentation for the /docs route. Each component has one .md file documenting the installation and usage of the component.

  8. examples - Holds all examples not part of /docs, like the main page.

  9. lib/registry - The main folder hosts the source code for different styles of every component. This is likely the main folder you'll be changing.

Sigma UI comes with 2 different style systems for every component in sigma-ui:

  1. Tailwind
  2. CSS

Every component added to the repository must support both versions, including the main source code and associated demos.

When adding or modifying components, please ensure that:

  1. You make the changes for every style.
  2. You update the documentation.
  3. You run pnpm build:registry to update the registry.

Development

Start by cloning the repository:

bash
git clone [email protected]:sigma-hub/sigma-ui-test.git

Install dependencies

bash
pnpm install

Run a workspace

You can use the pnpm --filter=[WORKSPACE] command to start the development process for a workspace or some of the shortcut command that we have setup.

Examples

  1. To run the sigma-ui.dev website:
pnpm dev
  1. To run the sigma-ui cli package:
pnpm dev:cli

Documentation

The documentation for this project is located in the www workspace. You can run the documentation locally by running the following command:

bash
pnpm dev

Documentation is written using md. You can find the documentation files in the apps/www/src/content directory.

CLI

The sigma-ui package is a CLI for adding components to your project. You can find the documentation for the CLI here.

Any changes to the CLI should be made in the packages/cli directory. If you can, it would be great if you could add tests for your changes.

Testing

Tests are written using Vitest. You can run all the tests from the root of the repository.

bash
pnpm test

Please ensure that the tests are passing when submitting a pull request. If you're adding new features, please include tests.

Commit Convention

Before you create a Pull Request, please check whether your commits comply with the commit conventions used in this repository.

When you create a commit we kindly ask you to follow the convention category(scope or module): message in your commit message while using one of the following categories:

  • feat / feature: all changes that introduce completely new code or new features

  • fix: changes that fix a bug (ideally you will additionally reference an issue if present)

  • refactor: any code related change that is not a fix nor a feature

  • docs: changing existing or creating new documentation (i.e. README, docs for usage of a lib or cli usage)

  • build: all changes regarding the build of the software, changes to dependencies or the addition of new dependencies

  • test: all changes regarding tests (adding new tests or changing existing ones)

  • ci: all changes regarding the configuration of continuous integration (i.e. github actions, ci system)

  • chore: all changes to the repository that do not fit into any of the above categories

    e.g. feat(components): add new prop to the avatar component

If you are interested in the detailed specification you can visit Conventional Commits.

Wrapping Radix-Vue Components

Radix-Vue hosts many low-level UI components that are used to build reusable components. There are many cases that you need to wrap Radix-Vue components.

Props & Events

All of the Radix-Vue compoennts expose their prop and emit types. We need to forward any props/events that are coming from outside to the Radix-Vue component.

To do so, we have a helper function named useForwardPropsEmits that combines props and events that must be binded to the child radix component.

To be more clear, the function useForwardPropsEmits takes in props and an optional emit function, and returns a computed object that combines the parsed props and emits as props.

Here's an example from Accordion root component.

vue
<script setup lang="ts">
import {
  AccordionRoot,
  type AccordionRootEmits,
  type AccordionRootProps,
  useForwardPropsEmits,
} from 'radix-vue'

const props = defineProps<AccordionRootProps>()
const emits = defineEmits<AccordionRootEmits>()

const forwarded = useForwardPropsEmits(props, emits)
</script>

<template>
  <AccordionRoot v-bind="forwarded">
    <slot />
  </AccordionRoot>
</template>

As you can see, AccordionRootEmits and AccordionRootProps types are imported from radix, combined with useForwardPropsEmits and then are binded using v-bind syntax.

CSS Classes

There are cases when we want to accept class as a prop in our sigma-ui component and then combine it with a default tailwind class on our radix-vue component via cn utility function.

In these cases, we can not use v-bind, because this would lead in double class binding.

Take a look at DrawerDescription.vue.

vue
<script lang="ts" setup>
import type { DrawerDescriptionProps } from 'vaul-vue'
import { DrawerDescription } from 'vaul-vue'
import { type HtmlHTMLAttributes, computed } from 'vue'
import { cn } from '@/lib/utils'

const props = defineProps<DrawerDescriptionProps & { class?: HtmlHTMLAttributes['class'] }>()

const delegatedProps = computed(() => {
  const { class: _, ...delegated } = props

  return delegated
})
</script>

<template>
  <DrawerDescription v-bind="delegatedProps" :class="cn('text-sm text-muted-foreground', props.class)">
    <slot />
  </DrawerDescription>
</template>

As you can see, we have created a computed property named delegatedProps to remove class from props, and only then bind the returned value to our radix component (DrawerDescription in this case).

As for our class, we first declared it as type of HtmlHTMLAttributes['class'] and used cn to merge tailwind classes from class prop and our own classes.

This pattern only needs to be applied when the cn utility is required. For instances where there are no default Tailwind classes that need to be merged with user-provided classes, this pattern is not necessary. A good example of this is the SelectValue.vue component.

vue
<script setup lang="ts">
import { SelectValue, type SelectValueProps } from 'radix-vue'

const props = defineProps<SelectValueProps>()
</script>

<template>
  <SelectValue v-bind="props">
    <slot />
  </SelectValue>
</template>

Boolean Props

When you are building a wrapper for a component, in some cases you want to ignore Vue Props Boolean Casting. You can either set default value as undefined for all the boolean field, or you can use useForwardProps composable.

Take a look at AccordionItem.vue

vue
<script setup lang="ts">
import { type HTMLAttributes, computed } from 'vue'
import { AccordionItem, type AccordionItemProps, useForwardProps } from 'radix-vue'
import { cn } from '@/lib/utils'

const props = defineProps<AccordionItemProps & { class?: HTMLAttributes['class'] }>()

const delegatedProps = computed(() => {
  const { class: _, ...delegated } = props

  return delegated
})

const forwardedProps = useForwardProps(delegatedProps)
</script>

<template>
  <AccordionItem
    v-bind="forwardedProps"
    :class="cn('border-b', props.class)"
  >
    <slot />
  </AccordionItem>
</template>

Since AccordionItemProps type has atleast one boolean property, we need to use useForwardProps on the entire props object.

Note that useForwardPropsEmits use useForwardProps under the hood.

Component as Root

Whenever your root component is a Component Primitive from vue, it's easier to use Primitive instead.

Let's take a look at Button.vue

vue
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { Primitive, type PrimitiveProps } from 'radix-vue'
import { type ButtonVariants, buttonVariants } from '.'
import { cn } from '@/lib/utils'

interface Props extends PrimitiveProps {
  variant?: ButtonVariants['variant']
  size?: ButtonVariants['size']
  class?: HTMLAttributes['class']
}

const props = withDefaults(defineProps<Props>(), {
  as: 'button',
})
</script>

<template>
  <Primitive
    :as="as"
    :as-child="asChild"
    :class="cn(buttonVariants({ variant, size }), props.class)"
  >
    <slot />
  </Primitive>
</template>

You'll need to extend PrimitiveProps in your props to support Primitive component. In most cases you would also need a default value for as property.

Edit this page on GitHub