diff --git a/@vates/parse-duration/index.js b/@vates/parse-duration/index.js index 66783a9def9..4d534d93aa6 100644 --- a/@vates/parse-duration/index.js +++ b/@vates/parse-duration/index.js @@ -6,9 +6,11 @@ exports.parseDuration = value => { if (typeof value === 'number') { return value } - const duration = ms(value) - if (duration === undefined) { - throw new TypeError(`not a valid duration: ${value}`) + if (typeof value === 'string' && value !== '') { + const duration = ms(value) + if (duration !== undefined) { + return duration + } } - return duration + throw new TypeError(`not a valid duration: ${value}`) } diff --git a/@vates/parse-duration/index.test.mjs b/@vates/parse-duration/index.test.mjs new file mode 100644 index 00000000000..dd873fd9373 --- /dev/null +++ b/@vates/parse-duration/index.test.mjs @@ -0,0 +1,22 @@ +import { describe, it } from 'node:test' +import { parseDuration } from '@vates/parse-duration' +import assert from 'node:assert/strict' + +describe('parseDuration()', () => { + it('should parse string', () => { + const input = '2 days' + const expected = 172800000 + assert.strictEqual(parseDuration(input), expected) + }) + + it('should return its input if already a number', () => { + const input = 172800000 + assert.strictEqual(parseDuration(input), input) + }) + + for (const input of [undefined, '', 'invalid duration']) { + it('should throw an error for ' + input, () => { + assert.throws(() => parseDuration(input), { message: `not a valid duration: ${input}` }) + }) + } +}) diff --git a/@vates/parse-duration/package.json b/@vates/parse-duration/package.json index ebe0062a769..4dd15ead04e 100644 --- a/@vates/parse-duration/package.json +++ b/@vates/parse-duration/package.json @@ -27,6 +27,7 @@ "ms": "^2.1.2" }, "scripts": { - "postversion": "npm publish --access public" + "postversion": "npm publish --access public", + "test": "node --test" } } diff --git a/@xen-orchestra/backups/_incrementalVm.mjs b/@xen-orchestra/backups/_incrementalVm.mjs index c0669ff9efb..2b58de903ba 100644 --- a/@xen-orchestra/backups/_incrementalVm.mjs +++ b/@xen-orchestra/backups/_incrementalVm.mjs @@ -3,7 +3,6 @@ import ignoreErrors from 'promise-toolbox/ignoreErrors' import { asyncMap } from '@xen-orchestra/async-map' import { CancelToken } from 'promise-toolbox' import { compareVersions } from 'compare-versions' -import { createVhdStreamWithLength } from 'vhd-lib' import { defer } from 'golike-defer' import { cancelableMap } from './_cancelableMap.mjs' @@ -227,9 +226,6 @@ export const importIncrementalVm = defer(async function importIncrementalVm( if (typeof stream === 'function') { stream = await stream() } - if (stream.length === undefined) { - stream = await createVhdStreamWithLength(stream) - } await xapi.setField('VDI', vdi.$ref, 'name_label', `[Importing] ${vdiRecords[id].name_label}`) await vdi.$importContent(stream, { cancelToken, format: 'vhd' }) await xapi.setField('VDI', vdi.$ref, 'name_label', vdiRecords[id].name_label) diff --git a/@xen-orchestra/backups/_runners/_vmRunners/_AbstractXapi.mjs b/@xen-orchestra/backups/_runners/_vmRunners/_AbstractXapi.mjs index a7dcc6340a0..ff36133375a 100644 --- a/@xen-orchestra/backups/_runners/_vmRunners/_AbstractXapi.mjs +++ b/@xen-orchestra/backups/_runners/_vmRunners/_AbstractXapi.mjs @@ -293,7 +293,7 @@ export const AbstractXapi = class AbstractXapiVmBackupRunner extends Abstract { await this._xapi.VDI_dataDestroy(vdiRef) Task.info(`Snapshot data has been deleted`, { vdiRef }) } catch (error) { - Task.warning(`Couldn't deleted snapshot data`, { error, vdiRef }) + Task.warning(`Couldn't delete snapshot data`, { error, vdiRef }) } } } diff --git a/@xen-orchestra/lite/README.md b/@xen-orchestra/lite/README.md index b6915250079..7cc19445d22 100644 --- a/@xen-orchestra/lite/README.md +++ b/@xen-orchestra/lite/README.md @@ -5,213 +5,6 @@ - `yarn` - `yarn dev` -## Conventions +## Guidelines -### File names - -| Type | Format | Exemple | -| ---------- | ---------------------------------------- | ----------------------------------- | -| Component | `components/.vue` | `components/FooBar.vue` | -| View | `views/View.vue` | `views/FooBarView.vue` | -| Composable | `composables/.composable.ts` | `composables/foo-bar.composable.ts` | -| Store | `stores/.store.ts` | `stores/foo-bar.store.ts` | -| Other | `libs/.ts` | `libs/foo-bar.ts` | - -For components and views, prepend the subdirectories names to the resulting filename. - -Example: `components/foo/bar/FooBarBaz.vue` - -### Vue Components - -Use Vue Single File Components (`*.vue`). - -Insert blocks in the following order: `template`, `script` then `style`. - -#### Template - -Use HTML. - -If your component only has one root element, add the component name as a class. - -```vue - - -``` - -#### Script - -Use composition API + TypeScript + `setup` attribute (` -``` - -#### CSS - -Always use `scoped` attribute (` -``` - -### Icons - -This project is using Font Awesome 6 Free. - -Icons can be displayed with the `UiIcon` component. - -Passing `undefined` as `icon` prop will disable the component (no need to use an additional `v-if` condition). - -Use the `busy` prop to display a loader icon. - -```vue - - - -``` - -#### Font weight <=> Style name - -Here is the equivalent between font weight and style name. - -| Style name | Font weight | -| ---------- | ----------- | -| Solid | 900 | -| Regular | 400 | - -### CSS - -Always use `rem` unit (`1rem` = `10px`) - -### Store - -Use Pinia store with setup function. - -State are `ref` - -Getters are `computed` - -Actions/Mutations are simple functions - -#### Naming convention - -For a `foobar` store, create a `store/foobar.store.ts` then use `defineStore('foobar', setupFunc)` - -#### Example - -```typescript -import { computed, ref } from 'vue' - -export const useFoobarStore = defineStore('foobar', () => { - const aStateVar = ref(0) - const otherStateVar = ref(0) - const aGetter = computed(() => aStateVar.value * 2) - const anAction = () => (otherStateVar.value += 10) - - return { - aStateVar, - otherStateVar, - aGetter, - anAction, - } -}) -``` - -### I18n - -Internationalization of the app is done with [Vue-i18n](https://vue-i18n.intlify.dev/). - -Locale files are located in `src/locales` directory. - -Source of truth is `en-US.json` file. - -To quickly check if there are missing translations in other locale files, open `main.ts` and check the `messages` -property of `createI18n()` for TypeScript error. - -#### Example - -```json -{ - "hello": "Hello", - "hello_name": "Hello {name}", - "hello_linked": "@:hello_name how are you?", - "hello_plural": "No hello | Hello to you | Hello to {count} persons" -} -``` - -```html - - -

{{ $t("hello") }}

- -

{{ $t("hello_name", { name: "World" }) }}

- -

{{ $t("hello_linked", { name: "World" }) }}

- -

{{ $tc("hello_plural", 0) }}

- -

{{ $tc("hello_plural", 1) }}

- -

{{ $tc("hello_plural", 4) }}

- - - - -

{{ $d(date, "date_short") }}

- -

{{ $d(date, "date_medium") }}

- -

{{ $d(date, "date_long") }}

- -

{{ $d(date, "datetime_short") }}

- -

{{ $d(date, "datetime_medium") }}

- -

{{ $d(date, "datetime_long") }}

- -

{{ $d(date, "time") }}

- - - - -

{{ $n(1234567.898765) }}

- -``` +Please follow the guidelines from the `@xen-orchestra/web-core/docs/guidelines` directory. diff --git a/@xen-orchestra/lite/package.json b/@xen-orchestra/lite/package.json index 429567b9449..be64c49ec76 100644 --- a/@xen-orchestra/lite/package.json +++ b/@xen-orchestra/lite/package.json @@ -56,7 +56,7 @@ "postcss-nested": "^6.0.1", "typescript": "~5.3.3", "vite": "^5.0.11", - "vue": "^3.4.13", + "vue": "~3.4.13", "vue-echarts": "^6.6.8", "vue-i18n": "^9.9.0", "vue-router": "^4.4.0", diff --git a/@xen-orchestra/lite/src/App.vue b/@xen-orchestra/lite/src/App.vue index f2cf425b2d9..425d2fccc5f 100644 --- a/@xen-orchestra/lite/src/App.vue +++ b/@xen-orchestra/lite/src/App.vue @@ -75,7 +75,7 @@ useUnreachableHosts() overflow: auto; flex: 1; height: calc(100vh - 5.5rem); - background-color: var(--background-color-secondary); + background-color: var(--color-neutral-background-secondary); &.no-ui { height: 100vh; diff --git a/@xen-orchestra/lite/src/components/AccountButton.vue b/@xen-orchestra/lite/src/components/AccountButton.vue index cc7581dbe64..40af204bed2 100644 --- a/@xen-orchestra/lite/src/components/AccountButton.vue +++ b/@xen-orchestra/lite/src/components/AccountButton.vue @@ -1,5 +1,5 @@ @@ -24,7 +24,7 @@

{{ $t('xoa-deploy-successful') }}

- + {{ $t('access-xoa') }} @@ -40,7 +40,7 @@ {{ $t('keep-page-open') }}

- + {{ $t('cancel') }} @@ -92,8 +92,14 @@
- - + +
@@ -156,8 +162,9 @@
{{ $t('disabled') }} + + {{ $t('enabled') }}
@@ -189,10 +196,10 @@ - + {{ $t('cancel') }} - + {{ $t('deploy') }} @@ -503,6 +510,7 @@ async function cancel() { display: flex; flex-direction: row; margin: 1.67rem 0; + & > * { min-width: 20rem; } @@ -520,13 +528,15 @@ async function cancel() { justify-content: center; align-items: center; min-height: 76.5vh; - color: var(--color-purple-base); + color: var(--color-normal-txt-base); text-align: center; padding: 5rem; margin: auto; + h2 { margin-bottom: 1rem; } + * { max-width: 100%; } @@ -535,17 +545,19 @@ async function cancel() { .not-available { font-size: 2rem; } + .status { - color: var(--color-grey-100); + color: var(--color-neutral-txt-primary); } .success { - color: var(--color-green-base); + color: var(--color-success-txt-base); } .danger { - color: var(--color-red-base); + color: var(--color-danger-txt-base); } + .success, .danger { &.ui-icon { @@ -559,7 +571,8 @@ async function cancel() { text-align: left; gap: 0.5em; } + .warning { - color: var(--color-orange-base); + color: var(--color-warning-txt-base); } diff --git a/@xen-orchestra/web-core/docs/contexts/color-context.md b/@xen-orchestra/web-core/docs/contexts/color-context.md index 3c0022a8c0c..4187b545e42 100644 --- a/@xen-orchestra/web-core/docs/contexts/color-context.md +++ b/@xen-orchestra/web-core/docs/contexts/color-context.md @@ -1,3 +1,11 @@ +> [!CAUTION] +> +> **DEPRECATED** +> +> ColorContext has been deprecated and shouldn’t be used anymore. +> +> It will be removed in the future. + - [Overview](#overview) @@ -37,19 +45,19 @@ When you set a color context, the variables are updated with the help of CSS cla ```css .color-context-info { - --color-context-primary: var(--color-purple-base); - --color-context-primary-hover: var(--color-purple-d20); - --color-context-primary-active: var(--color-purple-d40); - --color-context-primary-disabled: var(--color-grey-400); - - --color-context-secondary: var(--background-color-purple-10); - --color-context-secondary-hover: var(--background-color-purple-20); - --color-context-secondary-active: var(--background-color-purple-30); + --color-context-primary: var(--color-normal-txt-base); + --color-context-primary-hover: var(--color-normal-txt-hover); + --color-context-primary-active: var(--color-normal-txt-active); + --color-context-primary-disabled: var(--color-neutral-txt-secondary); + + --color-context-secondary: var(--color-normal-background-selected); + --color-context-secondary-hover: var(--color-normal-background-hover); + --color-context-secondary-active: var(--color-normal-background-active); } .color-context-success { - --color-context-primary: var(--color-green-base); - --color-context-primary-hover: var(--color-green-d20); + --color-context-primary: var(--color-success-txt-base); + --color-context-primary-hover: var(--color-success-txt-hover); /*...*/ } ``` diff --git a/@xen-orchestra/web-core/docs/css/interactive-elements.md b/@xen-orchestra/web-core/docs/css/interactive-elements.md deleted file mode 100644 index d7503c526e2..00000000000 --- a/@xen-orchestra/web-core/docs/css/interactive-elements.md +++ /dev/null @@ -1,33 +0,0 @@ -# Interactive elements - -When an element has user interaction (hover, focus, pressed, disabled, etc.), its CSS rules should be defined (if -applicable) in the following order: - -1. `.active, .selected` -2. `:hover, .hover, :focus-visible` -3. `:active, .pressed` -4. `:disabled, .disabled` - -## Example - -```css -.my-component { - /* color: ...; */ - - &:is(.active, .selected) { - /* color: ...; */ - } - - &:is(:hover, .hover, :focus-visible) { - /* color: ...; */ - } - - &:is(:active, .pressed) { - /* color: ...; */ - } - - &:is(:disabled, .disabled) { - /* color: ...; */ - } -} -``` diff --git a/@xen-orchestra/web-core/docs/css/variants.md b/@xen-orchestra/web-core/docs/css/variants.md deleted file mode 100644 index 5cb118e1717..00000000000 --- a/@xen-orchestra/web-core/docs/css/variants.md +++ /dev/null @@ -1,322 +0,0 @@ -# Variants - -To effectively manage CSS variants, it's recommended to use CSS custom properties. - -Begin by defining the properties that will be modified by the variants. - -When possible, maintain consistency by using identical names for both the variable and the property. - -Moreover, organizing variants by category (such as colors, sizes, etc.) and annotating them with comments can enhance -code readability and facilitate code folding. - -```postcss -/* COLOR VARIANTS */ -.my-component { - &.info { - --color: var(--color-purple-base); - } - - &.success { - --color: var(--color-green-base); - } -} - -/* SIZE VARIANTS */ -.my-component { - &.small { - --font-size: 1rem; - } - - &.medium { - --font-size: 1.6rem; - } - - &.large { - --font-size: 2.4rem; - } -} - -/* IMPLEMENTATION */ -.my-component { - color: var(--color); - font-size: var(--font-size); -} -``` - -## Explicit variants - -When possible, define all variants explicitly, including the default one. - -This approach ensures that the CSS is more intelligible, simpler to maintain, and easier to troubleshoot. - -❌ **Bad** - -```postcss -/* COLOR VARIANTS */ -.my-component { - .success { - --color: var(--color-green-base); - } -} - -/* SIZE VARIANTS */ -.my-component { - .small { - --font-size: 1rem; - } - - .large { - --font-size: 2.4rem; - } -} - -.my-component { - color: var(--color, var(--color-purple-base)); - font-size: var(--font-size, 1.6rem); -} -``` - -If you need a default property when no specific class is applied, define it at the top of the selector. - -✅ **Good** - -```postcss -/* COLOR VARIANTS */ -.my-component { - --color: var(--color-purple-base); - - &.free { - --color: var(--color-green-base); - } -} - -/* IMPLEMENTATION */ -.my-component { - color: var(--color); -} -``` - -When dealing with multiple default properties, or nested variants, you can group them in a `& {}` block to -enhance readability. - -✅ **Good** - -```postcss -/* COLOR VARIANTS */ -.my-component { - & { - --color: var(--color-purple-base); - --background-color: var(--background-color-purple-10); - --border-color: var(--color-purple-d20); - } - - &.free { - --color: var(--color-green-base); - --background-color: var(--background-color-green-10); - --border-color: var(--color-green-d20); - } -} - -/* IMPLEMENTATION */ -.my-component { - color: var(--color); -} -``` - -## Sub-variants - -For scenarios requiring the variants of variants (such as different colors based on different sizes), use nested -selectors. - -Rule of thumb: the parent selector should be "what is modified," and the child selector should be "who is modifying." - -For example, in the case of colors based on sizes, "what is modified" is the color, and "who is modifying" is the size. - -So the parent selector should be the color modifier, and the child selector should be the size modifier. - -As before, grouping variants by type and annotating them enhances maintainability and readability. - -```postcss -/* COLOR VARIANTS */ -.my-component { - &.info { - &.small { - --color: var(--color-purple-base); - } - - &.medium { - --color: var(--color-purple-l20); - } - - &.large { - --color: var(--color-purple-l40); - } - } - - &.success { - &.small { - --color: var(--color-green-base); - } - - &.medium { - --color: var(--color-green-l20); - } - - &.large { - --color: var(--color-green-l40); - } - } -} - -/* SIZE VARIANTS */ -.my-component { - &.small { - --font-size: 1rem; - } - - &.medium { - --font-size: 1.6rem; - } - - &.large { - --font-size: 2.4rem; - } -} - -/* IMPLEMENTATION */ -.my-component { - color: var(--color); - font-size: var(--font-size); -} -``` - -### Keep structure coherence - -Ensure the parent selector corresponds to the variant category (colors, sizes, etc.) to maintain logical structure and -coherence. - -In the following example, the variant is the color, so the font size should not be used as the parent selector: - -❌ **Bad** - -```postcss -.my-component { - .small { - .info { - --color: var(--color-purple-base); - } - - .success { - --color: var(--color-green-base); - } - } - - .medium { - .info { - --color: var(--color-purple-l20); - } - - .success { - --color: var(--color-green-l20); - } - } - - .large { - .info { - --color: var(--color-purple-l40); - } - - .success { - --color: var(--color-green-l40); - } - } -} -``` - -But if the variant is the font size, then it should be used as the parent selector: - -✅ **Good** - -```postcss -.my-component { - .small { - .info { - --font-size: 1rem; - } - - .success { - --font-size: 1.6rem; - } - } - - .medium { - .info { - --font-size: 1.6rem; - } - - .success { - --font-size: 2.4rem; - } - } - - .large { - .info { - --font-size: 2.4rem; - } - - .success { - --font-size: 1rem; - } - } -} -``` - -### Avoid mixing different variant types - -Mixing different variant types within the same selector is discouraged as it complicates the code structure and -readability. - -❌ **Bad** - -```postcss -.my-component { - &.small { - --font-size: 1rem; - - &.info { - --color: var(--color-purple-base); - } - - &.success { - --color: var(--color-green-base); - } - } - - &.medium { - --font-size: 1.6rem; - - &.info { - --color: var(--color-purple-l20); - } - - &.success { - --color: var(--color-green-l20); - } - } - - &.large { - --font-size: 2.4rem; - - &.info { - --color: var(--color-purple-l40); - } - - &.success { - --color: var(--color-green-l40); - } - } - - color: var(--color); - font-size: var(--font-size); -} -``` diff --git a/@xen-orchestra/web-core/docs/guidelines/component-definition.md b/@xen-orchestra/web-core/docs/guidelines/component-definition.md new file mode 100644 index 00000000000..8ab91c24d4f --- /dev/null +++ b/@xen-orchestra/web-core/docs/guidelines/component-definition.md @@ -0,0 +1,268 @@ +# Component definition + +Lexicon: + +- DS: Design System +- SFC: Single-File Component +- Component: A Vue component (being defined in the DS or not) +- DS Component: A component specifically defined in the Design System (DS) +- Subcomponent: A component that is part of a Component or a DS Component + +## Components and Subcomponents MUST be defined as Vue SFC (Single-File Component) + +## DS Components MUST be stored in their own directory. + +## Directory name MUST be in kebab-case (e.g. `my-component`) + +## Component name MUST be in PascalCase + +## DS Component/Subcomponent name MUST start with `Vts` (e.g. `VtsMyComponent.vue`) + +❌ Bad + +`components/Square.vue` + +✅ Good + +`components/square/VtsSquare.vue` + +## Components SHOULD be kept short and be split into multiple subcomponents if necessary, stored in the same directory as the main component. + +❌ Bad + +``` +/components/ + /square/ + /VtsSquare.vue + /square-icon/ + /VtsSquareIcon.vue <- This component is not part of the DS and will be used only in Square.vue +``` + +✅ Good + +``` +/components/ + /square/ + /VtsSquare.vue + /VtsSquareIcon.vue +``` + +> [!WARNING] +> If you think that a subcomponent is likely to be reused in other components, +> ask the DS team to define it in the DS. +> +> It will be then moved in its own directory, following the DS guidelines. + +## DS Components MUST start with an HTML comment containing the implemented version + +In the form `v1`, `v2`, etc. + +> [!TIP] +> The DS team can use a minor version to indicate a change in the DS that does not affect the component style. +> +> It must not be added to the Vue component version. + +❌ Bad + +```vue + + +``` + +✅ Good + +```vue + + +``` + +## Subcomponents MUST NOT have a version number + +If a component from the DS is split into multiple subcomponents, only the main component will have a version number + +## Component tags MUST follow `template`, `script` then `style` order, separated with an empty line + +## Class names MUST use kebab-case + +## Component root element's class name MUST be named after the component name + +If no style is applied to the root element, the class name will be omitted + +❌ Bad + +```vue + + +``` + +```vue + + +``` + +✅ Good + +```vue + + +``` + +```vue + + +``` + +## Class names SHOULD be short and MUST be meaningful + +❌ Bad + +```vue + +``` + +✅ Good + +```vue + +``` + +## Component MUST use ` +``` + +✅ Good + +```vue + +``` + +> [!TIP] +> See also [Component variants guidelines](./component-variants.md) +> to learn how to handle different component styles based on its props or states. + +## Optional slots container SHOULD use `v-if="$slots."` + +❌ Bad + +```vue + +``` + +✅ Good + +```vue + +``` + +## Component MUST use `defineSlots` when slots are used + +❌ Bad + +```vue + +``` + +```vue + +``` + +✅ Good + +```vue + + + +``` + +## Component SHOULD have a Story + +> [!TIP] +> For now, stories are stored in +> `@xen-orchestra/lite/src/stories` and can only be written for XO Lite and XO Core components. diff --git a/@xen-orchestra/web-core/docs/guidelines/component-variants.md b/@xen-orchestra/web-core/docs/guidelines/component-variants.md new file mode 100644 index 00000000000..5e65e80e129 --- /dev/null +++ b/@xen-orchestra/web-core/docs/guidelines/component-variants.md @@ -0,0 +1,310 @@ +# Component Variants + +A variant is a specific style that a component can have. It is usually determined by the value of a prop. + +These variants are defined in the Design System and are reflected in the component's CSS. + +See also: +[toVariants utility](../utils/to-variants.util.md) to help you generate variant CSS classes for your components. + +## Base class + +The root element of a component will have a specific CSS class following the pattern `vts-`. + +> [!TIP] +> Example: +> +> The class for a "Button" component would be `vts-button`. + +## Variant classes + +The root element will then have secondary classes which reflect the current variants being applied. + +The pattern for these classes is `--` (or `` for `true` boolean). + +> [!TIP] +> Example: +> +> If `color` prop is `success` and `size` prop is `medium` then the classes `color--success` and `size--medium` +> would be applied to the root element. + +## Converting Design System props into Vue props + +The first step will be to convert the Design System's props into Vue props. + +Some are easy to map, like `color` or `size`, which have a specific list of possible values. + +But others are more tricky, like a `state` prop in the Design System having values like `default`, `hover`, `active`, or +`disabled`. + +We can't simply create a `state` prop in Vue with these values (it wouldn't make sense for "hover" and "active" states). + +So in this case: + +- the "default" state would be represented as "no class applied." +- the "hover" and "active" states would be represented as `:hover` and `:active` pseudo-classes. +- the "disabled" state would be represented as a `disabled` `boolean` prop which would add a `disabled` class when `true`. + +## CSS variables + +Each CSS property that can be affected by a variant should have a corresponding CSS variable. + +The format for these variables is `----`. + +> [!TIP] +> Example: +> +> For a `VtsButton` component, the CSS variables could be `--vts-button--background-color`, or `--vts-button--padding`. + +### CSS variables for child elements + +If the CSS property to be changed is owned by a child element, the variable name should reflect that. + +The format for these variables is `--__--`. + +> [!TIP] +> Example: +> +> If we need to change `color` of a `.icon` inside our `VtsButton` component, the CSS variable will be +> `--vts-button__icon--color`. + +## Identifying which DS props affect which CSS variables + +The next step is to identify which CSS variables will be affected by each DS prop. + +For example, we could imagine: + +- a `color` prop affecting the `background-color`, `color`, and `border-color` properties. +- a "state" affecting the `background-color` property. +- a `size` prop affecting the `padding`, `gap`, and `font-size` properties. +- a `level` prop affecting the `color` and `padding` properties. + +## Grouping CSS variables declarations + +Once we know which CSS variables will be affected by each prop, we can group them accordingly. + +From the previous example, we can see that: + +- `border-color` is affected by `color` only. +- `gap` and `font-size` are affected by `size` only. +- `background-color` is affected by both "state" and `color`. +- `color` is affected by both `color` and `level`. +- `padding` is affected by both `size` and `level` + +So we could prepare our variables groups like this: + +```postcss +/* +COLOR +--vts-button--border-color +*/ +.vts-button { + /* We'll define the border-color here */ +} + +/* +SIZE +--vts-button--gap +--vts-button--font-size +*/ +.vts-button { + /* We'll define the gap and font-size here */ +} + +/* +COLOR + STATE +--vts-button--background-color +*/ +.vts-button { + /* We'll define the background-color here */ +} + +/* +COLOR + LEVEL +--vts-button--color +*/ +.vts-button { + /* We'll define the color here */ +} + +/* +SIZE + LEVEL +--vts-button--padding +*/ +.vts-button { + /* We'll define the padding here */ +} +``` + +## Filling the groups + +Lastly, we can now fill in the CSS variables accordingly. + +Let's start with the `COLOR` group: + +```postcss +/* +COLOR +--vts-button--border-color +*/ +.vts-button { + &.color--blue { + --vts-button--border-color: blue; + } + + &.color--red { + --vts-button--border-color: red; + } +} +``` + +Then the `SIZE` group: + +```postcss +/* +SIZE +--vts-button--gap +--vts-button--font-size +*/ +.vts-button { + &.size--small { + --vts-button--gap: 0.5rem; + --vts-button--font-size: 0.75rem; + } + + &.size--medium { + --vts-button--gap: 1rem; + --vts-button--font-size: 1rem; + } +} +``` + +Let's continue with the `COLOR + STATE` group: + +```postcss +/* +COLOR + STATE +--vts-button--background-color +*/ +.vts-button { + &.color--blue { + & { + /* default state */ + --vts-button--background-color: blue; + } + + &:hover { + --vts-button--background-color: skyblue; + } + + &:active { + --vts-button--background-color: darkblue; + } + + &:disabled { + --vts-button--background-color: aliceblue; + } + } + + &.color--red { + & { + /* default state */ + --vts-button--background-color: red; + } + + &:hover { + --vts-button--background-color: salmon; + } + + &:active { + --vts-button--background-color: darkred; + } + + &:disabled { + --vts-button--background-color: lightpink; + } + } +} +``` + +Moving on to the `COLOR + LEVEL` group: + +```postcss +/* +COLOR + LEVEL +--vts-button--color +*/ +.vts-button { + &.color--blue { + &.level--primary { + --vts-button--color: blue; + } + + &.level--secondary { + --vts-button--color: lightblue; + } + } + + &.color--red { + &.level--primary { + --vts-button--color: red; + } + + &.level--secondary { + --vts-button--color: lightcoral; + } + } +} +``` + +And finally, the `SIZE + LEVEL` group: + +```postcss +/* +SIZE + LEVEL +--vts-button--padding +*/ +.vts-button { + &.size--small { + &.level--primary { + --vts-button--padding: 0.25rem 0.5rem; + } + + &.level--secondary { + --vts-button--padding: 0.5rem 1rem; + } + } + + &.size--medium { + &.level--primary { + --vts-button--padding: 0.5rem 1rem; + } + + &.level--secondary { + --vts-button--padding: 1rem 2rem; + } + } +} +``` + +## Implementing the component base CSS + +Now that we have our CSS variables defined, we can implement the base CSS for our component. + +```postcss +/* ... variables definitions ... */ + +/* IMPLEMENTATION */ +.vts-button { + display: inline-flex; + align-items: center; + justify-content: center; + cursor: pointer; + border: 0.1rem solid var(--vts-button--border-color); + gap: var(--vts-button--gap); + font-size: var(--vts-button--font-size); + padding: var(--vts-button--padding); + background-color: var(--vts-button--background-color); + color: var(--vts-button--color); +} +``` diff --git a/@xen-orchestra/web-core/docs/guidelines/icons.md b/@xen-orchestra/web-core/docs/guidelines/icons.md new file mode 100644 index 00000000000..b082bd82862 --- /dev/null +++ b/@xen-orchestra/web-core/docs/guidelines/icons.md @@ -0,0 +1,26 @@ +# Icons + +XO Lite / 6 / Core projects are using Font Awesome 6 Free. + +Icons can be displayed with the `UiIcon` component. + +The component takes an `icon` prop that should be an icon object imported from `@fortawesome/free-solid-svg-icons` or `@fortawesome/free-regular-svg-icons`. + +If the `icon` prop is `undefined`, then the component will be ignored (no need to use an additional `v-if` condition). + +Use the `busy` prop to display a loader icon. + +## Example + +```vue + + + +``` diff --git a/@xen-orchestra/web-core/docs/guidelines/stores.md b/@xen-orchestra/web-core/docs/guidelines/stores.md new file mode 100644 index 00000000000..911509ef11b --- /dev/null +++ b/@xen-orchestra/web-core/docs/guidelines/stores.md @@ -0,0 +1,33 @@ +# Stores + +Stores definition must use Pinia setup function. + +State are `ref` + +Getters are `computed` + +Actions/Mutations are simple functions + +## Naming convention + +For a `foobar` store, create a `store/foobar.store.ts` then use `defineStore('foobar', setupFunc)` + +## Example + +```typescript +import { computed, ref } from 'vue' + +export const useFoobarStore = defineStore('foobar', () => { + const aStateVar = ref(0) + const otherStateVar = ref(0) + const aGetter = computed(() => aStateVar.value * 2) + const anAction = () => (otherStateVar.value += 10) + + return { + aStateVar, + otherStateVar, + aGetter, + anAction, + } +}) +``` diff --git a/@xen-orchestra/web-core/docs/utils/to-variants.util.md b/@xen-orchestra/web-core/docs/utils/to-variants.util.md new file mode 100644 index 00000000000..1f30e7b954d --- /dev/null +++ b/@xen-orchestra/web-core/docs/utils/to-variants.util.md @@ -0,0 +1,62 @@ +# toVariants utility + +This utility is used to convert a set of props into a list of CSS variants classes. + +- No class will be added for _falsy_ values +- `` class will be added for `true` values +- `--` class will be added for other values + +## Basic usage + +```ts +const props = defineProps<{ + label: string + color: 'blue' | 'red' + size: 'small' | 'large' + disabled?: boolean +}>() + +const variants = computed(() => + toVariants({ + color: props.color, + size: props.size, + disabled: props.disabled, + }) +) +``` + +If `color` is `'blue'`, `size` is `'small'`, and `disabled` is `false`, +then `variants` will be `['color--blue', 'size--small']`. + +## Advanced usage + +Variants don't have to be based on props, you can define them the way you want. + +Thanks to the way Vue works, they can also be mixed with other classes. + +```ts +const props = defineProps<{ + label: string + color: 'blue' | 'red' + size: 'small' | 'large' +}>() + +const isDisabled = inject('isParentDisabled', ref(false)) + +const typoClasses = { + small: 'typo p3-regular', + large: 'typo p2-medium', +} + +const classes = computed(() => [ + typoClasses[props.size], + { disabled: isDisabled.value }, + toVariants({ + color: props.color, + size: props.size.slice(0, 1), + state: isDisabled.value ? 'off' : 'on', + }), +]) +``` + +`classes` applied to the component will then look like `typo p3-regular disabled color-blue size-s state-off` diff --git a/@xen-orchestra/web-core/lib/assets/css/_colors-legacy.pcss b/@xen-orchestra/web-core/lib/assets/css/_colors-legacy.pcss deleted file mode 100644 index 61d6aedd6d5..00000000000 --- a/@xen-orchestra/web-core/lib/assets/css/_colors-legacy.pcss +++ /dev/null @@ -1,125 +0,0 @@ -:root { - --color-logo: #282467; - - --color-grey-000: #000000; - --color-grey-100: #1a1b38; - --color-grey-200: #595a6f; - --color-grey-300: #9899a5; - --color-grey-400: #bfbfc6; - --color-grey-500: #e5e5e7; - --color-grey-600: #ffffff; - - --background-color-primary: #ffffff; - --background-color-secondary: #f6f6f7; - - --color-purple-base: #8f84ff; - --color-purple-d20: color(#8f84ff blend(black 20%)); - --color-purple-d40: color(#8f84ff blend(black 40%)); - --color-purple-d60: color(#8f84ff blend(black 60%)); - --color-purple-l20: color(#8f84ff blend(white 20%)); - --color-purple-l40: color(#8f84ff blend(white 40%)); - --color-purple-l60: color(#8f84ff blend(white 60%)); - --background-color-purple-10: color(white blend(#8f84ff 10%)); - --background-color-purple-20: color(white blend(#8f84ff 20%)); - --background-color-purple-30: color(white blend(#8f84ff 30%)); - --background-color-purple-60: color(white blend(#8f84ff 60%)); - - --color-green-base: #2ca878; - --color-green-d20: color(#2ca878 blend(black 20%)); - --color-green-d40: color(#2ca878 blend(black 40%)); - --color-green-d60: color(#2ca878 blend(black 60%)); - --color-green-l20: color(#2ca878 blend(white 20%)); - --color-green-l40: color(#2ca878 blend(white 40%)); - --color-green-l60: color(#2ca878 blend(white 60%)); - --background-color-green-10: color(white blend(#2ca878 10%)); - --background-color-green-20: color(white blend(#2ca878 20%)); - --background-color-green-30: color(white blend(#2ca878 30%)); - --background-color-green-60: color(white blend(#2ca878 60%)); - - --color-orange-base: #ef7f18; - --color-orange-d20: color(#ef7f18 blend(black 20%)); - --color-orange-d40: color(#ef7f18 blend(black 40%)); - --color-orange-d60: color(#ef7f18 blend(black 60%)); - --color-orange-l20: color(#ef7f18 blend(white 20%)); - --color-orange-l40: color(#ef7f18 blend(white 40%)); - --color-orange-l60: color(#ef7f18 blend(white 60%)); - --background-color-orange-10: color(white blend(#ef7f18 10%)); - --background-color-orange-20: color(white blend(#ef7f18 20%)); - --background-color-orange-30: color(white blend(#ef7f18 30%)); - --background-color-orange-60: color(white blend(#ef7f18 60%)); - - --color-red-base: #be1621; - --color-red-d20: color(#be1621 blend(black 20%)); - --color-red-d40: color(#be1621 blend(black 40%)); - --color-red-d60: color(#be1621 blend(black 60%)); - --color-red-l20: color(#be1621 blend(white 20%)); - --color-red-l40: color(#be1621 blend(white 40%)); - --color-red-l60: color(#be1621 blend(white 60%)); - --background-color-red-10: color(white blend(#be1621 10%)); - --background-color-red-20: color(white blend(#be1621 20%)); - --background-color-red-30: color(white blend(#be1621 30%)); - --background-color-red-60: color(white blend(#be1621 60%)); -} - -:root.dark { - --color-logo: #e5e5e7; - - --color-grey-000: #ffffff; - --color-grey-100: #e5e5e7; - --color-grey-200: #bfbfc6; - --color-grey-300: #9899a5; - --color-grey-400: #595a6f; - --color-grey-500: #3a3b54; - --color-grey-600: #000000; - - --background-color-primary: #14141e; - --background-color-secondary: #17182b; - - --color-purple-base: #8f84ff; - --color-purple-d20: color(#8f84ff blend(white 20%)); - --color-purple-d40: color(#8f84ff blend(white 40%)); - --color-purple-d60: color(#8f84ff blend(white 60%)); - --color-purple-l20: color(#8f84ff blend(black 20%)); - --color-purple-l40: color(#8f84ff blend(black 40%)); - --color-purple-l60: color(#8f84ff blend(black 60%)); - --background-color-purple-10: color(#17182b blend(#8f84ff 25%)); - --background-color-purple-20: color(#17182b blend(#8f84ff 35%)); - --background-color-purple-30: color(#17182b blend(#8f84ff 45%)); - --background-color-purple-60: color(#17182b blend(#8f84ff 85%)); - - --color-green-base: #2ca878; - --color-green-d20: color(#2ca878 blend(white 20%)); - --color-green-d40: color(#2ca878 blend(white 40%)); - --color-green-d60: color(#2ca878 blend(white 60%)); - --color-green-l20: color(#2ca878 blend(black 20%)); - --color-green-l40: color(#2ca878 blend(black 40%)); - --color-green-l60: color(#2ca878 blend(black 60%)); - --background-color-green-10: color(#17182b blend(#2ca878 25%)); - --background-color-green-20: color(#17182b blend(#2ca878 35%)); - --background-color-green-30: color(#17182b blend(#2ca878 45%)); - --background-color-green-60: color(#17182b blend(#2ca878 85%)); - - --color-orange-base: #ef7f18; - --color-orange-d20: color(#ef7f18 blend(white 20%)); - --color-orange-d40: color(#ef7f18 blend(white 40%)); - --color-orange-d60: color(#ef7f18 blend(white 60%)); - --color-orange-l20: color(#ef7f18 blend(black 20%)); - --color-orange-l40: color(#ef7f18 blend(black 40%)); - --color-orange-l60: color(#ef7f18 blend(black 60%)); - --background-color-orange-10: color(#17182b blend(#ef7f18 25%)); - --background-color-orange-20: color(#17182b blend(#ef7f18 35%)); - --background-color-orange-30: color(#17182b blend(#ef7f18 45%)); - --background-color-orange-60: color(#17182b blend(#ef7f18 85%)); - - --color-red-base: #be1621; - --color-red-d20: color(#be1621 blend(white 20%)); - --color-red-d40: color(#be1621 blend(white 40%)); - --color-red-d60: color(#be1621 blend(white 60%)); - --color-red-l20: color(#be1621 blend(black 20%)); - --color-red-l40: color(#be1621 blend(black 40%)); - --color-red-l60: color(#be1621 blend(black 60%)); - --background-color-red-10: color(#17182b blend(#be1621 25%)); - --background-color-red-20: color(#17182b blend(#be1621 35%)); - --background-color-red-30: color(#17182b blend(#be1621 45%)); - --background-color-red-60: color(#17182b blend(#be1621 85%)); -} diff --git a/@xen-orchestra/web-core/lib/assets/css/base.pcss b/@xen-orchestra/web-core/lib/assets/css/base.pcss index 0a1409b4da6..6168eb6955e 100644 --- a/@xen-orchestra/web-core/lib/assets/css/base.pcss +++ b/@xen-orchestra/web-core/lib/assets/css/base.pcss @@ -1,4 +1,3 @@ -@import '_colors-legacy.pcss'; @import '_colors.pcss'; @import '_reset.pcss'; @import '_fonts.pcss'; @@ -28,6 +27,7 @@ scrollbar-width: auto; /* Chrome/Webkit */ + ::-webkit-scrollbar { width: var(--scrollbar-width); } diff --git a/@xen-orchestra/web-core/lib/components/CardNumbers.vue b/@xen-orchestra/web-core/lib/components/CardNumbers.vue index a8a4747ef5f..90a454dc277 100644 --- a/@xen-orchestra/web-core/lib/components/CardNumbers.vue +++ b/@xen-orchestra/web-core/lib/components/CardNumbers.vue @@ -53,11 +53,11 @@ const percentValue = computed(() => { /* COLOR VARIANTS */ .card-numbers { &.small { - --label-color: var(--color-grey-100); + --label-color: var(--color-neutral-txt-primary); } &.medium { - --label-color: var(--color-grey-300); + --label-color: var(--color-neutral-txt-secondary); } } @@ -89,13 +89,13 @@ const percentValue = computed(() => { } .value { - color: var(--color-grey-100); + color: var(--color-neutral-txt-primary); display: flex; gap: 0.2rem; align-items: center; } .unit { - color: var(--color-grey-100); + color: var(--color-neutral-txt-primary); } diff --git a/@xen-orchestra/web-core/lib/components/LegendTitle.vue b/@xen-orchestra/web-core/lib/components/LegendTitle.vue index 9ed36b560df..f034b34eb17 100644 --- a/@xen-orchestra/web-core/lib/components/LegendTitle.vue +++ b/@xen-orchestra/web-core/lib/components/LegendTitle.vue @@ -2,7 +2,7 @@ @@ -25,7 +25,7 @@ defineSlots<{ diff --git a/@xen-orchestra/web-core/lib/components/UiCard.vue b/@xen-orchestra/web-core/lib/components/UiCard.vue index 745e87fa484..e8297d6d928 100644 --- a/@xen-orchestra/web-core/lib/components/UiCard.vue +++ b/@xen-orchestra/web-core/lib/components/UiCard.vue @@ -18,8 +18,8 @@ defineProps() gap: 2.4rem; padding: 2.4rem; flex-direction: column; - background-color: var(--background-color-primary); - border: 0.1rem solid var(--color-grey-500); + background-color: var(--color-neutral-background-primary); + border: 0.1rem solid var(--color-neutral-border); border-radius: 0.8rem; } diff --git a/@xen-orchestra/web-core/lib/components/UiCounter.vue b/@xen-orchestra/web-core/lib/components/UiCounter.vue index 22095812f4c..9e426b6f780 100644 --- a/@xen-orchestra/web-core/lib/components/UiCounter.vue +++ b/@xen-orchestra/web-core/lib/components/UiCounter.vue @@ -13,7 +13,7 @@ import { computed } from 'vue' const props = withDefaults( defineProps<{ value: number | string - color?: CounterColor + color: CounterColor size?: CounterSize }>(), { size: 'small' } @@ -32,26 +32,34 @@ const classNames = computed(() => { diff --git a/@xen-orchestra/web-core/lib/components/cell-object/CellObject.vue b/@xen-orchestra/web-core/lib/components/cell-object/CellObject.vue index afb46d97623..abd2c07211b 100644 --- a/@xen-orchestra/web-core/lib/components/cell-object/CellObject.vue +++ b/@xen-orchestra/web-core/lib/components/cell-object/CellObject.vue @@ -11,8 +11,8 @@ v-if="isSupported && copiableId" :left-icon="faCopy" level="secondary" - size="extra-small" - :color="copied ? 'success' : 'info'" + size="small" + :color="copied ? 'success' : 'normal'" @click="copy(id)" > {{ copied ? $t('core.copied') : $t('core.copy-id') }} @@ -39,7 +39,7 @@ const { isSupported, copy, copied } = useClipboard() diff --git a/@xen-orchestra/web-core/lib/components/cell-text/CellText.vue b/@xen-orchestra/web-core/lib/components/cell-text/CellText.vue index 8ed9fd36bb5..0f32bd8419b 100644 --- a/@xen-orchestra/web-core/lib/components/cell-text/CellText.vue +++ b/@xen-orchestra/web-core/lib/components/cell-text/CellText.vue @@ -24,8 +24,8 @@ const slots = defineSlots<{ diff --git a/@xen-orchestra/web-core/lib/components/chip/ChipIcon.vue b/@xen-orchestra/web-core/lib/components/chip/ChipIcon.vue index e99a97f4e26..aa13f93de14 100644 --- a/@xen-orchestra/web-core/lib/components/chip/ChipIcon.vue +++ b/@xen-orchestra/web-core/lib/components/chip/ChipIcon.vue @@ -16,13 +16,13 @@ defineProps<{ diff --git a/@xen-orchestra/web-core/lib/components/dropdown/DropdownItem.vue b/@xen-orchestra/web-core/lib/components/dropdown/DropdownItem.vue index 0cc2c6cfdd2..d2c89f44388 100644 --- a/@xen-orchestra/web-core/lib/components/dropdown/DropdownItem.vue +++ b/@xen-orchestra/web-core/lib/components/dropdown/DropdownItem.vue @@ -1,14 +1,14 @@ @@ -24,7 +24,7 @@ import { computed, inject } from 'vue' defineProps<{ arrow?: boolean - color?: Color + color: Color disabled?: boolean icon?: IconDefinition info?: string @@ -40,128 +40,103 @@ const checkbox = inject( diff --git a/@xen-orchestra/web-core/lib/components/dropdown/DropdownList.vue b/@xen-orchestra/web-core/lib/components/dropdown/DropdownList.vue index 592e79d74b1..7262c0cff75 100644 --- a/@xen-orchestra/web-core/lib/components/dropdown/DropdownList.vue +++ b/@xen-orchestra/web-core/lib/components/dropdown/DropdownList.vue @@ -25,7 +25,7 @@ provide( padding: 0.4rem 0; gap: 0.2rem; border-radius: 0.4rem; - background: var(--background-color-primary); + background: var(--color-neutral-background-primary); box-shadow: var(--shadow-300); } diff --git a/@xen-orchestra/web-core/lib/components/dropdown/DropdownTitle.vue b/@xen-orchestra/web-core/lib/components/dropdown/DropdownTitle.vue index 853a941f68c..06b9b22d3bf 100644 --- a/@xen-orchestra/web-core/lib/components/dropdown/DropdownTitle.vue +++ b/@xen-orchestra/web-core/lib/components/dropdown/DropdownTitle.vue @@ -4,7 +4,7 @@ -->