Skip to content

Latest commit

 

History

History
 
 

frontend-shared

Frontend Shared

This package contains components and other code (such as WindiCSS config) that is shared between the app (Cypress web app) and launchpad (Cypress Electron app) packages. Any functionality that is intended to be the same in both can be added here and imported in those packages as needed. Base components like form inputs, cards, and modals, are written here, as well as higher-level components that exist in both apps, like the header.

Conceivably, other packages may be created that also import from this shared component package.

Building

For development

In this package, we use Cypress Component Tests to develop the components in isolation, and no E2E tests. E2E tests should be written in the packages that consume these components (app and launchpad). This means that there is no app to visit for development, instead, we open Cypress:

## from repo root
yarn workspace @packages/frontend-shared cypress:open

Developing

For the best development experience, you will want to use VS Code with the Volar extension. This will give you type completion inside vue files.

Testing

## from repo root
yarn workspace @packages/frontend-shared cypress:run:ct

Utility class usage

WindiCSS can create an awesome interactive summary showing our usage of utility classes and design tokens. Running this command will generate this report and serve it on localhost.

## from this directory
yarn windi

This will be useful from time to time so that we can audit our usage of these classes and extract repeated patterns into Windi shortcuts or otherwise consolidate them, when it makes sense to do so.

Link Components

There are two shared components involved with links - BaseLink, and ExternalLink. BaseLink is responsible for default colors and hover/focus styles. ExternalLink wraps BaseLink is responsible for managing the GraphQL mutation that triggers links to open the in the user's default browser.

Generate the theme for shiki

See the readme in the src/public/shiki/themes directory

Front-end Conventions, Underlying Ideas, and Gotchas

These apply to this package, app, and launchpad, as well as any future work in this Vue-Tailwind-GQL stack. The goal is for this to provide useful context for new developers adding features to the codebase, or making changes to existing features. There are pros and cons to all of these decisions, but rather than get into those in detail, this is just a document of what practices we are following.

Development Workflow

We recommend component-based test driven development. More details in the Testing Practices guide. To make changes to an existing component:

  1. Open Cypress and go to the spec that covers the component (often it's 1:1 but sometimes components are tested via their parents)
  2. Update the test to reflect the desired change (or part of it)
  3. Implement the change in the component
  4. Add Percy snapshot for any new unique states covered by the change

To create a new component:

  1. Add a component spec file and the component file itself as siblings in the desired location
  2. In the spec file, import and mount the component. If the component depends on a GQL fragment, use mountFragment to mount the component so it can receive test data through the gql prop.

Vue 3

If you are new to Vue 3, there are some new features we are using in this codebase that you should become familiar with.

But first, if you are coming from React to Vue 3, here's a small potential gotcha to note as you read and write Vue code: the idea of a ref in Vue is similar to a ref in React but with a major difference. In React, when a ref's value changes, it doesn't trigger an update, or get "noticed" at all, by default. In Vue, a ref is part of the reactivity system and when the value updates, the component knows this and the updated value is reflected wherever the value is referenced. This can mean DOM updates, watchers firing, etc.

Here are some features of Vue 3, and packages in the ecosystem, that are worth knowing about as we work in the codebase.

Composition API and <script setup>

We are using the Composition API and specifically the <script setup> syntax in our Vue Single File Components (SFCs). This removes a lot of boilerplate, and because of that it's not always obvious reading the code which Vue features are being leveraged, compared to Vue 2.

If you are familiar with the Options API, which was the main way to write component script sections in Vue 2, the separation of variables and functions into named parts of the Options object like computed and data provided a familiar, but unwieldy, structure in each component. The Composition API lets us use those features anywhere we like, without dividing things into a predefined structure. <script setup> is a way to write Composition API code with less boilerplate and some other advantages described in the docs.

TS-friendly, modularized state management - this is what we use instead of Vuex for the small amount of global state we have.

Broad collection of composable utilities that provides reactive values for various common events and DOM properties, CSS rules, local storage and a lot of other stuff. This library exposes many common low-level event listeners, exposing them to Vue and managing the necessary setup/teardown. It's like a front-end lodash, so where the VueUse implementation of something works for us, we prefer to use it instead of roll our own.

We are using some components from Headless UI as the basis for UI patterns like modals, custom dropdowns, and expanding panels. We use Headless UI because it is well documented and the accessibility features are properly thought out. These advantages outweigh the occasional workarounds we have to use in order to get sophisticated behavior working that Headless UI does not support.

Router

Only @packages/app has a router, so details are described in its README.

Styles

Tailwind and Windi

We use Tailwind through WindiCSS. The codebase is utility-driven and all CSS that can be achieved through utility classes is written that way. The main way to reuse CSS in multiple places is to extract a component that applies the utility classes and can wrap other elements as needed.

Explicit Pixel Values

WindiCSS can create CSS classes as build time based on what class names we use in our components. That means syntax like this will work:

<p class="p-20px">

This allows us to specify explicit pixel values for measurements. We follow this pattern throughout the Cypress App codebase.

As an example: instead of using the class m-2 which applies the rule margin: 0.5rem in Tailwind and usually creates a margin of 8px (with 16px font size), we write the class as m-8px, from which Windi will generate a class with the rule margin: 8px.

Icons

Custom Icon Library

Cy has a very custom icon library, to meet the following needs:

  • Most of our icons are duo-tone
  • They must be styled with different colors in different contexts
  • Since they're duotone, you want to target the specific strokes and fills of the SVGs to color them
  • We should be able to apply color styles to icons with the same WindiCSS approach we use for other styles - meaning we can write dynamic classes and use prefixes like hover: or group-focus: to change the colors.
  • We don't want to import icons in Vue SFCs for basic use in templates, they should 'just work'.

Adding new icons

To add a new icon:

  1. Export the icon from Figma (all icons should come from the design system in Figma) as an SVG.
  2. Add the SVG to the icons folder.
  3. Name the file following the existing convention in there: icon-name_x[size in pixels], e.g. arrow-down_x16.svg.
  4. Manually edit the SVG file to add classes icon-dark and icon-light to the dark and light internal elements, and save the file. If an icon path doesn't define a class, nothing bad will happen, it just won't get targeted by any styling. light and dark refer to the 2 main colors present in a duotone icon, since often there is a "light" and "dark" color in the design. These can be though of as primary and secondary color, color1, color2, or anything else.
  5. Finally, you don't need to expose anything. ./src/assets/icons is automatically watched and loaded 😮

Now the icon is ready to be used in Vue SFC templates like this:

/* This just works. No imports necessary */
<i-cy-path-to-icon_x16 />

This is possible through the use of the auto-importing feature of unplugin-icons, which is set up in vite.config.ts.

Importing Icons

To use an icon in tests, or to refer to it in the <script> block of a component, import it this way:

import MyIcon from '~icons/cy/path-to-icon_x16'

Styling Icons

This example renders a book icon from ./src/assets/book_x16.svg and makes it pink for the 'light' color and purple for the 'dark' color. It uses the hover: pseudoclass to invert the light/dark colors on hover. A class string formatted like this:

icon-light + -any-color-100

will be used to target paths and strokes inside the SVG that have the class icon-light and apply the specific color to those elements.

<i-cy-book_x16 class="
  icon-light-pink-100 
  icon-dark-purple-500
  hover:icon-light-purple-500
  hover:icon-dark-pink-100
" />

Implementation: Custom classes w/ a WindiCSS plugin

To support selecting specific paths while keeping Tailwind's incredibly helpful interaction helpers (e.g. group-hover or group-focus), we use a WindiCSS plugin. Windi configuration lives in the windi.config.ts file in this package.

Accessibility

We consider accessibility a core part of front-end code quality. When possible, components should be built out using standard semantic HTML elements. If there are no plain HTML solutions for a particular interaction, we can reach for a library (like HeadlessUI above) that implements known patterns in an accessible way. In rare cases we will augment our HTML with ARIA roles. Tests should use the accessible name or label for interactive elements as described in the testing guide.

The Accessibility Tree

The Accessibility Tree available in your browser's dev tools will show how the nature, structure, and labelling of the elements in the DOM is presented to assistive technology. We can use this to explore the accessibility of our components, especially when creating or reviewing more complex UI interactions, to make sure the content that appears there makes sense.

GraphQL in Vue Components

GraphQL is the main source of data and state from the server in the app and launchpad. In their <script> block, Vue components describe the data they will receive in Queries or Fragments, as well as the Mutations they will trigger, which can set data on the server or trigger side effects like launching a project and opening the the user's browser.

Our GraphQL frontend client is urql using the urql-vue package. This provides composables like useQuery and useMutation to simplify interacting with GraphQL.

By convention, we use a prop named gql to represent the source of data, so it is common to see things like props.gql.currentProject.

Use of GraphQL by shared components is limited to src/gql-components

In the long run, files in the src/components directory are intended as the foundation of a design system. As such they may be used in many contexts other than the Cypress App and Launchpad. There are some components that are only intended to be shared between App and Launchpad and make use of GraphQL queries and mutations. These will only work correctly if placed within src/gql-components directory, because only that directory is specified in graphql-codegen.yml. This is intended to maintain the separation between genuinely reusable components driven by props and events, and gql-driven components that are tightly bound to the implementation of App and Launchpad.

Generating Fixtures

Some components need spec JSON fixtures to test them. generate-stub-specs script helps you generate them.

# inside frontend-shared project
yarn generate-stub-specs <filename> <n> <baseTypeName> [--app]
  • filename is the name of the file you want to create.
  • n is the number of stub specs to be created in the file.
  • baseTypeName is Spec or FileParts.
  • --app option creates fixtures inside the app package. Without it, they're added inside the frontend-shared package.