To start the Kuzzle Admin Console in development-mode with hot-reload run the following commands:
$ npm install
$ npm run dev
The Kuzzle Admin Console will then be accessible at the following URL: http://localhost:8080
We follow the general Vuejs scaffolding for organizing our JS code.
The src/components
directory contains the files that constitute the UI of the application. The contents of this directory
are organized in sections, mimicking the menu hierarchy. Each section must contain a Layout.vue
file, used as the entry-point
to it. Each subsection must contain a Page.vue
file, used as the entry-point to it.
- Only use actions/store/getters in Vuex if your data have to be shared between sibling components (try to avoid using the store for parent-to-children communication).
- Vuex actions must be prefixed with a verb (doThing ...)
- Getters must be named after the data they get.
We use ESLint to enforce coding style conventions. ESLint triggers errors at build whenever conventions are not respected. See also .eslintrc.cjs
.
This section is strongly inspired by https://kaliop.github.io/frontend-style-guide/2.0/css/
Follow the formatting style used by Prettier.
- Indent with 2 spaces
- One selector per line
- One line per declaration (
property: value;
) - Always use
;
at the end of a declaration - Prefer single quotes (
'hello'
), and quote attribute values (input[type='text']
)
body {
font-size: 0.8rem;
}
.selector,
.other[type='text'] {
font-weight: bold;
color: black;
}
@media (min-width: 768px) {
.something {
display: block;
background-image: url('/assets/img/pattern.png');
}
}
Recommended: install Prettier (npm i -g prettier
or install for your editor) and use it to automatically reformat your CSS or Sass code.
Prefer Sass comments
/* A CSS comment (standard syntax) */
// A Sass comment (always removed from CSS output)
In Sass (SCSS) source files, prefer the Sass comment syntax, except for the top comment that describes each file. CSS/SCSS file header
Start each CSS filewith a header comment using the following format: /*! This is a header */.
When minifying the CSS, these comments will not be removed, which doesn’t add much to the file’s weight but makes it easier to break the minified CSS into several lines. Don’t repeat obvious information
// The teaser’s title
.Teaser-title {
…
}
It’s not always obvious which CSS units are best for what use cases. Here are some points of reference:
- Avoid physical units such as
cm
andpt
- Media queries: prefer
px
(over usingem
) font-size
: userem
,em
orpx
line-height
: use a unitless ratio, e.g.1.2
html {
// should be 16 * 1.25 = 20px
font-size: 125%;
}
body {
font-size: 0.8rem;
line-height: 1.4;
}
h1 {
font-size: 1.6rem;
line-height: 1.2;
}
@media (min-width: 1100px) {
h1 {
font-size: 2.4rem;
line-height: 1.1;
}
}
Viewport-relative units
1vw
is equal to 1% of the viewport width (100vw
)1vh
is equal to 1% of the viewport height (100vh
), but it can be unreliable in mobile browsers
Adding !important
to your declarations helps fixing specificity issues, but only in the very short term! Avoid it.
If you do use !important
in your code, always write a comment explaining why.
.MyComponent-inner {
// The JS library we use forces a width in JavaScript,
// but we really need a fluid width here!
width: auto !important;
}
We use the Sass preprocessor, with the SCSS syntax.
Sass files should have a .scss
extension.
We use sass
(and its Vite plugin) to compile .scss
files to CSS.
Use lowercase names with hyphens (not underscores), and follow an general-to-particular progression:
Global variables and mixins: $type-name
, @mixin some-name
must be declared in src/assets/styles/_variables.scss
Variables and mixins specific to a component: must be declared inside the component-scoped stylesheets (see below).
Selector nesting is good but it makes your code more difficult to read. Avoid unnecessary nesting.
- Nest once if helpful, twice at most.
- Keep the resulting CSS selectors as short as possible.
- Do not overuse the
&
symbol.
Do not use the &
symbol to create a class name that extends the parent class:
.Header {
…
&--dark {…}
&-logo {…}
}
In this example, if a developer looks at the HTML code and wants to find the CSS code for the Header--dark or Header-logo classes, they won’t be able to find it in the codebase!
// BAD: unnecessary nesting
.Header {
.Header-top {
.Header-logo {…}
}
}
.Nav {
ul {
li {
a {…}
}
}
}
// GOOD: limit nesting
.Header {
…
}
.Header-top {
…
}
.Header-logo {
…
}
.Nav a {
…
}
We use classes for selectors. We try to keep selectors short (ideally, just one class). We discourage defining selectors that go beyond 2 levels of depth (a state-variant and pseudo classes are not considered in depth-count).
Do not use the #some-id
selector (because of high specificity). If you really need to target an id, and cannot add a class, use the attribute selector: [id="some-id"]
.
// Bad selector: too specific!
#stream .tweet_header .author {…}
// Better selectors
.Tweet {…}
.Tweet-author {…}
.Tweet-author a {…}
.Tweet-retweetBtn {…}
.Tweet-retweetBtn[disabled] {…}
// And with Sass nesting...
.Tweet {
.Tweet-retweetBtn {
&[disabled]
}
}
In addition to classes, you can use:
- Element selectors (
h2
,.MainNav-item
a
, etc.). But keep in mind that sometimes the element can change, for example a<h2>
can be changed to a<h3>
, a<a>
to a<button>
, etc. - Attribute selectors, especially for dynamic states (
.MainNav a[disabled]
). - Pseudo-classes (
:empty
,:invalid
, …)
Utilities are simple styles that can be used on any element to trigger some basic function (in a few lines of CSS at most).
- Write utility classes in
camelCase
, without a prefix. - Avoid using many utility classes. They move a lot of the presentation logic into your HTML, and can hide that a component has dependencies (unlike mixins and variables).
- If you have several utility classes that work together, consider making a component-specific class.
// Utility classes
.icon {
display: inline-block;
vertical-align: middle;
width: 1em;
height: 1em;
}
.visuallyHidden {
position: absolute !important;
clip: rect(1px, 1px, 1px, 1px);
}
As Vuejs enables us to put stylesheets directly in components, we follow the convention that all component-specific styles must be written in the component's <style>
tag. Also, component-specific styles must be scoped
.
Also, remember you can access Sass variables declared in src/assets/variables.scss
in component-scoped stylesheets.
<style lang="sass" scoped>
/* component styles... */
</style>
Each component must have a root element which contains the others (if any). Use the component’s name as its class, for example:
<div class="Teaser">
…
</div>
Class naming inside component-styles must be as semantic as possible, in order to properly differenciate utility classes and component-specific classes. E.g. avoid naming like .pink-bg
inside a component: prefer .unicorn-container
instead.
Component-specific classes should also be specifically named in order to avoid being accidentally applied gobally-defined styles. E.g. a class .separator
in a Breadcrumbs
component may be renamed to .Breadcrumbs-separator
to avoid getting a possible .separator
global class.
Sometimes the component’s container or a component element can have slightly different styles in different places. In those situations we can create style variants, using classes like: Component--variantName
(for the container) or Component-element--variantName
(for an element in the component).
{# Alternative style for the first instance of this component in a list #}
<article class="Teaser Teaser--highlight">
<h2 class="Teaser-head">
<a href="…">…</a>
</h2>
<p class="Teaser-desc">…</p>
</article>
<article class="Teaser">
<h2 class="Teaser-head">
<a href="…">…</a>
</h2>
<p class="Teaser-desc">…</p>
</article>
We don't do very much responsive design in this project but we're likely to introduce it progressively.
Do not separate desktop and mobile styles into different files. You want to keep them tightly together:
.spaceBefore1 {
margin-top: 20px;
}
@media (min-width: 750px) {
.spaceBefore1 {
margin-top: 30px;
}
}
We may use a mobile-first approach. Here’s how it works:
- First, try to find the simpler “state” of a component or element style. Most of the time, it will look like the mobile layout.
- Then, add styles for larger viewports, in a media query.
Using Sass, you can use media query nesting to avoid repeating the selector and making the relationship between the styles clearer.
.MyComponent {
border: solid 1px black;
padding: 10px;
@media (min-width: 750px) {
border-width: 5px;
padding: 25px;
}
}
Avoid doing things like this, it’s doing extra work for nothing:
.MyComponent-image {
float: left;
margin-right: 20px;
@media (max-width: 749px) {
float: none;
margin-right: 0;
}
}
Finally, sometimes the mobile styles are rather specific and not useful for larger screens. In that case, try to separate styles in tree groups: common, small screens, and larger screens.
.MyComponent-image {
display: block;
outline: solid 1px rgba(0, 0, 0, 0.25);
outline-offset: -1px;
background-color: #eee;
@media (max-width: 749px) {
width: calc(100% - 40px);
max-width: 360px;
margin: 0 auto;
}
@media (min-width: 750px) {
float: right;
margin-right: 20px;
}
}
Try to use 2–3 main breakpoints and reuse them throughout the site or application. Using Sass, you can store those breakpoints in variables:
$bp-medium: 750px;
$bp-large: 1100px;
$bp-xlarge: 1400px;
// By convention, when using max-width you should
// remove 1px to avoid conflicts if the viewport
// width is exactly $bp-medium
@media (max-width: $bp-medium - 1px) {…}
@media (min-width: $bp-medium) {…}
Name media queries allow you to define a media query once, and reuse it every time. In CSS proposals, it looks like this:
@media (--my-media-query) {…}
Since this feature is not standardized or supported yet, we can mimick this behavior in Sass to make our Sass code easier to maintain. Use this mq-build
function to store full media queries in variables:
@import 'node_modules/sass-mq-build/mq-build';
$mq-small: mq-build(null, 750px);
$mq-medium: mq-build(750px, 1100px);
$mq-large: mq-build(1100px, null);
.something {
@media ($mq-small) {
color: green;
}
@media ($mq-medium) {
color: red;
}
}
To run the end-to-end tests in headless mode, run
npm run test:e2e
To run the end-to-end tests in headful mode, run
npm run dev
npm run test:e2e-local
We use Cypress.io to e2e test the UI. You can use it locally (via the local dashboard) or in the CI (using the cloud-hosted dashboard). Cypress is an awesome tool, but this has a price. They had to re-invent a little bit the wheel so, even if writing and running tests looks extremely smooth, it is still recommended to understand how this magic is achieved. Before writing tests, you are encouraged to read the following docs:
- https://docs.cypress.io/guides/core-concepts/introduction-to-cypress.html
- https://docs.cypress.io/guides/core-concepts/writing-and-organizing-tests.html
If you enjoy reading this extremely well-written documentation, we guarantee it's 100% worth reading.
The one and only guideline for writing tests, beyond the Cypress best-practices, is to use the data-cy
attribute to select DOM elements, rather than CSS classes or other selecting methods. We add the data-cy
to all the elements we want to accesss in our tests and try to follow some BEM-ish convention for the values of these attributes.
Sometimes you'll find it difficult to achieve stable testing. The most common scenario is when trying to interact with an element and no solid selector is available. In this case, the best thing to do is to enrich your component with BEM-compliant CSS classes. Make it explicit. Make it semantic. CSS classes, even if they are not associated with any style rule, are always useful to make your component more readable once it's mounted in the DOM. This is a kind of a rule of thumb: whenever your tests feel flaky, it's always a good idea to adjust your application to make it more testable.