Thank you for following our style guide! The team asks that you familiarize yourself with this guide and follow it for any contributions. Doing so makes PRs and general code collaboration much more effective :)
We'll also link to this document in cases where these guidelines have not been followed. If that's what brought you here, no stress! Thanks for your interest and your drive to contribute to open-source and activist in particular! ❤️
If you have questions or would like to communicate with the team, please join us in our public Matrix chat rooms. We'd be happy to hear from you!
- Vue and Nuxt
- TypeScript
- Tailwind
- Common styles
- Formatting
- Colors
- Font
- Text size
- Localization
- Images and Icons
- Tab size
- Padding
Vue and Nuxt ⇧
The frontend for activist is written in the framework Vue.js and specifically the meta-framework Nuxt.js. The team chose Vue because of its broad usage across the development industry as well as relative ease of use and adoption for new contributors. Most of all we appreciate the structure that Vue adds to a project by leveraging the order of HTML and adding scripting and styling on top. Nuxt expands on Vue seamlessly and includes many modules to make development much easier.
Vue files (.vue
) are Single-File Components that have <template>
, <script>
and <style>
blocks. Conventions for writing Vue for activist include:
<template>
blocks should come first,<script>
second and<style>
last- The Vue Composition API should be used in all cases
- TypeScript should be used wherever possible within
<script>
blocks withdefineProps
- Self-closing components (
<Component />
) should be used for any component that doesn't have content- Generally if a component has a
<slot>
then this would imply that it would normally have content and thus require a closing tag
- Generally if a component has a
- Use
camelCase
for prop names for both declaration and within single file components - For element attribute order please use the following:
<element
v-attributes=""
@attributes=""
ref=""
key=""
id=""
class=""
:class="{}"
props=""
other-attributes=""
aria-label=""
></element>
Note
Put the aria label as the last attribute on any given element so it's easy to see if it's missing (aria-label
for as an HTML attribute and ariaLabel
as a component prop)
Please see the Vue.js style guide for general suggestions on how to write Vue files.
Page routing should use the <NuxtLink />
component wherever possible to assure that the platform maintains the localization path of the user. If an external link via an <a>
tag should be set, then please include target="_blank"
to open a new tab (unless it's an email href).
Beyond this, to update the UI we have a system of computed variables in place that are derived and emitted on each page to the base app and then drilled down to the layout and corresponding components. Please add the following lines to all script blocks in the pages
directory:
import useRouteToName from "~/composables/useRouteToName";
const emit = defineEmits(["routeToName"]);
useRouteToName(emit);
activist uses Tailwind for CSS, and some parts of components will be conditionally rendered based on Tailwind breakpoints, but we want to avoid using it to show and hide whole components. The reason for this is that using CSS in this way means that unneeded TypeScript for the hidden components will still run on page load. Please use useBreakpoint
for all conditional rendering of full components.
-
✅ No TS ran:
<template> <MyComponent v-if="aboveMediumBP" /> </template> <script setup lang="ts"> import useBreakpoint from "~/composables/useBreakpoint"; const aboveMediumBP = useBreakpoint("md"); </script>
-
❌ TS still ran:
<template> <MyComponent class="hidden md:block" /> </template>
TypeScript ⇧
PRs are always welcome to improve the developer experience and project infrastructure!
Currently typescript.strict
and typescript.typeCheck
in nuxt.config.ts
are not enabled. This may change in the future. Strict type checks are not enabled to allow building the app outside Docker
. Local and Netlify builds proceed despite TS errors with strict checks disabled.
Note
For VS Code users: it is recommended to install the following extension to enable in-editor type-checking:
- Create general frontend types in the frontend/types directory
- When typing Arrays, use
arrayElementType[]
rather than the generic typeArray<T>
unless extending:
const strArray: string[] = ["Thank", "you", "for", "contributing!"];
- activist uses the Composition API, so please implement
<script setup lang="ts">
and usedefineProps
with the generic type argument.
// No need to define `props` if we won't be accessing them in the `<script>` block.
const props = defineProps<{
foo: string;
bar?: number; // optionalProp?
}>();
- Type assignments should be lower case, so
string
instead ofString
- Use
withDefaults
when types require default values as in the following example:
export interface Props {
foo: string;
}
const props = withDefaults(defineProps<Props>(), {
foo: "default",
});
See Vue and TypeScript docs for more information about typing component props.
There is a limited set of package types that are available in the global scope. The current list can be found in frontend/tsconfig.json
under "compilerOptions.types"
, with this list being modified as the project matures.
Before opening a new PR, it is recommended to first generate the current types, then manually check those types:
- cd into
frontend
- run
yarn run postinstall
to generate types infrontend/.nuxt
- run
yarn nuxi typecheck
Within VS Code TS errors are visible, however, running these commands will help to ensure the new code does not introduce unintended TS errors at build time. Existing TS errors may be ignored. PRs are always welcome to address these errors!
Tailwind ⇧
activist uses Tailwind CSS for CSS styling and Headless UI unstyled, accessible components for more complex page elements like dropdowns and popups. Tailwind styles are applied via space-separated class="STYLE"
attributes on HTML elements in Vue <template>
blocks. Generally these class attributes should be the first applied to an element and thus proceed all Vue component props so differences from shared styling are apparent:
- ✅
<MyComponent class="STYLE" propName="value"/>
- ❌
<MyComponent propName="value" class="STYLE"/>
Please note that as activist uses Tailwind, this means that <style>
blocks are often times not used within Vue Single-File Components. <style>
blocks should only be used in cases where including the styles within the <template>
block would be overly complex or if Tailwind does not support a certain style parameter. The team understands that Tailwind at times can lead to very long style classes, but because of this we make use of the custom classes below to combine commonly used elements into consistent, responsive drop-in attributes.
Common styles ⇧
The following are custom Tailwind classes from frontend/assets/css/tailwind.css that are consistently used within the activist frontend codes:
-
focus-brand
- Creates a custom brand styled orange ring around an element when it is focussed for both light and dark mode
- Should be used on all elements that the user can focus (buttons, links, dropdowns, menu items, etc)
-
link-text
- Color and hover color are defined for links for both light and dark mode
-
card-style
- Applies styles for consistent cards across activist's pages
- Colors are defined for light and dark mode with border width and radius also being applied
- Used in cases like about page sections, search results, etc
Note
There's also custom styles available to make development easier such as bg-breakpoint-test
that changes the background of the element it's applied to based on the current breakpoint.
Formatting ⇧
The activist frontend uses Prettier to format the code and prettier-plugin-tailwindcss to sort Tailwind CSS classes. Backend code that's written in Python should be formatted using black. The team suggests that you set up your environment to autoformat using te these formatters on save. We have workflows to check formatting for pull requests and will notify you if something's wrong :)
Colors ⇧
The file frontend/tailwind.config.ts defines all colors within the colors
section of the theme
configuration. All brand colors are split first by light
and dark
mode in their names and then the general usage of the color followed by qualifiers such as hover
. The reason for this naming criteria is to avoid repeat styling keywords like text-text-light
that might lead to confusion or leaving it as just text-light
rather than applying the usage and then the color. The prior style would correctly be applied via text-light-text
.
Note that for all colors we need to apply both the light and dark mode variants. In Tailwind this is done by placing the dark:
prefix before a class. An example of this is the following where we'll set the background of an element to the header color for both light and dark mode:
<!-- This div has a background that reacts to the color mode. -->
<div class="bg-light-layer-2 dark:bg-dark-layer-2"></div>
Note further that Tailwind allows for alpha components for opacity to be applied to colors directly within the class declaration. We thus do not need to save versions of colors with transparency unless they are inherently used with an alpha less than one. An example of a color that has an inherent non-one alpha is light-text
("rgba(0, 0, 0, 0.85)"
). To apply an alpha component to a color in Tailwind you follow it with a slash and the alpha that should be used as in the following example:
<!-- The background of this div has 40% opacity. -->
<div class="bg-light-cta-orange/40"></div>
Font ⇧
The fonts for activist are Red Hat Text and Red Hat Display as defined in frontend/tailwind.config.ts. Red Hat Text
is applied throughout the website and Red Hat Display
is used for all headers by applying font-display
. As headers are generally defined by responsive-h#
custom classes that include font-display
, it will be rare that you'll need to apply it directly. See the next section for more details.
Text size ⇧
frontend/assets/css/tailwind.css defines custom combinations of default and activist defined Tailwind header sizes. Responsive header classes all have font-display
applied to them. The naming criteria of these headers follows that of HTML headers so that the team remembers that when a responsive-h#
tag is used that it should be applied to a coinciding <h#>
tag for accessibility. Note that headers should generally have a bold
style applied to them as well, with for example page headers being defined as follows:
<!-- The size and weight styles for page headers. -->
<h1 class="font-bold responsive-h1">Page Header</h1>
Localization ⇧
activist is a global platform and must function in countless different regions around the world. To achieve this, all strings on the platform must be defined using keys found in the i18n directory of the frontend.
Note
All keys should be defined within the en-US.json file
- This is the source from which all the other languages are translated from
- Edits to the other files should be made on activist's public localization project on Weblate
- Do not put the JSON dictionaries into different levels!
- The purpose of the flat dictionaries is so that we can search for the key in the codebase and easily find its uses and where it's defined
- Do not include periods in aria-labels (screen reader user will configure their own preferences for a hard stop)
Localization keys should be defined based on the file in which they're used within the platform and the content that they refer to (CONTENT_REFERENCE
below). Please use the following rules as a guide if you find yourself needing to create new localization keys:
- Separate directories and references by
.
and PascalCase/camelCase file name components by_
in keys- Ex:
"components.search_bar.CONTENT_REFERENCE"
for theSearchBar
component
- Ex:
- Even though Nuxt allows for us to nest components in directories, avoid repetition in the directory path used to define the localization key
- Ex: If you're defining a key within
CardAbout
:- ✅
"components.card_about.CONTENT_REFERENCE"
- ❌
"components.card.card_about.CONTENT_REFERENCE"
- ✅
- Ex: If you're defining a key within
- Define keys based on the lowest level file in which they're used
- Use
_global
to indicate that a key is used in multiple places in a given directory- Ex: You're creating a key that's used by multiple cards:
- ✅
"components.card._global.CONTENT_REFERENCE"
- ❌
"components.card.INDIVIDUAL_COMPONENT.CONTENT_REFERENCE"
- ✅
- Ex: You're creating a key that's used by multiple cards:
- Please end all aria-label keys with
_alt_text
so the localization team knows that they're for screen readers - If you need a capitalized and lower case version of a word, signify the lower case version with
_lower
at the end of the key - For pages with long texts please follow the below naming criteria:
"header"
: The main header (h1) of the given page"section_#"
: A section that iterates by one with every header and subheader"section_#_#"
: A subsection, with other#_#
patterns also being possible (see below)"section_#_subheader"
: Marks the start of a new section (h2 and beyond)"section_#_paragraph_#"
: A paragraph with one or more sentences"section_#_paragraph_#_#"
: A paragraph with separate parts to insert things like links"section_#_list_#_item_#"
: An item in a list"section_#_list_#_item_#_#"
: A subitem of the given item
- If there are different uses of the same value in one file, then alphabetically combine the final keys with dashes (ex:
header_title
) - Please alphabetize the keys, with your code editor likely having built in functionality for this
- Please always assign the full key as a string to assure that i18n content checks can pick up if the key has been used
- Eg:
section_1_2
and notsection_{var_number}_2
- This makes sure that content writers and the i18n team are only working with language that's actively in use
- Eg:
Note
The activist community also maintains the i18n-check project that enforces all of the above in pull requests. Do your best and we'll help you out during the PR process! You can also join us in the localization room on Matrix if you have questions :)
Images and Icons ⇧
Please define all routes for images and icons in the respective url registry utils file and icon map enum.
activist uses nuxt-icon for all icons. Icons are defined via <Icon :name="IconMap.ICON_REF"/>
components, with Icônes being a good place to look for Iconify based files to import. The <Icon/>
component also has a size
argument that em
based arguments can be passed to. There's also a color
argument, but colors are handled with Tailwind CSS via the text-COLOR
class argument.
Custom icons for activist can further be found in the Icon directory of the frontend components. These icons can also be referenced via the <Icon>
component via their file name (ex: <Icon name="IconSupport">
for the grasped hands we use). For Tailwind coloration note that we need to use fill-COLOR
for the custom activist icons rather than text-COLOR
.
Tab size ⇧
Codes on the frontend for Vue (<template>
, <script>
and <style>
blocks), TypeScript, CSS and other related files should use two spaces for tabs. For the backend four spaces should be used for Python files.
Padding ⇧
There are a few custom padding classes that can be used for px
and py
styling as defined in frontend/assets/css/tailwind.css. Please use consistent custom padding classes to assure that elements move together at different breakpoints.