diff options
author | skjnldsv <skjnldsv@protonmail.com> | 2024-11-15 11:07:37 +0100 |
---|---|---|
committer | skjnldsv <skjnldsv@protonmail.com> | 2024-12-06 10:19:42 +0100 |
commit | cb472bebfe73f91f4f3fabc99f569083f28e00ad (patch) | |
tree | e7cafce5b76bfb334f64d1244767e1c1035f8762 | |
parent | adf8a454dd934b076bc907ff7202cd9e85b67525 (diff) | |
download | nextcloud-server-cb472bebfe73f91f4f3fabc99f569083f28e00ad.tar.gz nextcloud-server-cb472bebfe73f91f4f3fabc99f569083f28e00ad.zip |
feat(systemtags): add colors in bulk tagging action
Signed-off-by: skjnldsv <skjnldsv@protonmail.com>
-rw-r--r-- | apps/systemtags/appinfo/info.xml | 2 | ||||
-rw-r--r-- | apps/systemtags/src/components/SystemTagPicker.vue | 169 | ||||
-rw-r--r-- | apps/systemtags/src/css/fileEntryInlineSystemTags.scss | 10 | ||||
-rw-r--r-- | apps/systemtags/src/event-bus.d.ts | 4 | ||||
-rw-r--r-- | apps/systemtags/src/files_actions/inlineSystemTagsAction.spec.ts | 17 | ||||
-rw-r--r-- | apps/systemtags/src/files_actions/inlineSystemTagsAction.ts | 57 | ||||
-rw-r--r-- | apps/systemtags/src/services/api.ts | 10 | ||||
-rw-r--r-- | apps/systemtags/src/types.ts | 2 | ||||
-rw-r--r-- | apps/systemtags/src/utils/colorUtils.ts | 193 | ||||
-rw-r--r-- | package-lock.json | 342 | ||||
-rw-r--r-- | package.json | 1 |
11 files changed, 487 insertions, 320 deletions
diff --git a/apps/systemtags/appinfo/info.xml b/apps/systemtags/appinfo/info.xml index bfc33c6ff66..e2e84cce1c8 100644 --- a/apps/systemtags/appinfo/info.xml +++ b/apps/systemtags/appinfo/info.xml @@ -11,7 +11,7 @@ <summary>Collaborative tagging functionality which shares tags among people.</summary> <description>Collaborative tagging functionality which shares tags among people. Great for teams. (If you are a provider with a multi-tenancy installation, it is advised to deactivate this app as tags are shared.)</description> - <version>1.21.0</version> + <version>1.21.1</version> <licence>agpl</licence> <author>Vincent Petry</author> <author>Joas Schilling</author> diff --git a/apps/systemtags/src/components/SystemTagPicker.vue b/apps/systemtags/src/components/SystemTagPicker.vue index 8ed26ce9cb3..ada4e31643d 100644 --- a/apps/systemtags/src/components/SystemTagPicker.vue +++ b/apps/systemtags/src/components/SystemTagPicker.vue @@ -31,34 +31,57 @@ </div> <!-- Tags list --> - <div class="systemtags-picker__tags" + <ul class="systemtags-picker__tags" data-cy-systemtags-picker-tags> - <NcCheckboxRadioSwitch v-for="tag in filteredTags" + <li v-for="tag in filteredTags" :key="tag.id" - :label="tag.displayName" - :checked="isChecked(tag)" - :indeterminate="isIndeterminate(tag)" - :disabled="!tag.canAssign" :data-cy-systemtags-picker-tag="tag.id" - class="systemtags-picker__tag" - @update:checked="onCheckUpdate(tag, $event)"> - {{ formatTagName(tag) }} - </NcCheckboxRadioSwitch> - <NcButton v-if="canCreateTag" - :disabled="status === Status.CREATING_TAG" - alignment="start" - class="systemtags-picker__tag-create" - native-type="submit" - type="tertiary" - data-cy-systemtags-picker-button-create - @click="onNewTag"> - {{ input.trim() }}<br> - <span class="systemtags-picker__tag-create-subline">{{ t('systemtags', 'Create new tag') }}</span> - <template #icon> - <PlusIcon /> - </template> - </NcButton> - </div> + :style="tagListStyle(tag)" + class="systemtags-picker__tag"> + <NcCheckboxRadioSwitch :checked="isChecked(tag)" + :disabled="!tag.canAssign" + :indeterminate="isIndeterminate(tag)" + :label="tag.displayName" + class="systemtags-picker__tag-checkbox" + @update:checked="onCheckUpdate(tag, $event)"> + {{ formatTagName(tag) }} + </NcCheckboxRadioSwitch> + + <!-- Color picker --> + <NcColorPicker :data-cy-systemtags-picker-tag-color="tag.id" + :value="`#${tag.color}`" + :shown.sync="openedPicker" + class="systemtags-picker__tag-color" + @update:value="onColorChange(tag, $event)" + @submit="openedPicker = false"> + <NcButton :aria-label="t('systemtags', 'Change tag color')" type="tertiary"> + <template #icon> + <CircleIcon v-if="tag.color" :size="24" fill-color="var(--color-circle-icon)" /> + <CircleOutlineIcon v-else :size="24" fill-color="var(--color-circle-icon)" /> + <PencilIcon /> + </template> + </NcButton> + </NcColorPicker> + </li> + + <!-- Create new tag --> + <li> + <NcButton v-if="canCreateTag" + :disabled="status === Status.CREATING_TAG" + alignment="start" + class="systemtags-picker__tag-create" + native-type="submit" + type="tertiary" + data-cy-systemtags-picker-button-create + @click="onNewTag"> + {{ input.trim() }}<br> + <span class="systemtags-picker__tag-create-subline">{{ t('systemtags', 'Create new tag') }}</span> + <template #icon> + <PlusIcon /> + </template> + </NcButton> + </li> + </ul> <!-- Note --> <div class="systemtags-picker__note"> @@ -110,19 +133,28 @@ import escapeHTML from 'escape-html' import NcButton from '@nextcloud/vue/dist/Components/NcButton.js' import NcCheckboxRadioSwitch from '@nextcloud/vue/dist/Components/NcCheckboxRadioSwitch.js' import NcChip from '@nextcloud/vue/dist/Components/NcChip.js' +import NcColorPicker from '@nextcloud/vue/dist/Components/NcColorPicker.js' import NcDialog from '@nextcloud/vue/dist/Components/NcDialog.js' import NcEmptyContent from '@nextcloud/vue/dist/Components/NcEmptyContent.js' import NcLoadingIcon from '@nextcloud/vue/dist/Components/NcLoadingIcon.js' import NcNoteCard from '@nextcloud/vue/dist/Components/NcNoteCard.js' import NcTextField from '@nextcloud/vue/dist/Components/NcTextField.js' -import TagIcon from 'vue-material-design-icons/Tag.vue' import CheckIcon from 'vue-material-design-icons/CheckCircle.vue' +import CircleIcon from 'vue-material-design-icons/Circle.vue' +import CircleOutlineIcon from 'vue-material-design-icons/CircleOutline.vue' +import PencilIcon from 'vue-material-design-icons/Pencil.vue' import PlusIcon from 'vue-material-design-icons/Plus.vue' +import TagIcon from 'vue-material-design-icons/Tag.vue' +import { createTag, fetchTag, fetchTags, getTagObjects, setTagObjects, updateTag } from '../services/api' import { getNodeSystemTags, setNodeSystemTags } from '../utils' -import { createTag, fetchTag, fetchTags, getTagObjects, setTagObjects } from '../services/api' +import { elementColor, invertTextColor, isDarkModeEnabled } from '../utils/colorUtils' import logger from '../services/logger' +const mainBackgroundColor = getComputedStyle(document.body) + .getPropertyValue('--color-main-background') + .replace('#', '') || (isDarkModeEnabled() ? '000000' : 'ffffff') + type TagListCount = { string: number } @@ -139,15 +171,19 @@ export default defineComponent({ components: { CheckIcon, + CircleIcon, + CircleOutlineIcon, NcButton, NcCheckboxRadioSwitch, // eslint-disable-next-line vue/no-unused-components NcChip, + NcColorPicker, NcDialog, NcEmptyContent, NcLoadingIcon, NcNoteCard, NcTextField, + PencilIcon, PlusIcon, TagIcon, }, @@ -171,6 +207,7 @@ export default defineComponent({ return { status: Status.BASE, opened: true, + openedPicker: false, input: '', tags: [] as TagWithId[], @@ -329,7 +366,14 @@ export default defineComponent({ // Format & sanitize a tag chip for v-html tag rendering formatTagChip(tag: TagWithId): string { const chip = this.$refs.chip as NcChip - const chipHtml = chip.$el.outerHTML + const chipCloneEl = chip.$el.cloneNode(true) as HTMLElement + if (tag.color) { + const style = this.tagListStyle(tag) + Object.entries(style).forEach(([key, value]) => { + chipCloneEl.style.setProperty(key, value) + }) + } + const chipHtml = chipCloneEl.outerHTML return chipHtml.replace('%s', escapeHTML(sanitize(tag.displayName))) }, @@ -345,6 +389,11 @@ export default defineComponent({ return tag.displayName }, + onColorChange(tag: TagWithId, color: string) { + tag.color = color.replace('#', '') + updateTag(tag) + }, + isChecked(tag: TagWithId): boolean { return tag.displayName in this.tagList && this.tagList[tag.displayName] === this.nodes.length @@ -480,6 +529,28 @@ export default defineComponent({ showInfo(t('systemtags', 'File tags modification canceled')) this.$emit('close', null) }, + + tagListStyle(tag: TagWithId): Record<string, string> { + // No color, no style + if (!tag.color) { + return { + // See inline system tag color + '--color-circle-icon': 'var(--color-text-maxcontrast)', + } + } + + // Make the checkbox color the same as the tag color + // as well as the circle icon color picker + const primaryElement = elementColor(`#${tag.color}`, `#${mainBackgroundColor}`) + const textColor = invertTextColor(primaryElement) ? '#000000' : '#ffffff' + return { + '--color-circle-icon': 'var(--color-primary-element)', + '--color-primary': primaryElement, + '--color-primary-text': textColor, + '--color-primary-element': primaryElement, + '--color-primary-element-text': textColor, + } + }, }, }) </script> @@ -506,6 +577,48 @@ export default defineComponent({ gap: var(--default-grid-baseline); display: flex; flex-direction: column; + + li { + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + + // Make switch full width + :deep(.checkbox-radio-switch) { + width: 100%; + + .checkbox-content { + // adjust width + max-width: none; + // recalculate padding + box-sizing: border-box; + min-height: calc(var(--default-grid-baseline) * 2 + var(--default-clickable-area)); + } + } + } + + .systemtags-picker__tag-color button { + margin-inline-start: calc(var(--default-grid-baseline) * 2); + + span.pencil-icon { + display: none; + color: var(--color-main-text); + } + + &:focus, + &:hover, + &[aria-expanded='true'] { + .pencil-icon { + display: block; + } + .circle-icon, + .circle-outline-icon { + display: none; + } + } + } + .systemtags-picker__tag-create { :deep(span) { text-align: start; diff --git a/apps/systemtags/src/css/fileEntryInlineSystemTags.scss b/apps/systemtags/src/css/fileEntryInlineSystemTags.scss index 4cf72ed429f..d6b6d3a28bd 100644 --- a/apps/systemtags/src/css/fileEntryInlineSystemTags.scss +++ b/apps/systemtags/src/css/fileEntryInlineSystemTags.scss @@ -22,7 +22,7 @@ white-space: nowrap; overflow: hidden; text-overflow: ellipsis; - line-height: 22px; // min-size - 2 * 5px padding + line-height: 20px; // min-size - 2 * 5px padding - 2 * 1px border text-align: center; &--more { @@ -34,6 +34,14 @@ & + .files-list__system-tag { margin-inline-start: 5px; } + + // With color + &[data-systemtag-color] { + border-color: var(--systemtag-color); + color: var(--systemtag-color); + border-width: 2px; + line-height: 18px; // min-size - 2 * 5px padding - 2 * 2px border + } } @media (min-width: 512px) { diff --git a/apps/systemtags/src/event-bus.d.ts b/apps/systemtags/src/event-bus.d.ts index 4009f3f372b..f17be3dca53 100644 --- a/apps/systemtags/src/event-bus.d.ts +++ b/apps/systemtags/src/event-bus.d.ts @@ -3,10 +3,14 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ import type { Node } from '@nextcloud/files' +import type { TagWithId } from './types' declare module '@nextcloud/event-bus' { interface NextcloudEvents { 'systemtags:node:updated': Node + 'systemtags:tag:deleted': TagWithId + 'systemtags:tag:updated': TagWithId + 'systemtags:tag:created': TagWithId } } diff --git a/apps/systemtags/src/files_actions/inlineSystemTagsAction.spec.ts b/apps/systemtags/src/files_actions/inlineSystemTagsAction.spec.ts index 6861b5015a1..354837b91f6 100644 --- a/apps/systemtags/src/files_actions/inlineSystemTagsAction.spec.ts +++ b/apps/systemtags/src/files_actions/inlineSystemTagsAction.spec.ts @@ -3,10 +3,12 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ import { action } from './inlineSystemTagsAction' -import { describe, expect, test } from 'vitest' -import { File, Permission, View, FileAction } from '@nextcloud/files' +import { beforeEach, describe, expect, test, vi } from 'vitest' import { emit, subscribe } from '@nextcloud/event-bus' +import { File, Permission, View, FileAction } from '@nextcloud/files' import { setNodeSystemTags } from '../utils' +import * as serviceTagApi from '../services/api' +import { set } from 'lodash' const view = { id: 'files', @@ -53,6 +55,13 @@ describe('Inline system tags action conditions tests', () => { }) describe('Inline system tags action render tests', () => { + + beforeEach(() => { + vi.spyOn(serviceTagApi, 'fetchTags').mockImplementation(async () => { + return [] + }) + }) + test('Render something even when Node does not have system tags', async () => { const file = new File({ id: 1, @@ -165,7 +174,9 @@ describe('Inline system tags action render tests', () => { // Subscribe to the event const eventPromise = new Promise((resolve) => { - subscribe('systemtags:node:updated', resolve) + subscribe('systemtags:node:updated', () => { + setTimeout(resolve, 100) + }) }) // Change tags diff --git a/apps/systemtags/src/files_actions/inlineSystemTagsAction.ts b/apps/systemtags/src/files_actions/inlineSystemTagsAction.ts index afd54a4f7de..cc6cb55c632 100644 --- a/apps/systemtags/src/files_actions/inlineSystemTagsAction.ts +++ b/apps/systemtags/src/files_actions/inlineSystemTagsAction.ts @@ -3,18 +3,38 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ import type { Node } from '@nextcloud/files' +import type { TagWithId } from '../types' import { FileAction } from '@nextcloud/files' import { subscribe } from '@nextcloud/event-bus' import { t } from '@nextcloud/l10n' import '../css/fileEntryInlineSystemTags.scss' +import { elementColor, isDarkModeEnabled } from '../utils/colorUtils' +import { fetchTags } from '../services/api' import { getNodeSystemTags } from '../utils' +import logger from '../services/logger' + +// Init tag cache +const cache: TagWithId[] = [] const renderTag = function(tag: string, isMore = false): HTMLElement { const tagElement = document.createElement('li') tagElement.classList.add('files-list__system-tag') + tagElement.setAttribute('data-systemtag-name', tag) tagElement.textContent = tag + // Set the color if it exists + const cachedTag = cache.find((t) => t.displayName === tag) + if (cachedTag?.color) { + // Make sure contrast is good and follow WCAG guidelines + const mainBackgroundColor = getComputedStyle(document.body) + .getPropertyValue('--color-main-background') + .replace('#', '') || (isDarkModeEnabled() ? '000000' : 'ffffff') + const primaryElement = elementColor(`#${cachedTag.color}`, `#${mainBackgroundColor}`) + tagElement.style.setProperty('--systemtag-color', primaryElement) + tagElement.setAttribute('data-systemtag-color', 'true') + } + if (isMore) { tagElement.classList.add('files-list__system-tag--more') } @@ -35,6 +55,17 @@ const renderInline = async function(node: Node): Promise<HTMLElement> { return systemTagsElement } + // Fetch the tags if the cache is empty + if (cache.length === 0) { + try { + // Best would be to support attributes from webdav, + // but currently the library does not support it + cache.push(...await fetchTags()) + } catch (error) { + logger.error('Failed to fetch tags', { error }) + } + } + systemTagsElement.append(renderTag(tags[0])) if (tags.length === 2) { // Special case only two tags: @@ -84,6 +115,7 @@ export const action = new FileAction({ order: 0, }) +// Update the system tags html when the node is updated const updateSystemTagsHtml = function(node: Node) { renderInline(node).then((systemTagsHtml) => { document.querySelectorAll(`[data-systemtags-fileid="${node.fileid}"]`).forEach((element) => { @@ -92,4 +124,29 @@ const updateSystemTagsHtml = function(node: Node) { }) } +// Add and remove tags from the cache +const addTag = function(tag: TagWithId) { + cache.push(tag) +} +const removeTag = function(tag: TagWithId) { + cache.splice(cache.findIndex((t) => t.id === tag.id), 1) +} +const updateTag = function(tag: TagWithId) { + const index = cache.findIndex((t) => t.id === tag.id) + if (index !== -1) { + cache[index] = tag + } + updateSystemTagsColorAttribute(tag) +} +// Update the color attribute of the system tags +const updateSystemTagsColorAttribute = function(tag: TagWithId) { + document.querySelectorAll(`[data-systemtag-name="${tag.displayName}"]`).forEach((element) => { + (element as HTMLElement).style.setProperty('--systemtag-color', `#${tag.color}`) + }) +} + +// Subscribe to the events subscribe('systemtags:node:updated', updateSystemTagsHtml) +subscribe('systemtags:tag:created', addTag) +subscribe('systemtags:tag:deleted', removeTag) +subscribe('systemtags:tag:updated', updateTag) diff --git a/apps/systemtags/src/services/api.ts b/apps/systemtags/src/services/api.ts index 3262ccd3a87..ae99b2292b6 100644 --- a/apps/systemtags/src/services/api.ts +++ b/apps/systemtags/src/services/api.ts @@ -13,9 +13,10 @@ import { t } from '@nextcloud/l10n' import { davClient } from './davClient.js' import { formatTag, parseIdFromLocation, parseTags } from '../utils' import { logger } from '../logger.js' +import { emit } from '@nextcloud/event-bus' export const fetchTagsPayload = `<?xml version="1.0"?> -<d:propfind xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns"> +<d:propfind xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns" xmlns:nc="http://nextcloud.org/ns"> <d:prop> <oc:id /> <oc:display-name /> @@ -23,6 +24,7 @@ export const fetchTagsPayload = `<?xml version="1.0"?> <oc:user-assignable /> <oc:can-assign /> <d:getetag /> + <nc:color /> </d:prop> </d:propfind>` @@ -81,6 +83,7 @@ export const createTag = async (tag: Tag | ServerTag): Promise<number> => { }) const contentLocation = headers.get('content-location') if (contentLocation) { + emit('systemtags:tag:created', tag) return parseIdFromLocation(contentLocation) } logger.error(t('systemtags', 'Missing "Content-Location" header')) @@ -98,12 +101,13 @@ export const createTag = async (tag: Tag | ServerTag): Promise<number> => { export const updateTag = async (tag: TagWithId): Promise<void> => { const path = '/systemtags/' + tag.id const data = `<?xml version="1.0"?> - <d:propertyupdate xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns"> + <d:propertyupdate xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns" xmlns:nc="http://nextcloud.org/ns"> <d:set> <d:prop> <oc:display-name>${tag.displayName}</oc:display-name> <oc:user-visible>${tag.userVisible}</oc:user-visible> <oc:user-assignable>${tag.userAssignable}</oc:user-assignable> + <nc:color>${tag.color}</nc:color> </d:prop> </d:set> </d:propertyupdate>` @@ -113,6 +117,7 @@ export const updateTag = async (tag: TagWithId): Promise<void> => { method: 'PROPPATCH', data, }) + emit('systemtags:tag:updated', tag) } catch (error) { logger.error(t('systemtags', 'Failed to update tag'), { error }) throw new Error(t('systemtags', 'Failed to update tag')) @@ -123,6 +128,7 @@ export const deleteTag = async (tag: TagWithId): Promise<void> => { const path = '/systemtags/' + tag.id try { await davClient.deleteFile(path) + emit('systemtags:tag:deleted', tag) } catch (error) { logger.error(t('systemtags', 'Failed to delete tag'), { error }) throw new Error(t('systemtags', 'Failed to delete tag')) diff --git a/apps/systemtags/src/types.ts b/apps/systemtags/src/types.ts index 161e4d74247..6e4f03227e0 100644 --- a/apps/systemtags/src/types.ts +++ b/apps/systemtags/src/types.ts @@ -8,6 +8,8 @@ export interface BaseTag { userVisible: boolean userAssignable: boolean readonly canAssign: boolean // Computed server-side + etag?: string + color?: string } export type Tag = BaseTag & { diff --git a/apps/systemtags/src/utils/colorUtils.ts b/apps/systemtags/src/utils/colorUtils.ts new file mode 100644 index 00000000000..9bc0ca8c19d --- /dev/null +++ b/apps/systemtags/src/utils/colorUtils.ts @@ -0,0 +1,193 @@ +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +import Color from 'color' + +type hexColor = `#${string & ( + `${string}${string}${string}` | + `${string}${string}${string}${string}${string}${string}` +)}`; + +/** + * Is the current theme dark? + */ +export function isDarkModeEnabled() { + const darkModePreference = window.matchMedia('(prefers-color-scheme: dark)').matches + const darkModeSetting = document.body.getAttribute('data-themes')?.includes('dark') + return darkModeSetting || darkModePreference || false +} + +/** + * Is the current theme high contrast? + */ +export function isHighContrastModeEnabled() { + const highContrastPreference = window.matchMedia('(forced-colors: active)').matches + const highContrastSetting = document.body.getAttribute('data-themes')?.includes('highcontrast') + return highContrastSetting || highContrastPreference || false +} + +/** + * Should we invert the text on this background color? + * @param color RGB color value as a hex string + * @return boolean + */ +export function invertTextColor(color: hexColor): boolean { + return colorContrast(color, '#ffffff') < 4.5 +} + +/** + * Is this color too bright? + * @param color RGB color value as a hex string + * @return boolean + */ +export function isBrightColor(color: hexColor): boolean { + return calculateLuma(color) > 0.6 +} + +/** + * Get color for on-page elements + * theme color by default, grey if theme color is too bright. + * @param color the color to contrast against, e.g. #ffffff + * @param backgroundColor the background color to contrast against, e.g. #000000 + */ +export function elementColor( + color: hexColor, + backgroundColor: hexColor, +): hexColor { + const brightBackground = isBrightColor(backgroundColor) + const blurredBackground = mix( + backgroundColor, + brightBackground ? color : '#ffffff', + 66, + ) + + let contrast = colorContrast(color, blurredBackground) + const minContrast = isHighContrastModeEnabled() ? 5.6 : 3.2 + + let iteration = 0 + let result = color + const epsilon = 1.0 / 255.0 + while (contrast < minContrast && iteration++ < 100) { + const hsl = hexToHSL(result) + const l = Math.max( + 0, + Math.min(255, hsl.l + (brightBackground ? -epsilon : epsilon)), + ) + result = hslToHex({ h: hsl.h, s: hsl.s, l }) + contrast = colorContrast(result, blurredBackground) + } + + return result +} + +/** + * Get color for on-page text: + * black if background is bright, white if background is dark. + * @param color1 the color to contrast against, e.g. #ffffff + * @param color2 the background color to contrast against, e.g. #000000 + * @param factor the factor to mix the colors between -100 and 100, e.g. 66 + */ +export function mix(color1: hexColor, color2: hexColor, factor: number): hexColor { + if (factor < -100 || factor > 100) { + throw new RangeError('Factor must be between -100 and 100') + } + return new Color(color2).mix(new Color(color1), (factor + 100) / 200).hex() +} + +/** + * Lighten a color by a factor + * @param color the color to lighten, e.g. #000000 + * @param factor the factor to lighten the color by between -100 and 100, e.g. -41 + */ +export function lighten(color: hexColor, factor: number): hexColor { + if (factor < -100 || factor > 100) { + throw new RangeError('Factor must be between -100 and 100') + } + return new Color(color).lighten((factor + 100) / 200).hex() +} + +/** + * Darken a color by a factor + * @param color the color to darken, e.g. #ffffff + * @param factor the factor to darken the color by between -100 and 100, e.g. 32 + */ +export function darken(color: hexColor, factor: number): hexColor { + if (factor < -100 || factor > 100) { + throw new RangeError('Factor must be between -100 and 100') + } + return new Color(color).darken((factor + 100) / 200).hex() +} + +/** + * Calculate the luminance of a color + * @param color the color to calculate the luminance of, e.g. #ffffff + */ +export function calculateLuminance(color: hexColor): number { + return hexToHSL(color).l +} + +/** + * Calculate the luma of a color + * @param color the color to calculate the luma of, e.g. #ffffff + */ +export function calculateLuma(color: hexColor): number { + const rgb = hexToRGB(color).map((value) => { + value /= 255 + return value <= 0.03928 + ? value / 12.92 + : Math.pow((value + 0.055) / 1.055, 2.4) + }) + const [red, green, blue] = rgb + return 0.2126 * red + 0.7152 * green + 0.0722 * blue +} + +/** + * Calculate the contrast between two colors + * @param color1 the first color to calculate the contrast of, e.g. #ffffff + * @param color2 the second color to calculate the contrast of, e.g. #000000 + */ +export function colorContrast(color1: hexColor, color2: hexColor): number { + const luminance1 = calculateLuma(color1) + 0.05 + const luminance2 = calculateLuma(color2) + 0.05 + return Math.max(luminance1, luminance2) / Math.min(luminance1, luminance2) +} + +/** + * Convert hex color to RGB + * @param color RGB color value as a hex string + */ +export function hexToRGB(color: hexColor): [number, number, number] { + return new Color(color).rgb().array() +} + +/** + * Convert RGB color to hex + * @param color RGB color value as a hex string + */ +export function hexToHSL(color: hexColor): { h: number; s: number; l: number } { + const hsl = new Color(color).hsl() + return { h: hsl.color[0], s: hsl.color[1], l: hsl.color[2] } +} + +/** + * Convert HSL color to hex + * @param hsl HSL color value as an object + * @param hsl.h hue + * @param hsl.s saturation + * @param hsl.l lightness + */ +export function hslToHex(hsl: { h: number; s: number; l: number }): hexColor { + return new Color(hsl).hex() +} + +/** + * Convert RGB color to hex + * @param r red + * @param g green + * @param b blue + */ +export function rgbToHex(r: number, g: number, b: number): hexColor { + const hex = ((1 << 24) | (r << 16) | (g << 8) | b).toString(16).slice(1) + return `#${hex}` +} diff --git a/package-lock.json b/package-lock.json index 3c80c781711..aef3aab8322 100644 --- a/package-lock.json +++ b/package-lock.json @@ -42,6 +42,7 @@ "camelcase": "^8.0.0", "cancelable-promise": "^4.3.1", "clipboard": "^2.0.11", + "color": "^4.2.3", "core-js": "^3.38.1", "davclient.js": "github:owncloud/davclient.js.git#0.2.2", "debounce": "^2.1.0", @@ -5378,26 +5379,6 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/@testing-library/dom/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/@testing-library/dom/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, "node_modules/@testing-library/dom/node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -5471,26 +5452,6 @@ "node": ">=8" } }, - "node_modules/@testing-library/jest-dom/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/@testing-library/jest-dom/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", @@ -5617,26 +5578,6 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/@testing-library/vue/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/@testing-library/vue/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, "node_modules/@testing-library/vue/node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -9179,6 +9120,47 @@ "node": ">=6" } }, + "node_modules/color": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", + "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1", + "color-string": "^1.9.0" + }, + "engines": { + "node": ">=12.5.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "license": "MIT", + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, "node_modules/colord": { "version": "2.9.3", "resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz", @@ -10141,26 +10123,6 @@ "node": ">=8" } }, - "node_modules/cypress/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/cypress/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, "node_modules/cypress/node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -12444,28 +12406,6 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/eslint/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/eslint/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT", - "peer": true - }, "node_modules/eslint/node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -16125,24 +16065,6 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/jake/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/jake/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, "node_modules/jake/node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -17173,26 +17095,6 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/log-symbols/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/log-symbols/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, "node_modules/log-symbols/node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -17251,26 +17153,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/log-update/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/log-update/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, "node_modules/log-update/node_modules/slice-ansi": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", @@ -20468,13 +20350,6 @@ "postcss": "^8.2.9" } }, - "node_modules/postcss-values-parser/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, "node_modules/precinct": { "version": "12.1.2", "resolved": "https://registry.npmjs.org/precinct/-/precinct-12.1.2.tgz", @@ -20915,24 +20790,6 @@ "wrap-ansi": "^6.2.0" } }, - "node_modules/qrcode/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/qrcode/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "license": "MIT" - }, "node_modules/qrcode/node_modules/pngjs": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", @@ -22765,6 +22622,21 @@ "integrity": "sha512-+OmPgi01yHK/bRNQDoehUcV8fqs9nNJkG2DoWCnnLvj0lmowab7BH3v9776BG0y7dGEOLh0F7mfd37k+ht26Yw==", "license": "MIT" }, + "node_modules/simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/simple-swizzle/node_modules/is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", + "license": "MIT" + }, "node_modules/sinon": { "version": "5.0.7", "resolved": "https://registry.npmjs.org/sinon/-/sinon-5.0.7.tgz", @@ -22823,26 +22695,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/slice-ansi/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/slice-ansi/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, "node_modules/smart-buffer": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", @@ -24250,26 +24102,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/table/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/table/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, "node_modules/table/node_modules/json-schema-traverse": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", @@ -24979,26 +24811,6 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/ts-loader/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/ts-loader/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, "node_modules/ts-loader/node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -27496,26 +27308,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/wrap-ansi-cjs/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, "node_modules/wrap-ansi/node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -27532,26 +27324,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/wrap-ansi/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/wrap-ansi/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", diff --git a/package.json b/package.json index a585edb5a7b..d58d2dfa5c5 100644 --- a/package.json +++ b/package.json @@ -73,6 +73,7 @@ "camelcase": "^8.0.0", "cancelable-promise": "^4.3.1", "clipboard": "^2.0.11", + "color": "^4.2.3", "core-js": "^3.38.1", "davclient.js": "github:owncloud/davclient.js.git#0.2.2", "debounce": "^2.1.0", |