First of all, thank you for your interest in the library. We'd love to accept your fixes and improvements ✨
- Install the latest LTS version of Node.js.
- Install pnpm globally by running:
npm i -g pnpm@8
. - Set up commit signing for your contributions. Follow these steps:
Please note that our maintainers primarily develop on macOS. While developing on Linux or Windows is possible, there might be unintended issues. Don't hesitate to reach out for help if you encounter any problems.
-
Clone the repository by running:
git clone git@github.com:semrush/intergalactic.git && cd intergalactic
. -
Install project dependencies using:
pnpm install
.2.1 If you have and error with
libpng
likemake sure that libpng is installed
- you need to install it manually. On Mac OS the simplest way is to use homebrew:brew install libpng pkg-config
. -
Build components with:
pnpm build
.
Follow these steps to submit your changes:
- Fork the repository.
- Apply and commit your changes to your fork.
- Add a note to the appropriate changelog file (for example,
semcore/button/CHANGELOG.md
) for each change. Use the date when the changes were made for the new note. - Open a pull request to the main repository.
We appreciate your contributions and will review your pull request as soon as possible. Your efforts make a difference!
We have a robust playground designed for developing components. To get started:
- Run
pnpm start
and navigate tohttp://localhost:2077
in your browser to access the playground. - On the documentation website, copy example code by clicking the copy icon in the top-right corner of a code snippet. For instance, you can use the Input component example.
- Create a playground page using your favorite code editor (for example,
vi tools/playground/examples/input.tsx
) and paste the example code there. - Return to
http://localhost:2077
and reload the page. The link toinput.tsx
will appear in the top panel. Edit the playground page code, and after making changes, reload the page to see your modifications.
We use vitest for our testing needs.
- To prepare docker images for testing, use the command
pnpm test:setup
. - To start the test runner, use the command
pnpm test:docker
. - To execute tests and exit, use
pnpm test:docker run
. - To update snapshots, use the command
pnpm test:docker -- -u
. - To run tests for a specific component, use
pnpm test:docker button
. - To update snapshots for a specific component, use
pnpm test:docker button -- -u
.
We rely on biome for formatting and linting. It is integrated into our Git hooks and also offers a VS Code Extension. It's going to get IntelliJ Platform LSP very soon: biomejs/biome#185.
To preview the website locally, run pnpm website
. The site will be accessible at http://localhost:3000
. Keep in mind that you may need to reload the page to see any changes made to the documentation.
Ensuring the accessibility of our components is a priority. We conduct automated screen reader tests, focusing on VoiceOver screen reader in Safari on macOS. Here's how to set up and run these tests:
- To set up the environment, execute
pnpm vo-test:setup
. - Open
voiceOver Utility > General
on your mac and enable the setting "Allow VoiceOver to be controlled with AppleScript" - Run the tests using the command
pnpm vo-test
.
For rapid rebuilding of the playground and website, we use esbuild
to efficiently transform components on-the-fly. However, the build process for icon
and illustration
components is time-consuming and cannot be performed on-the-fly. Therefore, before running the playground or website, it's essential to pre-build icons and illustrations. You can accomplish this using the command pnpm build
(or pnpm build:icons
and pnpm build:illustration
).
To facilitate screenshot tests for components, we use our own screenshot service in the cloud. Occasionally, this service might yield slight pixel variations for the same code submitted to it. In such cases, you may need to restart CI/CD workflows several times until the tests pass. We acknowledge this issue and intend to address it in the future.
Certain components have text translations in multiple languages. When adding or modifying text, focus on English only. Following the pull request review, core maintainers will handle translations for other languages using Crowdin.
Our documentation website's foundation has undergone several iterations and may appear unconventional due to underlying changes. Knowing about its imperfections, we are actively working on migrating to VitePress for a more streamlined development experience.
The main way to deliver components is intergalactic
npm package.
Also each component is published as a distinct npm package, while a special @semcore/ui
package re-exports them collectively. The complex publishing process is fully automated through our CI/CD pipeline.
We rely on a set of design tokens to generate CSS variables (refer to semcore/utils/src/themes/default.css
). Although all components use these variables, for users it's not mandatory to declare them at the root level of the page. For proper component display, CSS variables' default theme is always included as a fallback value in the var
function (for example, color: var(--intergalactic-text-secondary, #6c6e79);
). After modifying the name of any CSS variable in component styles, running the pnpm process-theme
command is necessary. This command updates the fallback value in var
function and is integrated into the pre-commit hook.
Current components have performance issues, primarily tied to a 5000-character regular expression in semcore/utils/src/propsForElement.ts
and the declaration of class-based components.
Due to historical reasons, our code goes through specific Babel plugins (babel-plugin-root
and babel-plugin-styles
) that result in the final code functioning differently than it may initially appear. Below is a simplified example of the Link
component's code:
import React from "react";
import createComponent, { Component, Root, sstyled } from "@semcore/core";
import { Text } from "@semcore/typography";
import style from "./style/link.shadow.css";
class RootLink extends Component {
static style = style;
render() {
const SLink = Root;
const { styles, Children } = this.asProps;
return sstyled(styles)(
<SLink render={Text} tag="a">
<Children />
</SLink>
);
}
}
const Link = createComponent(RootLink);
export default Link;
After normal jsx to js transformation the following expression are getting returned:
sstyled(styles)(
React.createElement(
SLink,
{ render: Text, tag: "a" },
React.createElement(Children)
)
);
The babel-plugin-root
plugin ensures that all the props passed to a component are also passed to its root wrapper.
- The import of the
Root
variable from@semcore/core
is removed, as this variable is not really exported from the core. - The
Root
assignment expression is replaced with the value of thetag
prop. For instance,const SLink = Root;
is transformed intoconst SLink = 'tag';
. - The plugin identifies the component's root wrapper based on the variable assigned in the previous step.
- An import statement for the
assignProps
function is added, and this function is applied to the props of the root wrapper. The resulting code will look like the following:
sstyled(styles)(
React.createElement(
SLink,
assignProps({ render: Text, tag: "a" }, this.asProps),
React.createElement(Children)
)
);
The assignProps
function is responsible for smart props merging:
- It spreads the props.
- It merges
refs
andforwardRef
by encapsulating them within a single ref callback. - It spreads the value of the
style
prop. - It combines
className
props. - It wraps event handlers within a single event handler that sequentially calls each event handler. The sequence stops if any event handler returns
false
. Outer event handlers are invoked first.
While it's not trivial, this mechanism allows users to apply any props to components. For instance, in the Link
component, we don't need to explicitly pass href
, target
, rel
, or other crucial Link
props. These are automatically passed using assignProps
.
While this mechanism has limitations, two escape hatches are available:
- By adding
__excludeProps={['theme']}
, you can prevent thetheme
prop from being applied to the DOM node. - By adding
use:theme={modifiedTheme}
, you can apply a prop that takes precedence during prop assignment.
Long-term plans involve enhancing this mechanism for more flexible prop passing.
The babel-plugin-styles
plugin focuses on CSS transformations. For instance, consider the style file ./style/link.shadow.css
for the Link
component:
SLink {
display: inline-block;
font-family: inherit;
color: var(--intergalactic-text-link, #006dca);
&[active],
&:hover,
&:active {
color: var(--intergalactic-text-link-hover-active, #044792);
& SText {
border-color: currentColor;
}
}
&[enableVisited]:visited,
&[enableVisited]:visited:hover {
color: var(--intergalactic-text-link-visited, #8649e1);
}
}
This CSS file uses nesting and contains selectors targeting tag names used within the component code (for example, SLink
).
Here's how the plugin works:
- The plugin analyzes both the CSS and JS code of the component, identifying components prefixed with
S
. - It creates hashed CSS classes for the
SLink
component and for each attribute used as a selector in the CSS. - The plugin modifies the component's JS code to add a
className
like.SLink-xxx-enableVisited
if theenableVisited
prop is provided. This happens for each selector. - The import statement for the
.shadow.css
file is removed, and the CSS rules from the file are moved to the compiled JS output. These rules operate like CSS-in-JS, and they are incorporated into the page's stylesheets from the JS code.
If users prefer to include CSS in the final bundle in the traditional way, they can follow the instructions provided in this guide.
All code in this repository is under the MIT License. By sending a pull request you agree to the terms of contributing under the MIT license